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 instead if flow. Can not use statement like a=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) than true value will show it. When value is false, null, undefined it hides that attribute for example <button v-bind:disabled='isButtonDisabled'>B</button>. Shorthand for v-bind: is :. To pass all properties from parent to child you can use v-bind='$attrs'. Inside data binding you can use any javascript expressions (calling methods message.split('') and calculations number + 1) but assignment is not working (since it is a statement, it can be used on v-on but not v-bind), control flow if (true) {} is not available and this.data is not available (this is only available in methods or computed) https://vuejs.org/v2/guide/syntax.html#Using-JavaScript-Expressions For class and style 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 change message).
    • text and textarea use value property and input event (use modifier v-model.lazy='msg' if you want to sync after change event)
    • checkbox and radiobutton use checked property and change event
    • select field use value property and change event. It ignores initial value attributes, and uses vue instance data. Modifiers: .number typecast to Number, .trim strip whitespace. ```

    ```

  • conditional v-if='seen' directive to render block. Also supports else that immediatelly follows v-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 uses display: none css property to hide (so v-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). Use v-else-if like switch case statement. Use key 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 different key.

    <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 mutate items array with push(), pop(), sort()… and replace with new array with filter(), concat() or slice().

  • <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 call preventDefault() on triggered event. Other event modifiers are: .stop, .prevent, .capture, .self, .once and .passive (do not prevent default behavior, usefull for v-on:scroll.passive='onScroll'). Value of v-on attribute accepts name of a method which is defined under methods: { myMethod: function (event) {} }. There is also key modifiers, so it triggers that callback only when specific key is used v-on:keyup.page-down='onPageDown' ($event.key == 'PageDown') It can also accept js expression counter += 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 this say('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 example
    mounted: 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 using this.$refs['my-ref'].focus() (similar as document.querySelect("[ref='my-ref']").focus() but ref is not showed in DOM, vue will use data-v-123asd attribute). ref does not work inside v-if since that element is not rendered (use v-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 define transition duration (since v-enter is removed it’s properties will transit slowly to normal state) or define animation to use (no need to use v-enter since you can define keyframes for whole animation).
  • v-enter-to added one frame after element is inserted (when v-enter is removed), removed when trans finishes. For leave action, you should use v-leave-active (or v-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' with v-for. avoid using v-if on the same element as v-for (if not aplicable to single item then move to parent, otherwise use computed property to filter the list instead of using v-if)
  • user defined private properties should start with $_ and include component name like methods: { $_myComponent_update: function() {} }. Or you can define function outside of main params
    function myPrivateFunction() {
    }
    export default {
      methods: {
        publicMethod: {
          myPrivateFunction()
        }
      }
    }
    
  • single file components should be PascalCase.vue (kebab-case is only used when referencing in DOM). Use Base or App 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 name TodoListItem.
  • 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 : for v-bind:, @ for v-on: and # for v-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 to watch: { $router(to, from) { } }). You can use { path: '/user/:id', component: User, props: true } and props: ['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