├── .gitignore ├── .npmignore ├── .babelrc ├── examples ├── datetime-flux │ ├── .gitignore │ ├── src │ │ ├── alt.js │ │ ├── iso-config.js │ │ ├── actions │ │ │ └── TimeActions.js │ │ ├── stores │ │ │ └── TimeStore.js │ │ └── components │ │ │ ├── AltIsomorphicElement.js │ │ │ └── MyReactComponent.js │ ├── templates │ │ └── layout.jade │ ├── js │ │ └── client.js │ ├── package.json │ ├── server.js │ └── README.md ├── react-router-flux │ ├── .gitignore │ ├── src │ │ ├── alt.js │ │ ├── stores │ │ │ ├── HelloStore.js │ │ │ └── TimeStore.js │ │ ├── components │ │ │ ├── Hello.jsx │ │ │ ├── Time.jsx │ │ │ └── App.jsx │ │ └── routes.jsx │ ├── templates │ │ └── layout.jade │ ├── README.md │ ├── js │ │ └── client.js │ ├── package.json │ └── server.js └── iso-todomvc │ ├── .gitignore │ ├── js │ ├── alt.js │ ├── app.js │ ├── client.js │ ├── actions │ │ └── TodoActions.js │ ├── components │ │ ├── Header.react.js │ │ ├── MainSection.react.js │ │ ├── TodoApp.react.js │ │ ├── Footer.react.js │ │ ├── TodoTextInput.react.js │ │ └── TodoItem.react.js │ └── stores │ │ └── TodoStore.js │ ├── todomvc-common │ ├── bower.json │ ├── bg.png │ ├── readme.md │ └── base.css │ ├── css │ └── app.css │ ├── templates │ └── index.jade │ ├── package.json │ ├── server.js │ └── README.md ├── .eslintrc ├── min.config.js ├── web.config.js ├── package.json ├── LICENSE ├── src ├── core.js └── iso.js ├── CHANGELOG.md ├── core.js ├── README.md ├── dist ├── iso.min.js └── iso.js └── test └── index-test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | examples/ 2 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["airbnb"] 3 | } 4 | -------------------------------------------------------------------------------- /examples/datetime-flux/.gitignore: -------------------------------------------------------------------------------- 1 | js/bundle.js 2 | -------------------------------------------------------------------------------- /examples/react-router-flux/.gitignore: -------------------------------------------------------------------------------- 1 | js/bundle.js 2 | -------------------------------------------------------------------------------- /examples/iso-todomvc/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | js/bundle.js 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /examples/datetime-flux/src/alt.js: -------------------------------------------------------------------------------- 1 | var Alt = require('alt') 2 | module.exports = new Alt() 3 | -------------------------------------------------------------------------------- /examples/react-router-flux/src/alt.js: -------------------------------------------------------------------------------- 1 | var Alt = require('alt') 2 | module.exports = new Alt() 3 | -------------------------------------------------------------------------------- /examples/iso-todomvc/js/alt.js: -------------------------------------------------------------------------------- 1 | var Alt = require('alt') 2 | var alt = new Alt() 3 | 4 | module.exports = alt 5 | -------------------------------------------------------------------------------- /examples/iso-todomvc/todomvc-common/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todomvc-common", 3 | "version": "0.1.9" 4 | } 5 | -------------------------------------------------------------------------------- /examples/datetime-flux/templates/layout.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | body 4 | #app!= html 5 | script(src="/js/bundle.js") 6 | -------------------------------------------------------------------------------- /examples/iso-todomvc/todomvc-common/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goatslacker/iso/HEAD/examples/iso-todomvc/todomvc-common/bg.png -------------------------------------------------------------------------------- /examples/react-router-flux/templates/layout.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | body 4 | #app!= html 5 | script(src="/js/bundle.js") 6 | -------------------------------------------------------------------------------- /examples/iso-todomvc/todomvc-common/readme.md: -------------------------------------------------------------------------------- 1 | # todomvc-common 2 | 3 | > Bower component for some common utilities we use in every app 4 | 5 | 6 | ## License 7 | 8 | MIT 9 | -------------------------------------------------------------------------------- /examples/react-router-flux/src/stores/HelloStore.js: -------------------------------------------------------------------------------- 1 | var alt = require('../alt') 2 | 3 | function HelloStore() { 4 | this.name = 'Nobody' 5 | } 6 | 7 | module.exports = alt.createStore(HelloStore, 'HelloStore') 8 | -------------------------------------------------------------------------------- /examples/react-router-flux/src/stores/TimeStore.js: -------------------------------------------------------------------------------- 1 | var alt = require('../alt') 2 | 3 | function TimeStore() { 4 | this.time = Date.now() 5 | } 6 | 7 | module.exports = alt.createStore(TimeStore, 'TimeStore') 8 | -------------------------------------------------------------------------------- /examples/iso-todomvc/js/app.js: -------------------------------------------------------------------------------- 1 | var TodoApp = require('./components/TodoApp.react') 2 | var React = require('react') 3 | React.render( 4 | React.createElement(TodoApp, {}), 5 | document.getElementById('todoapp') 6 | ) 7 | -------------------------------------------------------------------------------- /examples/datetime-flux/src/iso-config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | markupClassName: 'your-custom-class', 5 | markupElement: 'main', 6 | dataClassName: 'configurable-class-names-are-good', 7 | dataElement: 'script' 8 | } -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "env": { 4 | "es6": true, 5 | "browser": true, 6 | }, 7 | "ecmaFeatures": { 8 | "modules": true, 9 | "jsx": true 10 | }, 11 | "rules": { 12 | "semi": 0, 13 | }, 14 | } 15 | -------------------------------------------------------------------------------- /examples/iso-todomvc/js/client.js: -------------------------------------------------------------------------------- 1 | var Iso = require('../../../') 2 | var React = require('react') 3 | var TodoApp = require('./components/TodoApp.react') 4 | 5 | var alt = require('./alt') 6 | 7 | Iso.bootstrap(function (state, _, container) { 8 | alt.bootstrap(JSON.stringify(state)) 9 | React.render(React.createElement(TodoApp), container) 10 | }) 11 | -------------------------------------------------------------------------------- /examples/datetime-flux/js/client.js: -------------------------------------------------------------------------------- 1 | var Iso = require('../../../') 2 | var React = require('react') 3 | var AltIsomorphicElement = require('../src/components/AltIsomorphicElement') 4 | var isoConfig = require('./../src/iso-config'); 5 | 6 | Iso.on('react', true, function (props, _, node) { 7 | React.render(React.createElement(AltIsomorphicElement, props), node) 8 | }, isoConfig) 9 | -------------------------------------------------------------------------------- /examples/react-router-flux/src/components/Hello.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react') 2 | 3 | var HelloStore = require('../stores/HelloStore') 4 | 5 | var App = React.createClass({ 6 | getInitialState() { 7 | return HelloStore.getState() 8 | }, 9 | 10 | render() { 11 | return
{`Hello, ${this.state.name}`}
12 | } 13 | }) 14 | 15 | module.exports = App 16 | -------------------------------------------------------------------------------- /min.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const config = require('./web.config') 3 | 4 | module.exports = Object.assign({}, config, { 5 | plugins: [ 6 | new webpack.optimize.UglifyJsPlugin(), 7 | ], 8 | output: { 9 | path: __dirname + '/dist', 10 | filename: '[name].min.js', 11 | library: 'Iso', 12 | libraryTarget: 'umd', 13 | }, 14 | }) 15 | -------------------------------------------------------------------------------- /examples/datetime-flux/src/actions/TimeActions.js: -------------------------------------------------------------------------------- 1 | var alt = require('../alt') 2 | 3 | class TimeActions { 4 | constructor() { 5 | this.generateActions('updateTime', 'setAsync') 6 | } 7 | 8 | setAsync(n) { 9 | setTimeout(function () { 10 | this.dispatch(n) 11 | }.bind(this), 500) 12 | } 13 | } 14 | 15 | module.exports = alt.createActions(TimeActions) 16 | -------------------------------------------------------------------------------- /examples/react-router-flux/src/components/Time.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react') 2 | 3 | var TimeStore = require('../stores/TimeStore') 4 | 5 | var App = React.createClass({ 6 | getInitialState() { 7 | return TimeStore.getState() 8 | }, 9 | 10 | render() { 11 | return
{`The time is now: ${this.state.time}`}
12 | } 13 | }) 14 | 15 | module.exports = App 16 | -------------------------------------------------------------------------------- /examples/iso-todomvc/js/actions/TodoActions.js: -------------------------------------------------------------------------------- 1 | var alt = require('../alt') 2 | 3 | class TodoActions { 4 | constructor() { 5 | this.generateActions( 6 | 'create', 7 | 'updateText', 8 | 'toggleComplete', 9 | 'toggleCompleteAll', 10 | 'destroy', 11 | 'destroyCompleted' 12 | ) 13 | } 14 | } 15 | 16 | module.exports = alt.createActions(TodoActions) 17 | -------------------------------------------------------------------------------- /web.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | context: __dirname + '/src', 3 | entry: { 4 | 'iso': ['./iso.js'], 5 | }, 6 | output: { 7 | path: __dirname + '/dist', 8 | filename: '[name].js', 9 | library: 'Iso', 10 | libraryTarget: 'umd', 11 | }, 12 | module: { 13 | loaders: [{ 14 | test: /\.js$/, 15 | loader: 'babel', 16 | exclude: /node_modules/, 17 | }], 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /examples/react-router-flux/src/components/App.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react') 2 | var { RouteHandler, Link } = require('react-router') 3 | 4 | var App = React.createClass({ 5 | render() { 6 | return ( 7 |
8 | Say hi 9 |
10 | What time is it? 11 |
12 | 13 |
14 | ) 15 | } 16 | }) 17 | 18 | module.exports = App 19 | -------------------------------------------------------------------------------- /examples/datetime-flux/src/stores/TimeStore.js: -------------------------------------------------------------------------------- 1 | var alt = require('../alt') 2 | var TimeActions = require('../actions/TimeActions') 3 | 4 | class TimeStore { 5 | constructor() { 6 | this.bindActions(TimeActions) 7 | this.time = 0 8 | this.asyncValue = undefined 9 | } 10 | 11 | onUpdateTime(time) { 12 | this.time = time 13 | } 14 | 15 | onSetAsync(n) { 16 | this.asyncValue = n 17 | } 18 | } 19 | 20 | module.exports = alt.createStore(TimeStore, 'TimeStore') 21 | -------------------------------------------------------------------------------- /examples/datetime-flux/src/components/AltIsomorphicElement.js: -------------------------------------------------------------------------------- 1 | var React = require('react') 2 | var alt = require('../alt') 3 | var IsomorphicMixin = require('alt/mixins/IsomorphicMixin') 4 | 5 | var MyReactComponent = require('./MyReactComponent') 6 | 7 | module.exports = React.createClass({ 8 | mixins: [IsomorphicMixin.create(alt)], 9 | 10 | render: function () { 11 | return React.createElement( 12 | 'div', 13 | null, 14 | React.createElement(MyReactComponent) 15 | ) 16 | } 17 | }) 18 | -------------------------------------------------------------------------------- /examples/react-router-flux/src/routes.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react') 2 | var Route = require('react-router').Route 3 | 4 | var App = require('./components/App.jsx') 5 | var Hello = require('./components/Hello.jsx') 6 | var Time = require('./components/Time.jsx') 7 | 8 | var routes = ( 9 | 10 | 11 | 12 | 13 | ) 14 | 15 | module.exports = routes 16 | -------------------------------------------------------------------------------- /examples/iso-todomvc/css/app.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * base.css overrides 10 | */ 11 | 12 | /** 13 | * We are not changing from display:none, but rather re-rendering instead. 14 | * Therefore this needs to be displayed normally by default. 15 | */ 16 | #todo-list li .edit { 17 | display: inline; 18 | } 19 | -------------------------------------------------------------------------------- /examples/iso-todomvc/templates/index.jade: -------------------------------------------------------------------------------- 1 | 2 | html(lang="en") 3 | head 4 | meta(charset="utf-8") 5 | title Alt - TodoMVC 6 | link(rel="stylesheet" href="todomvc-common/base.css") 7 | link(rel="stylesheet" href="css/app.css") 8 | body 9 | section!= html 10 | footer#info 11 | p Double-click to edit a todo 12 | p Alt example created by Josh Perez 13 | p View components created by Bill Fisher 14 | p Part of TodoMVC 15 | 16 | script(src="js/bundle.js") 17 | -------------------------------------------------------------------------------- /examples/react-router-flux/README.md: -------------------------------------------------------------------------------- 1 | # iso-react-router-flux 2 | 3 | > Isomorphic react application using flux and react-router 4 | 5 | ## Running This 6 | 7 | ```sh 8 | npm install; npm run build; npm start 9 | ``` 10 | 11 | Then open your browser to `localhost:8080` and enjoy. 12 | 13 | There are a few routes you can visit directly: 14 | 15 | `localhost:8080/hello` and `localhost:8080/time`. Hello will display a hello message whereas time will display the current time. 16 | 17 | For funsies you can include your own name as a parameter to hello: eg `localhost:8080/hello/jane` This will be seeded on the server and bootstrapped on the client. 18 | -------------------------------------------------------------------------------- /examples/react-router-flux/js/client.js: -------------------------------------------------------------------------------- 1 | var Iso = require('../../../') 2 | 3 | var Router = require('react-router') 4 | var React = require('react') 5 | 6 | var routes = require('../src/routes.jsx') 7 | 8 | var alt = require('../src/alt') 9 | 10 | // Once we bootstrap the stores, we run react-router using 11 | // Router.HistoryLocation 12 | // the element is created and we just render it into the container 13 | // and our application is now live 14 | Iso.bootstrap(function (state, _, container) { 15 | alt.bootstrap(state) 16 | 17 | Router.run(routes, Router.HistoryLocation, function (Handler) { 18 | var node = React.createElement(Handler) 19 | React.render(node, container) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /examples/iso-todomvc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "iso-todomvc", 3 | "version": "1.0.0", 4 | "description": "Example isomorphic flux architecture using alt.", 5 | "repository": "https://github.com/goatslacker/iso", 6 | "main": "js/app.js", 7 | "dependencies": { 8 | "alt": "^0.10.2", 9 | "express": "^4.10.7", 10 | "jade": "^1.8.2", 11 | "node-jsx": "^0.12.4", 12 | "object-assign": "^2.0.0", 13 | "react": "^0.12.2" 14 | }, 15 | "devDependencies": { 16 | "browserify": "^8.0.3", 17 | "reactify": "^0.17.1" 18 | }, 19 | "scripts": { 20 | "install": "cd ../.. ;npm install", 21 | "build": "browserify -t [reactify --es6] js/client.js > js/bundle.js" 22 | }, 23 | "author": "Josh Perez " 24 | } 25 | -------------------------------------------------------------------------------- /examples/react-router-flux/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "iso-react-router-flux", 3 | "version": "1.0.0", 4 | "description": "Isomorphic date/time application with react-router and flux", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "browserify -t [reactify --harmony] js/client.js > js/bundle.js", 8 | "start": "node server.js", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "author": "Josh Perez ", 12 | "license": "MIT", 13 | "dependencies": { 14 | "alt": "^0.10.2", 15 | "express": "^4.11.2", 16 | "jade": "^1.8.2", 17 | "node-jsx": "^0.12.4", 18 | "react": "^0.12.2", 19 | "react-router": "^0.11.6" 20 | }, 21 | "devDependencies": { 22 | "browserify": "^8.0.3", 23 | "reactify": "^0.17.1" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/datetime-flux/src/components/MyReactComponent.js: -------------------------------------------------------------------------------- 1 | let React = require('react') 2 | 3 | let TimeActions = require('../actions/TimeActions') 4 | let TimeStore = require('../stores/TimeStore') 5 | 6 | class MyReactComponent extends React.Component { 7 | constructor() { 8 | this.state = TimeStore.getState() 9 | } 10 | 11 | componentDidMount() { 12 | TimeStore.listen(() => this.setState(TimeStore.getState())) 13 | } 14 | 15 | componentWillUnmount() { 16 | TimeStore.unlisten(() => this.setState(TimeStore.getState())) 17 | } 18 | 19 | updateTime() { 20 | TimeActions.updateTime(Date.now()) 21 | } 22 | 23 | render() { 24 | return ( 25 |
26 |
{`Click me to update the time: ${this.state.time}`}
27 |
{`This is a unique ID: ${this.state.asyncValue}`}
28 |
29 | ) 30 | } 31 | } 32 | 33 | module.exports = MyReactComponent 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "iso", 3 | "version": "5.2.0", 4 | "description": "Isomorphic applications helper", 5 | "main": "dist/iso.js", 6 | "author": "Josh Perez ", 7 | "license": "MIT", 8 | "scripts": { 9 | "build": "npm run build:core && npm run build:web", 10 | "build:core": "babel ./src/core.js -o core.js", 11 | "build:web": "webpack --config web.config.js && webpack --config min.config.js", 12 | "lint": "eslint src", 13 | "test": "babel-node node_modules/.bin/_mocha -u exports test/*-test.js" 14 | }, 15 | "devDependencies": { 16 | "babel-cli": "6.2.0", 17 | "babel-core": "6.7.2", 18 | "babel-loader": "6.2.4", 19 | "babel-preset-airbnb": "1.0.1", 20 | "chai": "3.4.1", 21 | "cheerio": "0.19.0", 22 | "eslint": "1.10.3", 23 | "eslint-config-airbnb": "2.0.0", 24 | "eslint-plugin-react": "3.11.2", 25 | "jsdom": "7.1.1", 26 | "mocha": "2.3.4", 27 | "webpack": "1.12.14" 28 | }, 29 | "repository": { 30 | "type": "git", 31 | "url": "http://github.com/goatslacker/iso.git" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2014-present Dogfessional 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /examples/datetime-flux/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "datetime-flux-iso", 3 | "version": "2.0.0", 4 | "description": "An isomorphic date/time application example", 5 | "main": ".", 6 | "dependencies": { 7 | "alt": "^0.10.2", 8 | "express": "^4.11.2", 9 | "jade": "^1.9.2", 10 | "react": "^0.13.0-beta.1" 11 | }, 12 | "devDependencies": { 13 | "babel": "^4.0.1", 14 | "babelify": "^5.0.3", 15 | "browserify": "^8.0.3" 16 | }, 17 | "scripts": { 18 | "test": "echo \"Error: no test specified\" && exit 1", 19 | "build": "browserify -t [babelify] js/client.js > js/bundle.js", 20 | "start": "babel-node server.js" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/goatslacker/iso" 25 | }, 26 | "keywords": [ 27 | "isomorphic", 28 | "javascript", 29 | "iso", 30 | "flux", 31 | "alt", 32 | "react" 33 | ], 34 | "author": "Josh Perez ", 35 | "license": "MIT", 36 | "bugs": { 37 | "url": "https://github.com/goatslacker/iso/issues" 38 | }, 39 | "homepage": "https://github.com/goatslacker/iso" 40 | } 41 | -------------------------------------------------------------------------------- /examples/iso-todomvc/js/components/Header.react.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @jsx React.DOM 10 | */ 11 | 12 | var React = require('react'); 13 | var TodoActions = require('../actions/TodoActions'); 14 | var TodoTextInput = require('./TodoTextInput.react'); 15 | 16 | var Header = React.createClass({ 17 | 18 | /** 19 | * @return {object} 20 | */ 21 | render: function() { 22 | return ( 23 | 31 | ); 32 | }, 33 | 34 | /** 35 | * Event handler called within TodoTextInput. 36 | * Defining this here allows TodoTextInput to be used in multiple places 37 | * in different ways. 38 | * @param {string} text 39 | */ 40 | _onSave: function(text) { 41 | if (text.trim()){ 42 | TodoActions.create(text); 43 | } 44 | 45 | } 46 | 47 | }); 48 | 49 | module.exports = Header; 50 | -------------------------------------------------------------------------------- /src/core.js: -------------------------------------------------------------------------------- 1 | const rLt = //g 3 | const rLte = /</g 4 | const rGte = />/g 5 | const rEnc = /[<>]/ 6 | const rDec = /<|>/ 7 | 8 | const coerceToString = val => val ? String(val) : '' 9 | 10 | export default { 11 | encode(str) { 12 | const val = coerceToString(str) 13 | 14 | if (rEnc.test(val)) { 15 | return val.replace(rLt, '<').replace(rGt, '>') 16 | } 17 | 18 | return val 19 | }, 20 | 21 | decode(str) { 22 | const val = coerceToString(str) 23 | 24 | if (rDec.test(val)) { 25 | return val.replace(rLte, '<').replace(rGte, '>') 26 | } 27 | 28 | return val 29 | }, 30 | 31 | server(html, data, renderer, name = '') { 32 | const markup = html.reduce((nodes, html, i) => { 33 | const key = `${name}_${i}` 34 | return nodes + renderer.markup(html, key, name) 35 | }, '') 36 | 37 | const state = data.reduce((nodes, state, i) => { 38 | const key = `${name}_${i}` 39 | return nodes + renderer.data(state, key, name) 40 | }, '') 41 | 42 | return `${markup}\n${state}` 43 | }, 44 | 45 | client(onNode, selector) { 46 | if (!onNode) return 47 | 48 | const cache = selector() 49 | 50 | Object.keys(cache).forEach((key) => { 51 | const { state, node } = cache[key] 52 | onNode(state, node, key) 53 | }) 54 | }, 55 | } 56 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.5.1 4 | 5 | Iso is now escaping state information by default again. 6 | 7 | The following tokens are escaped: `<` and `>`. If you need to escape any other 8 | tokens you should do so yourself. 9 | 10 | ## 0.5.0 11 | 12 | ### Breaking Changes 13 | 14 | * `meta` property no longer exists. You can use the custom renderer and selector in order to add your own 15 | custom meta properties and retrieve them. 16 | 17 | * `config` was removed. You now pass in a `name` into the constructor which is then used to build up the keys. 18 | You can further configure hwo things are presented by passing in a custom renderer and/or selector. 19 | 20 | * `on` was removed as a static method of Iso. You should use `bootstrap` and pass in your own selector. 21 | 22 | IMPORTANT NOTE! 23 | 24 | Iso no longer escapes state information by default when it is sent down to the client. 25 | 26 | Previously the state was deployed into a data attribute in a div and thus the JSON stringified content had to be escaped for browsers to parse it correctly. 27 | Now the state is being placed inside a script tag with a type of 'application/json' and no longer escaped. This means that if you're not escaping your own data 28 | before passing it to iso, or you were relying on iso's escaping, you might have an XSS vulnerability and should carefully check your payloads. 29 | -------------------------------------------------------------------------------- /examples/datetime-flux/server.js: -------------------------------------------------------------------------------- 1 | var express = require('express') 2 | 3 | var React = require('react') 4 | var AltIsomorphicElement = require('./src/components/AltIsomorphicElement') 5 | 6 | var Iso = require('../../') 7 | var isoConfig = require('./src/iso-config') 8 | var app = express() 9 | 10 | // This is express boilerplate to make our bundled JS available as well 11 | // as our template 12 | var path = require('path') 13 | app.set('view engine', 'jade') 14 | app.set('views', path.join(__dirname, 'templates')) 15 | app.use('/js', express.static(path.join(__dirname, 'js'))) 16 | 17 | 18 | // Simulate an asynchronous event, lets say the time is stored in some storage 19 | // system that takes 250ms to retrieve. 20 | function getTimeFromServer(cb) { 21 | setTimeout(function () { 22 | cb(Date.now()) 23 | }, 250) 24 | } 25 | 26 | 27 | // Our only simple route, we retrieve the time from our asynchronous system 28 | // seed the stores with data 29 | // and render the html using iso and jade. 30 | app.get('/', function (req, res) { 31 | getTimeFromServer(function (time) { 32 | var rand = Math.random() 33 | 34 | var data = { 35 | TimeStore: { 36 | time: time, 37 | asyncValue: rand 38 | } 39 | } 40 | 41 | var node = React.createElement(AltIsomorphicElement, { 42 | altStores: data 43 | }) 44 | 45 | res.render('layout', { 46 | html: Iso.render(React.renderToString(node), { altStores: data }, { react: true }, isoConfig) 47 | }) 48 | }) 49 | }) 50 | 51 | app.listen(8080) 52 | -------------------------------------------------------------------------------- /src/iso.js: -------------------------------------------------------------------------------- 1 | import core from './core' 2 | 3 | const KEY_NAME = 'data-iso-key' 4 | 5 | const defaultRenderer = { 6 | markup(html, key) { 7 | if (!html) return '' 8 | return `
${html}
` 9 | }, 10 | 11 | data(state, key) { 12 | if (!state) return '' 13 | return `` 14 | }, 15 | } 16 | 17 | const defaultSelector = () => { 18 | const all = document.querySelectorAll(`[${KEY_NAME}]`) 19 | 20 | return Array.prototype.reduce.call(all, (cache, node) => { 21 | const key = node.getAttribute(KEY_NAME) 22 | 23 | if (!cache[key]) cache[key] = {} 24 | 25 | if (node.nodeName === 'SCRIPT') { 26 | try { 27 | const state = JSON.parse(core.decode(node.innerHTML)) 28 | cache[key].state = state 29 | } catch (e) { 30 | cache[key].state = {} 31 | } 32 | } else { 33 | cache[key].node = node 34 | } 35 | 36 | return cache 37 | }, {}) 38 | } 39 | 40 | export default class Iso { 41 | constructor(name = '', renderer = defaultRenderer) { 42 | this.name = name 43 | this.renderer = renderer 44 | this.html = [] 45 | this.data = [] 46 | } 47 | 48 | add(html, _state = {}) { 49 | const state = core.encode(JSON.stringify(_state)) 50 | this.html.push(html) 51 | this.data.push(state) 52 | return this 53 | } 54 | 55 | render() { 56 | return core.server(this.html, this.data, this.renderer, this.name) 57 | } 58 | 59 | static render(html, state = {}, name = '', renderer = defaultRenderer) { 60 | return new Iso(name, renderer).add(html, state).render() 61 | } 62 | 63 | static bootstrap(onNode, selector = defaultSelector) { 64 | return core.client(onNode, selector) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /examples/iso-todomvc/js/stores/TodoStore.js: -------------------------------------------------------------------------------- 1 | var alt = require('../alt') 2 | var merge = require('object-assign') 3 | 4 | var TodoActions = require('../actions/TodoActions') 5 | 6 | var todoStore = alt.createStore(class TodoStore { 7 | constructor() { 8 | this.bindActions(TodoActions) 9 | 10 | this.todos = {} 11 | } 12 | 13 | update(id, updates) { 14 | this.todos[id] = merge(this.todos[id], updates) 15 | } 16 | 17 | updateAll(updates) { 18 | for (var id in this.todos) { 19 | this.update(id, updates) 20 | } 21 | } 22 | 23 | onCreate(text) { 24 | text = text.trim() 25 | if (text === '') { 26 | return false 27 | } 28 | // hand waving of course. 29 | var id = (+new Date() + Math.floor(Math.random() * 999999)).toString(36) 30 | this.todos[id] = { 31 | id: id, 32 | complete: false, 33 | text: text 34 | } 35 | } 36 | 37 | onUpdateText(x) { 38 | var [ id, text ] = x 39 | text = text.trim() 40 | if (text === '') { 41 | return false 42 | } 43 | this.update(id, { text }) 44 | } 45 | 46 | onToggleComplete(id) { 47 | var complete = !this.todos[id].complete 48 | this.update(id, { complete }) 49 | } 50 | 51 | onToggleCompleteAll() { 52 | var complete = !todoStore.areAllComplete() 53 | this.updateAll({ complete }) 54 | } 55 | 56 | onDestroy(id) { 57 | delete this.todos[id] 58 | } 59 | 60 | onDestroyCompleted() { 61 | for (var id in this.todos) { 62 | if (this.todos[id].complete) { 63 | this.onDestroy(id) 64 | } 65 | } 66 | } 67 | 68 | static areAllComplete() { 69 | var { todos } = this.getState() 70 | for (var id in todos) { 71 | if (!todos[id].complete) { 72 | return false 73 | } 74 | } 75 | return true 76 | } 77 | }) 78 | 79 | module.exports = todoStore 80 | -------------------------------------------------------------------------------- /examples/iso-todomvc/js/components/MainSection.react.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @jsx React.DOM 10 | */ 11 | 12 | var React = require('react'); 13 | var ReactPropTypes = React.PropTypes; 14 | var TodoActions = require('../actions/TodoActions'); 15 | var TodoItem = require('./TodoItem.react'); 16 | 17 | var MainSection = React.createClass({ 18 | 19 | propTypes: { 20 | allTodos: ReactPropTypes.object.isRequired, 21 | areAllComplete: ReactPropTypes.bool.isRequired 22 | }, 23 | 24 | /** 25 | * @return {object} 26 | */ 27 | render: function() { 28 | // This section should be hidden by default 29 | // and shown when there are todos. 30 | if (Object.keys(this.props.allTodos).length < 1) { 31 | return null; 32 | } 33 | 34 | var allTodos = this.props.allTodos; 35 | var todos = []; 36 | 37 | for (var key in allTodos) { 38 | todos.push(); 39 | } 40 | 41 | return ( 42 |
43 | 49 | 50 |
    {todos}
51 |
52 | ); 53 | }, 54 | 55 | /** 56 | * Event handler to mark all TODOs as complete 57 | */ 58 | _onToggleCompleteAll: function() { 59 | TodoActions.toggleCompleteAll(); 60 | } 61 | 62 | }); 63 | 64 | module.exports = MainSection; 65 | -------------------------------------------------------------------------------- /examples/iso-todomvc/js/components/TodoApp.react.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @jsx React.DOM 10 | */ 11 | 12 | /** 13 | * This component operates as a "Controller-View". It listens for changes in 14 | * the TodoStore and passes the new data to its children. 15 | */ 16 | 17 | var Footer = require('./Footer.react'); 18 | var Header = require('./Header.react'); 19 | var MainSection = require('./MainSection.react'); 20 | var React = require('react'); 21 | var TodoStore = require('../stores/TodoStore'); 22 | 23 | /** 24 | * Retrieve the current TODO data from the TodoStore 25 | */ 26 | function getTodoState() { 27 | return { 28 | allTodos: TodoStore.getState().todos, 29 | areAllComplete: TodoStore.areAllComplete() 30 | }; 31 | } 32 | 33 | var TodoApp = React.createClass({ 34 | 35 | getInitialState: function() { 36 | return getTodoState(); 37 | }, 38 | 39 | componentDidMount: function() { 40 | TodoStore.listen(this._onChange); 41 | }, 42 | 43 | componentWillUnmount: function() { 44 | TodoStore.unlisten(this._onChange); 45 | }, 46 | 47 | /** 48 | * @return {object} 49 | */ 50 | render: function() { 51 | return ( 52 |
53 |
54 | 58 |
59 |
60 | ); 61 | }, 62 | 63 | /** 64 | * Event handler for 'change' events coming from the TodoStore 65 | */ 66 | _onChange: function() { 67 | this.setState(getTodoState()); 68 | } 69 | 70 | }); 71 | 72 | module.exports = TodoApp; 73 | -------------------------------------------------------------------------------- /examples/iso-todomvc/server.js: -------------------------------------------------------------------------------- 1 | require('node-jsx').install({ harmony: true }) 2 | 3 | var express = require('express') 4 | var React = require('react') 5 | 6 | var Iso = require('../../') 7 | var alt = require('./js/alt') 8 | var app = express() 9 | 10 | var path = require('path') 11 | app.set('view engine', 'jade') 12 | app.set('views', path.join(__dirname, 'templates')) 13 | app.use('/js', express.static(path.join(__dirname, 'js'))) 14 | app.use('/css', express.static(path.join(__dirname, 'css'))) 15 | app.use('/todomvc-common', express.static(path.join(__dirname, 'todomvc-common'))) 16 | 17 | var TodoApp = require('./js/components/TodoApp.react') 18 | 19 | // Bootstrap our flux stores, create the markup, send it to iso. 20 | app.get('/', function (req, res) { 21 | fictionalDatabaseWithTodos(function (todos) { 22 | var data = { TodoStore: { todos: todos } } 23 | 24 | alt.bootstrap(JSON.stringify(data)) 25 | 26 | var markup = React.renderToStaticMarkup(React.createElement(TodoApp)) 27 | res.render('index', { 28 | html: Iso.render(markup, data) 29 | }) 30 | }) 31 | }) 32 | 33 | app.listen(8080) 34 | 35 | 36 | // Load a bunch of fake todos. 37 | // Disclaimer: checking off boxes on the frontend doesn't affect this :( 38 | function fictionalDatabaseWithTodos(cb) { 39 | var tasks = [ 40 | ['Write readme'], 41 | ['Create react plugin', true], 42 | ['Write blog post'], 43 | ['Add the flux-todomvc example', true], 44 | ['Add the flux-chat example'], 45 | ['Release new pakage on npm'] 46 | ] 47 | 48 | var todos = tasks.reduce(function (obj, args) { 49 | var todo = createTodo.apply(null, args) 50 | obj[todo.id] = todo 51 | return obj 52 | }, {}) 53 | 54 | setTimeout(function () { 55 | cb(todos) 56 | }, 100) 57 | } 58 | 59 | function createTodo(text, complete) { 60 | var id = (Date.now() + Math.floor(Math.random() * 999999)).toString(36) 61 | return { 62 | id: id, 63 | complete: complete || false, 64 | text: text 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /core.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | var rLt = //g; 8 | var rLte = /</g; 9 | var rGte = />/g; 10 | var rEnc = /[<>]/; 11 | var rDec = /<|>/; 12 | 13 | var coerceToString = function coerceToString(val) { 14 | return val ? String(val) : ''; 15 | }; 16 | 17 | exports['default'] = { 18 | encode: function () { 19 | function encode(str) { 20 | var val = coerceToString(str); 21 | 22 | if (rEnc.test(val)) { 23 | return val.replace(rLt, '<').replace(rGt, '>'); 24 | } 25 | 26 | return val; 27 | } 28 | 29 | return encode; 30 | }(), 31 | decode: function () { 32 | function decode(str) { 33 | var val = coerceToString(str); 34 | 35 | if (rDec.test(val)) { 36 | return val.replace(rLte, '<').replace(rGte, '>'); 37 | } 38 | 39 | return val; 40 | } 41 | 42 | return decode; 43 | }(), 44 | server: function () { 45 | function server(html, data, renderer) { 46 | var name = arguments.length <= 3 || arguments[3] === undefined ? '' : arguments[3]; 47 | 48 | var markup = html.reduce(function (nodes, html, i) { 49 | var key = String(name) + '_' + String(i); 50 | return nodes + renderer.markup(html, key, name); 51 | }, ''); 52 | 53 | var state = data.reduce(function (nodes, state, i) { 54 | var key = String(name) + '_' + String(i); 55 | return nodes + renderer.data(state, key, name); 56 | }, ''); 57 | 58 | return String(markup) + '\n' + String(state); 59 | } 60 | 61 | return server; 62 | }(), 63 | client: function () { 64 | function client(onNode, selector) { 65 | if (!onNode) return; 66 | 67 | var cache = selector(); 68 | 69 | Object.keys(cache).forEach(function (key) { 70 | var _cache$key = cache[key]; 71 | var state = _cache$key.state; 72 | var node = _cache$key.node; 73 | 74 | onNode(state, node, key); 75 | }); 76 | } 77 | 78 | return client; 79 | }() 80 | }; 81 | -------------------------------------------------------------------------------- /examples/iso-todomvc/js/components/Footer.react.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @jsx React.DOM 10 | */ 11 | 12 | var React = require('react'); 13 | var ReactPropTypes = React.PropTypes; 14 | var TodoActions = require('../actions/TodoActions'); 15 | 16 | var Footer = React.createClass({ 17 | 18 | propTypes: { 19 | allTodos: ReactPropTypes.object.isRequired 20 | }, 21 | 22 | /** 23 | * @return {object} 24 | */ 25 | render: function() { 26 | var allTodos = this.props.allTodos; 27 | var total = Object.keys(allTodos).length; 28 | 29 | if (total === 0) { 30 | return null; 31 | } 32 | 33 | var completed = 0; 34 | for (var key in allTodos) { 35 | if (allTodos[key].complete) { 36 | completed++; 37 | } 38 | } 39 | 40 | var itemsLeft = total - completed; 41 | var itemsLeftPhrase = itemsLeft === 1 ? ' item ' : ' items '; 42 | itemsLeftPhrase += 'left'; 43 | 44 | // Undefined and thus not rendered if no completed items are left. 45 | var clearCompletedButton; 46 | if (completed) { 47 | clearCompletedButton = 48 | ; 53 | } 54 | 55 | return ( 56 |
57 | 58 | 59 | {itemsLeft} 60 | 61 | {itemsLeftPhrase} 62 | 63 | {clearCompletedButton} 64 |
65 | ); 66 | }, 67 | 68 | /** 69 | * Event handler to delete all completed TODOs 70 | */ 71 | _onClearCompletedClick: function() { 72 | TodoActions.destroyCompleted(); 73 | } 74 | 75 | }); 76 | 77 | module.exports = Footer; 78 | -------------------------------------------------------------------------------- /examples/iso-todomvc/js/components/TodoTextInput.react.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @jsx React.DOM 10 | */ 11 | 12 | var React = require('react'); 13 | var ReactPropTypes = React.PropTypes; 14 | 15 | var ENTER_KEY_CODE = 13; 16 | 17 | var TodoTextInput = React.createClass({ 18 | 19 | propTypes: { 20 | className: ReactPropTypes.string, 21 | id: ReactPropTypes.string, 22 | placeholder: ReactPropTypes.string, 23 | onSave: ReactPropTypes.func.isRequired, 24 | value: ReactPropTypes.string 25 | }, 26 | 27 | getInitialState: function() { 28 | return { 29 | value: this.props.value || '' 30 | }; 31 | }, 32 | 33 | /** 34 | * @return {object} 35 | */ 36 | render: function() /*object*/ { 37 | return ( 38 | 48 | ); 49 | }, 50 | 51 | /** 52 | * Invokes the callback passed in as onSave, allowing this component to be 53 | * used in different ways. 54 | */ 55 | _save: function() { 56 | this.props.onSave(this.state.value); 57 | this.setState({ 58 | value: '' 59 | }); 60 | }, 61 | 62 | /** 63 | * @param {object} event 64 | */ 65 | _onChange: function(/*object*/ event) { 66 | this.setState({ 67 | value: event.target.value 68 | }); 69 | }, 70 | 71 | /** 72 | * @param {object} event 73 | */ 74 | _onKeyDown: function(event) { 75 | if (event.keyCode === ENTER_KEY_CODE) { 76 | this._save(); 77 | } 78 | } 79 | 80 | }); 81 | 82 | module.exports = TodoTextInput; 83 | -------------------------------------------------------------------------------- /examples/react-router-flux/server.js: -------------------------------------------------------------------------------- 1 | require('node-jsx').install({ extension: '.jsx', harmony: true }) 2 | 3 | var Router = require('react-router') 4 | var React = require('react') 5 | var express = require('express') 6 | var Iso = require('../../') 7 | 8 | var routes = require('./src/routes') 9 | var alt = require('./src/alt') 10 | 11 | var app = express() 12 | 13 | // This is express boilerplate to make our bundled JS available as well 14 | // as our template 15 | var path = require('path') 16 | app.set('view engine', 'jade') 17 | app.set('views', path.join(__dirname, 'templates')) 18 | app.use('/js', express.static(path.join(__dirname, 'js'))) 19 | 20 | // Simulate an asynchronous event that takes 200ms to complete 21 | function getNameFromServer(cb) { 22 | setTimeout(function () { 23 | cb('Server') 24 | }, 200) 25 | } 26 | 27 | // Prior to running react-router we setup this route in order to handle data 28 | // fetching. We can pass data fetched via express' locals. 29 | app.get('/hello/:name?', function (req, res, next) { 30 | if (req.params.name) { 31 | res.locals.data = { HelloStore: { name: req.params.name } } 32 | next() 33 | } else { 34 | getNameFromServer(function (name) { 35 | res.locals.data = { 36 | HelloStore: { name: name } 37 | } 38 | next() 39 | }) 40 | } 41 | }) 42 | 43 | app.get('/time', function (req, res, next) { 44 | res.locals.data = { 45 | TimeStore: { time: Date.now() } 46 | } 47 | next() 48 | }) 49 | 50 | // This is where the magic happens, we take the locals data we have already 51 | // fetched and seed our stores with data. 52 | // Next we use react-router to run the URL that is provided in routes.jsx 53 | // Finally we use iso in order to render this content so it picks back up 54 | // on the client side and bootstraps the stores. 55 | app.use(function (req, res) { 56 | alt.bootstrap(JSON.stringify(res.locals.data || {})) 57 | 58 | var iso = new Iso() 59 | 60 | Router.run(routes, req.url, function (Handler) { 61 | var content = React.renderToString(React.createElement(Handler)) 62 | iso.add(content, alt.flush()) 63 | 64 | res.render('layout', { 65 | html: iso.render() 66 | }) 67 | }) 68 | }) 69 | 70 | app.listen(8080, function () { 71 | console.log('Listening on localhost:8080') 72 | }) 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Unmaintained/Deprecated 2 | 3 | Hi everyone! Airbnb was using this module for their server-rendering and client bootstrapping but have since moved to and open sourced [Hypernova](https://github.com/airbnb/hypernova) which is a service that server renders your JS views but also includes some browser JS which does the server to client bootstrapping. 4 | 5 | So this package/repo is now unmaintained and deprecated. I encourage you to check out Hypernova since it has very similar features. 6 | 7 | -- 8 | 9 | # Iso 10 | 11 | [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/goatslacker/iso?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 12 | 13 | > A helper class that allows you to hand off data from server to client. 14 | 15 | Iso is a class. You instantiate it, add your markup, add some data to go with it, and render it. 16 | On the clientside Iso picks up what you sent down and gives it back to you so you can bring your content to life. 17 | 18 | ## API 19 | 20 | ### constructor(name = '', renderer = defaultRenderer) 21 | 22 | The constructor takes in a `name` which is then used to build up a unique key for every added html, 23 | and a `renderer` which is used to determine how the data is prestented to the client. By default 24 | the renderer renders the markup into a div and the data into a script tag. 25 | 26 | ### Iso#add(html: string, data: ?object): this 27 | 28 | You provide the markup to `add` and some data you wish to pass down, and iso will save it internally. 29 | 30 | ### Iso#render(): string 31 | 32 | Once you're ready to collect your html you call `render` and a string will be returned to you. 33 | 34 | ### Iso.bootstrap(onNode: function, selector: function) 35 | 36 | `onNode` is a function that is called with the data, and a reference to the container node on the 37 | DOM. The `selector` is a function that you can configure to find the state and nodes on the DOM 38 | and return them. 39 | 40 | The returned payload from `selector` should be an Object which contains the state and node pair 41 | for each unique key. 42 | 43 | ```js 44 | { 45 | "foobar": { 46 | state: { name: "foo" }, 47 | node: DOMNode, 48 | }, 49 | } 50 | ``` 51 | 52 | ## Usage 53 | 54 | Sample: 55 | 56 | ```js 57 | // server.js 58 | const iso = new Iso() 59 | 60 | request.get('/', function (req, res) { 61 | iso.add('
Hello, World!
', { someSampleData: 'Hello, World!' }) 62 | res.render(iso.render()) 63 | }) 64 | 65 | // client.js 66 | Iso.bootstrap(function (state, node) { 67 | // Now I do something with this data, perhaps run it through some library and then append 68 | // the result to node? 69 | }) 70 | ``` 71 | 72 | ## License 73 | 74 | [MIT](http://josh.mit-license.org/) 75 | -------------------------------------------------------------------------------- /examples/iso-todomvc/js/components/TodoItem.react.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2014, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | * 9 | * @jsx React.DOM 10 | */ 11 | 12 | var React = require('react'); 13 | var ReactPropTypes = React.PropTypes; 14 | var TodoActions = require('../actions/TodoActions'); 15 | var TodoTextInput = require('./TodoTextInput.react'); 16 | 17 | var cx = require('react/lib/cx'); 18 | 19 | var TodoItem = React.createClass({ 20 | 21 | propTypes: { 22 | todo: ReactPropTypes.object.isRequired 23 | }, 24 | 25 | getInitialState: function() { 26 | return { 27 | isEditing: false 28 | }; 29 | }, 30 | 31 | /** 32 | * @return {object} 33 | */ 34 | render: function() { 35 | var todo = this.props.todo; 36 | 37 | var input; 38 | if (this.state.isEditing) { 39 | input = 40 | ; 45 | } 46 | 47 | // List items should get the class 'editing' when editing 48 | // and 'completed' when marked as completed. 49 | // Note that 'completed' is a classification while 'complete' is a state. 50 | // This differentiation between classification and state becomes important 51 | // in the naming of view actions toggleComplete() vs. destroyCompleted(). 52 | return ( 53 |
  • 59 |
    60 | 66 | 69 |
    71 | {input} 72 |
  • 73 | ); 74 | }, 75 | 76 | _onToggleComplete: function() { 77 | TodoActions.toggleComplete(this.props.todo.id); 78 | }, 79 | 80 | _onDoubleClick: function() { 81 | this.setState({isEditing: true}); 82 | }, 83 | 84 | /** 85 | * Event handler called within TodoTextInput. 86 | * Defining this here allows TodoTextInput to be used in multiple places 87 | * in different ways. 88 | * @param {string} text 89 | */ 90 | _onSave: function(text) { 91 | TodoActions.updateText(this.props.todo.id, text); 92 | this.setState({isEditing: false}); 93 | }, 94 | 95 | _onDestroyClick: function() { 96 | TodoActions.destroy(this.props.todo.id); 97 | } 98 | 99 | }); 100 | 101 | module.exports = TodoItem; 102 | -------------------------------------------------------------------------------- /dist/iso.min.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.Iso=e():t.Iso=e()}(this,function(){return function(t){function e(r){if(n[r])return n[r].exports;var u=n[r]={exports:{},id:r,loaded:!1};return t[r].call(u.exports,u,u.exports,e),u.loaded=!0,u.exports}var n={};return e.m=t,e.c=n,e.p="",e(0)}([function(t,e,n){t.exports=n(1)},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function u(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(e,"__esModule",{value:!0});var i=function(){function t(t,e){for(var n=0;n'+String(t)+"":""}return t}(),data:function(){function t(t,e){return t?'":""}return t}()},d=function(){var t=document.querySelectorAll("["+c+"]");return Array.prototype.reduce.call(t,function(t,e){var n=e.getAttribute(c);if(t[n]||(t[n]={}),"SCRIPT"===e.nodeName)try{var r=JSON.parse(a["default"].decode(e.innerHTML));t[n].state=r}catch(u){t[n].state={}}else t[n].node=e;return t},{})},l=function(){function t(){var e=arguments.length<=0||void 0===arguments[0]?"":arguments[0],n=arguments.length<=1||void 0===arguments[1]?f:arguments[1];u(this,t),this.name=e,this.renderer=n,this.html=[],this.data=[]}return i(t,[{key:"add",value:function(){function t(t){var e=arguments.length<=1||void 0===arguments[1]?{}:arguments[1],n=a["default"].encode(JSON.stringify(e));return this.html.push(t),this.data.push(n),this}return t}()},{key:"render",value:function(){function t(){return a["default"].server(this.html,this.data,this.renderer,this.name)}return t}()}],[{key:"render",value:function(){function e(e){var n=arguments.length<=1||void 0===arguments[1]?{}:arguments[1],r=arguments.length<=2||void 0===arguments[2]?"":arguments[2],u=arguments.length<=3||void 0===arguments[3]?f:arguments[3];return new t(r,u).add(e,n).render()}return e}()},{key:"bootstrap",value:function(){function t(t){var e=arguments.length<=1||void 0===arguments[1]?d:arguments[1];return a["default"].client(t,e)}return t}()}]),t}();e["default"]=l},function(t,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var n=//g,u=/</g,i=/>/g,o=/[<>]/,a=/<|>/,c=function(t){return t?String(t):""};e["default"]={encode:function(){function t(t){var e=c(t);return o.test(e)?e.replace(n,"<").replace(r,">"):e}return t}(),decode:function(){function t(t){var e=c(t);return a.test(e)?e.replace(u,"<").replace(i,">"):e}return t}(),server:function(){function t(t,e,n){var r=arguments.length<=3||void 0===arguments[3]?"":arguments[3],u=t.reduce(function(t,e,u){var i=String(r)+"_"+String(u);return t+n.markup(e,i,r)},""),i=e.reduce(function(t,e,u){var i=String(r)+"_"+String(u);return t+n.data(e,i,r)},"");return String(u)+"\n"+String(i)}return t}(),client:function(){function t(t,e){if(t){var n=e();Object.keys(n).forEach(function(e){var r=n[e],u=r.state,i=r.node;t(u,i,e)})}}return t}()}}])}); -------------------------------------------------------------------------------- /examples/datetime-flux/README.md: -------------------------------------------------------------------------------- 1 | # datetime-flux-iso 2 | 3 | > Isomorphic react application using flux. 4 | 5 | ## Running This 6 | 7 | ```sh 8 | npm install; npm run build; npm start 9 | ``` 10 | 11 | Then open your browser to `localhost:8080` and enjoy. 12 | 13 | ## What 14 | 15 | This is a simple ismorphic application which renders the current time and a random number sent from the server. 16 | 17 | The purpose of this application is to show how an isomorphic react application using flux could work with [iso](https://github.com/goatslacker/iso). 18 | 19 | One of the challenges with using flux isomorphically is how flux is structured. In flux, since the data only flows one way, all data changes start via the action triggers. This presents an issue on the server since actions are meant to be fire-and-forget and you can only dispatch one at a time. What this means is that an action has no callback and it's difficult to set off a chain of actions and know when they all completed. 20 | 21 | Store's themselves have listeners and you could theoretically use a store's listener along with `waitFor` to get close, but the React components usually rely on these store listeners in order to set their internal state which kicks off DOM diffing, thus making them unsuitable for usage on the server side. 22 | 23 | An isomorphic flux implementation could theoretically add callbacks to actions, but that goes against the spirit of flux, and doesn't look very sexy. 24 | 25 | ```js 26 | TimeAction.updateTime(Date.now(), function () { 27 | // do next thing... 28 | }) 29 | ``` 30 | 31 | Another challenge is that in flux stores are singletons. Pairing singleton data stores with concurrent requests is a recipe for disaster. One way of solving this dilemma is to create instances of these stores, but then the trade-off is that you're passing these instances around each component so they have a reference to the data and can use the appropriate store. This is both fragile and cumbersome. 32 | 33 | ```js 34 | class App extends React.Component { 35 | render() { 36 | return 37 | } 38 | } 39 | 40 | class TimeComponent extends React.Component { 41 | constructor(props) { 42 | this.state = props.fluxInstance.getStore('TimeStore').getState() 43 | } 44 | 45 | render() { 46 | return
    {this.state.time}
    47 | } 48 | } 49 | ``` 50 | 51 | Fortunately, flux's stores work very well when they are synchronous. This means we can seed the stores with data, render our application, and then revert the stores to their previous virgin state. [Alt](https://github.com/goatslacker/alt) is a flux implementation that facilitates this. 52 | 53 | alt uses a method called `bootstrap` which seeds the stores with data on the server, and then initializes them when the application starts on the client. Turning `TimeComponent` into something that looks a lot like plain flux. 54 | 55 | ```js 56 | // yay, references and plain old require! 57 | var TimeStore = require('../stores/TimeStore') 58 | 59 | class TimeComponent extends React.Component { 60 | constructor() { 61 | this.state = TimeStore.getState() 62 | } 63 | 64 | render() { 65 | return
    {this.state.time}
    66 | } 67 | } 68 | ``` 69 | 70 | Actions then are meant to only be used on the client-side once the application starts. On the server you can perform all the necessary data gathering, and once complete you seed the data. 71 | 72 | In this example, the random number sent from the server is in order to test flux's singleton stores. Two concurrent requests won't interfere with each other and the store's data will never collide. The time component allows you to click in order to change the time via React's onClick, this proves that the application was initialized correctly on the client. 73 | 74 | ## License 75 | 76 | [MIT](http://josh.mit-license.org/) 77 | -------------------------------------------------------------------------------- /examples/iso-todomvc/README.md: -------------------------------------------------------------------------------- 1 | # Alt TodoMVC Example 2 | 3 | > A copy of [flux-todomvc](https://github.com/facebook/flux/tree/master/examples/flux-todomvc) but using alt 4 | 5 | ## What is this? 6 | 7 | This is todomvc written to work with alt. It's mostly the same code as flux's todomvc, in fact I only changed a couple of lines in the view layer. The bulk of the changes were in the store and actions, and the removal of the dispatcher and the constants since alt handles those two for you. 8 | 9 | ## Learning Flux 10 | 11 | I won't document learning flux here, you can check out Flux's todomvc [README](https://github.com/facebook/flux/tree/master/examples/flux-todomvc/README.md) which has a great overview. Alt is essentially flux so the concepts translate over well. 12 | 13 | ## Alt and Flux 14 | 15 | Instead, I'll use this space to talk about why alt and compare it to flux. 16 | 17 | 18 | ### Folder Structure 19 | 20 | The folder structure is very similar with the difference in that alt omits the `constants` and `dispatcher` 21 | 22 | Your tree would look something like this: 23 | 24 | ``` 25 | ./ 26 | index.html 27 | js/ 28 | actions/ 29 | TodoActions.js 30 | app.js 31 | bundle.js 32 | components/ 33 | Footer.react.js 34 | Header.react.js 35 | MainSection.react.js 36 | TodoApp.react.js 37 | TodoItem.react.js 38 | TodoTextInput.react.js 39 | stores/ 40 | TodoStore.js 41 | ``` 42 | 43 | You can read more about what the rest of the files do [here](https://github.com/facebook/flux/blob/master/examples/flux-todomvc/README.md#todomvc-example-implementation). 44 | 45 | ### Terse Syntax 46 | 47 | One of the main benefits of alt is the terse syntax. The actions in flux are ~80 LOC, and the dispatcher is ~15 LOC. With alt you can write both in ~15 LOC. 48 | 49 | Here are the actions: 50 | 51 | ```js 52 | var alt = require('../alt') 53 | 54 | class TodoActions { 55 | constructor() { 56 | this.generateActions( 57 | 'create', 58 | 'updateText', 59 | 'toggleComplete', 60 | 'toggleCompleteAll', 61 | 'destroy', 62 | 'destroyCompleted' 63 | ) 64 | } 65 | } 66 | 67 | module.exports = alt.createActions(TodoActions) 68 | ``` 69 | 70 | The store on flux closk in at ~160 LOC. In alt the store is 80 LOC. 71 | 72 | Here's the store: 73 | 74 | ```js 75 | var alt = require('../alt') 76 | var merge = require('object-assign') 77 | 78 | var TodoActions = require('../actions/TodoActions') 79 | 80 | var todoStore = alt.createStore(class TodoStore { 81 | constructor() { 82 | this.bindActions(TodoActions) 83 | 84 | this.todos = {} 85 | } 86 | 87 | update(id, updates) { 88 | this.todos[id] = merge(this.todos[id], updates) 89 | } 90 | 91 | updateAll(updates) { 92 | for (var id in this.todos) { 93 | this.update(id, updates) 94 | } 95 | } 96 | 97 | onCreate(text) { 98 | text = text.trim() 99 | if (text === '') { 100 | return false 101 | } 102 | // hand waving of course. 103 | var id = (+new Date() + Math.floor(Math.random() * 999999)).toString(36) 104 | this.todos[id] = { 105 | id: id, 106 | complete: false, 107 | text: text 108 | } 109 | } 110 | 111 | onUpdateText(x) { 112 | var { id, text } = x 113 | text = text.trim() 114 | if (text === '') { 115 | return false 116 | } 117 | this.update(id, { text }) 118 | } 119 | 120 | onToggleComplete(id) { 121 | var complete = !this.todos[id].complete 122 | this.update(id, { complete }) 123 | } 124 | 125 | onToggleCompleteAll() { 126 | var complete = !todoStore.areAllComplete() 127 | this.updateAll({ complete }) 128 | } 129 | 130 | onDestroy(id) { 131 | delete this.todos[id] 132 | } 133 | 134 | onDestroyCompleted() { 135 | for (var id in this.todos) { 136 | if (this.todos[id].complete) { 137 | this.onDestroy(id) 138 | } 139 | } 140 | } 141 | 142 | static areAllComplete() { 143 | var { todos } = this.getState() 144 | for (var id in todos) { 145 | if (!todos[id].complete) { 146 | return false 147 | } 148 | } 149 | return true 150 | } 151 | }) 152 | 153 | module.exports = todoStore 154 | ``` 155 | 156 | 157 | ### Running 158 | 159 | Install the dependencies first 160 | 161 | ``` 162 | npm install 163 | ``` 164 | 165 | Build a package 166 | 167 | ``` 168 | npm run build 169 | ``` 170 | 171 | Run the server and open http://localhost:8080 in your browser 172 | 173 | ``` 174 | npm start 175 | ``` 176 | 177 | ## Credit 178 | 179 | The original flux TodoMVC application was created by [Bill Fisher](https://www.facebook.com/bill.fisher.771). All the view components and most of the rest of the code was written by Bill. The actions and stores have been alted by [Josh Perez](https://github.com/goatslacker) 180 | -------------------------------------------------------------------------------- /test/index-test.js: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai' 2 | import cheerio from 'cheerio' 3 | import Iso from '../' 4 | import { jsdom } from 'jsdom' 5 | 6 | const testState = (serverState, clientStateString) => { 7 | assert.isString(clientStateString, 'state from DOM is a string') 8 | const clientState = JSON.parse(clientStateString) 9 | 10 | Object.keys(serverState).forEach((key) => { 11 | assert(clientState[key] === serverState[key], `${key} is ${serverState[key]}`) 12 | }) 13 | } 14 | 15 | const escapeHtml = (html) => ( 16 | String(html) 17 | .replace(/&/g, '&') 18 | .replace(/"/g, '"') 19 | .replace(/'/g, ''') 20 | .replace(//g, '>') 22 | ) 23 | 24 | const customRenderer = { 25 | markup(html, key) { 26 | return `
    ${html}
    ` 27 | }, 28 | 29 | data(state, key) { 30 | const escaped = escapeHtml(state) 31 | return `
    ` 32 | }, 33 | } 34 | 35 | const getRenderedPair = (html) => { 36 | const $ = cheerio.load(html)('[data-iso-key]') 37 | const markup = $.first().text() 38 | const clientState = $.next().text() 39 | 40 | return { $, markup, clientState } 41 | } 42 | 43 | export default { 44 | 'html and state render using add': () => { 45 | const iso = new Iso('feature') 46 | const serverState = { 47 | foo: true, 48 | } 49 | 50 | iso.add('Hello World', serverState) 51 | 52 | const html = iso.render() 53 | 54 | const { markup, clientState } = getRenderedPair(html) 55 | 56 | assert(markup === 'Hello World', 'markup is correct') 57 | 58 | testState(serverState, clientState) 59 | }, 60 | 61 | 'Iso.render': () => { 62 | const serverState = { 63 | hello: 'World', 64 | } 65 | const html = Iso.render('test', serverState, 'hello-world') 66 | 67 | const { $, markup, clientState } = getRenderedPair(html) 68 | 69 | testState(serverState, clientState) 70 | 71 | assert($.attr('data-iso-key') === 'hello-world_0', 'the name is included in the iso key') 72 | 73 | assert(markup === 'test', 'html was rendered on page') 74 | }, 75 | 76 | 'Iso.render with a custom renderer': () => { 77 | const serverState = { 78 | goatslacker: 'iso', 79 | } 80 | 81 | const html = Iso.render('foo bar bear', serverState, 'custom-markup', customRenderer) 82 | 83 | const { $, markup, clientState } = getRenderedPair(html) 84 | 85 | assert($.first().attr('class') === 'iso-markup', 'the class name was inserted') 86 | assert($.next().attr('class') === 'iso-state', 'the class name was inserted') 87 | 88 | assert($.attr('data-iso-key') === 'custom-markup_0', 'the name is included in the iso key') 89 | 90 | assert(markup === 'foo bar bear', 'html was rendered on page') 91 | }, 92 | 93 | 'bootstrap': (done) => { 94 | const serverState = { foo: 'bar' } 95 | const html = '

    It works!

    ' 96 | 97 | const markup = Iso.render(html, serverState) 98 | global.document = jsdom(markup) 99 | 100 | Iso.bootstrap((state, node) => { 101 | assert(state.foo === 'bar', 'the state is in the DOM correctly') 102 | assert(node.innerHTML === html, 'the html was retrieved correctly') 103 | 104 | delete global.document 105 | done() 106 | }) 107 | }, 108 | 109 | 'bootstrap with custom selector': (done) => { 110 | const serverState = { foo: 'bar' } 111 | const html = '

    It works!

    ' 112 | 113 | const markup = Iso.render(html, serverState, '', customRenderer) 114 | 115 | global.document = jsdom(markup) 116 | 117 | Iso.bootstrap((state, node, key) => { 118 | assert(state.foo === 'bar', 'the state is in the DOM correctly') 119 | assert(node.innerHTML === html, 'the html was retrieved correctly') 120 | assert(key === 'MyComponent', 'the custom key was sent down') 121 | 122 | delete global.document 123 | done() 124 | }, () => { 125 | const node = document.querySelectorAll('.iso-markup')[0] 126 | const state = JSON.parse( 127 | document.querySelectorAll('.iso-state')[0].getAttribute('data-iso-state') 128 | ) 129 | 130 | return { 131 | MyComponent: { node, state } 132 | } 133 | }) 134 | }, 135 | 136 | 'bad state': (done) => { 137 | const markup = ` 138 |
    139 | 140 | ` 141 | 142 | global.document = jsdom(markup) 143 | 144 | Iso.bootstrap((state, node, key) => { 145 | assert(Object.keys(state).length === 0, 'empty object returned') 146 | delete global.document 147 | done() 148 | }) 149 | }, 150 | 151 | 'state that contains ': (done) => { 152 | const serverState = { foo: '' } 153 | const html = '

    It works!

    ' 154 | 155 | const markup = Iso.render(html, serverState) 156 | global.document = jsdom(markup) 157 | 158 | Iso.bootstrap((state, node) => { 159 | assert(state.foo === '', 'the state was properly decoded') 160 | assert(node.innerHTML === html, 'the html was retrieved correctly') 161 | 162 | delete global.document 163 | done() 164 | }) 165 | }, 166 | } 167 | -------------------------------------------------------------------------------- /dist/iso.js: -------------------------------------------------------------------------------- 1 | (function webpackUniversalModuleDefinition(root, factory) { 2 | if(typeof exports === 'object' && typeof module === 'object') 3 | module.exports = factory(); 4 | else if(typeof define === 'function' && define.amd) 5 | define([], factory); 6 | else if(typeof exports === 'object') 7 | exports["Iso"] = factory(); 8 | else 9 | root["Iso"] = factory(); 10 | })(this, function() { 11 | return /******/ (function(modules) { // webpackBootstrap 12 | /******/ // The module cache 13 | /******/ var installedModules = {}; 14 | 15 | /******/ // The require function 16 | /******/ function __webpack_require__(moduleId) { 17 | 18 | /******/ // Check if module is in cache 19 | /******/ if(installedModules[moduleId]) 20 | /******/ return installedModules[moduleId].exports; 21 | 22 | /******/ // Create a new module (and put it into the cache) 23 | /******/ var module = installedModules[moduleId] = { 24 | /******/ exports: {}, 25 | /******/ id: moduleId, 26 | /******/ loaded: false 27 | /******/ }; 28 | 29 | /******/ // Execute the module function 30 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 31 | 32 | /******/ // Flag the module as loaded 33 | /******/ module.loaded = true; 34 | 35 | /******/ // Return the exports of the module 36 | /******/ return module.exports; 37 | /******/ } 38 | 39 | 40 | /******/ // expose the modules object (__webpack_modules__) 41 | /******/ __webpack_require__.m = modules; 42 | 43 | /******/ // expose the module cache 44 | /******/ __webpack_require__.c = installedModules; 45 | 46 | /******/ // __webpack_public_path__ 47 | /******/ __webpack_require__.p = ""; 48 | 49 | /******/ // Load entry module and return exports 50 | /******/ return __webpack_require__(0); 51 | /******/ }) 52 | /************************************************************************/ 53 | /******/ ([ 54 | /* 0 */ 55 | /***/ function(module, exports, __webpack_require__) { 56 | 57 | module.exports = __webpack_require__(1); 58 | 59 | 60 | /***/ }, 61 | /* 1 */ 62 | /***/ function(module, exports, __webpack_require__) { 63 | 64 | 'use strict'; 65 | 66 | Object.defineProperty(exports, "__esModule", { 67 | value: true 68 | }); 69 | 70 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 71 | 72 | var _core = __webpack_require__(2); 73 | 74 | var _core2 = _interopRequireDefault(_core); 75 | 76 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 77 | 78 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 79 | 80 | var KEY_NAME = 'data-iso-key'; 81 | 82 | var defaultRenderer = { 83 | markup: function () { 84 | function markup(html, key) { 85 | if (!html) return ''; 86 | return '
    ' + String(html) + '
    '; 87 | } 88 | 89 | return markup; 90 | }(), 91 | data: function () { 92 | function data(state, key) { 93 | if (!state) return ''; 94 | return ''; 95 | } 96 | 97 | return data; 98 | }() 99 | }; 100 | 101 | var defaultSelector = function defaultSelector() { 102 | var all = document.querySelectorAll('[' + KEY_NAME + ']'); 103 | 104 | return Array.prototype.reduce.call(all, function (cache, node) { 105 | var key = node.getAttribute(KEY_NAME); 106 | 107 | if (!cache[key]) cache[key] = {}; 108 | 109 | if (node.nodeName === 'SCRIPT') { 110 | try { 111 | var state = JSON.parse(_core2['default'].decode(node.innerHTML)); 112 | cache[key].state = state; 113 | } catch (e) { 114 | cache[key].state = {}; 115 | } 116 | } else { 117 | cache[key].node = node; 118 | } 119 | 120 | return cache; 121 | }, {}); 122 | }; 123 | 124 | var Iso = function () { 125 | function Iso() { 126 | var name = arguments.length <= 0 || arguments[0] === undefined ? '' : arguments[0]; 127 | var renderer = arguments.length <= 1 || arguments[1] === undefined ? defaultRenderer : arguments[1]; 128 | 129 | _classCallCheck(this, Iso); 130 | 131 | this.name = name; 132 | this.renderer = renderer; 133 | this.html = []; 134 | this.data = []; 135 | } 136 | 137 | _createClass(Iso, [{ 138 | key: 'add', 139 | value: function () { 140 | function add(html) { 141 | var _state = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; 142 | 143 | var state = _core2['default'].encode(JSON.stringify(_state)); 144 | this.html.push(html); 145 | this.data.push(state); 146 | return this; 147 | } 148 | 149 | return add; 150 | }() 151 | }, { 152 | key: 'render', 153 | value: function () { 154 | function render() { 155 | return _core2['default'].server(this.html, this.data, this.renderer, this.name); 156 | } 157 | 158 | return render; 159 | }() 160 | }], [{ 161 | key: 'render', 162 | value: function () { 163 | function render(html) { 164 | var state = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; 165 | var name = arguments.length <= 2 || arguments[2] === undefined ? '' : arguments[2]; 166 | var renderer = arguments.length <= 3 || arguments[3] === undefined ? defaultRenderer : arguments[3]; 167 | 168 | return new Iso(name, renderer).add(html, state).render(); 169 | } 170 | 171 | return render; 172 | }() 173 | }, { 174 | key: 'bootstrap', 175 | value: function () { 176 | function bootstrap(onNode) { 177 | var selector = arguments.length <= 1 || arguments[1] === undefined ? defaultSelector : arguments[1]; 178 | 179 | return _core2['default'].client(onNode, selector); 180 | } 181 | 182 | return bootstrap; 183 | }() 184 | }]); 185 | 186 | return Iso; 187 | }(); 188 | 189 | exports['default'] = Iso; 190 | 191 | /***/ }, 192 | /* 2 */ 193 | /***/ function(module, exports) { 194 | 195 | 'use strict'; 196 | 197 | Object.defineProperty(exports, "__esModule", { 198 | value: true 199 | }); 200 | var rLt = //g; 202 | var rLte = /</g; 203 | var rGte = />/g; 204 | var rEnc = /[<>]/; 205 | var rDec = /<|>/; 206 | 207 | var coerceToString = function coerceToString(val) { 208 | return val ? String(val) : ''; 209 | }; 210 | 211 | exports['default'] = { 212 | encode: function () { 213 | function encode(str) { 214 | var val = coerceToString(str); 215 | 216 | if (rEnc.test(val)) { 217 | return val.replace(rLt, '<').replace(rGt, '>'); 218 | } 219 | 220 | return val; 221 | } 222 | 223 | return encode; 224 | }(), 225 | decode: function () { 226 | function decode(str) { 227 | var val = coerceToString(str); 228 | 229 | if (rDec.test(val)) { 230 | return val.replace(rLte, '<').replace(rGte, '>'); 231 | } 232 | 233 | return val; 234 | } 235 | 236 | return decode; 237 | }(), 238 | server: function () { 239 | function server(html, data, renderer) { 240 | var name = arguments.length <= 3 || arguments[3] === undefined ? '' : arguments[3]; 241 | 242 | var markup = html.reduce(function (nodes, html, i) { 243 | var key = String(name) + '_' + String(i); 244 | return nodes + renderer.markup(html, key, name); 245 | }, ''); 246 | 247 | var state = data.reduce(function (nodes, state, i) { 248 | var key = String(name) + '_' + String(i); 249 | return nodes + renderer.data(state, key, name); 250 | }, ''); 251 | 252 | return String(markup) + '\n' + String(state); 253 | } 254 | 255 | return server; 256 | }(), 257 | client: function () { 258 | function client(onNode, selector) { 259 | if (!onNode) return; 260 | 261 | var cache = selector(); 262 | 263 | Object.keys(cache).forEach(function (key) { 264 | var _cache$key = cache[key]; 265 | var state = _cache$key.state; 266 | var node = _cache$key.node; 267 | 268 | onNode(state, node, key); 269 | }); 270 | } 271 | 272 | return client; 273 | }() 274 | }; 275 | 276 | /***/ } 277 | /******/ ]) 278 | }); 279 | ; -------------------------------------------------------------------------------- /examples/iso-todomvc/todomvc-common/base.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | button { 8 | margin: 0; 9 | padding: 0; 10 | border: 0; 11 | background: none; 12 | font-size: 100%; 13 | vertical-align: baseline; 14 | font-family: inherit; 15 | color: inherit; 16 | -webkit-appearance: none; 17 | -ms-appearance: none; 18 | -o-appearance: none; 19 | appearance: none; 20 | } 21 | 22 | body { 23 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; 24 | line-height: 1.4em; 25 | background: #eaeaea url('bg.png'); 26 | color: #4d4d4d; 27 | width: 550px; 28 | margin: 0 auto; 29 | -webkit-font-smoothing: antialiased; 30 | -moz-font-smoothing: antialiased; 31 | -ms-font-smoothing: antialiased; 32 | -o-font-smoothing: antialiased; 33 | font-smoothing: antialiased; 34 | } 35 | 36 | button, 37 | input[type="checkbox"] { 38 | outline: none; 39 | } 40 | 41 | #todoapp { 42 | background: #fff; 43 | background: rgba(255, 255, 255, 0.9); 44 | margin: 130px 0 40px 0; 45 | border: 1px solid #ccc; 46 | position: relative; 47 | border-top-left-radius: 2px; 48 | border-top-right-radius: 2px; 49 | box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.2), 50 | 0 25px 50px 0 rgba(0, 0, 0, 0.15); 51 | } 52 | 53 | #todoapp:before { 54 | content: ''; 55 | border-left: 1px solid #f5d6d6; 56 | border-right: 1px solid #f5d6d6; 57 | width: 2px; 58 | position: absolute; 59 | top: 0; 60 | left: 40px; 61 | height: 100%; 62 | } 63 | 64 | #todoapp input::-webkit-input-placeholder { 65 | font-style: italic; 66 | } 67 | 68 | #todoapp input::-moz-placeholder { 69 | font-style: italic; 70 | color: #a9a9a9; 71 | } 72 | 73 | #todoapp h1 { 74 | position: absolute; 75 | top: -120px; 76 | width: 100%; 77 | font-size: 70px; 78 | font-weight: bold; 79 | text-align: center; 80 | color: #b3b3b3; 81 | color: rgba(255, 255, 255, 0.3); 82 | text-shadow: -1px -1px rgba(0, 0, 0, 0.2); 83 | -webkit-text-rendering: optimizeLegibility; 84 | -moz-text-rendering: optimizeLegibility; 85 | -ms-text-rendering: optimizeLegibility; 86 | -o-text-rendering: optimizeLegibility; 87 | text-rendering: optimizeLegibility; 88 | } 89 | 90 | #header { 91 | padding-top: 15px; 92 | border-radius: inherit; 93 | } 94 | 95 | #header:before { 96 | content: ''; 97 | position: absolute; 98 | top: 0; 99 | right: 0; 100 | left: 0; 101 | height: 15px; 102 | z-index: 2; 103 | border-bottom: 1px solid #6c615c; 104 | background: #8d7d77; 105 | background: -webkit-gradient(linear, left top, left bottom, from(rgba(132, 110, 100, 0.8)),to(rgba(101, 84, 76, 0.8))); 106 | background: -webkit-linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8)); 107 | background: linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8)); 108 | filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr='#9d8b83', EndColorStr='#847670'); 109 | border-top-left-radius: 1px; 110 | border-top-right-radius: 1px; 111 | } 112 | 113 | #new-todo, 114 | .edit { 115 | position: relative; 116 | margin: 0; 117 | width: 100%; 118 | font-size: 24px; 119 | font-family: inherit; 120 | line-height: 1.4em; 121 | border: 0; 122 | outline: none; 123 | color: inherit; 124 | padding: 6px; 125 | border: 1px solid #999; 126 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); 127 | -moz-box-sizing: border-box; 128 | -ms-box-sizing: border-box; 129 | -o-box-sizing: border-box; 130 | box-sizing: border-box; 131 | -webkit-font-smoothing: antialiased; 132 | -moz-font-smoothing: antialiased; 133 | -ms-font-smoothing: antialiased; 134 | -o-font-smoothing: antialiased; 135 | font-smoothing: antialiased; 136 | } 137 | 138 | #new-todo { 139 | padding: 16px 16px 16px 60px; 140 | border: none; 141 | background: rgba(0, 0, 0, 0.02); 142 | z-index: 2; 143 | box-shadow: none; 144 | } 145 | 146 | #main { 147 | position: relative; 148 | z-index: 2; 149 | border-top: 1px dotted #adadad; 150 | } 151 | 152 | label[for='toggle-all'] { 153 | display: none; 154 | } 155 | 156 | #toggle-all { 157 | position: absolute; 158 | top: -42px; 159 | left: -4px; 160 | width: 40px; 161 | text-align: center; 162 | /* Mobile Safari */ 163 | border: none; 164 | } 165 | 166 | #toggle-all:before { 167 | content: '»'; 168 | font-size: 28px; 169 | color: #d9d9d9; 170 | padding: 0 25px 7px; 171 | } 172 | 173 | #toggle-all:checked:before { 174 | color: #737373; 175 | } 176 | 177 | #todo-list { 178 | margin: 0; 179 | padding: 0; 180 | list-style: none; 181 | } 182 | 183 | #todo-list li { 184 | position: relative; 185 | font-size: 24px; 186 | border-bottom: 1px dotted #ccc; 187 | } 188 | 189 | #todo-list li:last-child { 190 | border-bottom: none; 191 | } 192 | 193 | #todo-list li.editing { 194 | border-bottom: none; 195 | padding: 0; 196 | } 197 | 198 | #todo-list li.editing .edit { 199 | display: block; 200 | width: 506px; 201 | padding: 13px 17px 12px 17px; 202 | margin: 0 0 0 43px; 203 | } 204 | 205 | #todo-list li.editing .view { 206 | display: none; 207 | } 208 | 209 | #todo-list li .toggle { 210 | text-align: center; 211 | width: 40px; 212 | /* auto, since non-WebKit browsers doesn't support input styling */ 213 | height: auto; 214 | position: absolute; 215 | top: 0; 216 | bottom: 0; 217 | margin: auto 0; 218 | /* Mobile Safari */ 219 | border: none; 220 | -webkit-appearance: none; 221 | -ms-appearance: none; 222 | -o-appearance: none; 223 | appearance: none; 224 | } 225 | 226 | #todo-list li .toggle:after { 227 | content: '✔'; 228 | /* 40 + a couple of pixels visual adjustment */ 229 | line-height: 43px; 230 | font-size: 20px; 231 | color: #d9d9d9; 232 | text-shadow: 0 -1px 0 #bfbfbf; 233 | } 234 | 235 | #todo-list li .toggle:checked:after { 236 | color: #85ada7; 237 | text-shadow: 0 1px 0 #669991; 238 | bottom: 1px; 239 | position: relative; 240 | } 241 | 242 | #todo-list li label { 243 | white-space: pre; 244 | word-break: break-word; 245 | padding: 15px 60px 15px 15px; 246 | margin-left: 45px; 247 | display: block; 248 | line-height: 1.2; 249 | -webkit-transition: color 0.4s; 250 | transition: color 0.4s; 251 | } 252 | 253 | #todo-list li.completed label { 254 | color: #a9a9a9; 255 | text-decoration: line-through; 256 | } 257 | 258 | #todo-list li .destroy { 259 | display: none; 260 | position: absolute; 261 | top: 0; 262 | right: 10px; 263 | bottom: 0; 264 | width: 40px; 265 | height: 40px; 266 | margin: auto 0; 267 | font-size: 22px; 268 | color: #a88a8a; 269 | -webkit-transition: all 0.2s; 270 | transition: all 0.2s; 271 | } 272 | 273 | #todo-list li .destroy:hover { 274 | text-shadow: 0 0 1px #000, 275 | 0 0 10px rgba(199, 107, 107, 0.8); 276 | -webkit-transform: scale(1.3); 277 | -ms-transform: scale(1.3); 278 | transform: scale(1.3); 279 | } 280 | 281 | #todo-list li .destroy:after { 282 | content: '✖'; 283 | } 284 | 285 | #todo-list li:hover .destroy { 286 | display: block; 287 | } 288 | 289 | #todo-list li .edit { 290 | display: none; 291 | } 292 | 293 | #todo-list li.editing:last-child { 294 | margin-bottom: -1px; 295 | } 296 | 297 | #footer { 298 | color: #777; 299 | padding: 0 15px; 300 | position: absolute; 301 | right: 0; 302 | bottom: -31px; 303 | left: 0; 304 | height: 20px; 305 | z-index: 1; 306 | text-align: center; 307 | } 308 | 309 | #footer:before { 310 | content: ''; 311 | position: absolute; 312 | right: 0; 313 | bottom: 31px; 314 | left: 0; 315 | height: 50px; 316 | z-index: -1; 317 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.3), 318 | 0 6px 0 -3px rgba(255, 255, 255, 0.8), 319 | 0 7px 1px -3px rgba(0, 0, 0, 0.3), 320 | 0 43px 0 -6px rgba(255, 255, 255, 0.8), 321 | 0 44px 2px -6px rgba(0, 0, 0, 0.2); 322 | } 323 | 324 | #todo-count { 325 | float: left; 326 | text-align: left; 327 | } 328 | 329 | #filters { 330 | margin: 0; 331 | padding: 0; 332 | list-style: none; 333 | position: absolute; 334 | right: 0; 335 | left: 0; 336 | } 337 | 338 | #filters li { 339 | display: inline; 340 | } 341 | 342 | #filters li a { 343 | color: #83756f; 344 | margin: 2px; 345 | text-decoration: none; 346 | } 347 | 348 | #filters li a.selected { 349 | font-weight: bold; 350 | } 351 | 352 | #clear-completed { 353 | float: right; 354 | position: relative; 355 | line-height: 20px; 356 | text-decoration: none; 357 | background: rgba(0, 0, 0, 0.1); 358 | font-size: 11px; 359 | padding: 0 10px; 360 | border-radius: 3px; 361 | box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.2); 362 | } 363 | 364 | #clear-completed:hover { 365 | background: rgba(0, 0, 0, 0.15); 366 | box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.3); 367 | } 368 | 369 | #info { 370 | margin: 65px auto 0; 371 | color: #a6a6a6; 372 | font-size: 12px; 373 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.7); 374 | text-align: center; 375 | } 376 | 377 | #info a { 378 | color: inherit; 379 | } 380 | 381 | /* 382 | Hack to remove background from Mobile Safari. 383 | Can't use it globally since it destroys checkboxes in Firefox and Opera 384 | */ 385 | 386 | @media screen and (-webkit-min-device-pixel-ratio:0) { 387 | #toggle-all, 388 | #todo-list li .toggle { 389 | background: none; 390 | } 391 | 392 | #todo-list li .toggle { 393 | height: 40px; 394 | } 395 | 396 | #toggle-all { 397 | top: -56px; 398 | left: -15px; 399 | width: 65px; 400 | height: 41px; 401 | -webkit-transform: rotate(90deg); 402 | -ms-transform: rotate(90deg); 403 | transform: rotate(90deg); 404 | -webkit-appearance: none; 405 | appearance: none; 406 | } 407 | } 408 | 409 | .hidden { 410 | display: none; 411 | } 412 | 413 | hr { 414 | margin: 20px 0; 415 | border: 0; 416 | border-top: 1px dashed #C5C5C5; 417 | border-bottom: 1px dashed #F7F7F7; 418 | } 419 | 420 | .learn a { 421 | font-weight: normal; 422 | text-decoration: none; 423 | color: #b83f45; 424 | } 425 | 426 | .learn a:hover { 427 | text-decoration: underline; 428 | color: #787e7e; 429 | } 430 | 431 | .learn h3, 432 | .learn h4, 433 | .learn h5 { 434 | margin: 10px 0; 435 | font-weight: 500; 436 | line-height: 1.2; 437 | color: #000; 438 | } 439 | 440 | .learn h3 { 441 | font-size: 24px; 442 | } 443 | 444 | .learn h4 { 445 | font-size: 18px; 446 | } 447 | 448 | .learn h5 { 449 | margin-bottom: 0; 450 | font-size: 14px; 451 | } 452 | 453 | .learn ul { 454 | padding: 0; 455 | margin: 0 0 30px 25px; 456 | } 457 | 458 | .learn li { 459 | line-height: 20px; 460 | } 461 | 462 | .learn p { 463 | font-size: 15px; 464 | font-weight: 300; 465 | line-height: 1.3; 466 | margin-top: 0; 467 | margin-bottom: 0; 468 | } 469 | 470 | .quote { 471 | border: none; 472 | margin: 20px 0 60px 0; 473 | } 474 | 475 | .quote p { 476 | font-style: italic; 477 | } 478 | 479 | .quote p:before { 480 | content: '“'; 481 | font-size: 50px; 482 | opacity: .15; 483 | position: absolute; 484 | top: -20px; 485 | left: 3px; 486 | } 487 | 488 | .quote p:after { 489 | content: '”'; 490 | font-size: 50px; 491 | opacity: .15; 492 | position: absolute; 493 | bottom: -42px; 494 | right: 3px; 495 | } 496 | 497 | .quote footer { 498 | position: absolute; 499 | bottom: -40px; 500 | right: 0; 501 | } 502 | 503 | .quote footer img { 504 | border-radius: 3px; 505 | } 506 | 507 | .quote footer a { 508 | margin-left: 5px; 509 | vertical-align: middle; 510 | } 511 | 512 | .speech-bubble { 513 | position: relative; 514 | padding: 10px; 515 | background: rgba(0, 0, 0, .04); 516 | border-radius: 5px; 517 | } 518 | 519 | .speech-bubble:after { 520 | content: ''; 521 | position: absolute; 522 | top: 100%; 523 | right: 30px; 524 | border: 13px solid transparent; 525 | border-top-color: rgba(0, 0, 0, .04); 526 | } 527 | 528 | .learn-bar > .learn { 529 | position: absolute; 530 | width: 272px; 531 | top: 8px; 532 | left: -300px; 533 | padding: 10px; 534 | border-radius: 5px; 535 | background-color: rgba(255, 255, 255, .6); 536 | -webkit-transition-property: left; 537 | transition-property: left; 538 | -webkit-transition-duration: 500ms; 539 | transition-duration: 500ms; 540 | } 541 | 542 | @media (min-width: 899px) { 543 | .learn-bar { 544 | width: auto; 545 | margin: 0 0 0 300px; 546 | } 547 | 548 | .learn-bar > .learn { 549 | left: 8px; 550 | } 551 | 552 | .learn-bar #todoapp { 553 | width: 550px; 554 | margin: 130px auto 40px auto; 555 | } 556 | } 557 | --------------------------------------------------------------------------------