0%

In VueJS, data is reactive and mutatable – You do not need to call API to change the data like ReactJS does:

this.setState({count: 1})

Whereas in VueJS you just have to do it in a more natural way:

this.count = 1

But this convenience comes at a price: To do this magic thing, VueJS has to intercept your setter/getter in object but due to some limitation of Javascript core, it can not detect changes in the following:

  1. If you directly mutate array member, like array[1] = ‘text’
  2. If you do not declare the data at initialization phrase. That’s why data option should be a function returning object

To the first one, VueJS actually overrides some native array methods like push/pop/splice etc. Calling this methods will notify the watcher system so that UI will get update automatically. For the second one, however, it is possible that we are not able to know all the data available to our component at the time of initialization. To tackle this genuine scenario, VueJS offers set & delete.

These two APIs are both offered at global level and instance level. You can go to official documentation for the usage. It is simple

Vue.set(this, 'count', 1) //equivalent to this.count = 1
Vue.set(this.array, 1, 'text') //equivalent to this.array.splice(1, 1, 'text')

You can also check this Codepen for detail: https://codepen.io/frankdai/pen/VwppqVP Try to switch the JS code between the commented line and see the effect.

Above being said, in VueJS 3.0, its reactivity system moves from Object.defineProperty to Proxy as Javascript evolves with new bulit-in APIs. So set and delete are dropped in 3.0. It is recommend update from 2.x given your application has not much technical debt or you are building a brand new VueJS app.

render() vs. template

All VueJS developers are familiar with the template option. But sometimes render function comes more handy and flexible compared with template. However, not all the power build-in syntaxes offered by template are available in render function. So we will walk through some of the most common interpolate between the two ways of rendering UI in VueJS:

.sync modifier

Similar to React, VueJS props flow in top-to-bottom direction, or one-way data flow. You are not allowed to mutate props in child components directly. However VueJS does offer .sync modifier so that child component can emit event to let parent component to mutate the prop.

this.$emit('update:name', newName)

<parent>
<child :name.sync="myName"></child>
</parent>

From official document, we can easily know .sync modifier is just shorthand version of a combination of props (name) and event listener (@update:name). So we can adapt to our render function like this:


createElement('div', {
props: {
"name": this.myName
},
on: {
"update:name": ($event) => {
this.myName = $event;
}
}
})

v-model

Just like .sync modifier, v-model is another syntax sugar for to mimic the behavior of two-way data binding. Though most of the time developers use it for the input/select elements but it can also be applied to component level. It consists of two key things here: a prop named ‘value’ and an event named ‘input’. So basically we can translate v-model into options object as following:

return createElement(MyInput, { 
props: { value: this.message },
on: { input: (value) => { this.message = value } }
})

From this simple piece of code we can know that a component can also use v-model to pass a prop and allow its child to emit an input event with payload to mutate the prop named value.

.native modifier

If parent listens to a child component event, by default it expects the event should be emitted from child using $emit methods.

<child @click="onChildComponentClick" />

The onChildComponentClick callback will be triggered when child component emits an event named ‘click’.

However you might think of a click event as in browser sense. To achieve this, you can use @click.native to tell VueJS you want to register a native event listener on the root element of child component.

In official example, VueJS told us how to do this in render function as well:

 on: {
click: this.clickHandler
},
// For components only. Allows you to listen to
// native events, rather than events emitted from
// the component using `vm.$emit`.
nativeOn: {
click: this.nativeClickHandler
},

Scoped Slots

Although in template we only use but in component instance, we have two properties: $slots and $scopedSlots. So if we want to pass data to scoped slots in render function, we need to call $scopedSlots instead:

return createElement('div', [
this.$scopedSlots.default({
name: this.myName
})
])

this.$scopedSlots.default is a function which accepts a single argument and passes it to its slot as data.

v-bind

When we have to pass a lot of props to child component, it is very annoying to list all of them in the template, especially if there is an intermediate component whose sole purpose is to render some stateless UI and pass whatever the props from its parent to its children.

In template, we can easily achieve that by utilizing v-bind directives:

<child v-bind="options"></child>

With ES6 object spread syntax, we can adapt this to render function with a single line of code:

createElement('div', {
props: {
...this.options
}
})

Conclusion

In fact, all the markup in template will be parsed into render function for generating Virtual DOM. If you have any input, you are more than welcomed to post your comment.

Single File Component (SFC) in VueJS

A majority of VueJS developers should be using .vue file in their day-to-day development. Built by VueJS core team as community plugin, Vue Loader is the de factor build tools which works behind-the-scene and transform the Single File Component (SFC) into format recognized by browser. Its popularity derives mostly from two major offerings by this plugin:

Firstly, it provides more user-friendly version of writing component markup instead of built-in template option or render function, though latter can also takes up format of JSX like React.

Secondly, it also supports Scoped CSS to avoid the painstaking nature of global CSS namespace. So components will not affect each other with their CSS selector.

With these two distinctive features, you will be able to write logic (Javascript), markup (HTML) and style (CSS) in one single file. And today we will take a deep dive into Scoped SCSS:

How does it work?

CSS is of global namespace by nature. So in large scaled application we certainly want to avoid name conflicts when components are authored by different developers. So how does Vue Loader manage to keep CSS rules limited to the component itself?

In simple terms, when Vue Loader compiles each component, it will do a number of following things:

  1. When compiling <template> node, it will generate an unique id for each component and add to its HTML attribute like data-v-763db97b.

  2. When compiling <style> node, it will add this id to each CSS rule as attribute selector. So essentially if you have this simple CSS rule in .vue file

.red {
color:red
}

It will be transformed into

.red[data-v-763db97b] {
color:red
}

Vue Loader allows developer to write plain CSS or use preproceser like SCSS/LESS in the style node. After adding the attribute selector, these style nodes will be handled over to other loaders like style-loader and scss-loaders for next action. Developers can also configure post-css action in Webpack like minification, adding vendor prefix etc.

Let’s go to some of the details for Scoped CSS:

How to override a child component

Sometime we use a 3rd party component as child component and we want to override certain style. You write an rule in the parent component but it will not take effect in child, because the two components are of the different data-v-id. In this case, we will have to use /deep/ selector provided by Vue Loader:

/deep/ .child-component-class {
color: red
}

With the deep indicator, Vue Loader will transform it into different selector order like:

[data-v-763db97b] .child-component-class{
color:red
}

Compared with previous example, the order between class selector and attribute selector are swapped with an extra space between them. So browser will apply this to every child elements with this class name, including grandchildren component.

@import directives

If you are going to use SCSS/LESS in style node, Vue Loader will not be able to understand your code dependence. Each component will create its own context during pre-processing compilation. So if you have two components calling @import directives to include a common file for dependence so that you can use variable, mixin or call @extend, this file will be included into the final bundle for twice.

/*main.scss*/
.underline {
text-decoration: underline;
}
@import './common/main.scss'
.text {
@extend .underline;
}

In the final bundle, .underline class will appear twice in company with different attribute id selector. To avoid these duplication, we can move some of the common shared CSS code back to global namespace. It will be almost impossible to make entire app CSS scoped.

Root Element of Child Component

Vue Loader will add parent id into the root element of child

//parent component
<div class="root">
<div>Hello</div>
<child-component />
</div>
//child component
<div class="root">
<p>World</p>
</div>

If parent id is 123456 and child id is 654321, it will be complied to this:

<div class="root" data-v-123456> 
<div data-v-123456>Hello</div>
<div class="root" data-v-123456 data-v-654321>
<p data-v-654321>World</p>
</div>
</div>

So if you write an rule in parent component with .root selector, it will affect child component unintentionally.

Runtime Variable

This is not much related to SFC but we discuss this matter here as this is one of the most common requests. Imagine a service that allows its users to enter any hex value as color and change theme for personalization. How can we achieve that without overriding the entire CSS?

SCSS and LESS do let developers define variables but these are always build time. User’s color variable will be saved in server backend and has to be retrieved in runtime via Javascript code.

So we can use var function provided by browser. After calling API we will override global variable

getColor().then((color)=>{
var sheet = window.document.styleSheets[0];
sheet.insertRule(`:root { color: ${color}; }`, sheet.cssRules.length);
})

Conclusion

As above explanation, you will have a better comprehension of how Vue Loader Scoped CSS works and certain limitation with it. In the future we will talk about some alternatives scope CSS solutions like CSS-in-JS or CSS modules.

In React or VueJS, we often author components that are considered as layout components, which sets the generic layout UI like header/sidebar/footer etc so that they can be reused in different places. Each time a layout component is called, it does not care about what to be inserted into its children. In VueJS, we have Slots API and in React we have props.children to achieve that.

However, in certain scenarios we want to pass some data from parent into its children, even the parent does not know what kind of component will be of its children, given the fact that it is a generic layout component. How can we do so?

Let’s make a demo: The parent component will start counting how many seconds you have stayed in this web page (yep, that’s exactly what you saw in reactjs.org) and child component will consume this count and render it.

VueJS

We will resort what is referred as Scoped Slots


let TimeOnPage = {
template: `<div>
<slot v-bind:second="second" />
</div>`
data () {
return {second: 0}
},
mounted () {
setInterval(()=>this.second++, 1000)
}
}

let Child = {
template: `<time-on-page>
<template v-slot="slotProps">
{{slotProps.second}}
</template>
</time-on-page>`,
components: {TimeOnPage}
}

In the parent TimeOnPage we can use v-bind to bind an object to slot with structure like

slotProps = {
second: 0 //0 is reactive in child component this.second
}

so in child components can access this object. It is a common practice to name this object to slotProps in child v-slot directive but you can actually name it to whatever you would like.

React Hooks

With React Hooks we can easily share state between components by using customized hooks:

const useTimeOnPage = () => {
const [second, setSecond] = useState(0);
useEffect(() => {
setTimeout(() => {
setSecond(second + 1);
}, 1000);
}, [second]);
return second;
}
const Demo = () => {
const second = useTimeOnPage();
return <div>second</div>
}

However this is not exactly same as our topic, since we want to keep the data in the parent component and share in its immediate children. Here we can use a technique that is often referred as Render Props:

const Child = (props) => {
return <div>{props.second}s</div>;
};

const TimeOnPage = (props) => {
const [second, setSecond] = useState(0);
useEffect(() => {
setTimeout(() => {
setSecond(second + 1);
}, 1000);
}, [second]);
return <div>{props.render(second)}</div>;
};

<TimeOnPage render={(second) => <Child second={second} />

In this example, TimeOnPage will accept a function as props (The prop name can be called anything but usually we will name it as render) and pass its data as function arguments. The function will return a component which can access these arguments in this closure.

Old-fashion Declarative Rendering

In VueJS, most of time if we want to render a component based on user interaction or certain events, we will do it in a declarative way by using v-if directive:

<parent>
<button @click="showDialog = true">Click to show a dialog</button>
<dialog v-if="showDialog" />
</parent>

Now let’s make it a bit complex. We will call some API when user click this button. When such API failed, we will show the dialog component which has two buttons, cancel and retry, both of which we need to setup an event listener for user interaction. Then the code become:

<parent>
<button @click="onClick">Click to show a dialog</button>
<dialog v-if="showDialog" @confirm="doRetry" @cancel="doCancel" />
</parent>
<script>
export default {
methods: {
onClick ()=>{
doSomeAPICall().catch(()=>this.showDialog =true)
},
doRetry ()=>{doSomeAPICall()},
doCancel ()=>{}
}
}
</script>

Issues

Though above code works perfectly, it can be very cumbersome especially when dialog component is being used in a lot of places. So how can we improve this?

One solution actually lies in how Unit Testing tools work like Mount API in Vue Test Utils. So we can actually do this

doSomeAPICall().then(()=>
mount(successDialog)
).catch(()=>
mount(errorDialog)
)

Vue.extend and the Constructor

Here is how to do it. We can import the Vue Component and use it in this way:

import Dialog from './Dialog.vue'
let DialogConstructor = Vue.extend(Dialog)

doSomeAPICall().catch(()=>{
let dialog = new DialogConstructor
dialog.$mount()
document.body.appendChild(dialog.$el) //you can append it anyway you like
})

Apparently there are certain advantages over this approach:

  1. No more looking back to template for logic reference. Everything reside in the API promise callback.

  2. The entire rendering process can be further encapsulated into function for code reuse, e.g you can wrap the cancel/retry into promise call with its own then/catch.

  3. You can mount the dialog into everywhere you want instead of parent component. (Though you can also do it inside the component via el option)

A bit more digging into this constructor:

How to pass props to the component:

let dialog = new DialogConstructor({
propsData: {
propA: 'foo',
propB: 'bar'
}
})

How to listen to event from dialog component:

dialog.$on('confirm', ()=>{ doSomething() })

You can even manipulate child component data directly, though it is not recommended since it can make code hard to read and debug:

dialog.dataA = 'foo'

Conclusion

Vue.extend API can return an constructor so that rendering a component entirely in JS code can be possible. It can be used in frequently used component for better code reuse.