I’ve been impressed with the mark that Vue.js has been making lately. Having used it in the past and wanting to use it again, I got curious about what it looks like working with Vue in Dart.
Prefer a video?
Having demonstrated that it’s possible to use JavaScript libraries in Dart web apps, we will go through the Vue.js “Getting started” page and rewrite the examples in Dart, using the js
interop package.
Before we begin:
1. Set up your project
Set-up your web project quickly with Stagehand:
$ mkdir vue_dart && cd vue_dart
$ stagehand web-simple
2. Install the js interop package
Ensure that the js
dependency is added to your pubspec.yaml
file:
dependencies:
js: ^0.6.1+1
Save and run pub get
to update your dependencies.
3. Import the Vue.js library
In web/index.html
in the <head>
before <script defer src="main.dart.js"></script>
import the dev version of the library:
<!-- development version, includes helpful console warnings -->
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
Now we can go through the examples!
Getting started
Create a web/app.dart
file with our library declaration and imports:
@JS()
library vue_interop;
import 'package:js/js.dart';
// TODO: The rest of the code to go here
Declarative rendering
Here’s the first example of a template with the message
property placeholder:
<div id="app">
{{ message }}
</div>
Create an annotated factory constructor for Vue
:
@JS()
class Vue {
external factory Vue(VueOptions options);
}
The JavaScript api takes an object literal when a new Vue
instance is declared. Notice the declaration of the VueOptions
type instead of Map
? We cannot use Dart Map
s here since they are opaque in JavaScript.
Therefore, we need to create a factory constructor to house our options:
@JS()
@anonymous
class VueOptions {
external factory VueOptions({ String el, VueDataOptions data });
external String get el;
external VueDataOptions get data;
}
And the data prop is a VueDataOptions
object:
@JS()
@anonymous
class VueDataOptions {
external factory VueDataOptions({
String message = '', // Set to empty string as default
});
external String get message;
}
Return to web/main.dart
and let's use these factories:
// Relative imports
import './app.dart';
void main() {
Vue(VueOptions(
el: '#app',
data: VueDataOptions(
message: 'Hello Vue!',
),
));
}
You should now see the text “Hello Vue!” on the screen:
In addition to string interpolation we can also bind element attributes.
<div id="app-2">
<span v-bind:title="message">
Hover your mouse over me for a few seconds
to see my dynamically bound title!
</span>
</div>
No changes needed on our factory, just declare a call:
// web/main.dart
...
void main() {
...
// App 2 example
Vue(VueOptions(
el: '#app-2',
data: VueDataOptions(
message: 'You loaded this page on ${DateTime(2018).toLocal()}',
),
));
}
Conditionals
Use the v-if
attribute to toggle the presence of an element:
<div id="app-3">
<span v-if="seen">Now you see me</span>
</div>
Since we are watching a new property (seen
), let’s add a getter for this in our factory:
@JS()
@anonymous
class VueDataOptions {
external factory VueDataOptions({
String message = '',
bool seen = null, // <-- Added this
});
external String get message;
external bool get seen; // <-- Added this
}
And in web/main.dart
:
...
void main() {
...
// App 3 example
var app3 = Vue(VueOptions(
el: '#app-3',
data: VueDataOptions(seen: true),
));
}
In the snippet above we have assigned the result of calling Vue()
to an app3
variable. The docs demonstrate doing app3.seen = false
, which means we have to add a getter of type boolean to Vue
class in web/app.dart
:
@JS()
class Vue {
external factory Vue(VueOptions options);
external void set seen(bool val); // <-- Added this
}
And in web/main.dart
, we will do:
import 'dart:async'; // <-- Added this line to use `Future.delayed`
// Relative imports
import './todo.dart'; // <-- Added this line
import './app.dart';
void main() {
...
...
// App 3 example
var app3 = Vue(VueOptions(
el: '#app-3',
data: VueDataOptions(seen: true),
));
// Added a delay to see disappearing text
Future.delayed(Duration(seconds: 2), () async {
app3.seen = false;
// Added a delay and then restored text visibility
await Future.delayed(Duration(seconds: 2));
app3.seen = true;
});
}
Loops
The v:for
attribute is used when iterating over arrays:
<div id="app-4">
<ol>
<li v-for="todo in todos">
{{ todo.text }}
</li>
</ol>
</div>
This introduces a new factory constructor, which we’ll call Todo
.
Create web/todo.dart
with our factory class:
@JS()
library todo;
import 'package:js/js.dart';
@JS()
@anonymous
class Todo {
external factory Todo({String text});
external String get text;
}
And in web/app.dart
, let’s define a list of Todo
s:
@JS()
class Vue {
external factory Vue(VueOptions options);
external void set seen(bool val);
external List<Todo> get todos; // <-- Added this line
}
...
@JS()
@anonymous
class VueDataOptions {
external factory VueDataOptions({
String message = '',
bool seen = null,
List<Todo> todos = const [],
});
external String get message;
external bool get seen;
external List<Todo> get todos; // <-- Added this line
}
And in web/main.dart
we’ll use it:
...
...
void main() {
...
...
// App 4 example
var app4 = Vue(VueOptions(
el: '#app-4',
data: VueDataOptions(todos: [
Todo(text: 'Learn Dart'),
Todo(text: 'Learn Aqueduct'),
Todo(text: 'Build something awesome!'),
]),
));
}
In order to add a new item to the todos list like the docs show:
app4.todos.push({ text: 'New item' });
We’ll need to add a getter for todos
on Vue
:
// web/app.dart
...
...
@JS()
class Vue {
external factory Vue(VueOptions options);
external void set seen(bool val);
external List<Todo> get todos; // <-- Added this line
}
And in web/main.dart
:
...
...
// App 4 example
var app4 = Vue(VueOptions(
el: '#app-4',
data: VueDataOptions(todos: [
Todo(text: 'Learn Dart'),
Todo(text: 'Learn Aqueduct'),
Todo(text: 'Build something awesome!'),
]),
));
app4.todos.add(Todo(text: 'New item')); // <-- Added this line
Conclusion
And this brings us to the end of Part 1. In part 2, we will look at handling user input and composition with Vue components.