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 |
24 |
todos
25 |
30 |
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 |
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 | [](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 |
70 |
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 = '