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?

  1. Be productive almost immediately
  2. Simple (in practice!) component model
  3. Fast, easy-to-optimize rendering
  4. Must scale well to large apps
  5. Top-class dev workflow
  6. 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-resource

github.com/vuejs/vue-router

github.com/vuejs/vuex

github.com/vuejs/vue-cli

github.com/vuejs/vue-devtools

github.com/vuejs/vue-validator

github.com/vuejs/vue-touch

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

Give it a day

(seriously!)

 

vuejs.org/guide

 

Vue.js

By Chris Fritz

Vue.js

  • 4,132