client-side routing: navigating between views without leaving the app
options:
hash-based client-side routing, e.g.:
example.com/#/home
example.com/#/shop/cart
client-side routing based on on the history API, e.g.:
example.com/home
example.com/shop/cart
for the second method, the server needs additional configuration
// router/index.js
const routes = [
{
path: '/',
name: 'Home',
component: Home,
},
{
path: '/about',
name: 'About',
component: About,
},
];
<router-link to="/">Home</router-link>
<router-view />
we can pass down data to the route:
<router-view :todos="todos" />
const routes = [
{
path: '/todos/:todoId',
component: TodoDetailView,
},
];
access the parameter:
Details of todo: {{ $route.params.todoId }}
this.$router.push('/');
active links will receive the .router-link-active
class
(this is for Vue 3)
two-way binding for inputs:
<input v-model="newTitle" />
two-way binding in custom components:
<star-rating v-model="productRating" />
<todo-item
v-model:title="todo.title"
v-model:completed="todo.completed"
/>
implementation of star-rating
:
modelValue
update:modelValue
implementation of todo-item
:
title
, completed
update:title
, update:completed
<li>
{{ completed ? "DONE: " : "TODO: " }}
<input
:value="title"
@input="$emit('update:title', $event.target.value)"
:style="{ border: 'none' }"
/>
<button @click="$emit('update:completed', !completed)">
toggle
</button>
</li>
minimal prop definition of a component:
export default {
props: ['title', 'completed'],
};
export default {
props: {
title: String,
completed: Boolean,
},
};
export default {
props: {
title: {
type: String,
required: true,
},
completed: {
type: Boolean,
default: false,
},
},
};
setting up a Vue TypeScript project:
during project creation with Vue-CLI, choose "Manually select features"
<script lang="ts">
// ...
</script>
for better type annotations support:
export default {
name: 'TodoApp',
// ...
};
becomes:
import { defineComponent } from 'vue';
export default defineComponent({
name: 'TodoApp',
// ...
});
configuration of Vetur in VS Code:
Activate option: Vetur > Experimental: Template Interpolation Service
declaring the structure of the component state:
type ComponentState = {
todos: Array<Todo>;
};
export default defineComponent({
data(): ComponentState {
// ...
},
});
defining props with TypeScript annotations:
import { defineComponent, PropType } from 'vue';
// define interface / type "Todo" here
export default defineComponent({
props: {
type: [] as PropType<Array<Todo>>,
required: true,
},
});
Vue 3 only:
defining events and event payloads (inside validator functions):
export default defineComponent({
emits: {
/* eslint-disable @typescript-eslint/no-unused-vars */
delete(id: number) {
return true;
},
toggle(id: number) {
return true;
},
/* eslint-enable @typescript-eslint/no-unused-vars */
},
});
In more complex applications or components it makes sense to manage the state (model) separately from the view.
Often the entire application state is represented by a data model and every change to the state will be done by triggering a change to the data model.
a mutation describes some action that took place in the application (in other tools: action)
a mutation has a type and potentially a payload property
example mutations as they will appear in the devtools:
{
"type": "addTodo",
"payload": {
"title": "learn Vue"
}
}
{
"type": "deleteCompletedTodos"
}
Manual use of a Vuex store:
const store = new Vuex.Store({
// set up store here
});
store.commit('addTodo', { title: 'foo' });
store.commit('addTodo', { title: 'bar' });
store.commit('setTodoCompleted', {
id: 1,
completed: true,
});
store.commit('deleteTodo', { id: 2 });
console.log(JSON.stringify(store.state));
configuration of Vuex:
const store = new Vuex.Store({
state: { todos: [] },
getters: {
numIncomplete(state) {
return state.todos.filter((t) => !t.completed).length;
},
},
mutations: {
addTodo(state, payload) {
// ...
},
deleteTodo(state, payload) {
// ...
},
},
actions: {
async loadFromApi(context) {
// ...
},
},
});
Using a Vuex store in a Vue app:
new Vue({
// ...
store: store,
});
The store will be available via $store
inside components
Accessing the Vuex state from components:
<todo-item v-for="todo in $store.state.todos" ... />
<p>Incomplete todos: {{ $store.getters.numIncomplete }}</p>
Triggering mutations from Vue:
<todo-item
:todo="todo"
@toggle="$store.commit('toggleTodo', { id: todo.id })"
@delete="$store.commit('deleteTodo', { id: todo.id })"
/>
Triggering an action from Vue:
export default {
async created() {
this.$store.dispatch('loadFromApi');
},
};
animation of enter / leave transitions:
<transition name="fade">
<button>delete</button>
</transition>
use name fade for transition
defining fade transition:
during animation:
.fade-enter-active {
transition: opacity 5s;
}
.fade-leave-active {
transition: opacity 5s;
}
start / end styles:
.fade-enter {
opacity: 0;
}
.fade-leave-to {
opacity 0;
}
transitions for groups of elements where individual elements can appear / disappear
<transition-group name="list-fade" tag="ul">
<li v-for="todo in todos" :key="todo.id">
{{ todo.title }}
</li>
</transition-group>
.list-fade-enter-active,
.list-fade-leave-active {
transition: opacity 0.5s;
}
.list-fade-enter,
.list-fade-leave-to {
opacity: 0;
}
documentation: https://v3.vuejs.org/guide/transitions-overview.html
concept from React: JSX - combination of JavaScript and XML
Render functions may be used as an alternative to templates
export default {
name: 'Foo',
render() {
return (
<div>
foo
<button>bar</button>
</div>
);
},
};