diff --git a/.eslintrc b/.eslintrc index de698e0a..d24a669e 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,6 +1,7 @@ { "env": { "node": true, + "browser": true, "es6": true }, "ecmaFeatures": { diff --git a/dist/alt-with-addons.js b/dist/alt-with-addons.js index 48930912..5510a5d2 100644 --- a/dist/alt-with-addons.js +++ b/dist/alt-with-addons.js @@ -1568,18 +1568,24 @@ var StoreMixin = { return x; }; + var makeActionHandler = function makeActionHandler(action) { + return function (x) { + var fire = function fire() { + loadCounter -= 1; + action(intercept(x, action, args)); + }; + return typeof window === 'undefined' ? function () { + return fire(); + } : fire(); + }; + }; + // if we don't have it in cache then fetch it if (shouldFetch) { loadCounter += 1; /* istanbul ignore else */ if (spec.loading) spec.loading(intercept(null, spec.loading, args)); - spec.remote.apply(spec, [state].concat(args)).then(function (v) { - loadCounter -= 1; - spec.success(intercept(v, spec.success, args)); - })['catch'](function (v) { - loadCounter -= 1; - spec.error(intercept(v, spec.error, args)); - }); + return spec.remote.apply(spec, [state].concat(args)).then(makeActionHandler(spec.success))['catch'](makeActionHandler(spec.error)); } else { // otherwise emit the change now _this.emitChange(); diff --git a/dist/alt.js b/dist/alt.js index 4e010ec8..5a543585 100644 --- a/dist/alt.js +++ b/dist/alt.js @@ -986,18 +986,24 @@ var StoreMixin = { return x; }; + var makeActionHandler = function makeActionHandler(action) { + return function (x) { + var fire = function fire() { + loadCounter -= 1; + action(intercept(x, action, args)); + }; + return typeof window === 'undefined' ? function () { + return fire(); + } : fire(); + }; + }; + // if we don't have it in cache then fetch it if (shouldFetch) { loadCounter += 1; /* istanbul ignore else */ if (spec.loading) spec.loading(intercept(null, spec.loading, args)); - spec.remote.apply(spec, [state].concat(args)).then(function (v) { - loadCounter -= 1; - spec.success(intercept(v, spec.success, args)); - })['catch'](function (v) { - loadCounter -= 1; - spec.error(intercept(v, spec.error, args)); - }); + return spec.remote.apply(spec, [state].concat(args)).then(makeActionHandler(spec.success))['catch'](makeActionHandler(spec.error)); } else { // otherwise emit the change now _this.emitChange(); diff --git a/src/alt/store/StoreMixin.js b/src/alt/store/StoreMixin.js index 63e58677..4a438f2f 100644 --- a/src/alt/store/StoreMixin.js +++ b/src/alt/store/StoreMixin.js @@ -51,20 +51,24 @@ const StoreMixin = { : value == null const intercept = spec.interceptResponse || (x => x) + const makeActionHandler = (action) => { + return (x) => { + const fire = () => { + loadCounter -= 1 + action(intercept(x, action, args)) + } + return typeof window === 'undefined' ? (() => fire()) : fire() + } + } + // if we don't have it in cache then fetch it if (shouldFetch) { loadCounter += 1 /* istanbul ignore else */ if (spec.loading) spec.loading(intercept(null, spec.loading, args)) - spec.remote(state, ...args) - .then((v) => { - loadCounter -= 1 - spec.success(intercept(v, spec.success, args)) - }) - .catch((v) => { - loadCounter -= 1 - spec.error(intercept(v, spec.error, args)) - }) + return spec.remote(state, ...args) + .then(makeActionHandler(spec.success)) + .catch(makeActionHandler(spec.error)) } else { // otherwise emit the change now this.emitChange() diff --git a/src/utils/AltIso.js b/src/utils/AltIso.js new file mode 100644 index 00000000..2a40d493 --- /dev/null +++ b/src/utils/AltIso.js @@ -0,0 +1,24 @@ +import Iso from 'iso' +import * as Render from './Render' + +export default { + define: Render.withData, + + render(alt, Component, props) { + // recycle state + alt.recycle() + + if (typeof window === 'undefined') { + return Render.toString(Component, props).then((markup) => { + return Iso.render(markup, alt.takeSnapshot()) + }) + } else { + return Promise.resolve( + Iso.bootstrap((state, _, node) => { + alt.bootstrap(state) + Render.toDOM(Component, props, node) + }) + ) + } + } +} diff --git a/src/utils/Render.js b/src/utils/Render.js new file mode 100644 index 00000000..7fe9c3d2 --- /dev/null +++ b/src/utils/Render.js @@ -0,0 +1,124 @@ +import React from 'react' + +export function withData(fetch, MaybeComponent) { + function bind(Component) { + return React.createClass({ + contextTypes: { + buffer: React.PropTypes.object.isRequired + }, + + childContextTypes: { + buffer: React.PropTypes.object.isRequired + }, + + getChildContext() { + return { buffer: this.context.buffer } + }, + + componentWillMount() { + if (!this.context.buffer.locked) { + this.context.buffer.push( + fetch(this.props) + ) + } + }, + + render() { + return this.context.buffer.locked + ? React.createElement(Component, this.props) + : null + } + }) + } + + // works as a decorator or as a function + return MaybeComponent ? bind(MaybeComponent) : Component => bind(Component) +} + +function usingDispatchBuffer(buffer, Component) { + return React.createClass({ + childContextTypes: { + buffer: React.PropTypes.object.isRequired + }, + + getChildContext() { + return { buffer } + }, + + render() { + return React.createElement(Component, this.props) + } + }) +} + +class DispatchBuffer { + constructor(renderStrategy) { + this.promisesBuffer = [] + this.locked = false + this.renderStrategy = renderStrategy + } + + push(v) { + this.promisesBuffer.push(v) + } + + fill(Element) { + return this.renderStrategy(Element) + } + + clear() { + this.promisesBuffer = [] + } + + flush(Element) { + return Promise.all(this.promisesBuffer).then((data) => { + // fire off all the actions synchronously + data.forEach((f) => { + if (Array.isArray(f)) { + f.forEach(x => x()) + } else { + f() + } + }) + this.locked = true + + return this.renderStrategy(Element) + }).catch(() => { + // if there's an error still render the markup with what we've got. + return this.renderStrategy(Element) + }) + } +} + + +function renderWithStrategy(strategy) { + return (Component, props) => { + // create a buffer and use context to pass it through to the components + const buffer = new DispatchBuffer((Node) => { + return React[strategy](Node) + }) + const Container = usingDispatchBuffer(buffer, Component) + + // cache the element + const Element = React.createElement(Container, props) + + // render so we kick things off and get the props + buffer.fill(Element) + + // flush out the results in the buffer synchronously setting the store + // state and returning the markup + return buffer.flush(Element) + } +} + +export function toDOM(Component, props, documentNode) { + const buffer = new DispatchBuffer() + buffer.locked = true + const Node = usingDispatchBuffer(buffer, Component) + const Element = React.createElement(Node, props) + buffer.clear() + return React.render(Element, documentNode) +} + +export const toStaticMarkup = renderWithStrategy('renderToStaticMarkup') +export const toString = renderWithStrategy('renderToString') diff --git a/test/alt-iso-browser-test.js b/test/alt-iso-browser-test.js new file mode 100644 index 00000000..2d64b63a --- /dev/null +++ b/test/alt-iso-browser-test.js @@ -0,0 +1,147 @@ +import { jsdom } from 'jsdom' +import React from 'react' +import Alt from '../' +import AltContainer from '../AltContainer' +import AltIso from '../utils/AltIso' +import { assert } from 'chai' + +const alt = new Alt() + +const UserActions = alt.generateActions('receivedUser', 'failed') + +const UserSource = { + fetchUser() { + return { + remote(state, id, name) { + return new Promise((resolve, reject) => { + setTimeout(() => resolve({ id, name }), 10) + }) + }, + + success: UserActions.receivedUser, + error: UserActions.failed + } + } +} + +class UserStore { + static displayName = 'UserStore' + + constructor() { + this.user = null + + this.exportAsync(UserSource) + this.bindActions(UserActions) + } + + receivedUser(user) { + this.user = user + } + + failed(e) { + console.error('Failure', e) + } +} + +const userStore = alt.createStore(UserStore) + +const NumberActions = alt.generateActions('receivedNumber', 'failed') + +const NumberSource = { + fetchNumber() { + return { + remote(state, id) { + return new Promise((resolve, reject) => { + setTimeout(() => resolve(id), 5) + }) + }, + + success: NumberActions.receivedNumber, + error: NumberActions.failed + } + } +} + +class NumberStore { + static displayName = 'NumberStore' + + constructor() { + this.n = [] + this.exportAsync(NumberSource) + this.bindActions(NumberActions) + } + + receivedNumber(n) { + this.n = n + } + + failed(e) { + console.error(e) + } +} + +const numberStore = alt.createStore(NumberStore) + +@AltIso.define((props) => { + return Promise.all([ + userStore.fetchUser(props.id, props.name), + numberStore.fetchNumber(props.id) + ]) +}) +class User extends React.Component { + render() { + return ( +