Single Page Apps

with

Packaging, live reload from Webpack

Routing from monorouter

dispatching (with micro-architecture) from Flux

Components from React (options for Polymer, vanilla)

 

 

 

Much Thanks To

The Center For Open Science

Learn • Experience • Share

CVJS

Intermission

Can You Smell Words?

skunky
smoky
sour
spicy
spoiled
stagnant
stench
stinking
sulphur
sweaty
sweet
tart
tempting
vinegary
woody
yeasty

acidy
acrid
antiseptic
aromatic
balmy
biting
bitter
briny
burnt
citrusy
comforting
corky
damp
dank
distinctive

earthy
fishy
flowery
fragrant
fresh
fruity
gamy
gaseous
heavy
lemony
medicinal
metallic
mildewed
minty

moldy

musky
musty
odorless
peppery
perfumed
piney
pungent
putrid
reek
rose
rotten
savoury
scented
sharp
sickly

Scope

• Familiarize with VDOM

• Show one way binding

• Show a SPA workflow 

• Show component integration 

Requirements

  • Code reuse
  • Modern Browser App
  • Component Architecture
  • Messaging between components

Green Field Internet App

[Single Page Application]

We're Rich!?

 It probably pays to look at the most popular 3 or 4 in each class before deciding on your js architecture.  [large frameworks, functional, streaming, graphing, 3d, animation, reactive, jquery-like, eventing, packaging, dependency management, routing, history, dialects ...]

  • many frameworks
  • many many libraries

What we actually want

  • Clear starting point

  • A clear, but not enforced, standard way to do things

  • Explicitly clear separation of concerns, so we can mix and match and replace as needed

  • Easy dependency management

  • A way to use existing solutions so we don’t have to re-invent everything

  • A development workflow where we can switch from development mode to production with a simple boolean in a config.

http://blog.andyet.com/2014/08/13/opinionated-rundown-of-js-frameworks

from Aug 13, 2014 ・Henrik Joreteg

Aside

VDOM?

Internal Representation of all or part of a page

aTextbox = {
  tag: 'input',
  attrs: {type: 'text'},
  children: []
}

function toHTMLElement(vdomElement){
  attributes = _.pairs(vdomElement.attrs)
                .reduce(
                  function(sum,KVArray){
                    return sum.concat([KVArray[0],'="',KVArray[1],'" '].join(""))
                  },"")
 
  return ['<' + vdomElement.tag, attributes, '/>'].join(" ")
}

toHTMLElement(aTextbox) => <input type="text" />

React.js, mithril, mercury, ractive.js, jsonml

Aside:  setState

Christopher Chedeau  http://calendar.perfplanet.com/2013/diff/

Aside:  render

Christopher Chedeau  http://calendar.perfplanet.com/2013/diff/

Aside:  shouldRender

Christopher Chedeau  http://calendar.perfplanet.com/2013/diff/

Webpack

  • CommonJS/AMD (Asynchronous Module Definition)
  • Compile dialects 
  • Code Splitting (multiple entry points)
  • Live Reload (socket.io module injection)
var webpack = require('webpack');

var port = JSON.parse(process.env.npm_package_config_port || 3000),
    //subdomain = JSON.parse(process.env.npm_package_config_subdomain),
    //url = subdomain ?
    //'https://' + subdomain + '.localtunnel.me' :
    url = 'http://localhost:' + port;

module.exports = {
    // If it gets slow on your project, change to 'eval':
    devtool: 'source-map',
    entry: [
        'webpack-dev-server/client?' + url,
        'webpack/hot/only-dev-server',
        './web/js/app'
    ],
    output: {
        path: __dirname +"/web/js",
        //path: __dirname +"/web",
        filename: 'bundle.js',
        publicPath: '/web/'
    },
    plugins: [
        new webpack.HotModuleReplacementPlugin(),
        new webpack.NoErrorsPlugin()
    ],
    resolve: {
        extensions: ['', '.js', '.jsx', '.cjsx', '.coffee']
    },
    module: {
        loaders: [
            { test: /\.js$/, loaders: ['react-hot', 'jsx?harmony'] },
            { test: /\.css$/, loader: "style!css" },
            { test: /\.cjsx$/, loaders: ['react-hot', 'coffee', 'cjsx']},
            { test: /\.coffee$/, loader: 'coffee' }
        ]
    }
};
{
  "name": "cvjs demo",
  "version": "0.1.2",
  "description": "environment",
  "scripts": {
    "start": "node server.js"
  },
  "config": {
    "port": 3000,
    "subdomain": null
  },
  "repository": {
    "type": "git",
    "url": ""
  },
  "keywords": [
    "react",
    "reactjs",
    "boilerplate",
    "hot",
    "hot",
    "reload",
    "hmr",
    "live",
    "edit",
    "webpack"
  ],
  "author": "Jon Tiemann <jtiemann@digitalpersonae.com>",
  "license": "MIT",
  "bugs": {
    "url": "https://bitbucket.com/jtiemann/huh/issues"
  },
  "homepage": "",
  "devDependencies": {
    "react": "^0.12.0",
    "jsx-loader": "~0.12.2",
    "react-hot-loader": "^0.5.0",
    "webpack": "^1.4.5",
    "webpack-dev-server": "1.6.4",
    "mcfly": "0.0.2",
      "style-loader": "^0.8.2",
      "css-loader": "^0.9.0",
      "monorouter": "^0.7.1",
      "monorouter-react": "^0.2.0",
      "cjsx-loader": "^0.3.0",
      "coffee-loader": "^0.7.2",
      "coffee-react": "^1.0.2",
      "coffee-script": "^1.8.0",
      "gulp": "^3.8.8"
  },
  "dependencies": {
    "react": "^0.12.0",
    "flux": "^2.0.1",
    "underscore": "^1.7.0",
    "invariant": "^1.0.2",
    "object-assign": "^1.0.0",
    "jsx-loader": "~0.12.2",
    "react-hot-loader": "~0.5.0",
      "monorouter": "^0.7.1",
      "monorouter-react": "^0.2.0",
      "react-component-width-mixin": "^1.1.0"
  },
    "peerDependencies": {
        "react": "*"
    }
}

Flux

  • Components Actions are sent to dispatcher 
  • Dispatcher announces "I have a payload" to all Stores, and it's message type. 
  • Stores transform(setState) and emit "I changed"
  • View listens for "change" and renders (using new state)

Flux

Reflux

/*
 * Copyright (c) 2014, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 *
 * @providesModule Dispatcher
 * @typechecks
 */

"use strict";

var invariant = require('./invariant');

var _lastID = 1;
var _prefix = 'ID_';

  function Dispatcher() {
    this.$Dispatcher_callbacks = {};
    this.$Dispatcher_isPending = {};
    this.$Dispatcher_isHandled = {};
    this.$Dispatcher_isDispatching = false;
    this.$Dispatcher_pendingPayload = null;
  }

  /**
   * Registers a callback to be invoked with every dispatched payload. Returns
   * a token that can be used with `waitFor()`.
   *
   * @param {function} callback
   * @return {string}
   */
  Dispatcher.prototype.register=function(callback) {
    var id = _prefix + _lastID++;
    this.$Dispatcher_callbacks[id] = callback;
    return id;
  };

  /**
   * Removes a callback based on its token.
   *
   * @param {string} id
   */
  Dispatcher.prototype.unregister=function(id) {
    invariant(
      this.$Dispatcher_callbacks[id],
      'Dispatcher.unregister(...): `%s` does not map to a registered callback.',
      id
    );
    delete this.$Dispatcher_callbacks[id];
  };

  /**
   * Waits for the callbacks specified to be invoked before continuing execution
   * of the current callback. This method should only be used by a callback in
   * response to a dispatched payload.
   *
   * @param {array<string>} ids
   */
  Dispatcher.prototype.waitFor=function(ids) {
    invariant(
      this.$Dispatcher_isDispatching,
      'Dispatcher.waitFor(...): Must be invoked while dispatching.'
    );
    for (var ii = 0; ii < ids.length; ii++) {
      var id = ids[ii];
      if (this.$Dispatcher_isPending[id]) {
        invariant(
          this.$Dispatcher_isHandled[id],
          'Dispatcher.waitFor(...): Circular dependency detected while ' +
          'waiting for `%s`.',
          id
        );
        continue;
      }
      invariant(
        this.$Dispatcher_callbacks[id],
        'Dispatcher.waitFor(...): `%s` does not map to a registered callback.',
        id
      );
      this.$Dispatcher_invokeCallback(id);
    }
  };

  /**
   * Dispatches a payload to all registered callbacks.
   *
   * @param {object} payload
   */
  Dispatcher.prototype.dispatch=function(payload) {
    invariant(
      !this.$Dispatcher_isDispatching,
      'Dispatch.dispatch(...): Cannot dispatch in the middle of a dispatch.'
    );
    this.$Dispatcher_startDispatching(payload);
    try {
      for (var id in this.$Dispatcher_callbacks) {
        if (this.$Dispatcher_isPending[id]) {
          continue;
        }
        this.$Dispatcher_invokeCallback(id);
      }
    } finally {
      this.$Dispatcher_stopDispatching();
    }
  };

  /**
   * Is this Dispatcher currently dispatching.
   *
   * @return {boolean}
   */
  Dispatcher.prototype.isDispatching=function() {
    return this.$Dispatcher_isDispatching;
  };

  /**
   * Call the callback stored with the given id. Also do some internal
   * bookkeeping.
   *
   * @param {string} id
   * @internal
   */
  Dispatcher.prototype.$Dispatcher_invokeCallback=function(id) {
    this.$Dispatcher_isPending[id] = true;
    this.$Dispatcher_callbacks[id](this.$Dispatcher_pendingPayload);
    this.$Dispatcher_isHandled[id] = true;
  };

  /**
   * Set up bookkeeping needed when dispatching.
   *
   * @param {object} payload
   * @internal
   */
  Dispatcher.prototype.$Dispatcher_startDispatching=function(payload) {
    for (var id in this.$Dispatcher_callbacks) {
      this.$Dispatcher_isPending[id] = false;
      this.$Dispatcher_isHandled[id] = false;
    }
    this.$Dispatcher_pendingPayload = payload;
    this.$Dispatcher_isDispatching = true;
  };

  /**
   * Clear bookkeeping used for dispatching.
   *
   * @internal
   */
  Dispatcher.prototype.$Dispatcher_stopDispatching=function() {
    this.$Dispatcher_pendingPayload = null;
    this.$Dispatcher_isDispatching = false;
  };


module.exports = Dispatcher;

https://github.com/kenwheeler/mcfly

/** McFly */

var Flux = new McFly();

/** Store */

_todos = [];

function addTodo(text){
    _todos.push(text);
}

var TodoStore = Flux.createStore({
    getTodos: function(){
       return _todos;
    }
}, function(payload){
    if(payload.actionType === "ADD_TODO") {
        addTodo(payload.text);
        TodoStore.emitChange();
    }
});

/** Actions */

var TodoActions = Flux.createActions({
    addTodo: function(text){
       return {
          actionType: "ADD_TODO",
          text: text
       }
    }
});

function getState(){
   return {
       todos: TodoStore.getTodos()
   }
}

/** Controller View */

var TodosController = React.createClass({
    mixins: [TodoStore.mixin],
    getInitialState: function(){
        return getState();
    },
    onChange: function() {
        this.setState(getState());
    },
    render: function() {
        return <Todos todos={this.state.todos} />;
    }
});

/** Component */

var Todos = React.createClass({
    addTodo: function(){
        TodoActions.addTodo('test');
    },
    render: function() {
        return (
        <div className="todos_app">
            <ul className="todos">
                { this.props.todos.map(function(todo, index){
                    return <li key={index}>Todo {index}</li>
                })}
            </ul>
            <button onClick={this.addTodo}>Add Todo</button>
        </div>
        )
    }
});
 
React.render(<TodosController />, document.body);
 /** Boilerplate Flux Store */
 addChangeListener: function(callback) {
    this.on(CHANGE_EVENT, callback);
  },

  /**
   * @param {function} callback
   */
  removeChangeListener: function(callback) {
    this.removeListener(CHANGE_EVENT, callback);


AppDispatcher.register(function(payload) {
  var action = payload.action;
  var text;

  switch(action.actionType) {
    case TodoConstants.TODO_CREATE:
      text = action.text.trim();
      if (text !== '') {
        create(text);
      }
      break;

    default:
      return true;
  }

 /** Boilerplate Flux Action */

var TodoActions = {

  /**
   * @param  {string} text
   */
  create: function(text) {
    AppDispatcher.handleViewAction({
      actionType: TodoConstants.TODO_CREATE,
      text: text
    });
  },

Less Flux Boilerplate Code

Cairngorm

/**
 * ==========================================================
 * 
 * This is not an official port of Cairngorm MVC Framework
 * @author Babelium Project -> http://www.babeliumproject.com
 * 
 * ==========================================================
 * 
 * @source Cairngorm (Flex) Open Source Code:
 * http://sourceforge.net/adobe/cairngorm/code/839/tree/cairngorm/trunk/frameworks/cairngorm/com/adobe/cairngorm/
 * @source Simple Javascript Inheritance (./base.js):
 * http://ejohn.org/blog/simple-javascript-inheritance/#postcomment
 * @source Singleton Pattern w and w/o private members:
 * http://stackoverflow.com/questions/1479319/simplest-cleanest-way-to-implement-singleton-in-javascript
 * 
 */

/* ============================================================
 * control/FrontController.as
 * ==========================================================*/

var Cairngorm = {};

Cairngorm.FrontController = Class.extend(
{
  /**
   * Constructor
   */
  init : function ()
  {
    this.commands = {};
  },
  
  /**
   * Add command
   * @param evType = Event Type (String)
   * @param commandRef = Class
   */
  addCommand : function (evType, commandRef)
  {	
    if ( evType == null )
      return;
    
    this.commands[evType] = commandRef;
    Cairngorm.EventDispatcher.addEventListener(evType, this);
  },
  
  /**
   * Remove Command
   * @param commandName = String
   */
  removeCommand : function ( commandName )
  {
    if ( commandName == null )
      return;
    
    this.commands[commandName] = null;
    delete this.commands[commandName];
  },
  
  /**
   * Execute Command
   * @param ev = CairngormEvent
   */
  executeCommand : function ( ev )
  {
    new this.commands[ev.type](ev.data).execute();
  }
});


/* ============================================================
 * control/CairngormEventDispatcher.as
 * ==========================================================*/

Cairngorm.EventDispatcher = (function()
{
  // Private interface
  var _listeners = {};
  
  // Public interface
  return {
    
    /**
     * Add Event Listener
     * @param type = Event Type (String)
     * @param listener = function
     */
    addEventListener : function ( type, listener )
    {
      if ( type != null && typeof listener.executeCommand == 'function' )
        _listeners[type] = listener;
    },
    
    /**
     * Dispatch event
     * @param ev = Cairngorm Event
     */
    dispatchEvent : function ( ev )
    {	
      if ( _listeners[ev.type] != null )
        _listeners[ev.type].executeCommand(ev);
    }
  };
})();

/* ============================================================
 * control/CairngormEvent.as
 * ==========================================================*/

Cairngorm.Event = Class.extend(
{	
  /**
   * Constructor
   * @param type = String
   */
  init : function ( type, data )
  {
    this.type = type;
    this.data = data != null ? data : {};
  },
  
  /**
   * Dispatch Cairngorm Event 
   */
  dispatch : function ()
  {
    return Cairngorm.EventDispatcher.dispatchEvent(this);
  }
});


/* ============================================================
 * command/Command.as
 * ==========================================================*/

Cairngorm.Command = Class.extend(
{	
  /**
   * Constructor
   */
  init : function ( data )
  {
    this.data = data;
  },
  
  /**
   * Execute an action
   */
  execute : function () {}
});


/* ============================================================
 * business/HTTPServices.as
 * ==========================================================*/

Cairngorm.HTTPServices = Class.extend(
{
  /**
   * Constructor
   */
  init : function ()
  {
    this.services = {};
  },
    
  /**
   * Finds a service by name
   * @param name : service id
   * @return RemoteObject
   */
  getService : function ( name )
  {
    return this.services[name];
  },
  
  /**
   * Register a service identified by its id
   * @param name : service id
   * @param service : httpservice
   */
  registerService : function ( name, service )
  {
    this.services[name] = service;
  }

});


/* ============================================================
 * RemoteObject.as
 * ==========================================================*/

Cairngorm.HTTPService = Class.extend(
{
  /**
   * Constructor
   */
  init : function ( gateway, service )
  {
    this.target = gateway.target;
    this.method = gateway.method;
    this.service = service;
  },

  call : function ( params, responder ) 
  {
    if ( params == null )
      params = "";

    if ( this.method == "get" )
    {
      var src = this.target + this.service + "&" + params;

      $.ajax(
      {
        // Target url
        url : src,
        // The success call back.
        success : responder.onResult,
        // The error handler.
        error : responder.onFault
      });
    }
  }

});


/* ============================================================
 * business/ServiceLocator.as
 * ==========================================================*/

Cairngorm.ServiceLocator = (function()
{
  // Private interface
  var _httpServices = new Cairngorm.HTTPServices();
  // TODO var _remoteObjects = null;
  // TODO var _webServices = null;
  
  // Public interface
  return {
    
    /**
     * Finds http service by name
     * @return HTTPService
     */
    getHttpService : function ( name )
    {
      return _httpServices.getService(name);
    },
  
    /**
     * Register a service identified by its id
     * @param name : service id
     * @param service : HTTPService
     */
    registerHttpService : function ( name, service )
    {
      _httpServices.registerService(name, service);
    }
  };

})();


/* ============================================================
 * vo/ValueObject.as
 * ==========================================================*/

Cairngorm.VO = Class.extend(
{
  init : function (){},
  
  /**
   * Convert this object's properties
   * to json object
   */
  toJSON : function ()
  {
    var jsonObj = {};
    
    for ( var i in this )
      if ( typeof this[i] != "function" )
        jsonObj[i] = this[i];
    
    return jsonObj;
  },
  
  /**
   * Convert this object's properties
   * to json string
   */
  toJSONStr : function ()
  {
    var jsonStr = "{";
    
    for ( var i in this.toJSON() )
    {
      if ( jsonStr.length != 1 )
        jsonStr += ",";

      jsonStr += '"' + i + '": "' + this[i] + '"';
    }
    
    jsonStr += "}";
    
    return jsonStr;
  },
  
  /**
   * Convert this to base64
   */
  toBase64 : function ()
  {
    return Base64.encode(this.toJSONStr());
  }
  
});


//base.js
/* Simple JavaScript Inheritance
 * By John Resig http://ejohn.org/
 * MIT Licensed.
 */
// Inspired by base2 and Prototype
(function(){
  var initializing = false, fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/;
  // The base Class implementation (does nothing)
  this.Class = function(){};
  
  // Create a new Class that inherits from this class
  Class.extend = function(prop) {
    var _super = this.prototype;
    
    // Instantiate a base class (but only create the instance,
    // don't run the init constructor)
    initializing = true;
    var prototype = new this();
    initializing = false;
    
    // Copy the properties over onto the new prototype
    for (var name in prop) {
      // Check if we're overwriting an existing function
      prototype[name] = typeof prop[name] == "function" && 
        typeof _super[name] == "function" && fnTest.test(prop[name]) ?
        (function(name, fn){
          return function() {
            var tmp = this._super;
            
            // Add a new ._super() method that is the same method
            // but on the super-class
            this._super = _super[name];
            
            // The method only need to be bound temporarily, so we
            // remove it when we're done executing
            var ret = fn.apply(this, arguments);        
            this._super = tmp;
            
            return ret;
          };
        })(name, prop[name]) :
        prop[name];
    }
    
    // The dummy class constructor
    function Class() {
      // All construction is actually done in the init method
      if ( !initializing && this.init )
        this.init.apply(this, arguments);
    }
    
    // Populate our constructed prototype object
    Class.prototype = prototype;
    
    // Enforce the constructor to be what we expect
    Class.prototype.constructor = Class;

    // And make this class extendable
    Class.extend = arguments.callee;
    
    return Class;
  };
})();

https://github.com/yahoo/flux-router-component

https://github.com/yahoo/flux-examples

https://github.com/yahoo/fetchr

https://github.com/yahoo/dispatchr

Monorouter

  • Back, Forward
  • Bookmarks (Initializing state)
  • client and/or server
monorouter
==========

monorouter is an isomorphic JavaScript router by [@matthewwithanm] and
[@lettertwo]. It was designed for use with ReactJS but doesn't have any direct
dependencies on it and should be easily adaptable to other virtual DOM
libraries.

While it can be used for both browser-only and server-only routing, it was
designed from the ground up to be able to route apps on both sides of the wire.

**Note: This project is in beta and we consider the API in flux. Let us know if
you have any ideas for improvement!**


Usage
-----

Defining a router looks like this:

```javascript
var monorouter = require('monorouter');
var reactRouting = require('monorouter-react');

monorouter()
  .setup(reactRouting())
  .route('/', function(req) {
    this.render(MyView);
  })
  .route('/pets/:name/', function(req) {
    this.render(PetView, {petName: req.params.name});
  });
```

Here, we're simply rendering views for two different routes. With monorouter, a
"view" is any function that returns a DOM descriptor.

The router can be used on the server with express and [connect-monorouter]:

```javascript
var express = require('express');
var router = require('./path/to/my/router');
var monorouterMiddleware = require('connect-monorouter');

var app = express();
app.use(monorouterMiddleware(router));

var server = app.listen(3000, function() {
    console.log('Listening on port %d', server.address().port);
});
```

And in the browser:

```javascript
router
  .attach(document)
  .captureClicks(); // This is optional—it uses the router to handle normal links.
```

See [the examples][monorouter examples] for a more in-depth look and more
tricks!

more Routers

https://github.com/yahoo/flux-router-component

https://github.com/rackt/react-router

https://github.com/STRML/react-router-component

https://github.com/skevy/react-nested-router

JS Client Side Components

  • Plain JS, Polymer, React, ...
  • Reusable
  • Composable (attr interface, namespaces, mixins)
  • Probably not a panacea...but hopeful

https://gist.github.com/spoike/b9d6dddeb495c3e75477

better cheat sheet

Look at some code

/** @jsx React.DOM */
'use strict';

require("!style!css!../css/app.css");
var React = require('react');
var FluxCounterApp = require('./components/FluxCounterApp.react');
var monorouter = require('monorouter');
var reactRouting = require('monorouter-react');
var PetList = require('../views/PetList');
var PetDetail = require('../views/PetDetail');
var Avatar = require('./components/Avatar');
var ContactMgr = require('./components/ContactMgr');
var Examples = require('./components/example');

monorouter()
    .setup(reactRouting())
    .route('/web/cvjs/', function(req){
        this.render(function(){
            return  <div id="wrapper">
                <div id="jt-meetup1"></div>
            </div>
        })
    })
    .route('index', '/web/', function(req) {
        console.log("yo")
        //this.render(PetList);
        this.render(function(){
            return <div>
                       <ContactMgr />
                       <FluxCounterApp />
                       <Examples />
                        <Avatar username="brigittrue"  />
                    </div>
            }
        )
        console.log("yo2")
    })
    .route('pet', '/web/pet/:name', function(req) {
        //this.render(PetDetail, {petName: req.params.name});
        this.render(function() {
            return <div>
                <ContactMgr />
                <FluxCounterApp />
                </div>
        });
    })
    .attach(document.body)
    .captureClicks();


//React.render(
//  <FluxCounterApp />,
//  document.getElementById('flux-counter')
//);

In Summary

Packaging improving

One way data flow simplifies

components improving

ECMA 6

Thanks... and coming soon

CVJS 2014-12

By Jon Tiemann

CVJS 2014-12

  • 707