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
- arrows
- classes
- enhanced object literals
- template strings
- destructuring
- default + rest + spread
- let + const
- iterators + for..of
- generators
- comprehensions
- unicode
- modules
- module loaders
- map + set + weakmap + weakset
- proxies
- symbols
- subclassable built-ins
- promises
- math + number + string + object APIs
- binary and octal literals
- reflect api
- tail calls
ECMA 6
Thanks... and coming soon
CVJS 2014-12
By Jon Tiemann
CVJS 2014-12
- 697