Vue Js
Contents |
Installation
Demo app on https://github.com/duleorlovic/hello_vue Integration with rails
rails new hello_vue --webpack=vue
this will add vue
, vue-loader
and vue-template-compiler
to package.json
and create app/javascript/packs/vue.js
and app/javascript/app.vue
(and
some conf file config/webpack/loaders/vue.js
and update
config/webpack/environment.js
).
Run bin/webpack-dev-server
and rails s
and edit
# app/javascript/packs/vue.js
import Vue from 'vue/dist/vue.esm'
import App from '../app.vue'
document.addEventListener('DOMContentLoaded', () => {
const app = new Vue({
el: '#hello',
data: {
message: "Can you say hello?"
},
components: { App }
})
})
Basic directive helpers
<div id='app'>
<input v-model='message'>
<ul>
<li v-for='item in items' :key='item'>
</li>
</ul>
</div>
Interpolation:
- mustaches
{ { message }}
can be used only as inner block (not html attributes, which need v-bind directive so instead<img source='{ {}}'>
use<img :src='comment.image'>
). Use ternary expression insteadif
flow. Can not use statement likea=1
. When you need to output html than use<span v-html='rawHtml'></span>
will output without html encoding - For attributes you have to use
<div v-bind:id='dynamicId'></div>
. When html atribute is boolean (it’s presence is important, not value) thantrue
value will show it. When value isfalse
,null
,undefined
it hides that attribute for example<button v-bind:disabled='isButtonDisabled'>B</button>
. Shorthand forv-bind:
is:
. To pass all properties from parent to child you can usev-bind='$attrs'
. Inside data binding you can use any javascript expressions (calling methodsmessage.split('')
and calculationsnumber + 1
) but assignment is not working (since it is a statement, it can be used onv-on
but notv-bind
), control flowif (true) {}
is not available andthis.data
is not available (this is only available in methods or computed) https://vuejs.org/v2/guide/syntax.html#Using-JavaScript-Expressions Forclass
andstyle
attribute you can also use object or array syntax<div :class='{ active: isActive, 'text-danger': hasError }'></div> <div :class='[ isActive ? 'active' : '', hasError ? 'text-danger': '' ]'></div>
Directives:
- model
v-model='message'
is used for two way binding.v-bind='message'
is only one way binding (if used on input element, it will not changemessage
).- text and textarea use
value
property andinput
event (use modifierv-model.lazy='msg'
if you want to sync afterchange
event) - checkbox and radiobutton use
checked
property andchange
event - select field use
value
property andchange
event. It ignores initialvalue
attributes, and uses vue instance data. Modifiers:.number
typecast to Number,.trim
strip whitespace. ```
```
- text and textarea use
- conditional
v-if='seen'
directive to render block. Also supports else that immediatelly followsv-if
.<h1 v-if="awesome">Vue is awesome!</h1> <h1 v-else>Oh no 😢</h1>
You can attach to
<template v-if='ok'
to wrap other elements and<template>
itself will not be rendered.v-show
will always render, but it usesdisplay: none
css property to hide (sov-show
has higher initial render cost but no cost for show hide since event listeners and child components inside no need to be destroyed/created). Usev-else-if
like switch case statement. Usekey
to distinguish between different elements, if you do not want them to share same value. For example, changing loginType will change only placeholder, but value will be the same, so we need differentkey
.<template v-if="loginType === 'username'"> <label>Username</label> <input placeholder="Enter your username" key="username-input"> </template> <template v-else> <label>Email</label> <input placeholder="Enter your email address" key="email-input"> </template>
- loops
v-for='item in items'
you need a key (which is uniq) so it keeps track of changes between dom and virtual dom.v-bind:key='item.id'
or shorthand:key='item.id'
. You can get the index also<li v-for='(item, index) in items'> . </li>
You can iterate object properties and also index
<li v-for='(value, name, index) in object'> . : </li>
Use
key
attribute so updating does not only patch inline el, but completelly remove/create<div v-for="item in items" v-bind:key="item.id">
. You can mutateitems
array withpush()
,pop()
,sort()
… and replace with new array withfilter()
,concat()
orslice()
. <span v-once>{ { message }}</span>
will render only once even you change data for inside model interpolation{ { message }}
<span v-html='rawHtml'></span>
will output without html encoding- events
v-on:click='shuffle'
or shorthand@click='shuffle'
. You can use modifiers (start with dot) like<form v-on:submit.prevent='onSubmit'>
which callpreventDefault()
on triggered event. Other event modifiers are:.stop
,.prevent
,.capture
,.self
,.once
and.passive
(do not prevent default behavior, usefull forv-on:scroll.passive='onScroll'
). Value ofv-on
attribute accepts name of a method which is defined undermethods: { myMethod: function (event) {} }
. There is also key modifiers, so it triggers that callback only when specific key is usedv-on:keyup.page-down='onPageDown'
($event.key == 'PageDown'
) It can also accept js expressioncounter += 1
but that is not feasible. It can also accept inline js statement, method invocation with parameter (in this case, if you need event: which is default first parameter, you can pass event using special variable$event
like thissay('hi', $event)
).<div id='example-3'> <button v-on:click='say("hi")'>Say hi</button> <button v-on:click='say("what")'>Say what</button> </div> new Vue({ el: '#example-3', methods: { say: function(message) { alert(message) } } })
is
attribute is usefull when you want to rename<table><tr is='blog-post-component'>
or for dynamic component<component v-bind:is='currentTabComponent'></component>
. Dynamic component is chached so not recreated each time you activate it, using<keep-alive>
wrapper.- dynamic arguments
<a v-on:[eventName]='doSomething'> is equivalent to <a v-on:focus='doSomething'> when eventName=='focus'
Tips
- instead of attaching to
beforeDestoy
you can use$on
or$once
(only once) to destroy objects, for examplemounted: function () { this.attachDatepucker('startDateInput') }, methods: { attachDatepucker: function(refName) { var picker = new Pikaday({ field: this.$refs[refname], format: 'YYYY-MM-DD' }) this.$once('hook:beforeDestroy', function () { picker.destroy() }) } }
- use
<div ref='my-ref'>
to reference usingthis.$refs['my-ref'].focus()
(similar asdocument.querySelect("[ref='my-ref']").focus()
but ref is not showed in DOM, vue will usedata-v-123asd
attribute). ref does not work insidev-if
since that element is not rendered (usev-show
instead, but still focusing does not work since it is not visible untill js finishes running method… I end up using $nextTick (similar to setTimeout)this.$nextTick(() => { this.$refs['new-title'].focus() }) // this is similar to old unused way const vm = this setTimeout(function() { vm.$refs['new-title'].focus() }, 0)
- for alert and notice you can use https://github.com/euvl/vue-notification/blob/master/README.md
yarn add vue-notification # app/javascript/packs/application.js import Notifications from 'vue-notification' Vue.use(Notifications) # app/javascript/app.vue <template> <div> <notifications group="alerts" position="top center" /> <router-view></router-view> </div> </template> # app/javascript/components/sign-in.vue this.$notify({ group: 'alert', type: 'error', title: 'Error', text: 'Invalid credentials' })
v-cloak
hides any un-compiled data bindings until the Vue instance is ready- custom filter
123
Vue.filter('currency', function(value) { return '$' + value.toFixed(2) })
Vue instance
var vm = new Vue({
el: '#example',
mixins:[
myMixin
]
data: {
asd: 1 // you can access data with `vm.asd`
},
// lifecycle hooks created, mounted, updated, destroyed
// `this` points to the vm instance
// do not use arrow functions since they do not have `this`
created: function() {
console.log('asd is: ' + this.asd)
},
computed: {
// getters for computed values are like methods/helpers, but result is
// cached as long as dependencies are not changed (message is not updated).
// `Date.now()` is not reactive dependency so it will never update
reverseMessage: function() {
return this.message.split('').reverse().join('')
},
// computed property can also define a setter `vm.fullName = 'Joe Doe'
fullName: {
get: function() {
return this.firstName + ' ' + this.lastName
},
set: function(newValue) {
var names = newValue.split(' ')
this.firstName = names[0]
this.lastName = names[names.length - 1]
}
}
},
methods: {
reverseMessageAlwaysCalculated: function() {
return this.message.split('').reverse().join('')
}
},
watch: {
// It is easier to use computed property than to watch for all changes on
// dependent properties for example FullName depends on First and Last name
firstName: function(val) {
this.fullName = val + ' ' + this.lastName
},
lastName: function(val) {
this.fullName = this.firstName + ' ' + val
}
}
})
vm.asd // => 1
vm.asd = 2 // reactiveness in action will update data.asd == 2
vm.non_bind = 1 // this was not defined at time of instantiating so no update
// instance properties starts with dollar $, like $data, $el
vm.$data === data // => true
vm.$el === document.getElementById('example') // => true
// $watch is an instance method
vm.$watch('a', function (newValue, oldValue) {
// This callback will be called when `vm.a` changes
})
Do not use arrow functions since this
will not be bound to view instance.
Transitions and animations
Vue will sniff whether the target element has CSS transitions or animations
property and add css classes at appropriate timings. Default name is v-
.
v-enter
added before element is inserted, removed after element is inserted and this is very short (one frame). Use this to define starting opacity/transform before transition is applied…v-enter-active
added before element is inserted, removed when transition finishes. Use this to definetransition
duration (sincev-enter
is removed it’s properties will transit slowly to normal state) or defineanimation
to use (no need to usev-enter
since you can define keyframes for whole animation).v-enter-to
added one frame after element is inserted (whenv-enter
is removed), removed when trans finishes. For leave action, you should usev-leave-active
(orv-leave-to
) to define properties for end state.
.v-enter-active, .v-leave-active {
transition: all .75s ease;
}
.v-enter, .v-leave-active {
opacity: 0;
transform: translate(30px, 0);
}
You can use custom class names, for example with Animate.js https://vuejs.org/v2/guide/transitions.html#Custom-Transition-Classes
<transition
enter-active-class="animated tada"
leave-active-class="animated bounceOutRight"
>
<transition-group tag='ul name='items'>
</transition-group>
.items-move {
transition: transform 0.5s ease;
}
Components
Data must be function not an object (because if we instantiate two components,
they should be totally separated).
Pass data to component’s props
that are attributes for components. They
are passed with v-bind:name_of_attribute='value_of_attribute'
.
Props can be defined as array of names, or object where key is props name and
value is object with following attributes type: String
(also Number, Boolean,
Array, Function, Promise), required: true
, default: 1
, validator:
function(value) { return ['draft', 'public'].indexOf(value) !== -1 }
. Props
are one-way-down binding. Any change in parent will update childs so do not
manually update props (you can use computed property).
To send data to parent use this.$emit('update:name-of-attribute', newValue)
and parent can listen that event v-on:update:name-of-attribute'='doc.title =
$event
. Shorthand for this is using modifier .sync
like here <text-document
v-bind:title.sync='doc.title'></text-document>
.
In single file vue
<script>
import MyComponent from 'components/my-component.vue'
export default {
components: {MyComponent}
}
Or in global javascript. You can import component and attach using
import AppComponent from 'app-component'
Vue.component('app-component', AppComponent)
# use with <app-component></app-component>
or you can define template in text/x-template
script tag.
Vue.component('app-component', {
template: '#componentTemplate',
data: function() {
return {
count: 0
}
},
props: ['comment']
})
<ul>
<li is='app-component' v-for='comment in comments' :key='comment'
:comment='comment'></li>
</ul>
<script type='text/x-template' id='componentTemplate'>
<li>
</li>
</script>
Defining html markup for component can be using script type=’text/x-template’
(as in above example), using inline html template: '<div...'
or single file
.vue components (this requires preprocessors like webpack).
Single file vue component https://vuejs.org/v2/guide/single-file-components.html
enable us to use <style lang='stylus' scoped>
styles, <template
lang="jade">
.
Listening to child component events is vith v-on='name-of-event'
and child
should $emit('name-of-event')
(always use kebab-case because html is case
insensitive so myEvent becomes myevent). Passing variable is catched using
$event
or in case of function, it is first parameter.
<blog-post
v-on:enlarge-text="postFontSize += $event
v-on:enlarge-text="onEnlargeText"
></blog-post>
methods: {
onEnlargeText: function (enlargeAmount) {
this.postFontSize += enlargeAmount
}
}
<button v-on:click="$emit('enlarge-text', 0.1)">
Enlarge text
</button>
When parent is using v-model
<enlarge-text v-model='count'></enlarge-text>
this is equivalent v-bind:value='count'
and v-on:input='count = $event'
than in component you can use value prop and you should emit input event
<template>
<div>
My Component
<input
v-on:input="$emit('input', $event.target.value)"
v-bind:value='value'
>
</div>
</template>
<script>
export default {
props: ['value']
}
</script>
Any other (non props) attribute is passed directly to the root element of the component.
You can use <slot></slot>
to get inner html <my-component>inner
html </my-component>
. Note that `` is compiled in
parent scope.
To pass to the specific slot you use <template v-slot:name-of-slot>
. Note
that it is like v-bind:
(using colon argument :
not the value of attribute).
Attributes slot
and slot-scope
are deprecated.
You can define <template v-slot:default>...
and for each named slot. In case
there is only default template, you can merge attributees to parent so we have
<current-user v-slot>This will populate default <slot></slot></current-user>
So defining is using <slot name='my-name'>
# base-layout.vue
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<slot name='footer' v-bind:user='user'>default </slot>
</div>
to populate named slot you can use <template v-slot:my-name>
# index.vue
<base-layout>
<template v-slot:header>
<h1>Here might be a page title</h1>
</template>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
<template v-slot:footer='slotProps'>I'm footer </footer>
</base-layout>
To make user
available to the slot in the parent, we can create slot props
with <slot v-bind:nameForParent='nameInChildComponent'>
. In parent scope, you
can access to child data inside slot by defining name v-slot='slotProps'
like:
<template v-slot:footer='slotProps'></template>
.
You can use ES6 destructuring <template v-slot='{ user }'>
.
Vue cli
# yarn will install old ~2 version
# yarn global add @vue/cli
npm install -g @vue/cli
vue create my-project
Running server
yarn serve # which is actually `vue-cli-service serve`
Styleguide
Install eslint and create configuration .eslintrc.js
yarn add eslint --dev
npx eslint --init
TODO: https://docs.gitlab.com/ee/development/fe_guide/style/vue.html
In components use following order: <template>
than <script>
than <style>
- component name should be multi-word
- always use
:key='item.id'
withv-for
. avoid usingv-if
on the same element asv-for
(if not aplicable to single item then move to parent, otherwise use computed property to filter the list instead of usingv-if
) - user defined private properties should start with
$_
and include component name likemethods: { $_myComponent_update: function() {} }
. Or you can define function outside of main paramsfunction myPrivateFunction() { } export default { methods: { publicMethod: { myPrivateFunction() } } }
- single file components should be PascalCase.vue (kebab-case is only used when
referencing in DOM). Use
Base
orApp
prefix for presentational components (they only contain html elements or other base components, no code). Alphabetically sorted will first list base components.The
prefix is for single instance components (they never accepts props). Subcomponents should retain nameTodoListItem
. - self closing components
<MyComponent/>
- prop name case should be camelCase during declaration, but kebab-case in
templates
props: { greetingText: String } <WelcomeMessage greeting-text="hi"/>
- more than one property should be in separate lines
<MyComponent foo='a' bar='b' />
- do not use directive shorthands
:
forv-bind:
,@
forv-on:
and#
forv-slot
. - component options order
https://vuejs.org/v2/style-guide/#Component-instance-options-order-recommended
- el - Side Effects, triggers effects outside the component
- name, parent - Global Awareness, requires knowledge beyond the component
- functional - Component Type, changes the type of component
- delimiters, comments - Template Modifiers, changes the way templates are compiled
- components, directives, filters - Template Dependencies, assets used in template
- extends, mixins - Composition, merges properties into the options
- inheritAttrs, model, props/propsData - Interface
- data, (validations from vuelidate), computed - Local State, local reactive properties
- watch, beforeCreate, created, beforeMount, mounted, beforeUpdate, updated, activated, deactivated, beforeDestroy, destroyed - Events, callbacks triggered by reactive events
- methods - Non-Reactive Properties, instance properties independent of the reactivity system
- template/render, renderError - Rendering, declarative description of the component output
- element attribute order:
- is - Definition, provides the component options
- v-for - List Rendering, creates multiple variations of the same element
- v-if, v-else-if, v-else, v-show, v-cloak - Conditionals, whether the element is rendered
- v-pre, v-once - Render Modifiers, changes the way the element renders
- id - Global Awareness, requires kknowledge beyod component
- ref, key - Unique Attributes, attributes that require unique values
- v-model - Two Way Binding, compbining binding and events
- v-on - Events, component event listeners
- v-html, v-text - Content, overrides the content of the element
- in scoped styles, use class instead element selectors because of speed
SCSS style
Use scoped
attribute which will write all component css using data selector
p[data-v-123] { }
so it not affect other components.
Use css module (not sure how)
<template>
<button :class="[$style.button, $style.buttonClose]">X</button>
</template>
<!-- Using CSS modules -->
<style module>
.button {
border: none;
border-radius: 2px;
}
.buttonClose {
background-color: red;
}
</style>
To include sass from other modules
<style lang="sass">
@import 'typeface-roboto/index.css'
</style>
If you include .css it gives error
Failed to compile.
./app/javascript/components/ListAcquisition.vue?vue&type=style&index=0&lang=sass& (./node_modules/css-loader/dist/cjs.js??ref--4-1!./node_modules/vue-loader/lib/loaders/stylePostLoader.js!./node_modules/postcss-loader/src??ref--4-2!./node_modules/resolve-url-loader!./node_modules/sass-loader/lib/loader.js??ref--4-4!./node_modules/vue-loader/lib??vue-loader-options!./app/javascript/components/ListAcquisition.vue?vue&type=style&index=0&lang=sass&)
Module not found: Error: Can't resolve './files/roboto-latin-100.woff' in '/home/orlovic/rails/action/app/javascript/components'
so we need to add resolve-url-loader, but since this is .css it won’t help (it must be .scss to be processed with sass and resolve-url-loader). https://github.com/KyleAMathews/typefaces/issues/79
Vuex
Using props and events works only for parent child relation, not for siblings, so better is to use one common store using vuex. https://docs.gitlab.com/ee/development/fe_guide/vue.html#testing-vuex When a user clicks on an action, we need to dispatch it. This action will commit a mutation that will change the state. Note: The action itself will not update the state, only a mutation should update the state.
Inside components you can access store using this.$store
attribute. Even
better is using mapState
which enables you to define arrow functions or create
properties by listing the names so use count
instead of
this.$store.state.count
. Similarly mapGetters
is used for getters,
mapMutations
for .commit
and mapActions
for .dispatch
(they requries
root store: store
injection).
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'
const store = new Vuex.Store({
state: {
count: 0,
todos: [
{ id: 1, text: 'first', done: true },
{ id: 2, text: 'second', done: false }
],
},
getters: {
doneTodos: state => {
return state.todos.filter(todo => todo.done)
},
doneTodosCount: (state, getters) => {
return getters.doneTodos.length
},
getTodoById: (state) => (id) => {
return state.todos.find(todo => todo.id === id)
}
}
mutations: {
// call with store.commit('INCREMENT', { amount: 10 })
INCREMENT (state, payload) {
state.count += payload.amount
}
},
actions: {
// invoke with store.dispatch('incrementAsync')
//
// context.commit context.state context.getters
incrementAsync (context) {
setTimeout(function() {
context.commit('INCREMENT')
}, 1000)
},
// instead of actionA (context) we can use argument destructuring
actionA ({ commit }) {
return new Promise((resolve) => {
setTimeout(() => {
commit('INCREMENT')
resolve({data: {email: '[email protected]'}})
}, 1000)
})
},
actionB ({ dispatch, commit }) {
dispatch('actionA').then(() => {
commit('INCREMENT')
})
}
// assuming `getData()` and `getOtherData()` return Promises
async actionA ({ commit }) {
commit('gotData', await getData())
},
async actionB ({ dispatch, commit }) {
await dispatch('actionA') // wait to finish
commit('gotOtherData', await getOtherData())
}
}
})
const app = new Vue({
el: '#hello',
store: store,
// instead of using full store.state.count you can use mapState
// computed: {
// count () {
// return store.state.count
// }
// }
computed: mapState({
count: state => state.count,
countAlias: 'count',
})
computed: {
// mix the getters into computed with object spread operator
...mapState(['count']),
...mapGetters([
'doneTodosCount',
'getTodoById'
]),
},
methods: {
...mapMutations(['INCREMENT']),
...mapActions(['incrementAsync', 'actionB'])
}
})
Getters are computed properties for stores, they accept parameters state
,
getters
(this
is not usefull to access other getters, so we need to use
second parameter getters
).
Mutations are similar to events. event registration is using name ie type
and handler function that receive state
param. Second parameter can be
payload (used as argument when commiting a mutation). Since state is
reactive, when we mutate the state, components observing the state will update
automatically. Mutation are synchronous transactions, to handle asynchronous,
use Actions (which use Mutations).
Actions are called with .dispatch
and also accepts payload and all params can
be one object store.dispatch(type: 'increment', amout: 10)
Modules contains its own state, mutations, actions…
const store = new Vuex.Store({
modules: {
a: moduleA,
b: moduleB
},
state: {
count: 2
},
mutation: {
increment
}
})
// use like
store.state.count
store.state.a.count
store.state.b.count
// if `namespaces: true` than mutations, getters and actions are not global and
// we need to use path (for getters use square brackets)
store.commit('a/increment')
store.dispatch('b/increment')
store.getters['b/count']
// for getters use additional argument rootState, rootGetters
getters: {
someGetter (state, getters, rootState, rootGetters) {}
}
// for actions use rootGetters
actions: {
someAction({ dispatch, commit, getters, rootGetters }) {
getters.someGetter
rootGetters.someGetter
dispatch('someOtherAction')
dispatch('someOtherAction', null, { root: true })
commit('someMutation')
commit('someMutation', null, { root: true })
}
}
// in components when `namespace: true` you need to provide path to module
methods: mapActions('some/module/path', [
foo
])
Use strict mode to raise error when you change state without mutation
const store = new Vuex.Store({
// use strict mode to raise error when you change state without mutation
strict: process.env.NODE_ENV !== 'production',
actions: {
changeCountWithoutMutation ({ state }) {
state.count += 1
}
}
})
"Error: [vuex] do not mutate vuex store state outside mutation handlers."
Handle form inputs with two-way computed property (with getter and setter)
// in template
<input v-model='vuexData'>
// in store
mutations: {
updateVuexData (state, value) {
state.vuexData = value
}
},
// in component
computed: {
vuexData: {
get () {
return this.$store.state.vuexData
},
set (value) {
this.$store.commit('updateVuexData', value)
}
}
},
Note that you should define all state properties upfront so Vuex can observe and
update dependent components automatically. If you want to add new properties,
instead of obj.newProp = 123
you should either:
- use
Vue.set(obj, 'newProp', 123)
- use spread
state.obj = { ...state.obj, newProp: 123 }
Testing vuex https://vuex.vuejs.org/guide/testing.html https://scrimba.com/p/pnyzgAP/cPGkpJhq
Axios
To work with rails you need to use cookie with form_authenticity_token
and
respond to :json
https://github.com/duleorlovic/hello_vue/commit/7dc7154a80d1ef2e7bff1aa516c1dc23de236d87
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
before_action :_set_csrf_cookie
respond_to :html, :json
def _set_csrf_cookie
cookies['CSRF-TOKEN'] = form_authenticity_token
end
end
# config/routes.rb
scope :api, defaults: {format: :json} do
devise_scope :user do
post 'signup', to: 'devise/registrations#create'
post 'login', to: 'devise/sessions#create'
delete 'logout', to: 'devise/sessions#destroy'
end
end
# app/javascript/api.js
import axios from 'axios'
axios.defaults.xsrfCookieName = "CSRF-TOKEN";
axios.defaults.xsrfHeaderName = "X-CSRF-TOKEN";
axios.defaults.withCredentials = true;
const BASE_PATH = '/api';
export default {
// Auth
getUser() {
return axios.get(`${BASE_PATH}/user`)
},
login(payload) {
return axios.post(`${BASE_PATH}/login`, payload)
},
}
Router
https://router.vuejs.org/
Adding with
yarn add vue-router
# app/javascript/packs/hello_vue.js
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const routes = [
{ path: '/foo', component: { template: '<div>foo</div>' } },
{ path: '/bar', component: { template: '<bar>bar</div>' } }
]
const router = new VueRouter({
routes
})
const app = new Vue({
el: '#hello',
router
})
# app/views/pages/home.html.erb
<p>
<router-link to='/foo'>Go to foo</router-link>
<router-link to='/bar'>Go to bar</router-link>
</p>
<router-view></router-view>
By injecting router
you can access this.$router
inside any component.
For current active $route
object you have access to:
$route.path
,$route.query
and$route.hash
/path?query=123#hash$route.params
for dynamic route/users/:id
, since$route
is the same (renders same component) lifecycle hooks of the components will not be called (for that hooks you need towatch: { $router(to, from) { } }
). You can use{ path: '/user/:id', component: User, props: true }
andprops: ['id']
inside User component, so you can use{ { id }}
instead of{ { this.$route.params.id }}
Match everything with path: '*'
, match partial path: 'user-*'
.
Manually go to specific route with this.$router.push('/user-admin')
location
can be an object $router.push({ path: '/user-admin', query: { plan: 'id' }})
.
Note that when you use path: '/user/${userId}''
than params: { userId }
is
ignored. If you want to use params than you need to use name name: 'user',
params: { userId }
.
Changing path without new history entry is with $router.replace(location)
.
Going up/down in history $router.go(-1)
(as as history.back()
)
Use mode: 'history',
to see real path change path/user-admin
instead of hash
path/#/user-admin
. For Rails you need to render same vue page for example
get '*path', to: 'pages#home'
in routes.
Named views can be used to render components on two places for one specific path
<router-view name='default'></router-view>
<router-view name='a'></router-view>
<router-view name='b'></router-view>
const router = new VueRouter({
routes: [
{ path: '/',
components: {
default: ComponentDefault,
a: ComponentA,
b: ComponentB
}
}
]
})
Meta attribute { path: 'bar', meta: { requiresAuth: true }
and check for that
router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.requiresAuth)) {
// this route requires auth, check if logged in
// if not, redirect to login page.
if (!auth.loggedIn()) {
next({
path: '/login',
query: { redirect: to.fullPath }
})
} else {
next()
}
} else {
next() // make sure to always call next()!
}
})
Nested routes https://router.vuejs.org/guide/essentials/nested-routes.html
Use children
in router definition and template should have it’s own
<router-view></router-view>
.
Data fetching https://router.vuejs.org/guide/advanced/data-fetching.html could
be before or after route change (this is easier since we can show loading and
fetch data in created
hook, no need to think how to load data for each next
route).
Mixins
You can share specific properties in mixins. Hook functions
Import can be absolute (relative path will prevent easy moving files around) but there could be package with same name, so better is to put them inside folder.
Plugins are similar Here is example of console log as mixin, plugin or global. https://stackoverflow.com/questions/45547089/how-to-bind-console-log-to-l-in-vue-js
// app/javascript/plugins/log.js
export default {
// use inside components like: this.$log('hi')
install (Vue, options) {
Vue.prototype.$log = process.env.NODE_ENV === 'production' ? function() {} : console.log.bind(console)
}
}
// app/javascript/packs/application.js
import log from '../plugins/log'
Vue.use(log)
Bootstrap
https://bootstrap-vue.org/docs#using-module-bundlers
Adding bootstrap and bootstrap-vue will import 'vue'
not vue.esm as we do in
rails so we need to set up alias
https://github.com/vuetifyjs/vuetify/discussions/4068#discussioncomment-24987
yarn add bootstrap-vue bootstrap
# config/webpack/environment.js
environment.config.resolve.alias = { 'vue$': 'vue/dist/vue.esm.js' };
# app/javascript/packs/hello_vue.js
import { BootstrapVue, IconsPlugin } from 'bootstrap-vue'
Vue.use(BootstrapVue)
Vue.use(IconsPlugin)
# app/javascript/stylesheet/application.scss
@import '~bootstrap';
@import '~bootstrap-vue';
Validation can be implemented with Veevalidate https://bootstrap-vue.org/docs/reference/validation
Veevalidate
yarn add vee-validate
and use in component so that it wraps input with
v-model
or :value
binding.
# app/javascript/components/my-component.vuue
<script>
import { ValidationProvider } from 'vee-validate'
export default {
components: {
ValidationProvider,
},
</script>
Vuelidate
https://github.com/vuelidate/vuelidate/ You can install in whole Vue app
# app/javascript/packs/hello_vue.js
import Vuelidate from 'vuelidate'
Vue.use(Vuelidate)
or for specific component (as in example)
import { validationMixin } from 'vuelidate'
export default {
mixins: [validationMixin],
validations: { ... }
}
Define validations
property after data
property for each data item.
# app/javascript/components/sign-up.vue
import { required, minLength } from 'vuelidate/lib/validators'
export default {
data () {
return {
email: '',
password: ''
}
},
validations: {
email: {
required,
},
password: {
minLength: minLength(6)
}
},
methods: {
validateState(name) {
const { $dirty, $error } = this.$v[name];
return $dirty ? !$error : null;
},
onSubmit() {
this.$v.$touch();
if (this.$v.$anyError) {
return;
}
alert("Form submitted!");
}
}
}
In template you can use <b-form-input>
with model $v.email.$model
and
:state
prop on it to bind to true/false whether error exists. Also use
<b-form-invalid-feedback>
adjacent to <b-form-input>
and it will be shown if
there is a .is-invalid
class on adjacent input.
https://bootstrap-vue.org/docs/components/form#feedback-helpers
For checkbox use state='false'
to add .d-block
class so it is visible.
<b-form-invalid-feedback :state='validateState("accept")'>You need to accept in
order to proceed
<b-form @submit.prevent="onSubmit">
<b-form-input
v-model.trim='$v.email.$model'
:state='validateState("email")'
>
</b-form-input>
<b-form-invalid-feedback id='email-feedback'>Field is required</b-form-invalid-feedback>
</b-form>
but you can use vuelidate without bootstrap-vue, just check for
$v.password.minLength
or other validations.
https://vuelidate.js.org/#sub-basic-form
<div class="error" v-if="!$v.email.required">Field is required</div>
You can create addition api to prevent server errors using async validation https://vuelidate.js.org/#sub-asynchronous-validation For error messages you can use https://github.com/dobromir-hristov/vuelidate-error-extractor Also server errors https://github.com/vuelidate/vuelidate/issues/124 https://github.com/vuelidate/vuelidate/issues/277 can be inserted like in https://jsfiddle.net/doginthehat/LoLbfsoe/ and for both client and server side error this is my example https://jsfiddle.net/duleorlovic/0tdpu32a/16/
Errors
[Vue warn]: You may have an infinite update loop in a component render function.
This occurs when you iterate v-for
over some array which is updated during
render (it could be that you use v-bind : instead v-on @ <a href='#'
:click='toggleActiveService(service)' v-for='service in services'
)
Gridsome
Install with yarn global add @gridsome/cli
and create project
gridsome create my-gridsome-site
cd my-gridsome-site
gridsome develop
Vuepress
For documentation
Nuxt.js
Similar to gatsby for React.js
Quasar
UI framework
Integrating with rails
Wrap whole layout in vue so it can load and parse json
import TurbolinksAdapter from 'vue-turbolinks'
import Vue from 'vue/dist/vue.esm'
import AppComponent from '../app-component.vue'
Vue.use(TurbolinksAdapter)
Vue.component('app-component', AppComponent)
document.addEventListener('turbolinks:load', () => {
const app = new Vue({
el: '[data-behavior="vue"]',
})
})
Pass json using props
<app-component :messsage=='<%= data.to_json %>'></app-component>
You can also overwrite template but not recomended since code is not in the same place as the markup.
<app-component inline-template>
<h1></h1>
</app-component>
https://nebulab.it/blog/build-rails-application-vuejs-using-jsx/ Rails engine to write javascript and vue code in ruby https://github.com/basemate/matestack-ui-core
Tutorials
video series https://gorails.com/series/using-vuejs-with-rails
todo: https://nebulab.it/blog/build-rails-application-vuejs-using-jsx/ https://medium.com/@yuliaoletskaya/rails-api-jwt-auth-vuejs-spa-part-2-roles-601e4372a7e7 https://technology.doximity.com/articles/token-authentication-with-rails-vue-graphql-and-devise
http://quasar-framework.org/ https://www.reddit.com/r/javascript/comments/6v1ki7/weex_vs_framework7_vs_quasar/ https://blog.logrocket.com/an-imperative-guide-to-forms-in-vue-js-7536bfa374e0 https://vuejsdevelopers.com/2017/10/23/vue-js-tree-menu-recursive-components/ https://www.classandobjects.com/tutorial/using_vue_js_with_rails https://www.thepolyglotdeveloper.com/2017/11/pass-data-between-routes-vuejs-web-application/ https://github.com/calirojas506/vue-inspector https://engineering.doximity.com/articles/five-traps-to-avoid-while-unit-testing-vue-js https://wyeworks.com/blog/2018/1/16/Testing-Vuejs-in-Rails-with-Webpacker-and-Jest https://alligator.io/vuejs/vue-parceljs/ http://epic-spinners.epicmax.co/#/ https://blog.codeship.com/vuejs-components-with-coffeescript-for-rails/ https://github.com/epicmaxco/vuestic-admin