- Notifications
You must be signed in to change notification settings - Fork332
Vue.js Component Style Guide
pablohpsilva/vuejs-component-style-guide
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
This guide provides a uniform way to structure yourVue.js code. Making it:
- easier for developers/team members to understand and find things.
- easier for IDEs to interpret the code and provide assistance.
- easier to (re)use build tools you already use.
- easier to cache and serve bundles of code separately.
This guide is inspired by theRiotJS Style Guide byDe Voorhoede.
- Module based development
- vue component names
- Keep component expressions simple
- Keep component props primitive
- Harness your component props
- Assign
this
tocomponent
- Component structure
- Component event names
- Avoid
this.$parent
- Use
this.$refs
with caution - Use component name as style scope
- Document your component API
- Add a component demo
- Lint your component files
- Create components when needed
Always construct your app out of small modules which do one thing and do it well.
A module is a small self-contained part of an application. The Vue.js library is specifically designed to help you createview-logic modules.
Small modules are easier to learn, understand, maintain, reuse and debug. Both by you and other developers.
Each Vue component (like any module) must beFIRST:Focused (single responsibility),Independent,Reusable,Small andTestable.
If your component does too much or gets too big, split it up into smaller components which each do just one thing. As a rule of thumb, try to keep each component file less than 100 lines of code.Also ensure your Vue component works in isolation. For instance by adding a stand-alone demo.
Each component name must be:
- Meaningful: not over specific, not overly abstract.
- Short: 2 or 3 words.
- Pronounceable: we want to be able to talk about them.
Vue component names must also be:
- Custom element spec compliant:include a hyphen, don't use reserved names.
app-
namespaced: if very generic and otherwise 1 word, so that it can easily be reused in other projects.
- The name is used to communicate about the component. So it must be short, meaningful and pronounceable.
<!-- recommended --><app-header></app-header><user-list></user-list><range-slider></range-slider><!-- avoid --><btn-group></btn-group><!-- short, but unpronounceable. use `button-group` instead --><ui-slider></ui-slider><!-- all components are ui elements, so is meaningless --><slider></slider><!-- not custom element spec compliant -->
Vue.js's inline expressions are 100% Javascript. This makes them extremely powerful, but potentially also very complex. Therefore you shouldkeep expressions simple.
- Complex inline expressions are hard to read.
- Inline expressions can't be reused elsewhere. This can lead to code duplication and code rot.
- IDEs typically don't have support for expression syntax, so your IDE can't autocomplete or validate.
If it gets too complex or hard to readmove it to methods or computed properties!
<!-- recommended --><template><h1> {{ `${year}-${month}` }}</h1></template><scripttype="text/javascript">exportdefault{computed:{month(){returnthis.twoDigits((newDate()).getUTCMonth()+1);},year(){return(newDate()).getUTCFullYear();}},methods:{twoDigits(num){return('0'+num).slice(-2);}},};</script><!-- avoid --><template><h1> {{ `${(new Date()).getUTCFullYear()}-${('0' + ((new Date()).getUTCMonth()+1)).slice(-2)}` }}</h1></template>
While Vue.js supports passing complex JavaScript objects via these attributes, you should try tokeep the component props as primitive as possible. Try to only useJavaScript primitives (strings, numbers, booleans) and functions. Avoid complex objects.
- By using an attribute for each prop separately the component has a clear and expressive API;
- By using only primitives and functions as props values our component APIs are similar to the APIs of native HTML(5) elements;
- By using an attribute for each prop, other developers can easily understand what is passed to the component instance;
- When passing complex objects it's not apparent which properties and methods of the objects are actually being used by the custom components. This makes it hard to refactor code and can lead to code rot.
Use a component attribute per props, with a primitive or function as value:
<!-- recommended --><range-slider:values="[10, 20]":min="0":max="100":step="5"@on-slide="updateInputs"@on-end="updateResults"></range-slider><!-- avoid --><range-slider:config="complexConfigObject"></range-slider>
In Vue.js your component props are your API. A robust and predictable API makes your components easy to use by other developers.
Component props are passed via custom HTML attributes. The values of these attributes can be Vue.js plain strings (:attr="value"
orv-bind:attr="value"
) or missing entirely. You shouldharness your component props to allow for these different cases.
Harnessing your component props ensures your component will always function (defensive programming). Even when other developers later use your components in ways you haven't thought of yet.
- Use defaults for props values.
- Use
type
option tovalidate values to an expected type.[1*] - Check if props exists before using it.
<template><inputtype="range"v-model="value":max="max":min="min"></template><scripttype="text/javascript">exportdefault{props:{max:{type:Number,// [1*] This will validate the 'max' prop to be a Number.default(){return10;},},min:{type:Number,default(){return0;},},value:{type:Number,default(){return4;},},},};</script>
Within the context of a Vue.js component element,this
is bound to the component instance.Therefore when you need to reference it in a different context, ensurethis
is available ascomponent
.
In other words: DoNOT code things likevar self = this;
anymore if you're usingES6. You're safe using Vue components.
- Using ES6, there's no need to save
this
to a variable; - In general, when using arrow functions the lexical scope is kept
- If you'reNOT using ES6 and, therefore, not using
Arrow Functions
, you'd have to addthis
to a variable. That's the only exception.
<scripttype="text/javascript">exportdefault{methods:{hello(){return'hello';},printHello(){console.log(this.hello());},},};</script><!-- avoid --><scripttype="text/javascript">exportdefault{methods:{hello(){return'hello';},printHello(){constself=this;// unnecessaryconsole.log(self.hello());},},};</script>
Make it easy to reason and follow a sequence of thoughts. See the How.
- Having the component export a clear and grouped object, makes the code easy to read and easier for developers to have a code standard.
- Alphabetizing the properties, data, computed, watches, and methods makes them easy to find.
- Again, grouping makes the component easier to read (name; extends; props, data and computed; components; watch and methods; lifecycle methods, etc.);
- Use the
name
attribute. Usingvue devtools and that attribute will make your development/testing easier; - Use a CSS naming Methodology, likeBEM, orrscss -details?;
- Use the template-script-style .vue file organization, as recomended by Evan You, Vue.js creator.
Component structure:
<templatelang="html"><divclass="RangeSlider__Wrapper"><!-- ... --></div></template><scripttype="text/javascript">exportdefault{// Do not forget this little guyname:'RangeSlider',// share common functionality with component mixinsmixins:[],// compose new componentsextends:{},// component properties/variablesprops:{bar:{},// Alphabetizedfoo:{},fooBar:{},},// variablesdata(){},computed:{},// when component uses other componentscomponents:{},// methodswatch:{},methods:{},// component Lifecycle hooksbeforeCreate(){},mounted(){},};</script><stylescoped> .RangeSlider__Wrapper {/* ... */ }</style>
Vue.js provides all Vue handler functions and expressions are strictly bound to the ViewModel. Each component events should follow a good naming style that will avoid issues during the development. See theWhy below.
- Developers are free to use native likes event names and it can cause confusion down the line;
- The freedom of naming events can lead to aDOM templates incompatibility;
- Event names should be kebab-cased;
- A unique event name should be fired for unique actions in your component that will be of interest to the outside world, like: upload-success, upload-error or even dropzone-upload-success, dropzone-upload-error (if you see the need for having a scoped prefix);
- Events should either end in verbs in the infinitive form (e.g. client-api-load) or nouns (e.g drive-upload-success) (source);
Vue.js supports nested components which have access to their parent context. Accessing context outside your vue component violates theFIRST rule ofcomponent based development. Therefore you shouldavoid usingthis.$parent
.
- A vue component, like any component, must work in isolation. If a component needs to access its parent, this rule is broken.
- If a component needs access to its parent, it can no longer be reused in a different context.
- Pass values from the parent to the child component using attribute/properties.
- Pass methods defined on the parent component to the child component using callbacks in attribute expressions.
- Emit events from child components and catch it on parent component.
Vue.js supports components to have access to other components and basic HTML elements context viaref
attribute. That attribute will provide an accessible way throughthis.$refs
to a component or DOM element context. In most cases, the need to accessother components context viathis.$refs
could be avoided. This is why you should be careful when using it to avoid wrong component APIs.
- A vue component, like any component,must work in isolation. If a component does not support all the access needed, it was badly designed/implemented.
- Properties and events should be sufficient to most of your components.
- Create a good component API.
- Always focus on the component purpose out of the box.
- Never write specific code. If you need to write specific code inside a generic component, it means its API isn't generic enough or maybe you need a new component to manage other cases.
- Check all the props to see if something is missing. If it is, create an issue or enhance the component yourself.
- Check all the events. In most cases developers forget that Child-Parent communication (events) is needed, that's why they only remember the Parent-Child communication (using props).
- Props down, events up! Upgrade your component when requested with a good API and isolation as goals.
- Using
this.$refs
on components should be used when props and events are exhausted and having it makes sense (see the example below). - Using
this.$refs
to access DOM elements (instead of doingjQuery
,document.getElement*
,document.queryElement
) is just fine, when the element can't be manipulated with data bindings or for a directive.
<!-- good, no need for ref --><range:max="max":min="min"@current-value="currentValue":step="1"></range>
<!-- good example of when to use this.$refs --><modalref="basicModal"><h4>Basic Modal</h4><buttonclass="primary"@click="$refs.basicModal.hide()">Close</button></modal><button@click="$refs.basicModal.open()">Open modal</button><!-- Modal component --><template><divv-show="active"><!-- ... --></div></template><script>exportdefault{// ...data(){return{active:false,};},methods:{open(){this.active=true;},hide(){this.active=false;},},// ...};</script>
<!-- avoid accessing something that could be emitted --><template><range:max="max":min="min"ref="range":step="1"></range></template><script>exportdefault{// ...methods:{getRangeCurrentValue(){returnthis.$refs.range.currentValue;},},// ...};</script>
Vue.js component elements are custom elements which can very well be used as style scope root.Alternatively the component name can be used as CSS class namespace.
- Scoping styles to a component element improves predictability as its prevents styles leaking outside the component element.
- Using the same name for the module directory, the Vue.js component and the style root makes it easy for developers to understand they belong together.
Use the component name as a namespace prefix based on BEM and OOCSSand use thescoped
attribute on your style class.The use ofscoped
will tell your Vue compiler to add a signature on every class that your<style>
have. That signature will force your browser (if it supports) to apply your componentsCSS on all tags that compose your component, leading to a no leaking css styling.
<stylescoped>/* recommended */ .MyExample { } .MyExampleli { } .MyExample__item { }/* avoid */ .My-Example { }/* not scoped to component or module name, not BEM compliant */</style>
A Vue.js component instance is created by using the component element inside your application. The instance is configured through its custom attributes. For the component to be used by other developers, these custom attributes - the component's API - should be documented in aREADME.md
file.
- Documentation provides developers with a high level overview to a component, without the need to go through all its code. This makes a component more accessible and easier to use.
- A component's API is the set of custom attributes through which its configured. Therefore these are especially of interest to other developers which only want to consume (and not develop) the component.
- Documentation formalises the API and tells developers which functionality to keep backwards compatible when modifying the component's code.
README.md
is the de facto standard filename for documentation to be read first. Code repository hosting services (Github, Bitbucket, Gitlab etc) display the contents of the the README's, directly when browsing through source directories. This applies to our module directories as well.
Add aREADME.md
file to the component's module directory:
range-slider/├── range-slider.vue├── range-slider.less└── README.md
Within the README file, describe the functionality and the usage of the module. For a vue component its most useful to describe the custom attributes it supports as those are its API:
The range slider lets the user to set a numeric range by dragging a handle on a slider rail for both the start and end value.
This module uses thenoUiSlider for cross browser and touch support.
<range-slider>
supports the following custom component attributes:
attribute | type | description |
---|---|---|
min | Number | number where range starts (lower limit). |
max | Number | Number where range ends (upper limit). |
values | Number[]optional | Array containing start and end value. E.g.values="[10, 20]" . Defaults to[opts.min, opts.max] . |
step | Numberoptional | Number to increment / decrement values by. Defaults to 1. |
on-slide | Functionoptional | Function called with(values, HANDLE) while a user drags the start (HANDLE == 0 ) or end (HANDLE == 1 ) handle. E.g.on-slide={ updateInputs } , withcomponent.updateInputs = (values, HANDLE) => { const value = values[HANDLE]; } . |
on-end | Functionoptional | Function called with(values, HANDLE) when user stops dragging a handle. |
For customising the slider appearance see theStyling section in the noUiSlider docs.
Add aindex.html
file with demos of the component with different configurations, showing how the component can be used.
- A component demo proves the component works in isolation.
- A component demo gives developers a preview before having to dig into the documentation or code.
- Demos can illustrate all the possible configurations and variations a component can be used in.
Linters improve code consistency and help trace syntax errors. .vue files can be linted adding theeslint-plugin-html
in your project. If you choose, you can start a project with ESLint enabled by default usingvue-cli
;
- Linting component files ensures all developers use the same code style.
- Linting component files helps you trace syntax errors before it's too late.
To allow linters to extract the scripts from your*.vue
files, put script inside a<script>
component andkeep component expressions simple (as linters don't understand those). Configure your linter to allow global variablesvue
and componentprops
.
ESLint requires an extraESLint HTML plugin to extract the script from the component files.
Configure ESLint in a.eslintrc
file (so IDEs can interpret it as well):
{"extends":"eslint:recommended","plugins": ["html"],"env": {"browser":true },"globals": {"opts":true,"vue":true }}
Run ESLint
eslint src/**/*.vue
JSHint can parse HTML (using--extra-ext
) and extract script (using--extract=auto
).
Configure JSHint in.jshintrc
file (so IDEs can interpret it as well):
{"browser":true,"predef": ["opts","vue"]}
Run JSHint
jshint --config modules/.jshintrc --extra-ext=html --extract=auto modules/
Note: JSHint does not acceptvue
as extension, but onlyhtml
.
Vue.js is a component framework based. Not knowing when to create components can lead to issues like:
- If the component is too big, it probably will be hard to (re)use and maintain;
- If the component is too small, your project gets flooded, harder to make components communicate;
- Always remember to build your components for your project needs, but you should also try to think of them being able to work out of it. If they can work out of your project, such as a library, it makes them a lot more robust and consistent;
- It's always better to build your components as early as possible since it allows you to build your communications (props & events) on existing and stable components.
- First, try to build obvious components as early as possible such as: modal, popover, toolbar, menu, header, etc. Overall, components you know for sure you will need later on. On your current page or globally;
- Secondly, on each new development, for a whole page or a portion of it, try to think before rushing in. If you know some parts of it should be a component, build it;
- Lastly, if you're not sure, then don't! Avoid polluting your project with "possibly useful later" components, they might just stand there forever, empty of smartness. Note it's better to break it down as soon as you realize it should have been, to avoid the complexity of compatibility with the rest of the project;
Mixins encapsulate reusable code and avoid duplication. If two components share the same functionality, a mixin can be used. With mixins, you can focus on the individual component task and abstract common code. This helps to better maintain your application.
Let's say you have a mobile and desktop menu component whose share some functionality. We can abstract the corefunctionalities of both into a mixin like this.
constMenuMixin={data(){return{language:'EN'}},methods:{changeLanguage(){if(this.language==='DE')this.$set(this,'language','EN')if(this.language==='EN')this.$set(this,'language','DE')}}}exportdefaultMenuMixin
To use the mixin, simply import it into both components (I only show the mobile component).
<template><ulclass="mobile"><li@click="changeLanguage">Change language</li></ul></template><script>importMenuMixinfrom'./MenuMixin'exportdefault{mixins:[MenuMixin]}</script>
Fork it and Pull Request what you think it should be good to have or just create anIssue.
- Brazilian Portuguese: Pablo Henrique Silvagithub:pablohpsilva,twitter: @PabloHPSilva
- Russian: Mikhail Kuznetcovgithub:shershen08,twitter: @legkoletat
About
Vue.js Component Style Guide
Topics
Resources
Uh oh!
There was an error while loading.Please reload this page.