🚨 WARNING: I haven't spent that much time in Vue, and this page has gotten a little dated. A lot of this info will not necessarily be best practice, especially for v3.
Resources
- 7 Ways to Define a Component Template in Vue.js
- Good for getting into some of the nitty-gritty of templating
- "React for Vue Developers" by Sebastian De Deyne
- Guide works the other way around too; helpful if you are used to React and are new to Vue
Quick Start
- Install the CLI (which will also install core)
npm install -g @vue/cli
- Options for quick start:
- Scaffold project:
vue create {{project name}}
orvue create .
(same folder)
- Preview a single
.vue
filevue serve {{filename}}
- Scaffold project:
Vue CLI - UI
You can launch the cli-ui by running vue ui
, if CLI version > 3, and vue is installed globally
Gotchas of note (or at least ones I wish someone had told me about)
- Don't use arrow function syntax for anything that needs to access a Vue instance through
this
. See SO - Iterators (v-for) require unique keys (same as React)
- Error: > Custom elements in iteration require 'v-bind:key' directives.
- Just do something like
v-for="foo in arr" v-bind:key="foo.bar"
- Trying to bind a string value to
style
orclass
attributes; it must be an array or object (link) - Trying to bind non-static asset paths as strings
- For webpack to work with non-static assets, you can pass paths in one of two ways.
- Non-binded (normal attribute):
<img src="./assets/party-parrot.gif" />
- Or, if you are binding, you need to generate the string of the asset path with
require()
, you cannot simply return the string<template> <img :src="imagePath" /> </template> <script> module.exports = { data: function() { // Needs to be the result from require(), not plain string!!! const partyParrotSrc = require('./assets/party-parrot.gif'); return { imagePath: partyParrotSrc }; } } </script>
- Non-binded (normal attribute):
- Note that this doesn't apply to static assets. Details
- For webpack to work with non-static assets, you can pass paths in one of two ways.
- Don't use
_
prefix for any Vue component variables, props, etc. - Don't forget the name! (for global registration)
- Either as a key/pair on
Vue.extend({name:'my-component'})
object, or first argument toVue.component('my-component',{})
- Either as a key/pair on
How do I...
- Use expressions (+, ternary, etc) within attribute binding?
- Like this:
<div v-bind:key="item.text + item.link"></div>
- And, of course, very easy to use within mustache outside of attributes:
<div>Key: {{item.text + item.link}}</div>
- WARNING: both the
class
andstyle
attributes are exceptions to this; you need to pass in objects instead of strings, and forstyle
, you need to use camelCase
- Like this:
- Use props
- Use placeholder elements for use with conditionals
- Although
<template>
tags aren't strictly the same thing as ReactFragments
, they are close in that they don't echo out as actual HTML and can be used as placeholders to attach conditional logic on.
- Although
- Subscribe / piggyback / add callback to
v-model
binding?- Don't overthink it! Using a bi-directional
v-model
binding does not preclude you from attaching other listeners. So, for a standard form input binding where you want the change event, you could so something like this:<select v-model="myOption" v-on:change="handleMyOptionChange"> </select>
- Don't overthink it! Using a bi-directional
- Apply animations / transitions to elements as they are conditionally rendered
- Alligator.io has one of the best guides on this
- Official docs
- Tip: I highly recommend using named CSS animations, with keyframes, if you have anything mildly complicated in terms of effects.
Loading script files
You might be tempted to put a <script></script>
tag directly in your Vue component - don't do it! It will either throw errors, hang the build process, or cause unexpected behavior.
If this is a local script, the best route is always to use built in imports / webpack.
However, if the script is an external script (hosted URL), here are some alternatives:
-
Inject script into page on component mount
export default { mounted(){ let scriptElem = document.createElement('script'); scriptElem.src = 'https://example.com/script.js'; document.body.appendChild(scriptElem); } }
- Note that this will keep reinjecting the script every time the component is mounted, so you might want to use something like this that checks if the script was already injected:
data: { scriptSrc: 'https://example.com/script.js' } mounted: function() { if (!document.querySelector('script[src="' + this.scriptSrc + '"]')){ const scriptElem = document.createElement('script'); scriptElem.src = this.scriptSrc; document.body.appendChild(scriptElem); } }
-
Add it to the main
index.html
that loads Vue
Binding
You can bind attributes of an element to a piece of data/state, by using v-bind
. This is also called a directive.
For example, to bind an image:
<img v-bind:src="myImageSrc">
<script>
new Vue({
el: '#app',
data: {
myImageSrc: 'images/banner.jpg'
}
});
</script>
Vue.extend()
?
In using Vue components
(especially single-file-components), you might see two different styles of exporting the component so that it can be reused. In the docs, you usually see:
export default {
name: 'myComponent',
props: {
msg: String
},
data: {
myArr: []
}
}
However, there is another way:
import Vue from 'vue';
export default Vue.extend({
name: 'myComponent',
props: {
msg: String
},
data: ()=> ({
myArr: []
})
})
Note: notice that
data
must be a function that returns the data when usingextend
The main difference is that Vue.extend()
is an API call that explicitly wraps the export in a Vue constructor subclass - that is to say that rather than exporting a generic JS object, your SFC will be exporting a component constructor that is set to construct a component based on the input you fed into Vue.extend(input)
. For practical purposes, this has a couple important influences:
- For TypeScript, and intellisense, it means that
imports
in other files can pick up on the actual Vue component class structure and how your definition fits into it. See this for details. - You can instantiate new component instances simply by using the
new
keyword and the component constructor returned byVue.extend()
. SO.
Echoing out HTML without wrapper elements
If you need to output a single element, without wrappers, from a SFC or component instance, it can be difficult. Especially if you have your HTML as a string.
For example, you can use the v-html
binding to set the inner HTML of an element... but it can't be used on the root <template>
wrapper, which means no matter what, you will end up with needing a wrapper element around your HTML. Vue used to have triple brace directive, but that was deprecated in v2. All of this is summarized in this issue.
So what is left? We have to explore using Vue at a lower level and hook into the actual template and rendering features. There are a couple ways you can do this:
Use the render function directly
Vue exposes a special method on component instances called render
, from which you need to always return a root VNode
(Virtual DOM Node). You can generate a VNode by calling createElement
(for example, createElement('div')
), or by using the vue template compiler methods (more on that further down).
The basics of the render method are here.
Vue.component('my-component',{
data: {
myVal: 'Hello'
},
render: function
});
You can also combine this approach with the Vue templating engine. The Vue API has a special method, Vue.compile
, which takes a template string and returns a render
method that in turn returns VNode
, so you directly assign the result of Vue.compile
to render
, like so:
let vueRes = Vue.compile('<div>{{ myVal }}</div>');
Vue.component('my-component',{
data: {
myVal: 'Hello'
},
render: vueRes.render
});
Basic details on the API can be found here, and the advanced documentation is kind of shared with vue-template-compiler. Note that this adds some overhead to your project.
As an alternative to compile
, you can also pass template as a prop to createElement, which basically does the same thing (requires full version of Vue):
Vue.component('my-component',{
data: {
myVal: 'Hello'
},
render: function(createElement){
return createElement({
template: '<div>{{ myVal }}</div>'
});
}
});
Be careful about the differences between a
functional
(aka stateless) component's render function, and a regular render function. They have different signatures, and the value ofthis
changes within them. See below:
Render in functional
vs non-functional
In a functional component (denoted by prop functional: true
), render takes two arguments, with the second being context
. The value of this
is also different in a functional component. For example, in a regular render function, you could get the props with this.$props
, but in a functional one, you would want to use context.props
Use the Vue Compiler + the template
property
Instead of writing components using <template>
in SFCs, you can actually pass an HTML string by using the template
property on a Vue component instance:
Vue.component('my-component',{
data: {
myVal: 'Hello'
},
template: '<div>{{ myVal }}</div>'
});
Note that this doesn't actually really help avoiding wrapping if you are dynamically creating your template string from props, since it requires a const input. If you need a more flexible approach, use the
render()
based approach.
However, this requires have the full Vue compiler + runtime installed, as opposed to just the runtime. See this and this.
According to Vue, this adds an "extra 10kb payload for your app"
Technically, If you only need the compiler for a single file, you can pull it in by changing your import from:
import Vue from 'vue'
To:
import Vue from 'vue/dist/vue.esm'
// or
import Vue from 'vue/dist/vue.js'
// or
import vue from 'vue/dist/vue.common'
, but this is really not recommended, and you should just change your global config. If you are using the new CLI config, you can simply use runtimeCompiler: true
.
v-runtime-template
v-runtime-template is an interesting library that looks to reduce the complexity of these approaches. The site alligator.io has a good writeup on using it here.
Dynamically creating components from string, AJAX, etc.
Most of this overlaps with the section above, on "Echoing out HTML" - that is to say, your options are mainly one of:
- Using the
template
prop - Using the
render
function prop - assigning therender
function generated byVue.compile()
- Using the
render
function prop - directly
Note that the Vue docs actually has a dedicated section on Async components; the gist of their advice seems to be to pass a promise wrapper as the second argument to Vue.component
, that eventually resolves with the template
string. There is also a good S/O answer that illustrates this.
TypeScript stuff
Typescript in SFC (.vue
files)
This is something I'm currently having issues with - it compiles fine, but intellisense does not seem to be kicking in when editing TS inside as SFC using <script lange="ts"></script>
. Even when using the go-to extension Vetur.
One solution that worked for me was to remove the lang="ts"
, and then add it back, and suddenly intellisense started working again.
Or, you can always keep your TS as a separate file (this actually can help keep things clean with large components anyways):
views/Home.vue
:
<template>
</template>
<script lang="ts">
// @ts-ignore
import c from './home.ts';
export default c;
</script>
views/Home.ts
import Vue from 'vue';
export default Vue.extend({
// ...
});
Oddities in creating components with Vue.extend
- You might get strange type errors, that don't actually prevent transpiling success, if you fail to annotate the return types of your custom functions (in
methods
, orcomputed
, or evenVNode
) (SO) - data needs to be a function that returns an object, not the object itself
- However, don't use an arrow function (despite many docs recommending it) or else you lose the correct scope of
this
- However, don't use an arrow function (despite many docs recommending it) or else you lose the correct scope of
let myComponent = Vue.extend({
name: 'MyComponent',
data: function(){
return {
name: 'Joshua'
}
},
computed: {
lowerName: function(): string {
return this.name.toLowerCase()
}
},
methods: {
logName: function(): void {
console.log(this.name);
}
}
});
Make sure to include name
when using Vue.extend() API
If you omit the name field when using Vue.extend({})
, you are likely to run into issues with hot-reloading, and this specific error:
vue.runtime.esm.js?... [Vue warn]: Unknown custom element: ___ - did you register the component correctly? For recursive components, make sure to provide the "name" option.
Issues with props
, this
showing as any
, computed
, etc.
I noticed a strange issue with TypeScript and Vue.extend()
, where it would randomly stop working within a SFC (.vue) file as far as TSC and intellisense goes. Mostly it seemed on the intellisense side, where things would still compile, but was making editing almost unusable because it was breaking this
, which was showing as any
within methods, computed, etc. I'm sure this will get better with time and newer Vue versions, but the issue, at least for me, is completely resolved by just using TypeScript outside of SFC (single file components).
In many cases, literally all I had to do to fix my issues was copy all the TS, move to a separate TS file, paste it, and then import it within the SFC I wanted to use it in. For example, something like this:
MyComponent.vue
:
<template>
<!-- stuff -->
</template>
<script lang="ts">
import c from './MyComponent-Code.ts';
export default c;
</script>
And then in MyComponent-Code.ts
, follow the normal pattern of extending Vue:
import Vue from 'vue';
export default Vue.extend({
// ...
});
Complex prop types
Prop types can be a little confusing with TS & Vue, and there are multiple ways to declare them.
TypeScript Interfaces as Vue Prop Types
Let's say we have a custom interface we want to accept as a prop (really taking in an object):
interface Human {
name: String,
age: Number
}
To use this custom interface as a prop type, you now have two options.
Option A: cast as function that returns interface:
export default Vue.extend({
props: {
person: {
type: Object as () => Human
},
}
});
Option B: A newer syntax, explicit casting to PropType
// You need to make sure to import PropType from Vue
import Vue, {PropType} from 'vue'
export default Vue.extend({
props: {
person: {
type: Object as PropType<Human>
}
},
});
Thanks to this article for clearing these options up.
Unions in Vue Props
If you need to use a union type, you can put the types that make up the union within an array, and have that as the prop type:
Vue.extend({
props: {
unionProp: {
type: [String, Number]
}
}
});
You can even include interfaces as part of the union (example).
Prop Type Safety and Compile-Time Checking
The default way of declaring props on a component and then passing them on the parent enforces run-time type checking, but not static / compile-time type checking, even with TypeScript. This is a rather important distinction to make, since a CI/CD system might let you merge code that compiles fine, but immediately explodes when the page tries to load and leaves the user with a blank screen, missing components, and/or fatal errors.
⚠ Warning: By compile-time, I mean the compiling / transpiling of the TypeScript by TSC, not Vue's template compiler or any Vue internals.
Honestly, every time I look into this and ways around it, I end up unhappy with what I find. It looks like even with the newest Vue 3 Composition API and the improved TypeScript support it comes with, prop types are still checked at run-time, but not at compile-time.
Here are the exceptions / workaround:
- Use
jsx
ortsx
instead of.vue
SFC template files- I have had trouble finding documentation that confirms or denies this, but I have a feeling that part of why static type checking on props is basically not a thing is due to the complexity of Vue SFC templates; they are their own thing, separate from JS/TS.
- I also find type inference / intellisense support in SFCs to be dodgy already; even with Vetur
- Since
jsx
is essentially just JavaScript, it can be more easily understood (natively) by both the build system and the IDE - Here is an example of someone who got prop type safety working, with IDE support too, by using JSX
- The downside here is that SFCs are a huge selling point of Vue; JSX is closely coupled with the React project, and using JSX with Vue just feels... off
- I have had trouble finding documentation that confirms or denies this, but I have a feeling that part of why static type checking on props is basically not a thing is due to the complexity of Vue SFC templates; they are their own thing, separate from JS/TS.
- Pass prop TS type to consumer, manually enforce type before passing / binding
- In your component, you could
export
the type / interface that represents part of your props, and then in the parent, import it. Next, manually type a variable as that type, before passing it through in the<template>
section. - On S/O, I came across an example of how someone used this manual approach
- This also leads me to another complaint; it seems as though you cannot re-use TS interfaces as props directly; you can only re-use them as nested properties (see above section on TS interfaces as Vue props).
- In your component, you could
⚠ Warning: Again, I have had trouble finding documentation on why props are checked this way (part of my frustration!), but another reason might be intentional. Just as I am complaining about the lack of static type checking with props because it lets type errors sneak through to the runtime, the inverse is true as well - even if Vue perfectly supported static prop type checking, just because the code compiled without errors doesn't mean it won't later throw runtime exceptions for bad props.
After all, props are usually highly dynamic, and you might end up passing user input, API responses, and other things you can't always anticipate through props.
Using VSCode with Vue
If you are using the popular "Vetur" VSCode plugin, and want to change the tab spacing / indentation, you need to change the option in workspace settings JSON:
{
"vetur.format.options.tabSize": 4
}
Vue Router
Interrupting the normal flow
You can use "Navigation Guards" to modify routing programatically, as it happens.
For example, you could use beforeEach
to redirect user to login view
based on state.
const router = new VueRouter({...});
router.beforeEach((to, from, next) => {
// ...
let isLoggedIn = getIsLoggedIn();
if (!isLoggedIn && to.path!=='/login'){
// Force redirect to /login
next('/login');
}
else {
// Proceed as normal
next();
}
});
Be aware that it a little too easy to write a circular loop this way and end up with a maximum call stack size exceeded
error.
Preserving a component's state on route change
keep-alive
can be used to preserve a component as it is swapped out, but keep in mind that you need to specify the individual component to be preserved, not the route itself. And if using include
to specify a specific component, it needs to match the name.
<keep-alive include="subComponent">
<router-view></router-view>
</keep-alive>
Vuex
Actions vs Mutations?
TLDR:
- How are you trying to update state?
- Simple, synchronously
- Use mutations
- Complex and/or using async (api,db calls, etc)
- Use actions
- Simple, synchronously
From the official docs (emphasis mine):
Actions are similar to mutations, the differences being that:
- Instead of mutating the state, actions commit mutations.
- Actions can contain arbitrary asynchronous operations.
What does this mean in practical terms? Well, it really seems to come down to complexity and if you need to modify state using async.
If you just need to modify a single bit of state, and the entire chain of logic depends on simple synchronous actions, then you are better off using a mutation. Example:
Settings panel -> user unchecks "allow tracking" -> triggers Vuex mutation to set
state.allowTracking
tofalse
.
No part of the above chain required an async flow, such as waiting for a response from another service.
A more complex example that would require using actions
is:
Settings panel -> user unchecks "allow tracking" -> triggers Vuex action to...
- set
state.allowTracking
tofalse
- (Async) tell third-party tracking API to delete user's log
- If success, unsets state variable that points to ID of log
Vuetify
Important Reminder from docs:
In order for your application to work properly, you must wrap it in a
v-app
component. This component is required for ensuring proper cross-browser compatibility. It can exist anywhere inside the body, but must be the parent of ALL Vuetify components.
Resources
- Default icons - search - link
Icons
Version 1.5 used Material, Version 2.0+ uses Material Design Icons (prefixed with mdi
). MDI is actually not installed/served locally - it actually inserts a CSS link tag into your index.html
file, pointing to https://cdn.jsdelivr.net/npm/@mdi/.../materialdesignicons.min.css
. You can easily swap this out for any icon set, such as Material or FontAwesome.
Using the grid system
12 point grid system
Structure should follow:
- Pre 2.0:
v-container
->v-layout
->v-flex
- Post 2.0:
v-container
->v-row
->v-col
Example - a 2x2 grid:
<v-container grid-list-md>
<v-layout wrap row>
<v-flex xs6>
<div class="myContent"></div>
</v-flex>
<v-flex xs6>
<div class="myContent"></div>
</v-flex>
<v-flex xs6>
<div class="myContent"></div>
</v-flex>
<v-flex xs6>
<div class="myContent"></div>
</v-flex>
</v-layout>
</v-container>
Coming from Materialize
Some standard conversions
What | Materialize | Vuetify |
---|---|---|
Text Alignment | Link - .left-align - .right-align - .center-align |
Link - .text-left - .text-right - .text-center Or, specify screen size - .text-md-center (only center on medium viewport size) |
auto-center via margins | NA<button style="margin-left:auto; margin-right:auto;"></button> |
Shortcut class: mx-auto (docs) |
Hide on {size} | Link - .hide (all)- .hide-on-{size} |
Link - d-none (all)- hidden-{size}-{condition} |
Global Styling in Vue
Refer to this as the go-to guide.
Options:
- In your root
App.vue
SFC, add an "unscoped" style section - Create a static CSS file, and import it the regular way with
<link rel="stylesheet" href="...">
in yourindex.html
served file. - Use a dedicated loader to load non-standard CSS sources, such as SASS, which would fall under the guide for pre-processors.
- Non-CLI Setup
- Usually this is accomplished by adding a config for the loader in your webpack config file, under
rules
. Essentially the rule says that iftest
matches flie content (based on filename or<style>
block), thenuse
____ loader to pre-process. - SASS/SCSS example
- Usually this is accomplished by adding a config for the loader in your webpack config file, under
- CLI Setup
-
WARNING: The CLI setup uses some "magic" to hide a lot of the manual config that is required with webpack. Unless you eject, ignore the guides for webpack configs.
- If you have created your project with CLI 3, and both
sass-loader
andsass
is installed inpackage.json
, you should just be able to start using SASS right away:- by writing
<style lang="scss"></style>
in your templates. - Or by using
import ./myScssFile.scss
in your mainTS
orJS
file that loads Vue.
- by writing
- If you want to mess with specific config options, such as globally importing variables, or processing scss files, you will want to create/edit a file called
vue.config.js
in your project root.- using global variables
- Importing
scss
files. - link - For Sass-syntax conflicts with Scss, I have a partial guide here.
-
- Non-CLI Setup
Vue 3.0
Composition API
📘 --> Main Doc Entry Point