A view on componentized views with Vue
(last pun, I promise!)
Who am I?
-
Web UI/visualization developer and consultant
-
Member of the Vue.js team
-
Educator passionate about learning, but even more passionate about productivity
What are components?
HTML
- todo-list.html
- _todo.html
- _button.html
CSS
- todo-list.css
JS
- todo-list.js
TodoList
HTML
CSS
JS
Todo
HTML
CSS
JS
Button
HTML
CSS
JS
Components are the future
...but which component system?
"My biggest frustration is the constant migration from tool to tool or framework to framework. Never any time to master one before something better comes along."
The elephant in the room...
What would it take?
- Be productive almost immediately
- Simple (in practice!) component model
- Fast, easy-to-optimize rendering
- Must scale well to large apps
- Top-class dev workflow
- Stable and rich ecosystem
Coming from React, this is what I needed
Getting started
- Documentation
- Learning curve
- Support
Documentation
Learning curve
<!DOCTYPE html>
<html>
<head>
<title>Hello World</title>
<script src="https://cdn.jsdelivr.net/vue/latest/vue.js"></script>
</head>
<body>
<div>{{ greeting }} World</div>
<script>
new Vue({
el: 'body',
data: {
greeting: 'Hello'
}
})
</script>
</body>
</html>
Hello world!
React
Learning curve
<!DOCTYPE html>
<html>
<head>
<title>Hello World</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.0.2/react.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.0.2/react-dom.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.23/browser.js"></script>
</head>
<body>
<div id="app"></div>
<script type="text/babel">
var App = React.createClass({
getInitialState: function () {
return {
greeting: 'Hello'
}
},
render: function () {
return <div>{ this.state.greeting } World</div>
}
})
ReactDOM.render(
<App/>,
document.getElementById('app')
)
</script>
</body>
</html>
and with React...
Learning curve
<!DOCTYPE html>
<html>
<head>
<title>Todos</title>
<script src="https://cdn.jsdelivr.net/vue/latest/vue.js"></script>
</head>
<body>
<input v-model="newTodoText" @keyup.enter="addTodo">
<ul>
<li v-for="(index, todo) in todos">
{{ todo.text }}
<button @click="removeTodo(index)">X</button>
</li>
</ul>
<script>
new Vue({
el: 'body',
data: {
newTodoText: '',
todos: []
},
methods: {
addTodo: function () {
if (this.newTodoText) {
this.todos.push({ text: this.newTodoText })
this.newTodoText = ''
}
},
removeTodo: function (index) {
this.todos.splice(index, 1)
}
}
})
</script>
</body>
</html>
Simple todo app
React
Learning curve
<!DOCTYPE html>
<html>
<head>
<title>Todos</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.0.2/react.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.0.2/react-dom.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.23/browser.js"></script>
</head>
<body>
<div id="app"></div>
<script type="text/babel">
var App = React.createClass({
getInitialState: function () {
return {
newTodoText: '',
todos: []
}
},
render: function () {
return (
<div>
<input
value={this.state.newTodoText}
onChange={this.updateNewTodoText}
onKeyUp={this.addTodo}
/>
<ul>
{
this.state.todos.map(function (todo, index) {
return (
<li>
{ todo.text }
{' '}
<button
onClick={
function () {
this.removeTodo(index)
}.bind(this)
}
>X</button>
</li>
)
}.bind(this))
}
</ul>
</div>
)
},
updateNewTodoText: function (event) {
this.setState({
newTodoText: event.target.value
})
},
addTodo: function (event) {
if (event.which === 13 && this.state.newTodoText) {
this.setState({
todos: this.state.todos.concat([{
text: this.state.newTodoText
}]),
newTodoText: ''
})
}
},
removeTodo: function (index) {
this.setState({
todos: this.state.todos.filter(function (_, todoIndex) {
return todoIndex !== index
})
})
}
})
ReactDOM.render(
<App/>,
document.getElementById('app')
)
</script>
</body>
</html>
and with React...
Learning curve
<!DOCTYPE html>
<html>
<head>
<title>Reusable Components</title>
<script src="https://cdn.jsdelivr.net/vue/latest/vue.js"></script>
</head>
<body>
<p>
It will have been 2 minutes since I loaded this page in
<counter
:initial-value="120"
:increment-by="-1"
:stop-at="0"
></counter>
seconds.
</p>
<script>
Vue.component('counter', {
template: '<span>{{ number }}</span>',
props: {
initialValue: {
type: Number,
default: 0
},
incrementBy: {
type: Number,
default: 1
},
interval: {
type: Number,
default: 1000
},
stopAt: {
type: Number
}
},
data: function () {
return {
number: this.initialValue
}
},
created: function () {
this.counter = setInterval(function () {
this.number += this.incrementBy
if (this.stopAt === this.number) {
clearInterval(this.counter)
}
}.bind(this), this.interval)
},
beforeDestroy: function () {
clearInterval(this.counter)
}
})
new Vue({ el: 'body' })
</script>
</body>
</html>
Reusable components
React
Learning curve
<!DOCTYPE html>
<html>
<head>
<title>Reusable Components</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.0.2/react.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.0.2/react-dom.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.23/browser.js"></script>
</head>
<body>
<div id="app"></div>
<script type="text/babel">
var Counter = React.createClass({
propTypes: {
initialValue: React.PropTypes.number,
incrementBy: React.PropTypes.number,
interval: React.PropTypes.number,
stopAt: React.PropTypes.number
},
getDefaultProps: function () {
return {
initialValue: 0,
incrementBy: 1,
interval: 1000
}
},
getInitialState: function () {
return {
number: this.props.initialValue
}
},
componentWillMount: function () {
this.counter = setInterval(function () {
this.setState({
number: this.state.number + this.props.incrementBy
})
if (this.props.stopAt === this.state.number) {
clearInterval(this.counter)
}
}.bind(this), this.props.interval)
},
componentWillUnmount: function () {
clearInterval(this.counter)
},
render: function () {
return <span>{ this.state.number }</span>
}
})
var App = React.createClass({
render: function () {
return (
<p>
It will have been 2 minutes since I loaded this page in
{' '}
<Counter
initialValue={120}
incrementBy={-1}
stopAt={0}
/>
{' '}
seconds.
</p>
)
}
})
ReactDOM.render(
<App/>,
document.getElementById('app')
)
</script>
</body>
</html>
and with React...
Support
- GitHub
- Discourse forum
- Gitter chat
Components
Build on web standards, don't reinvent them
<template>
<button class="btn btn-{{ kind }}">
<slot></slot>
</button>
</template>
<script>
export default {
props: {
kind: {
validator (value) {
return ['primary', 'warning'].indexOf(value) !== -1
},
required: true
}
}
}
</script>
<style lang="scss" scoped>
$text-color: #fff;
$primary-bg-color: #0074d9;
$warning-bg-color: #d5a13c;
.btn {
color: $text-color;
&.btn-primary {
background: $primary-bg-color;
&:hover {
background: lighten($primary-bg-color, 20%);
}
}
&.btn-warning {
background: $warning-bg-color;
&:hover {
background: lighten($warning-bg-color, 20%);
}
}
}
</style>
React
Components
React... reinventing them
import { Component } from 'react'
import Radium from 'radium'
import color from 'color'
@Radium
class Button extends Component {
static propTypes = {
kind: React.PropTypes
.oneOf(['primary', 'warning'])
.isRequired
}
render () {
return (
<button
style={[
styles.base,
styles[this.props.kind]
]}>
{this.props.children}
</button>
)
}
}
const buttonDesign = {
textColor: '#fff',
types: {
primary: {
backgroundColor: '#0074d9'
},
warning: {
backgroundColor: '#d5a13c'
}
}
}
const styles = {
base: {
color: buttonDesign.textColor,
},
primary: {
background: buttonDesign.primary.backgroundColor,
':hover': {
background: color(buttonDesign.primary.backgroundColor).lighten(0.2).hexString()
}
},
warning: {
background: buttonDesign.warning.backgroundColor,
':hover': {
background: color(buttonDesign.warning.backgroundColor).lighten(0.2).hexString()
}
},
}
Rendering
<template>
<button @click="count += 1">Add to count</button>
<button @click="items.push(count)">Save count</button>
<div>Count: {{ count }}</div>
Saved counts:
<ul>
<li v-for="item in items">Saved: {{ item }}</li>
</ul>
</template>
<script>
export default {
data () {
return {
count: 0,
items: []
}
}
}
</script>
Speed and ease of optimization
this is already optimized - no special measures taken
React
Rendering
import { Component } from 'react'
import { Map, List } from 'immutable'
import { immutableRenderDecorator } from 'react-immutable-render-mixin'
@immutableRenderDecorator
export default class Counter extends Component
constructor (props) {
super(props)
this.state = {
data: Map({
count: 0,
items: List()
})
}
this.handleCountClick = ::this.handleCountClick
this.handleAddItemClick = ::this.handleAddItemClick
},
handleCountClick () {
this.setState(({data}) => ({
data: data.update('count', v => v + 1)
}))
},
handleAddItemClick () {
this.setState(({data}) => ({
data: data.update('items', list => list.push(data.get('count')))
}))
},
render () {
const { data } = this.state
return (
<div>
<button onClick={this.handleCountClick}>Add to count</button>
<button onClick={this.handleAddItemClick}>Save count</button>
<div>Count: {data.get('count')}</div>
Saved counts:
<ul>
{data.get('items').map(item =>
<li>Saved: {item}</li>
)}
</ul>
</div>
)
}
})
React with immutable data and
custom shouldComponentUpdate
Scale
Dev Workflow
In-browser devtools
Dev Workflow
With time travel!
Dev Workflow
Hot module reloading
Ecosystem
Is anyone actually using it in production?
Ecosystem
Official support for practically required libs (like for routing and state management)
github.com/vuejs/vue-router
github.com/vuejs/vuex
github.com/vuejs/vue-cli
github.com/vuejs/vue-validator
github.com/vuejs/vue-rx
...
Ajax
Routing
Flux State
CLI Generator
Devtools
Linting
Touch Events
Observables
...
Ecosystem
Is the future stable?
- Even slimmer API (~90% compatible with 1.x and even less to learn for newcomers)
- 2-4x speed improvement
- Server-side rendering
- Mobile compile target?
Vue 2.0
Vue.js
By Chris Fritz
Vue.js
- 4,132