Learn to build an app using Vue.js from an Angular Dev’s viewpoint.
Angular 💙 React 💚 Vue.js
Let’s be clear.
This is a post about:
- Two frontend JavaScript frameworks.
- Two very popular frameworks.
- Two framworks in the single-page app space.
This is not a post about:
- Choosing one framework over another.
- Pitching one framework as better than the other.
- Starting a flame war 🔥
What this post is really about:
Learning Vue.js from the perspective of an Angular frontend architect and engineer.
An Angular Todo App
Before I created an app using Vue.js I wanted to have a basis for comparison, knowledge transfer and understanding. So, I created a fairly simply “To Do” app using Angular, Angular Material and NgRx. This stack is pretty popular, so I wanted to use a stack that was both popular and that I was familiar with.
Here is a stackblitz of my Angular To Do app:
Download
In this post I’ll be working with a demo Vue.js application. You can clone the repository or download a zip file of the source code:
Installation
We’ll be using the Vue CLI (version 3 of this writing). As such, we need to install the CLI globally using yarn or npm:
$ npm install -g @vue/cli
$ yarn global add@vue/cli
I also recommend that you install the Vue.js devtools. This can be helpful when debugging a Vue app, and it works seamlessly with the Vuex state management library, similar to the Redux Devtools for Redux and NgRx.
Create Project
Next, create a new Vue project using the CLI:
$ vue create todo
You’ll be presented with several options during this process. Here are the options that I chose for this project:
- Please pick a preset: Manually select features
- Check the features needed for your project: Babel, Vuex, Linter
- Pick a linter / formatter config: Prettier
- Pick additional lint features: Lint on save
- Where do you prefer placing config for Babel, PostCSS, ESLint, etc.? In dedicated config files
After completing the installation, go ahead and start up the app via:
$ cd todo
$ yarn serve
Remove HelloWorld
To get started, let’s remove the HelloWorld
component:
- Remove the src/components/HelloWorld.vue file.
- In src/App.vue, remove the
hello-component
element in the template. - Then, remove the import statement
- And remove the reference to
HelloWorld
in thecomponents
property for the app.
Install Vuetify
In doing a little research, it seems the the Veutify component library is very widely used and implements the Material Design. This made the library an easy choice for my app.
$ vue add vuetify
Here are the options that I chose when adding the vuetify project:
- Use a pre-made template? (will replace App.vue and HelloWorld.vue) No
- Use custom theme? No
- Use a-la-carte components? No
- Use babel/polyfill? Yes
With Vuetify installed, open the src/plugins/vuetify.js file so we can specify the theme:
import 'vuetify/dist/vuetify.min.css';
import Vue from 'vue';
import Vuetify from 'vuetify';
import colors from 'vuetify/es5/util/colors';
Vue.use(Vuetify, {
theme: {
primary: colors.indigo,
secondary: colors.pink.accent2,
accent: colors.pink.accent3,
error: colors.red.accent3,
},
});
Update App Template
After installing Vuetify, let’s update the app template in src/App.vue:
<template>
<v-app class="app">
<v-content>
<v-flex xs12 sm8 offset-sm2 md6 offset-md3>
<h1>Hello</h1>
</v-flex>
</v-content>
<v-footer :fixed="fixed" app>
<span>Angular 💚 Vue</span>
</v-footer>
</v-app>
</template>
Most of the markup here is specific to the Vuetify application layout:
- The
v-app
component is used to determine breakpoints when using Vuetify. All Vuetify layouts must be wrapped in this component. - We wrap our application’s content in the
v-content
component. - The
v-flex
component is part of Vuetify’s flexbox-based layout. For an Angular developer, this is similar to the @angular/flex-layout module. - The
v-footer
component is out application’s sticky footer.
Go ahead and serve up the app. You should see a simple Application shell with the ‘Hello’ title.
$ yarn serve
Update App
We also need to specify the fixed
data value in our app’s root App
component to configure Vuetify.
Open src/App.vue and update it as follows:
<script>
export default {
name: 'App',
data() {
return {
fixed: false
}
},
}
</script>
The data option enables reactivity in Vue.js. The data that we specify in a component instance is reactive in that whenever the data is mutated the value is propogated throughout the component, and our application.
You can read more about reactivity in Vue.js and how change detection works.
Create <todo-list>
Next, let’s create a simple component. This is where we should really start to learn how to build apps using Vue with a component architecture.
🤔 One thing I could not find is a way to use the Vue CLI to generate a new component. It’s relatively trivial, but it would be cool if you could generate the scaffolding similar to how the Angular CLI works.
First, create a new src/components/TodoList.vue file.
Then, define the <template>
for our new component:
<template>
<v-list>
<todo
v-for="todo in todos"
:key="todo.id"
:todo="todo"
@change="$emit('change', $event)"
></todo>
</v-list>
</template>
Let’s quickly review:
- First, it’s important to note that I’m going to be using the single file approach to building components using Vue. This felt similar to Angular in that we are not defining a component globally using
Vue.component()
. It also felt different because the default option for generating a component using the Angular CLI creates separate file for the template, component (script), and styles. - We use the
template
element to wrap content that will be used to generate the Vue component. I love that Vue uses this standards approach to defining templates (not that Angular does not generally adhere to standards). - Next, we use the
v-list
component from Vuetify to create a list. This is similar to themat-list
component in the @angular/material module. - Finally, we are going to define a custom
todo
component that will output the to-do item. When the to-do is changed, we will emit achange
event to the parent component, passing along the$event
value that was emitted.
You’ll also notice that the todo
component uses Vue’s builtin directives:
- The
v-for
iterates over an iterable, adding and removing elements to/from the DOM as necessary. This is similar to the AngularNgForOf
structural directive. - The
:key="todo.id"
directive is similar to Angular’s[key]="todo.id"
input binding syntax in templates. It’s also important to note that I’m using the shorthand of thev-bind
directive here. The full syntax is:v-bind:key="todo.id"
. This is some nice syntax sugar provided by Vue, similar to the*ngFor
shorthand. - We also bind the
todo
property using thev-bind
shorthand syntax::todo="todo"
. - Note the
@change="doSomething()
output binding. This is similar to Angular’s(change)="doSomething($event)
syntax. This is also shorthand for Vue’sv-on
directive. The full syntax is:v-on:change="doSomething()"
. - In order to emit an event to the parent component we use the $emit() instance methjod. First, we specify the event name followed by the value. This is similar to using Angular’s
EventEmitter.emit()
method.
Next, let’s wire up the template using Vue:
<script>
export default {
name: 'todo-list',
props: {
todos: Array
}
}
</script>
Let’s review:
- We export a new Vue definition object. This is the same object that is the second argument to the
Vue.component()
method if you are not using a single file component. - This object has a
name
property that is the selector for the component. This is similar to Angular’sselector
property in the configuration object specified with the@Component()
decorator. - We then define the properties for the
<todo-list>
component in theprops
object. This is similar to Angular’s input bindings in a component’s class using the@Input()
decorator. We declare that the propertytodos
will be anArray
.
You can learn more about the Component Basics and Component Registration in the Vue docs.
Create <todo>
With our <todo-list>
component created, let’s quickly define the child <todo>
component.
Create a new src/components/Todo.vue file and then define the template:
<template>
<v-list-tile>
<v-list-tile-action>
<v-checkbox
v-model="todo.complete"
@change="$emit('change', todo)"
></v-checkbox>
</v-list-tile-action>
<v-list-tile-content>
<v-list-tile-title v-bind:class="{complete: todo.complete}"
>{{ todo.task }}</v-list-tile-title
>
</v-list-tile-content>
</v-list-tile>
</template>
Much of the template is based on using lists with Vuetify. Let’s focus on the Vue-specific syntax:
- The
v-model
directive binds a form value in Vue. This is similar to Angular’sngModel
directive. We are binding to thecomplete
value of a to-do item. - We use the
@change
event handler to emit achange
event to the parent component, specifying the currenttodo
object. Again, this is similar to Angular’s output binding syntax. - We use the
v-bind:class
directive to add the ‘complete’ class to thev-list-tile-title
component if the task has been completed. This is similar to Angular’sngClass
directive. - Finally, we use the double curlybrace syntax for string interpolation in our templates, just like in Angular. Nice 👍.
With the template defined, let’s wire it up:
<script>
export default {
name: 'todo',
props: {
todo: Object
}
}
</script>
<style lang="scss" scoped>
.complete {
text-decoration: line-through;
}
</style>
Similar to the <todo-list>
component, we export a configuration object from the module with the name
and props
properties.
In this case we expect the todo
property to be an Object
.
We also style up our component a bit using the standard <style>
element.
I’m going to use Sass for this project, similar to my Angular project, so we need to specify the lang
attribute.
We also scope the styles to this component by specifying the scoped
attribute.
This is similar’s to Angular’s view encapsulation.
Update <todo-list>
With the <todo>
component created we need to circle back to our <todo-list>
component and declare this new child component.
Open src/components/TodoList.vue and add the components
property to the vue options object:
<script>>
import Todo from '@/components/Todo.vue';
export default {
name: 'todo-list',
components: {
Todo
},
props: {
todos: Array
}
};
</script>
You might be wondering about the import path. The at-symbol ( @ ) is an alias to /src directory in our project.
Update <app>
Similarly, we also need to update the <app>
component to declare the TodoList
component.
Open src/App.vue and import the TodoList.vue file, and define the new child component in the components
property in the options object:
<script>
import TodoList from '@/components/TodoList.vue';
export default {
name: 'App',
data() {
return {
fixed: false
};
},
components: {
TodoList
}
}
</script>
We also need to specify two more options:
- The
computed
property - The
methods
property
A computed
property enables us to define properties for the component that are functions, where we can compute any necessary values.
This is similar to pure pipes in Angular because the value is cached.
From what I could tell the difference is that pure pipes in Angular are cached based on the input value to the transform()
method, whereas Vue’s computed
properties are cached based on their dependencies.
The methods
property defines methods that our component instance can invoke.
In Angular this is somewhat similar to invoking a method in our component.
Open src/App.vue and declare these new properties:
<script>
import TodoList from '@/components/TodoList.vue';
export default {
name: 'App',
data() {
return {
fixed: false
};
},
components: {
TodoList
},
computed: {
todos() {
return [
{
task: 'Watch Ozark Season 2',
complete: false
},
{
task: 'Use Vuex in my awesome app',
complete: true
}
];
},
completeTodos() {
return this.todos.filter(todo => todo.complete);
},
incompleteTodos() {
return this.todos.filter(todo => !todo.complete);
}
},
methods: {
onChange(todo) {
// todo: dispatch action to update state
console.log(todo);
}
}
};
</script>
Let’s quickly review:
- First, we declare a new
computed
property in the Vue options object. - Each computed property is a getter function that will be invoked by Vue.
- The
todos()
computed property returns an array of to-do items. - The
completeTodos()
computed property returns an array of to-do items wherecomplete
istrue
. - The
incompleteTodos()
computed property returns an array of to-do items wherecomplete
isfalse
. - Then, we declare a new
methods
property. - The
onChange()
function accepts atodo
. Eventually we’ll want to dispatch an action to mutate the object in the application’s state.
Finally, update the template to include our new todo-list
component.
We’ll use Vuetify to add the list within a Material Design card:
<template>
<v-app class="app">
<v-content>
<v-flex xs12 sm8 offset-sm2 md6 offset-md3>
<h1>Hello</h1>
<v-card class="todo-card">
<v-card-title primary-title>
<h1>To Do List</h1>
</v-card-title>
<todo-list
v-if="incompleteTodos.length > 0"
:todos="incompleteTodos"
@change="onChange"
></todo-list>
<div v-else class="text-xs-center">
<h4>All Done! 🏖</h4>
</div>
</v-card>
</v-flex>
</v-content>
<v-footer :fixed="fixed" app>
<span>Angular 💚 Vue</span>
</v-footer>
</v-app>
</template>
Again, we’ll skip over the details on using Vuetify to create the card.
However, let’s review the newly-added <todo-list>
component:
- We use the
v-if
directive to toggle the display of the list when theincompleteTodos
computed property has a length greater than zero. - We use the
v-bind
shorthand syntax to bind thetodos
property to theincompleteTodos
computed property. - We use the
v-on
shorthand syntac to invoke theonChange()
method when thechange
event is emitted.
Use Sass Loader
Before our application will build we need to install the Sass loader via npm or yarn:
$ npm install -D sass-loader node-sass
$ yarn add -D sass-loader node-sass
Go ahead and serve the application:
$ npm serve
$ yarn serve
If all goes well, you should see the list of incomplete to-do items:
Create <todo-form>
Obviously, our users need to be able to create new to-do items to add to their list.
So, let’s create a new <todo-form>
component.
Create the src/components/TodoForm.vue file:
<template>
<v-form>
<v-text-field
v-model="todo.task"
label="Task"
required
></v-text-field>
</v-form>
</template>
<script>
export default {
name: 'todo-form',
props: {
todo: Object
}
}
</script>
Let’s review the new <todo-form>
component:
- Fist, we wrap our form in the
v-form
component. This is part of Vuetify. - We also use the
v-text-field
component to create a Material Design input element. - The
v-model
attribute binds thetask
value of thetodo
object to the value of the input. - We export the Vue options object, specifying the
name
andprops
properties. We are expecting thetodo
property to be anObject
.
Let’s add a new card to the App.vue template, wire up the new child component and add a new onAdd
method:
<template>
<v-app class="app">
<v-content>
<v-flex xs12 sm8 offset-sm2 md6 offset-md3>
<h1>Hello</h1>
<v-card class="todo-card">
<v-card-title primary-title>
<h1>To Do List</h1>
</v-card-title>
<todo-list
v-if="incompleteTodos.length > 0"
:todos="incompleteTodos"
@change="onChange"
></todo-list>
<div v-else class="text-xs-center">
<h4>All Done! 🏖</h4>
</div>
</v-card>
<v-card class="todo-card">
<v-card-title primary-title>
<h1>Add To Do</h1>
</v-card-title>
<todo-form :todo="todo"></todo-form>
<v-card-actions class="actions">
<v-btn @click="onAdd" color="primary">Add</v-btn>
</v-card-actions>
</v-card>
</v-flex>
</v-content>
<v-footer :fixed="fixed" app>
<span>Angular 💚 Vue</span>
</v-footer>
</v-app>
</template>
<script>
import TodoForm from '@/components/TodoForm.vue';
import TodoList from '@/components/TodoList.vue';
export default {
name: 'App',
data() {
return {
fixed: false,
todo: {
task: null,
},
};
},
components: {
TodoForm,
TodoList,
},
computed: {
todos() {
return [
{
task: 'Watch Ozark Season 2',
complete: false,
},
{
task: 'Use Vuex in my awesome app',
complete: true,
},
];
},
completeTodos() {
return this.todos.filter((todo) => todo.complete);
},
incompleteTodos() {
return this.todos.filter((todo) => !todo.complete);
},
},
methods: {
onAdd: function () {
// todo: dispatch action to add todo
},
onChange(todo) {
// todo: dispatch action to update state
console.log(todo);
},
},
};
</script>
<style lang="scss" scoped>
.app {
padding: 20px;
.todo-card {
margin: 20px 0;
padding: 20px;
.actions {
flex-direction: row;
justify-content: flex-end;
}
}
}
</style>
I’ve also add some styling to our application. If you serve the app you should see a new form for adding a to-do:
$ npm serve
$ yarn serve
Storing State
In the next post we’ll wire up the Vuex library to store the state of our application, similar to Redux or NgRx.