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.
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 lets 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.
As always, I hope this was insightful and you learned something new today.
Subscribe to my YouTube channel for the latest videos on Dart. Thanks!
Like, share and follow me 😍 for more content on Dart.
Top comments (2)
Nice article! Only one question. Why do you want to use Dart instead of plain JS or TS?
Why do you want to adapt that language instead of one with larger community?
Hey Kovács, thanks for your question.
The pull Dart has on me is the fact that as a statically-typed language from the get-go, it also comes with tooling that has made me productive working in it. Dart shines when building applications that can be made sense of while it grows.
I’ve found myself working on large JS projects and I’ve struggled to understand it, due to the fact that JavaScript as a dynamically-typed language means that working out the flow of data and the structure of that data requires more mental effort. I've also struggled to stay ahead of type-related errors on larger projects. The amount of times I’ve right clicked a method in VS Code and used the “Go to definition” command to be met with no feedback, still annoys me today.
TypeScript, however is a good effort by Microsoft to address this concern. My only qualms with this is that TS code I’ve seen looks more like C# rather than JavaScript, especially when you include namespaces and interfaces, so it can be difficult to grasp at times. I would have loved to have seen TS stand on it’s own two feet, rather than be tied to the JS ecosystem. Ryan Dahl seems to be looking into that with his Deno project. This tells me that Dart may have been on the right track by not tying itself too much to the JS ecosystem. I haven't really done much with Flow, so can't comment on that.
This series with Vue.js is an investigation using the js package. I'm not selling any silver bullets. My ideal framework of choice for SPA development would be AngularDart, although I still need to look into it :)
This is beginning to feel like an essay, so I'll leave it at that.