From a8378bf65405dd11b953893722bc33b21a8767f6 Mon Sep 17 00:00:00 2001 From: jordangarcia Date: Wed, 15 Jul 2015 04:29:09 -0700 Subject: [PATCH 001/118] Cleanup README, move things to docs website, add microcosm shoutout @nhunzaker :) --- README.md | 1002 +---------------- docs/grunt/build-site.js | 2 - ...06-async-actions-and-optimistic-updates.md | 9 +- docs/src/docs/07-api.md | 159 ++- docs/src/docs/99-core-concepts.md | 374 ++++++ examples/shopping-cart/README.md | 2 + 6 files changed, 526 insertions(+), 1022 deletions(-) create mode 100644 docs/src/docs/99-core-concepts.md diff --git a/README.md b/README.md index efc82d5..c4f77ac 100644 --- a/README.md +++ b/README.md @@ -8,21 +8,9 @@ Traditional Flux architecture built with ImmutableJS data structures. -[Why you should use NuclearJS](http://optimizely.github.io/nuclear-js/). +## Documentation -## How NuclearJS differs from other Flux implementations - -1. All app state is in a singular immutable map, think Om. In development you can see your entire application state at every point in time thanks to awesome debugging tools built into NuclearJS. - -2. State is not spread out through stores, instead stores are a declarative way of describing some top-level domain of your app state. For each key in the app state map a store declares the initial state of that key and how that piece of the app state reacts over time to actions dispatched on the flux system. - -3. Stores are not reference-able nor have any `getX` methods on them. Instead Nuclear uses a functional lens concept called **getters**. In fact, the use of getters obviates the need for any store to know about another store, eliminating the confusing `store.waitsFor` method found in other flux implementations. - -4. NuclearJS is insanely efficient - change detection granularity is infinitesimal, you can even observe computed state where several pieces of the state map are combined together and run through a transform function. Nuclear is smart enough to know when the value of any computed changes and only call its observer if and only if its value changed in a way that is orders of magnitude more efficient than traditional dirty checking. It does this by leveraging ImmutableJS data structure and using a `state1 !== state2` reference comparison which runs in constant time. - -5. Automatic data observation / rendering -- automatic re-rendering is built in for React in the form of a very lightweight mixin. It is also easily possible to build the same functionality for any UI framework such as VueJS, AngularJS and even Backbone. - -6. NuclearJS is not a side-project, it's used as the default Flux implementation that powers all of Optimizely. It is well tested and will continue to be maintained for the foreseeable future. Our current codebase has over dozens of stores, actions and getters, we even share our prescribed method of large scale code organization and testing strategies. +[http://optimizely.github.io/nuclear-js/](http://optimizely.github.io/nuclear-js/) ## Design Philosophy @@ -44,742 +32,25 @@ NuclearJS can be downloaded from npm. npm install nuclear-js ``` -## Let's see some examples - -Let's see what the original [Flux Chat Example](https://github.com/facebook/flux/tree/master/examples/flux-chat) looks like in NuclearJS. - -All of the above code lives in [examples/flux-chat](./examples/flux-chat) - -##### `flux.js` - -```js -// create the Nuclear reactor instance, this will act as our dispatcher and interface for data fetching -var Nuclear = require('nuclear-js') - -module.exports = new Nuclear.Reactor({ - debug: true, -}) -``` - -### Modules - -The prescribed way of code organization in NuclearJS is to group all stores, actions and getters of the same domain in a module. - -##### Example Module File Structure - -For the flux-chat example we will create a chat module that holds all of the domain logic for the chat aspect. For smaller projects there may only need to be one module, but for larger projects using many modules can decouple your codebase and make it much easier to manage. - -```js -modules/chat -├── stores/ - └── thread-store.js - └── current-thread-id-store.js -├── actions.js // exports functions that call flux.dispatch -├── action-types.js // constants for the flux action types -├── getters.js // getters exposed by the module providing read access to module's stores -├── index.js // MAIN ENTRY POINT - facade that exposes a public api for the module -└── tests.js // module unit tests that test the modules stores, getters, and actions -``` - -##### `modules/chat/index.js` - -```js -var flux = require('../../flux') - -flux.registerStores({ - currentThreadID: require('./stores/current-thread-id-store'), - threads: require('./stores/thread-store'), -}) - -module.exports = { - actions: require('./actions'), - - getters: require('./getters'), -} -``` - -- Modules expose a single public API, the `index.js` file. It is improper for an outside piece of code to require any file within the module except the `index.js` file. - -- Stores are registered lazily through the module's index.js. This may seem weird at first, but in NuclearJS stores are more of an implementation detail and not ever directly referenceable. - -- Data access to the module's store values is done entirely through the getters it exposes. This provides a decoupling between the store implementation and how the outside world references the state that a module manages. A getter is a contract between the outside world and the module that a particular piece of information is accessible. The evaluator of a getter does not care about the underlying store representation. - -### Stores - -##### `modules/chat/stores/thread-store.js` - -```js -var Nuclear = require('nuclear-js') -var toImmutable = Nuclear.toImmutable -var actionTypes = require('../action-types') - -module.exports = new Nuclear.Store({ - getInitialState() { - // for Nuclear to be so efficient all state must be immutable data - // mapping of threadID => Thread - return toImmutable({}) - }, - - initialize() { - // all action handlers are pure functions that take the current state and payload - this.on(actionTypes.ADD_MESSAGE, addMessage) - this.on(actionTypes.CLICK_THREAD, setMessagesRead) - } -}) - -/** - * @type Message - * id {GUID} - * threadID {GUID} - * threadName {GUID} - * authorName {String} - * text {String} - * isRead {Boolean} - * timestamp {Timestamp} - */ - -/** - * @param {Immutable.Map} - * @param {Object} payload - * @param {Message} payload.message - */ -function addMessage(state, { message }) { - var msg = toImmutable(message) - var threadID = msg.get('threadID') - - return state.withMutations(threads => { - // use standard ImmutableJS methods to transform state when handling an action - if (!threads.has(threadID)) { - threads.set(threadID, toImmutable({ - threadID: threadID, - threadName: msg.get('threadName'), - messages: toImmutable([]), - })) - } - - // push new message into thread and sort by message timestamp - threads.update(threadID, thread => { - var sortedMessages = thread.get('messages') - .push(msg) - .sortBy(msg => msg.get('timestamp')) - - return thread.set('messages', sortedMessages) - }) - }) -} - -/** - * Mark all messages for a thread as "read" - * @param {Immutable.Map} - * @param {Object} payload - * @param {GUID} payload.threadID - */ -function setMessagesRead(state, { threadID }) { - return state.updateIn([threadID, 'messages'], messages => { - return messages.map(msg => msg.set('isRead', true)) - }) -} -``` - -##### `modules/message/stores/current-thread-id-store.js` - -```js -var Nuclear = require('nuclear-js') -var toImmutable = Nuclear.toImmutable -var actionTypes = require('../action-types') - -module.exports = new Nuclear.Store({ - getInitialState() { - // only keeps track of the current threadID - return null - }, - - initialize() { - // all action handlers are pure functions that take the current state and payload - this.on(actionTypes.CLICK_THREAD, setCurrentThreadID) - } -}) - -function setCurrentThreadID(state, { threadID }) { - // return the new value of the store's state - return threadID -} -``` - -At this point defined how our application manages state over time by creating and registering the thread store and currentThreadID store. When defining stores there is no need to worry about computable state like the most recent message in each thread, this is all handled through getters. - -### Getters - -Getters can take 2 forms: - - 1. A KeyPath such as `['messages']` which equates to a `state.getIn(['messages'])` on the app state `Immutable.Map`. - 2. An array with the form `[ [keypath | getter], [keypath | getter], ..., tranformFunction]` - -##### `modules/chat/getters.js` - -```js -// it is idiomatic to facade all data access through getters, that way a component only has to subscribe to a getter making it agnostic -// to the underlying stores / data transformation that is taking place -exports.threadsMap = ['threads'] - -exports.threads = [ - exports.threadsMap, - threadsMap => threadsMap.toList() -] - -exports.currentThread = [ - ['currentThreadID'], - exports.threadsMap, - (currentThreadID, threadsMap) => threadsMap.get(currentThreadID) -] - -exports.latestThread = [ - exports.threads, - threads => { - return threads - .sortBy(thread => { - thread.get('messages').last().get('timestamp') - }) - .last() - } -] - -exports.currentThreadID = [ - exports.currentThread, - thread => thread ? thread.get('threadID') : null -] - -exports.unreadCount = [ - exports.threads, - threads => { - return threads.reduce((accum, thread) => { - if (!thread.get('messages').last().get('isRead')) { - accum++ - } - return accum - }, 0) - } -] -``` - -Since stores are registered on the Nuclear Reactor by the module's index file, then a module is the only part of the system that knows the store ids. If this information needs to be made public, the module will export a getter of the form `[]`. - -### Actions - -##### `module/chat/actions.js` - -```js -var flux = require('../../flux') -var actionTypes = require('./action-types') -var getters = require('./getters') - -/** - * Handles the receiving of messages into the flux system - * @param {Message[]} messages - */ -exports.receiveAll = function(messages) { - messages.forEach(message => { - flux.dispatch(actionTypes.ADD_MESSAGE, { message }) - }) -} - -/** - * Creates a message - * @param {String} text - * @param {GUID} threadName - */ -exports.createMessage = function(text, threadID) { - var timestamp = Date.now() - var id = 'm_' + timestamp - var threadName = flux.evaluate([ - getters.threadsMap, - threadsMap => threadsMap.getIn([threadID, 'threadName']) - ]) - var authorName = 'Jordan' - - flux.dispatch(actionTypes.ADD_MESSAGE, { - message: { id, threadID, threadName, authorName, timestamp, text } - }) -} - -exports.clickThread = function(threadID) { - flux.dispatch(actionTypes.CLICK_THREAD, { threadID }) -} -``` - -### Hooking it up to a component - -###### `components/ThreadSection.react.js` - -```js -var React = require('react'); -var flux = require('../flux'); -var Chat = require('../modules/chat'); - -var ThreadListItem = require('./ThreadListItem.react'); - -var ThreadSection = React.createClass({ - mixins: [flux.ReactMixin], - - getDataBindings() { - return { - threads: Chat.getters.threads, - unreadCount: Chat.getters.unreadCount, - currentThreadID: Chat.getters.currentThreadID, - } - }, - - render: function() { - var threadListItems = this.state.threads.map(thread => { - return ( - - ); - }, this); - var unread = - this.state.unreadCount === 0 ? - null : - Unread threads: {this.state.unreadCount}; - return ( -
-
- {unread} -
-
    - {threadListItems} -
-
- ); - }, -}); - -module.exports = ThreadSection; -``` - -`flux.ReactMixin` handles all of the pub/sub between the flux system and component and will only render the component via a `setState` call whenever any of the subscribed getters' value changes. The mixin will also automatically unsubscribe from observation when the component is unmounted. - -##### `ThreadListItem.react.js` - -```js -var React = require('react'); -var Chat = require('../modules/chat'); -var cx = require('react/lib/cx'); - -var ReactPropTypes = React.PropTypes; - -var ThreadListItem = React.createClass({ - - propTypes: { - thread: ReactPropTypes.object, - currentThreadID: ReactPropTypes.string - }, - - render: function() { - var thread = this.props.thread; - var lastMessage = thread.get('messages').last(); - var dateString = (new Date(lastMessage.get('timestamp'))).toLocaleTimeString() - return ( -
  • -
    {thread.get('threadName')}
    -
    - {dateString} -
    -
    - {lastMessage.get('text')} -
    -
  • - ); - }, - - _onClick: function() { - var threadID = this.props.thread.get('threadID') - if (this.props.currentThreadID !== threadID) { - Chat.actions.clickThread(threadID); - } - } - -}); - -module.exports = ThreadListItem; -``` - -## Core Concepts - -The easiest way to think about how NuclearJS is modelling the state of your system is to imagine it all as a single map (or JavaScript object). If you are familiar with Om then the concept of a singular App State is very familiar already. - -Each entry in this top level map contains a portion of the entire app state for a specific domain and are managed by **stores**. - -Imagine modelling a shopping cart. Our app state would look like: - -```js -{ - items: [ - { name: 'Soap', price: 5, quantity: 2 }, - { name: 'The Adventures of Pluto Nash DVD', price: 10, quantity: 1 }, - { name: 'Fig Bar', price: 3, quantity: 10 }, - ], - - taxPercent: 5 -} -``` - -In this example we would have an `itemStore` and a `taxPercentStore` to model this state. Notice a few important things -are left out in this model of our application state, such as the subtotal, the amount of tax and the total. This doesn't -live in our app state because those are all examples of **computable state**, and we have a very elegant solution for calculating them that we will touch on momentarily. - -### But first let's go over some NuclearJS Vocabulary - -#### Reactor - -In Nuclear a Reactor is the container that holds your app state, it's where you register stores, dispatch actions and read the current state of your system. Reactor's are the only stateful part of Nuclear and have only 3 API methods you REALLY need to know: `dispatch`, `get`, and `observe`. Don't worry, extensive API docs will be provided for all of these methods. - -#### Stores - -Stores define how a portion of the application state will behave over time, they also provide the initial state. Once a store has been attached to a Reactor you will never reference it directly. Calling `reactor.dispatch(actionType, payload)` will ensure that all stores receive the action and get a chance to update themselves. Stores are a self-managing state, providing a single canonical place to define the behavior a domain of your application over time. - -#### KeyPaths - -KeyPaths are a pointer to some piece of your application state. They can be represented as a `Array`. - -`['foo', 'bar']` is an example of a valid keypath, analogous to `state['foo']['bar']` in JavaScript. - -#### Getters - -As described above, the state of a reactor is hidden away internally behind the [Stores](#stores) abstraction. In order to get a hold of part of that state, you need to ask the [Reactor](#reactor) for it using a simple protocol referred to, informally, as a Getter. - -Getters can take 2 forms: - - 1. A [KeyPath](#keypaths) as described above - 2. An array with the form `[ [keypath | getter], [keypath | getter], ..., transformFunction]` - Note - Often you'll pass the Getter to `reactor.evaluate` to get its value, but we'll touch on the reactor API later. - -If you've used [AngularJS](https://angularjs.org/), the 2nd form will seem familiar. It's essentially a way of specifying -which app values get injected into the transform function at the end. Here's an example of the form itself, but keep in mind that it may make more sense in the context of the examples below, - -```js -// Our first getter takes in the `items` portion of the app state and -// returns (presumably) the sum of `item.price * item.quantity` for all the items -var subtotalGetter = [ - // a KeyPath - ['items'], - // and a transform function - function(items) { ... } -] - -// This getter requests 2 values be passed into its transform function - the result -// of the subtotalGetter and the `taxPercent` value from the app state. -var totalGetter = [ - // A Getter - subtotalGetter, - // A KeyPath - ['taxPercent'], - // Composition Function - function(subtotal, taxPercent) { - return (subtotal * taxPercent) + subtotal - } -] -``` - -Notice that you can use getters as dependencies to other getters. This is an extremely powerful abstraction, and one that you'll undoubtedly want to become familiar with in your nuclear journey. - -But you need to know one thing about getter transform functions - they MUST be pure functions (that is, a given set input values results in a [deterministic](http://en.wikipedia.org/wiki/Deterministic_algorithm) output). By making the transform functions pure, you can test Getters easier, compose them easier, and nuclear can [memoize](http://en.wikipedia.org/wiki/Memoization) calls to them, making Getter dependency resolution very performant. - -__For the astute reader__ - You probably already noticed if you have experience in functional languages, but because Getters -are simply arrays full of strings and pure functions, they are serializable. Since JS can stringify pure functions, your getters are nothing more than data that could be stored, sent over the wire, etc. - -## Back To Our Example - -First lets create the `itemStore` and `taxPercentStore` and hook it up to our reactor. - -```js -var Map = require('immutable').Map -var List = require('immutable').List -var Nuclear = require('nuclear-js') - -var itemStore = new Nuclear.Store({ - // the parameter is optional, if not supplied will default to an `Immutable.Map({})` - // Store state must be an ImmutableJS data structure or an immutable JavaScript primitive - // like Number or String - getInitialState: function() { - return List() - }, - - initialize: function() { - // register a handler for `reactor.dispatch('addItem', payload)` - this.on('addItem', function(state, payload) { - // a handler is passed the current state and the action payload - // it performs an immutable transformation of the store's underlying state - // in response to the action and returns the new state - return state.push(Map({ - name: payload.name, - price: payload.price, - quantity: payload.quantity || 1, - })) - }) - } -}) - -var taxPercentStore = new Nuclear.Store({ - getInitialState: function() { - return 0 - }, - - initialize: function() { - // this will get called via `reactor.dispatch('setTaxPercent', 10)` - // where the payload is a primitive value (number) - this.on('setTaxPercent', function(oldPercent, newPercent) { - return newPercent - }) - } -}) - -var reactor = new Nuclear.Reactor() -reactor.registerStores({ - items: itemStore, - taxPercent: taxPercentStore, -}) - -// Let's use a Getter (the first form, a [KeyPath](#keypaths)) to retrieve parts of the app state -console.log(reactor.evaluate(['items'])) // List [] -console.log(reactor.evaluate(['taxPercent'])) // 0 - -reactor.dispatch('addItem', { - name: 'Soap', - price: 5, - quantity: 2, -}) - -console.log(reactor.evaluate(['items'])) // List [ Map { name: 'Soap', price: 5, quantity: 2 } ] -``` - -### Computing Subtotal, Tax and Total - -```js -var subtotalGetter = [ - ['items'], - function(items) { - // items is of type `Immutable.List` - return items.reduce(function(total, item) { - return total + (item.get('price') * item.get('quantity')) - }, 0) - } -] - -var taxGetter = [ - subtotalGetter, - ['taxPercent'], - function(subtotal, taxPercent) { - return subtotal * (taxPercent / 100) - } -] - -var totalGetter = [ - subtotalGetter, - taxGetter, - function(subtotal, tax) { - return subtotal + tax - } -] - -console.log(reactor.evaluate(subtotalGetter)) // 10 -console.log(reactor.evaluate(taxGetter)) // 0 -console.log(reactor.evaluate(totalGetter)) // 10 - -reactor.dispatch('setTaxPercent', 10) - -console.log(reactor.evaluate(subtotalGetter)) // 11 -console.log(reactor.evaluate(taxGetter)) // 1 -console.log(reactor.evaluate(totalGetter)) // 12 -``` - -### Let's do something more interesting... - -Imagine we want to know any time the total is over 100. Let's use `reactor.observe`. - -```js -var over100Getter = [ - totalGetter, - function(total) { - return total > 100 - } -] - -reactor.observe(over100Getter, function(isOver100) { - if (isOver100) { - alert('Shopping cart over 100!') - } -}) -``` - -Actually that wasn't that interesting... let's make the threshold dynamic. - -```js -var budgetStore = Nuclear.Store({ - getInitialState: function() { - return Infinity - }, - initialize: function() { - this.on('setBudget', function(currentBudget, newBudget) { - return newBudget - } - } -}) - -// stores can be attached at any time -reactor.registerStores({ - budget: budgetStore, -}) - -var isOverBudget = [ - totalGetter, - ['budget'], - function(total, budget) { - return total > budget - } -] - -reactor.observe(isOverBudget, function(isOver) { - // this will be automatically re-evaluated only when the total or budget changes - if (isOver) { - var budget = reactor.evaluate(['budget']) - alert('Is over budget of ' + budget) - } -}) -``` - -**By using this pattern of composing Getters together, the majority of your system becomes purely functional transforms.** +## Examples -### Hooking up a UI: React +- [Shopping Cart Example](./examples/shopping-cart) - General overview of NuclearJS concepts: actions, stores and getters with ReactJS +- [Flux Chat Example](./examples/flux-chat) - classic facebook flux chat example written in NuclearJS +- [Rest API Example](./examples/rest-api) - shows how to deal with fetching data from an API using NuclearJS conventions -Syncing reactor stores and React component state is effortless using `reactor.ReactMixin`. - -```js -var React = require('react') - -var ShoppingCart = React.createClass({ - mixins: [reactor.ReactMixin], - - // simply implement this function to keep a component's state - // in sync with a Nuclear Reactor - getDataBindings() { - return { - // can reference a reactor KeyPath - items: ['items'], - taxPercent: ['taxPercent'], - // or reference a Getter - subtotal: getSubtotal, - tax: getTax, - total: getTotal, - // or inline a getter - expensiveItems: ['items', items => { - return items.filter(item => item > 100) - }] - } - }, - - render() { - var itemRows = this.state.items.map(function(item) { - return ( - - {item.get('quantity')} - {item.get('name')} - {item.get('price')} - - ) - }) - return ( -
    - - - - - - - {itemRows} - - - - - - - - - - - - -
    Quantity:Name:Price:
    subtotal:{this.state.subtotal}
    tax @ {this.state.taxPercent}%{this.state.taxPercent}
    total:{this.state.total}
    -
    - ) - } -}) -``` - -Whenever any of the reactor values being observed from `getDataBindings()` changes then `setState()` will be called with the updated value and the component will be re-rendered. Thus your React components always stay in sync with your app state! - -### Hooking up a UI: VueJS - -Syncing reactor stores to VueJS components is simple using the [NuclearVueMixin](https://github.com/jordangarcia/nuclear-vue-mixin). +## How NuclearJS differs from other Flux implementations -```js -var Vue = require('vue') -var NuclearVueMixin = require('nuclear-vue-mixin') +1. All app state is in a singular immutable map, think Om. In development you can see your entire application state at every point in time thanks to awesome debugging tools built into NuclearJS. -var ShoppingCart = new Vue({ - mixins: [NuclearVueMixin(reactor)], +2. State is not spread out through stores, instead stores are a declarative way of describing some top-level domain of your app state. For each key in the app state map a store declares the initial state of that key and how that piece of the app state reacts over time to actions dispatched on the flux system. - getDataBindings: function() { - return { - // can reference a reactor KeyPath - items: ['items'], - taxPercent: ['taxPercent'], - // or reference a Getter - subtotal: getSubtotal, - tax: getTax, - total: getTotal, - } - }, +3. Stores are not reference-able nor have any `getX` methods on them. Instead Nuclear uses a functional lens concept called **getters**. In fact, the use of getters obviates the need for any store to know about another store, eliminating the confusing `store.waitsFor` method found in other flux implementations. - template: require('text!./shopping-cart.html'), -}) -``` +4. NuclearJS is insanely efficient - change detection granularity is infinitesimal, you can even observe computed state where several pieces of the state map are combined together and run through a transform function. Nuclear is smart enough to know when the value of any computed changes and only call its observer if and only if its value changed in a way that is orders of magnitude more efficient than traditional dirty checking. It does this by leveraging ImmutableJS data structure and using a `state1 !== state2` reference comparison which runs in constant time. -In `shopping-cart.html` +5. Automatic data observation / rendering -- automatic re-rendering is built in for React in the form of a very lightweight mixin. It is also easily possible to build the same functionality for any UI framework such as VueJS, AngularJS and even Backbone. -```html - - - - - - - - - - - - - - - - - - - - - - - -
    Quantity:Name:Price:
    {{ item.quantity }}{{ item.name }}{{ item.price | currency }}
    subtotal:{{ subtotal }}
    tax @ {{ taxPercent }}%{{ tax }}
    total:{{ total }}
    -``` +6. NuclearJS is not a side-project, it's used as the default Flux implementation that powers all of Optimizely. It is well tested and will continue to be maintained for the foreseeable future. Our current codebase has over dozens of stores, actions and getters, we even share our prescribed method of large scale code organization and testing strategies. ## Performance @@ -789,249 +60,12 @@ You can read more of the implementation here: [src/evaluator.js](./src/evaluator ## API Documentation -### Reactor - -#### Constructor - -#### `Nuclear.Reactor` - -```js -var reactor = new Nuclear.Reactor(config) -// or -var reactor = Nuclear.Reactor(config) -``` - -**Configuration Options** - -`config.debug` Boolean - if true it will log the entire app state for every dispatch. - -#### `Reactor#dispatch(messageType, messagePayload)` - -Dispatches a message to all registered Stores. This process is done synchronously, all registered `Store`s are passed this message and all components are re-evaluated (efficiently). After a dispatch, a Reactor will emit the new state on the `reactor.changeEmitter` - -ex: `reactor.dispatch('addUser', { name: 'jordan' })` - -#### `Reactor#batch(fn)` - -Allows multiple dispatches within the `fn` function before notifying any observers. - -```js -reactor.batch(function() { - reactor.dispatch('addUser', { name: 'jordan' }) - reactor.dispatch('addUser', { name: 'james' }) -}) - -// does a single notify to all observers -``` - -#### `Reactor#evaluate(Getter | KeyPath)` - -Returns the immutable value for some KeyPath or Getter in the reactor state. Returns `undefined` if a keyPath doesn't have a value. - -```js -reactor.evaluate(['users', 'active']) -reactor.evaluate([ - ['users', 'active'], - ['filters', 'username'], - /** - * @param {Immutable.List} activeUsers - * @param {String} usernameFilter - * @return {Immutable.List} - */ - function(activeUsers, usernameFilter) { - return activeUsers.filter(function(user) { - return user.get('username').indexOf(usernameFilter) !== -1 - } - }, -]) -``` - -#### `Reactor#evaluateToJS(...keyPath, [transformFn])` - -Same as `evaluate` but coerces the value to a plain JS before returning. - -#### `Reactor#observe(keyPathOrGetter, handlerFn)` - -Takes a getter or keyPath and calls the handlerFn with the evaluated value whenever the getter or keyPath changes. - -**Note**: You cannot call `flux.dispatch` within the handle function of a `flux.observe`. This violates one of the fundamental design patterns in Flux architecture, which forbids cascading dispatches on the system which cause highly unpredictive systems. - -```js -reactor.observe([ - ['items'] - function(items) { - console.log('items changed'); - } -]) -``` - -#### `Reactor#serialize()` - -Returns a plain javascript object representing the application state. By defualt this maps over all stores and returns `toJS(storeState)`. - -```js -reactor.loadState(reactor.serialize()) -``` - -#### `Reactor#loadState( state )` - -Takes a plain javascript object and merges into the reactor state, using `store.deserialize` - -This can be useful if you need to load data already on the page. - -```js -reactor.loadState({ - stringStore: 'bar', - listStore: [4,5,6], -}) -``` - -#### `Reactor#registerStores(stores)` - -`stores` - an object of storeId => store instance - -```js -reactor.registerStores({ - 'threads': require('./stores/thread-store'), - 'currentThreadID': require('./stores/current-thread-id-store'), -}) -``` - -#### `Reactor#reset()` - -Causes all stores to be reset to their initial state. Extremely useful for testing, just put a `reactor.reset()` call in your `afterEach` blocks. - -#### `Reactor#ReactMixin` - -Exposes the ReactMixin to do automatic data binding. - -```js -var ThreadSection = React.createClass({ - mixins: [flux.ReactMixin], - - getDataBindings() { - return { - threads: Chat.getters.threads, - unreadCount: Chat.getters.unreadCount, - currentThreadID: Chat.getters.currentThreadID, - } - }, - - render: function() { - var threadListItems = this.state.threads.map(thread => { - return ( - - ); - }, this); - var unread = - this.state.unreadCount === 0 ? - null : - Unread threads: {this.state.unreadCount}; - return ( -
    -
    - {unread} -
    -
      - {threadListItems} -
    -
    - ); - }, -}); -``` - -### Store - -#### Constructor - -```js -module.exports = new Nuclear.Store({ - getInitialState: function() { - // method must return an immutable value for NuclearJS to take advantage of efficient equality checks - return toImmutable({}) - }, - - initialize: function() { - // sets up action handlers via `this.on` - this.on('SOME_ACTION', function(state, payload) { - // action handler takes state + payload and returns new state - }) - }, -}) -``` - -#### `Store#getInitialState` - -Defines the starting state for a store. Must return an immutable value. By default it returns an `Immutable.Map` - -#### `Store#initialize` - -Responsible for setting up action handlers for the store using `this.on(actionTypes, handlerFn)` - -#### `Store#serialize` - -Serialization method for the store's data, by default its implemented as `Nuclear.toJS' which converts ImmutableJS objects to plain javascript. -This is overridable for your specific data needs. - -```js -// serializing an Immutable map while preserving numerical keys -Nuclear.Store({ - // ... - serialize(state) { - if (!state) { - return state; - } - return state.entrySeq().toJS() - }, - // ... -}) -``` - -#### `Store#deserialize` - -Serialization method for the store's data, by default its implemented as `Nuclear.toImmutable' which converts plain javascript objects to ImmutableJS data structures. -This is overridable for your specific data needs. - -```js -// deserializing an array of arrays [[1, 'one'], [2, 'two']] to an Immutable.Map -Nuclear.Store({ - // ... - deserialize(state) { - return Immutable.Map(state) - }, - // ... -}) -``` - -### Utilities - -NuclearJS comes with several utility functions that are exposed on the `Nuclear` variable. - -#### `Nuclear.Immutable` - -Provides access to the ImmutableJS `Immutable` object. - -#### `Nuclear.toImmutable(value)` - -Coerces a value to its immutable counterpart, can be called on any type safely. It will convert Objects to `Immutable.Map` and Arrays to `Immutable.List`. - -#### `Nuclear.toJS(value)` - -Will coerce an Immutable value to its mutable counterpart. Can be called on non-immutable values safely. - -#### `Nuclear.isImmutable(value)` : Boolean - -Returns true if the value is an ImmutableJS data structure. +[https://optimizely.github.io/nuclear-js/docs/07-api.html](API Documentation) -#### `Nuclear.isKeyPath(value)` : Boolean +## For Smaller Applications -Returns true if the value is the format of a valid keyPath. +NuclearJS was designed first and foremore for large scale production codebases. For a much more lightweight Flux implementation that shares many of the same ideas and design principles of NuclearJS check out [Microcosm](https://github.com/vigetlabs/microcosm) -#### `Nuclear.isGetter(value)` : Boolean +## Contributing -Returns true if the value is the format of a valid getter. +Contributions are welcome, especially with the documentation website and examples. See [CONTRIBUTING.md](./CONTRIBUTING.md) for more details. diff --git a/docs/grunt/build-site.js b/docs/grunt/build-site.js index 44176d5..2497342 100644 --- a/docs/grunt/build-site.js +++ b/docs/grunt/build-site.js @@ -4,8 +4,6 @@ var glob = require('glob') var async = require('async') var fm = require('front-matter') var fs = require('fs') -//var Remarkable = require('remarkable'); -//var md = new Remarkable(); var marked = require('marked') require('babel/register')({ diff --git a/docs/src/docs/06-async-actions-and-optimistic-updates.md b/docs/src/docs/06-async-actions-and-optimistic-updates.md index 0e9964d..6e075c1 100644 --- a/docs/src/docs/06-async-actions-and-optimistic-updates.md +++ b/docs/src/docs/06-async-actions-and-optimistic-updates.md @@ -158,9 +158,12 @@ export default React.createClass({ ## Further Reading -This ends our getting started example, for further reading checkout the following: +This ends our getting started example, for a more in depth look all of the above example code lives [here](https://github.com/optimizely/nuclear-js/tree/master/examples/shopping-cart). + +For additional documentation and resources checkout the following: - [API Documentation](./07-api.html) -- [Shopping Cart Example Code](https://github.com/optimizely/nuclear-js/tree/master/examples/shopping-cart) -- [Flux Chat Example Code](https://github.com/optimizely/nuclear-js/tree/master/examples/flux-chat) +- [Flux Chat Example](https://github.com/optimizely/nuclear-js/tree/master/examples/flux-chat) - classic facebook flux chat example written in NuclearJS +- [Rest API Example](https://github.com/optimizely/nuclear-js/tree/master/examples/rest-api) - shows how to deal with fetching data from an API using NuclearJS conventions +More coming soon... diff --git a/docs/src/docs/07-api.md b/docs/src/docs/07-api.md index 4c31abf..9ef0bf0 100644 --- a/docs/src/docs/07-api.md +++ b/docs/src/docs/07-api.md @@ -5,15 +5,46 @@ section: "Guide" # API Documentation -## Reactor +### Reactor -### `Reactor#dispatch(messageType, messagePayload)` +#### Constructor + +#### `Nuclear.Reactor` + +```javascript +var reactor = new Nuclear.Reactor(config) +// or +var reactor = Nuclear.Reactor(config) +``` + +**Configuration Options** + +`config.debug` Boolean - if true it will log the entire app state for every dispatch. + +#### `Reactor#dispatch(messageType, messagePayload)` Dispatches a message to all registered Stores. This process is done synchronously, all registered `Store`s are passed this message and all components are re-evaluated (efficiently). After a dispatch, a Reactor will emit the new state on the `reactor.changeEmitter` -ex: `reactor.dispatch('addUser', { name: 'jordan' })` +```javascript +reactor.dispatch('addUser', { name: 'jordan' }) +``` + +#### `Reactor#batch(fn)` + +_added in 1.1_ + +Allows multiple dispatches within the `fn` function before notifying any observers. + +```javascript +reactor.batch(function() { + reactor.dispatch('addUser', { name: 'jordan' }) + reactor.dispatch('addUser', { name: 'james' }) +}) + +// does a single notify to all observers +``` -### `Reactor#evaluate(Getter | KeyPath)` +#### `Reactor#evaluate(Getter | KeyPath)` Returns the immutable value for some KeyPath or Getter in the reactor state. Returns `undefined` if a keyPath doesn't have a value. @@ -35,11 +66,11 @@ reactor.evaluate([ ]) ``` -### `Reactor#evaluateToJS(...keyPath, [transformFn])` +#### `Reactor#evaluateToJS(...keyPath, [transformFn])` -Same as `evaluate` but coerces the value to a plain JS before returning +Same as `evaluate` but coerces the value to a plain JS before returning. -### `Reactor#observe(keyPathOrGetter, handlerFn)` +#### `Reactor#observe(keyPathOrGetter, handlerFn)` Takes a getter or keyPath and calls the handlerFn with the evaluated value whenever the getter or keyPath changes. @@ -54,7 +85,32 @@ reactor.observe([ ]) ``` -### `Reactor#registerStores(stores)` +#### `Reactor#serialize()` + +_added in 1.1_ + +Returns a plain javascript object representing the application state. By defualt this maps over all stores and returns `toJS(storeState)`. + +```javascript +reactor.loadState(reactor.serialize()) +``` + +#### `Reactor#loadState( state )` + +_added in 1.1_ + +Takes a plain javascript object and merges into the reactor state, using `store.deserialize` + +This can be useful if you need to load data already on the page. + +```javascript +reactor.loadState({ + stringStore: 'bar', + listStore: [4,5,6], +}) +``` + +#### `Reactor#registerStores(stores)` `stores` - an object of storeId => store instance @@ -65,11 +121,11 @@ reactor.registerStores({ }) ``` -### `Reactor#reset()` +#### `Reactor#reset()` Causes all stores to be reset to their initial state. Extremely useful for testing, just put a `reactor.reset()` call in your `afterEach` blocks. -### `Reactor#ReactMixin` +#### `Reactor#ReactMixin` Exposes the ReactMixin to do automatic data binding. @@ -106,26 +162,16 @@ var ThreadSection = React.createClass({
      {threadListItems} -
    + ); }, }); ``` -## Constructors - -### `Nuclear.Reactor` - -```javascript -var reactor = new Nuclear.Reactor(config) -``` - -**Configuration Options** - -`config.debug` Boolean - if true it will log the entire app state for every dispatch. +### Store -### `Nuclear.Store` +#### Constructor ```javascript module.exports = new Nuclear.Store({ @@ -143,30 +189,77 @@ module.exports = new Nuclear.Store({ }) ``` -## Utilities +#### `Store#getInitialState` + +Defines the starting state for a store. Must return an immutable value. By default it returns an `Immutable.Map` + +#### `Store#initialize` + +Responsible for setting up action handlers for the store using `this.on(actionTypes, handlerFn)` + +#### `Store#serialize` + +_added in 1.1_ + +Serialization method for the store's data, by default its implemented as `Nuclear.toJS' which converts ImmutableJS objects to plain javascript. +This is overridable for your specific data needs. + +```javascript +// serializing an Immutable map while preserving numerical keys +Nuclear.Store({ + // ... + serialize(state) { + if (!state) { + return state; + } + return state.entrySeq().toJS() + }, + // ... +}) +``` + +#### `Store#deserialize` + +_added in 1.1_ + +Serialization method for the store's data, by default its implemented as `Nuclear.toImmutable' which converts plain javascript objects to ImmutableJS data structures. +This is overridable for your specific data needs. + +```javascript +// deserializing an array of arrays [[1, 'one'], [2, 'two']] to an Immutable.Map +Nuclear.Store({ + // ... + deserialize(state) { + return Immutable.Map(state) + }, + // ... +}) +``` + +### Utilities NuclearJS comes with several utility functions that are exposed on the `Nuclear` variable. -### `Nuclear.Immutable` +#### `Nuclear.Immutable` Provides access to the ImmutableJS `Immutable` object. -### `Nuclear.toImmutable(value)` +#### `Nuclear.toImmutable(value)` -Coerces a value to its immutable counterpart, can be called on any type safely. It will convert Objects to `Immutable.Map` and Arrays to `Immutable.List` +Coerces a value to its immutable counterpart, can be called on any type safely. It will convert Objects to `Immutable.Map` and Arrays to `Immutable.List`. -### `Nuclear.toJS(value)` +#### `Nuclear.toJS(value)` Will coerce an Immutable value to its mutable counterpart. Can be called on non-immutable values safely. -### `Nuclear.isImmutable(value)` : Boolean +#### `Nuclear.isImmutable(value)` : Boolean Returns true if the value is an ImmutableJS data structure. -### `Nuclear.isKeyPath(value)` : Boolean +#### `Nuclear.isKeyPath(value)` : Boolean -Returns true if the value is the format of a valid keyPath +Returns true if the value is the format of a valid keyPath. -### `Nuclear.isGetter(value)` : Boolean +#### `Nuclear.isGetter(value)` : Boolean -Returns true if the value is the format of a valid getter +Returns true if the value is the format of a valid getter. diff --git a/docs/src/docs/99-core-concepts.md b/docs/src/docs/99-core-concepts.md new file mode 100644 index 0000000..da3ec3a --- /dev/null +++ b/docs/src/docs/99-core-concepts.md @@ -0,0 +1,374 @@ +--- +title: "Core Concepts (old)" +section: "Guide" +--- + +## Core Concepts + +The easiest way to think about how NuclearJS is modelling the state of your system is to imagine it all as a single map (or JavaScript object). If you are familiar with Om then the concept of a singular App State is very familiar already. + +Each entry in this top level map contains a portion of the entire app state for a specific domain and are managed by **stores**. + +Imagine modelling a shopping cart. Our app state would look like: + +```javascript +{ + items: [ + { name: 'Soap', price: 5, quantity: 2 }, + { name: 'The Adventures of Pluto Nash DVD', price: 10, quantity: 1 }, + { name: 'Fig Bar', price: 3, quantity: 10 }, + ], + + taxPercent: 5 +} +``` + +In this example we would have an `itemStore` and a `taxPercentStore` to model this state. Notice a few important things +are left out in this model of our application state, such as the subtotal, the amount of tax and the total. This doesn't +live in our app state because those are all examples of **computable state**, and we have a very elegant solution for calculating them that we will touch on momentarily. + +### But first let's go over some NuclearJS Vocabulary + +#### Reactor + +In Nuclear a Reactor is the container that holds your app state, it's where you register stores, dispatch actions and read the current state of your system. Reactor's are the only stateful part of Nuclear and have only 3 API methods you REALLY need to know: `dispatch`, `get`, and `observe`. Don't worry, extensive API docs will be provided for all of these methods. + +#### Stores + +Stores define how a portion of the application state will behave over time, they also provide the initial state. Once a store has been attached to a Reactor you will never reference it directly. Calling `reactor.dispatch(actionType, payload)` will ensure that all stores receive the action and get a chance to update themselves. Stores are a self-managing state, providing a single canonical place to define the behavior a domain of your application over time. + +#### KeyPaths + +KeyPaths are a pointer to some piece of your application state. They can be represented as a `Array`. + +`['foo', 'bar']` is an example of a valid keypath, analogous to `state['foo']['bar']` in JavaScript. + +#### Getters + +As described above, the state of a reactor is hidden away internally behind the [Stores](#stores) abstraction. In order to get a hold of part of that state, you need to ask the [Reactor](#reactor) for it using a simple protocol referred to, informally, as a Getter. + +Getters can take 2 forms: + + 1. A [KeyPath](#keypaths) as described above + 2. An array with the form `[ [keypath | getter], [keypath | getter], ..., transformFunction]` + Note - Often you'll pass the Getter to `reactor.evaluate` to get its value, but we'll touch on the reactor API later. + +If you've used [AngularJS](https://angularjs.org/), the 2nd form will seem familiar. It's essentially a way of specifying +which app values get injected into the transform function at the end. Here's an example of the form itself, but keep in mind that it may make more sense in the context of the examples below, + +```javascript +// Our first getter takes in the `items` portion of the app state and +// returns (presumably) the sum of `item.price * item.quantity` for all the items +var subtotalGetter = [ + // a KeyPath + ['items'], + // and a transform function + function(items) { ... } +] + +// This getter requests 2 values be passed into its transform function - the result +// of the subtotalGetter and the `taxPercent` value from the app state. +var totalGetter = [ + // A Getter + subtotalGetter, + // A KeyPath + ['taxPercent'], + // Composition Function + function(subtotal, taxPercent) { + return (subtotal * taxPercent) + subtotal + } +] +``` + +Notice that you can use getters as dependencies to other getters. This is an extremely powerful abstraction, and one that you'll undoubtedly want to become familiar with in your nuclear journey. + +But you need to know one thing about getter transform functions - they MUST be pure functions (that is, a given set input values results in a [deterministic](http://en.wikipedia.org/wiki/Deterministic_algorithm) output). By making the transform functions pure, you can test Getters easier, compose them easier, and nuclear can [memoize](http://en.wikipedia.org/wiki/Memoization) calls to them, making Getter dependency resolution very performant. + +__For the astute reader__ - You probably already noticed if you have experience in functional languages, but because Getters +are simply arrays full of strings and pure functions, they are serializable. Since JS can stringify pure functions, your getters are nothing more than data that could be stored, sent over the wire, etc. + +## Back To Our Example + +First lets create the `itemStore` and `taxPercentStore` and hook it up to our reactor. + +```javascript +var Map = require('immutable').Map +var List = require('immutable').List +var Nuclear = require('nuclear-js') + +var itemStore = new Nuclear.Store({ + // the parameter is optional, if not supplied will default to an `Immutable.Map({})` + // Store state must be an ImmutableJS data structure or an immutable JavaScript primitive + // like Number or String + getInitialState: function() { + return List() + }, + + initialize: function() { + // register a handler for `reactor.dispatch('addItem', payload)` + this.on('addItem', function(state, payload) { + // a handler is passed the current state and the action payload + // it performs an immutable transformation of the store's underlying state + // in response to the action and returns the new state + return state.push(Map({ + name: payload.name, + price: payload.price, + quantity: payload.quantity || 1, + })) + }) + } +}) + +var taxPercentStore = new Nuclear.Store({ + getInitialState: function() { + return 0 + }, + + initialize: function() { + // this will get called via `reactor.dispatch('setTaxPercent', 10)` + // where the payload is a primitive value (number) + this.on('setTaxPercent', function(oldPercent, newPercent) { + return newPercent + }) + } +}) + +var reactor = new Nuclear.Reactor() +reactor.registerStores({ + items: itemStore, + taxPercent: taxPercentStore, +}) + +// Let's use a Getter (the first form, a [KeyPath](#keypaths)) to retrieve parts of the app state +console.log(reactor.evaluate(['items'])) // List [] +console.log(reactor.evaluate(['taxPercent'])) // 0 + +reactor.dispatch('addItem', { + name: 'Soap', + price: 5, + quantity: 2, +}) + +console.log(reactor.evaluate(['items'])) // List [ Map { name: 'Soap', price: 5, quantity: 2 } ] +``` + +### Computing Subtotal, Tax and Total + +```javascript +var subtotalGetter = [ + ['items'], + function(items) { + // items is of type `Immutable.List` + return items.reduce(function(total, item) { + return total + (item.get('price') * item.get('quantity')) + }, 0) + } +] + +var taxGetter = [ + subtotalGetter, + ['taxPercent'], + function(subtotal, taxPercent) { + return subtotal * (taxPercent / 100) + } +] + +var totalGetter = [ + subtotalGetter, + taxGetter, + function(subtotal, tax) { + return subtotal + tax + } +] + +console.log(reactor.evaluate(subtotalGetter)) // 10 +console.log(reactor.evaluate(taxGetter)) // 0 +console.log(reactor.evaluate(totalGetter)) // 10 + +reactor.dispatch('setTaxPercent', 10) + +console.log(reactor.evaluate(subtotalGetter)) // 11 +console.log(reactor.evaluate(taxGetter)) // 1 +console.log(reactor.evaluate(totalGetter)) // 12 +``` + +### Let's do something more interesting... + +Imagine we want to know any time the total is over 100. Let's use `reactor.observe`. + +```javascript +var over100Getter = [ + totalGetter, + function(total) { + return total > 100 + } +] + +reactor.observe(over100Getter, function(isOver100) { + if (isOver100) { + alert('Shopping cart over 100!') + } +}) +``` + +Actually that wasn't that interesting... let's make the threshold dynamic. + +```javascript +var budgetStore = Nuclear.Store({ + getInitialState: function() { + return Infinity + }, + initialize: function() { + this.on('setBudget', function(currentBudget, newBudget) { + return newBudget + } + } +}) + +// stores can be attached at any time +reactor.registerStores({ + budget: budgetStore, +}) + +var isOverBudget = [ + totalGetter, + ['budget'], + function(total, budget) { + return total > budget + } +] + +reactor.observe(isOverBudget, function(isOver) { + // this will be automatically re-evaluated only when the total or budget changes + if (isOver) { + var budget = reactor.evaluate(['budget']) + alert('Is over budget of ' + budget) + } +}) +``` + +**By using this pattern of composing Getters together, the majority of your system becomes purely functional transforms.** + +### Hooking up a UI: React + +Syncing reactor stores and React component state is effortless using `reactor.ReactMixin`. + +```javascript +var React = require('react') + +var ShoppingCart = React.createClass({ + mixins: [reactor.ReactMixin], + + // simply implement this function to keep a component's state + // in sync with a Nuclear Reactor + getDataBindings() { + return { + // can reference a reactor KeyPath + items: ['items'], + taxPercent: ['taxPercent'], + // or reference a Getter + subtotal: getSubtotal, + tax: getTax, + total: getTotal, + // or inline a getter + expensiveItems: ['items', items => { + return items.filter(item => item > 100) + }] + } + }, + + render() { + var itemRows = this.state.items.map(function(item) { + return ( + + {item.get('quantity')} + {item.get('name')} + {item.get('price')} + + ) + }) + return ( +
    + + + + + + + {itemRows} + + + + + + + + + + + + +
    Quantity:Name:Price:
    subtotal:{this.state.subtotal}
    tax @ {this.state.taxPercent}%{this.state.taxPercent}
    total:{this.state.total}
    +
    + ) + } +}) +``` + +Whenever any of the reactor values being observed from `getDataBindings()` changes then `setState()` will be called with the updated value and the component will be re-rendered. Thus your React components always stay in sync with your app state! + +### Hooking up a UI: VueJS + +Syncing reactor stores to VueJS components is simple using the [NuclearVueMixin](https://github.com/jordangarcia/nuclear-vue-mixin). + +```javascript +var Vue = require('vue') +var NuclearVueMixin = require('nuclear-vue-mixin') + +var ShoppingCart = new Vue({ + mixins: [NuclearVueMixin(reactor)], + + getDataBindings: function() { + return { + // can reference a reactor KeyPath + items: ['items'], + taxPercent: ['taxPercent'], + // or reference a Getter + subtotal: getSubtotal, + tax: getTax, + total: getTotal, + } + }, + + template: require('text!./shopping-cart.html'), +}) +``` + +In `shopping-cart.html` + +```html + + + + + + + + + + + + + + + + + + + + + + + +
    Quantity:Name:Price:
    {{ item.quantity }}{{ item.name }}{{ item.price | currency }}
    subtotal:{{ subtotal }}
    tax @ {{ taxPercent }}%{{ tax }}
    total:{{ total }}
    +``` diff --git a/examples/shopping-cart/README.md b/examples/shopping-cart/README.md index 7e2c18d..b25b095 100644 --- a/examples/shopping-cart/README.md +++ b/examples/shopping-cart/README.md @@ -1,5 +1,7 @@ # Shopping Cart Example +See [NuclearJS: Getting Started](https://optimizely.github.io/nuclear-js/docs/01-getting-started.html) for a detailed explanation of this example. + Taken (and slightly modified) from [https://github.com/voronianski/flux-comparison](https://github.com/voronianski/flux-comparison) ## To Run From 04d8f13bffe7046995dcaa2925662bfa5f16b796 Mon Sep 17 00:00:00 2001 From: jordangarcia Date: Wed, 15 Jul 2015 04:30:55 -0700 Subject: [PATCH 002/118] Fix api docs link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c4f77ac..17a4426 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ You can read more of the implementation here: [src/evaluator.js](./src/evaluator ## API Documentation -[https://optimizely.github.io/nuclear-js/docs/07-api.html](API Documentation) +[API Documentation](https://optimizely.github.io/nuclear-js/docs/07-api.html) ## For Smaller Applications From 8b5fb6cad832db1d6ec957099c23def01c47e462 Mon Sep 17 00:00:00 2001 From: jordangarcia Date: Wed, 15 Jul 2015 06:33:01 -0700 Subject: [PATCH 003/118] Update TODO --- docs/TODO.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/TODO.md b/docs/TODO.md index 93fe4f8..3d5b63c 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -2,8 +2,9 @@ - [x] links to docs, API, github - [x] build pipeline for docs from MD files -- [ ] navbar working in mobile -- [ ] async actions doc page +- [x] navbar working in mobile +- [x] async actions doc page - [ ] api documentation generation using code source - [ ] scaffold design patterns/examples area +- [ ] mobile side navbar - [ ] build pipeline for examples, create example component, possibly with code editing From db9783e07a3522fa8d01cd0f13cc41d4008e8b59 Mon Sep 17 00:00:00 2001 From: jordangarcia Date: Wed, 15 Jul 2015 16:59:15 +0200 Subject: [PATCH 004/118] Minor cleanups on README --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 17a4426..d099c7e 100644 --- a/README.md +++ b/README.md @@ -34,9 +34,9 @@ npm install nuclear-js ## Examples -- [Shopping Cart Example](./examples/shopping-cart) - General overview of NuclearJS concepts: actions, stores and getters with ReactJS -- [Flux Chat Example](./examples/flux-chat) - classic facebook flux chat example written in NuclearJS -- [Rest API Example](./examples/rest-api) - shows how to deal with fetching data from an API using NuclearJS conventions +- [Shopping Cart Example](./examples/shopping-cart) general overview of NuclearJS concepts: actions, stores and getters with ReactJS. +- [Flux Chat Example](./examples/flux-chat) classic facebook flux chat example written in NuclearJS. +- [Rest API Example](./examples/rest-api) shows how to deal with fetching data from an API using NuclearJS conventions. ## How NuclearJS differs from other Flux implementations @@ -64,8 +64,8 @@ You can read more of the implementation here: [src/evaluator.js](./src/evaluator ## For Smaller Applications -NuclearJS was designed first and foremore for large scale production codebases. For a much more lightweight Flux implementation that shares many of the same ideas and design principles of NuclearJS check out [Microcosm](https://github.com/vigetlabs/microcosm) +NuclearJS was designed first and foremore for large scale production codebases. For a much more lightweight Flux implementation that shares many of the same ideas and design principles check out [Microcosm](https://github.com/vigetlabs/microcosm). ## Contributing -Contributions are welcome, especially with the documentation website and examples. See [CONTRIBUTING.md](./CONTRIBUTING.md) for more details. +Contributions are welcome, especially with the documentation website and examples. See [CONTRIBUTING.md](./CONTRIBUTING.md). From 76ae664fa8c8a826c61cb3141cf63caf17615279 Mon Sep 17 00:00:00 2001 From: Kirill Date: Wed, 15 Jul 2015 20:11:14 +0300 Subject: [PATCH 005/118] Action handlers error handling console.group is now closing correctly if the error was thrown from the Action handler --- src/reactor.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/reactor.js b/src/reactor.js index 3ab7309..7fb6ea1 100644 --- a/src/reactor.js +++ b/src/reactor.js @@ -247,11 +247,16 @@ class Reactor { // let each core handle the message this.__stores.forEach((store, id) => { - var currState = state.get(id) - var newState = store.handle(currState, actionType, payload) - - if (this.debug && newState === undefined) { - var error = 'Store handler must return a value, did you forget a return statement' + var currState = state.get(id), newState, dispatchError + + try { + newState = store.handle(currState, actionType, payload) + } catch(e) { + dispatchError = e + } + + if (this.debug && (newState === undefined || dispatchError)) { + var error = dispatchError || 'Store handler must return a value, did you forget a return statement' logging.dispatchError(error) throw new Error(error) } From f743bfacd06b04c18797ad75d7832680b1ca0732 Mon Sep 17 00:00:00 2001 From: Baraa Hamodi Date: Wed, 15 Jul 2015 13:17:15 -0700 Subject: [PATCH 006/118] Update README.md --- README.md | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index d099c7e..6cbeb71 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,6 @@ [![Build Status](https://travis-ci.org/optimizely/nuclear-js.svg?branch=master)](https://travis-ci.org/optimizely/nuclear-js) [![Coverage Status](https://coveralls.io/repos/optimizely/nuclear-js/badge.svg?branch=master)](https://coveralls.io/r/optimizely/nuclear-js?branch=master) [![Join the chat at https://gitter.im/optimizely/nuclear-js](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/optimizely/nuclear-js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) - [![Sauce Test Status](https://saucelabs.com/browser-matrix/nuclearjs.svg)](https://saucelabs.com/u/nuclearjs) Traditional Flux architecture built with ImmutableJS data structures. @@ -14,19 +13,19 @@ Traditional Flux architecture built with ImmutableJS data structures. ## Design Philosophy -- **Simple over Easy** - The purpose of NuclearJS isn't to write the most expressive TodoMVC anyone's ever seen. The goal of NuclearJS is to provide a way to model data that is easy to reason about and decouple at very large scale. +- **Simple Over Easy** - The purpose of NuclearJS isn't to write the most expressive TodoMVC anyone's ever seen. The goal of NuclearJS is to provide a way to model data that is easy to read and decouple at very large scale. -- **Immutable** - A means for less defensive programming, more predictability and better performance +- **Immutable** - A means for less defensive programming, more predictability and better performance. - **Functional** - The framework should be implemented functionally wherever appropriate. This reduces incidental complexity and pairs well with Immutability. -- **Smallest Amount of State Possible** - Using Nuclear should encourage the modelling of your application state in the most minimal way possible. +- **Smallest Amount of State Possible** - Using NuclearJS should encourage the modeling of your application state in the most minimal way possible. - **Decoupled** - A NuclearJS system should be able to function without any sort of UI or frontend. It should be backend/frontend agnostic and be able to run on a NodeJS server. ## Installation -NuclearJS can be downloaded from npm. +NuclearJS can be downloaded from [npm](https://www.npmjs.com/). ``` npm install nuclear-js @@ -34,9 +33,9 @@ npm install nuclear-js ## Examples -- [Shopping Cart Example](./examples/shopping-cart) general overview of NuclearJS concepts: actions, stores and getters with ReactJS. -- [Flux Chat Example](./examples/flux-chat) classic facebook flux chat example written in NuclearJS. -- [Rest API Example](./examples/rest-api) shows how to deal with fetching data from an API using NuclearJS conventions. +- [Shopping Cart Example](./examples/shopping-cart) - Provides a general overview of basic NuclearJS concepts: actions, stores and getters with ReactJS. +- [Flux Chat Example](./examples/flux-chat) - A classic Facebook flux chat example written in NuclearJS. +- [Rest API Example](./examples/rest-api) - Shows how to deal with fetching data from an API using NuclearJS conventions. ## How NuclearJS differs from other Flux implementations @@ -44,9 +43,9 @@ npm install nuclear-js 2. State is not spread out through stores, instead stores are a declarative way of describing some top-level domain of your app state. For each key in the app state map a store declares the initial state of that key and how that piece of the app state reacts over time to actions dispatched on the flux system. -3. Stores are not reference-able nor have any `getX` methods on them. Instead Nuclear uses a functional lens concept called **getters**. In fact, the use of getters obviates the need for any store to know about another store, eliminating the confusing `store.waitsFor` method found in other flux implementations. +3. Stores are not reference-able nor have any `getX` methods on them. Instead NuclearJS uses a functional lens concept called **getters**. In fact, the use of getters obviates the need for any store to know about another store, eliminating the confusing `store.waitsFor` method found in other flux implementations. -4. NuclearJS is insanely efficient - change detection granularity is infinitesimal, you can even observe computed state where several pieces of the state map are combined together and run through a transform function. Nuclear is smart enough to know when the value of any computed changes and only call its observer if and only if its value changed in a way that is orders of magnitude more efficient than traditional dirty checking. It does this by leveraging ImmutableJS data structure and using a `state1 !== state2` reference comparison which runs in constant time. +4. NuclearJS is insanely efficient - change detection granularity is infinitesimal, you can even observe computed state where several pieces of the state map are combined together and run through a transform function. NuclearJS is smart enough to know when the value of any computed changes and only call its observer if and only if its value changed in a way that is orders of magnitude more efficient than traditional dirty checking. It does this by leveraging ImmutableJS data structure and using a `state1 !== state2` reference comparison which runs in constant time. 5. Automatic data observation / rendering -- automatic re-rendering is built in for React in the form of a very lightweight mixin. It is also easily possible to build the same functionality for any UI framework such as VueJS, AngularJS and even Backbone. @@ -54,7 +53,7 @@ npm install nuclear-js ## Performance -Getters are only calculated whenever their dependencies change. So if the dependency is a keypath then it will only recalculate when that path in the app state map has changed (which can be done as a simple `state.getIn(keyPath) !== oldState.getIn(keyPath)` which is an `O(log32(n))` operation. The other case is when a getter is dependent on other getters. Since every getter is a pure function, Nuclear will only recompute the getter if the values if its dependencies change. +Getters are only calculated whenever their dependencies change. So if the dependency is a keypath then it will only recalculate when that path in the app state map has changed (which can be done as a simple `state.getIn(keyPath) !== oldState.getIn(keyPath)` which is an `O(log32(n))` operation. The other case is when a getter is dependent on other getters. Since every getter is a pure function, NuclearJS will only recompute the getter if the values if its dependencies change. You can read more of the implementation here: [src/evaluator.js](./src/evaluator.js) @@ -64,7 +63,7 @@ You can read more of the implementation here: [src/evaluator.js](./src/evaluator ## For Smaller Applications -NuclearJS was designed first and foremore for large scale production codebases. For a much more lightweight Flux implementation that shares many of the same ideas and design principles check out [Microcosm](https://github.com/vigetlabs/microcosm). +NuclearJS was designed first and foremost for large scale production codebases. For a much more lightweight Flux implementation that shares many of the same ideas and design principles check out [Microcosm](https://github.com/vigetlabs/microcosm). ## Contributing From f12fbdc2c5573c9c20d0baddc6ae42526be06b23 Mon Sep 17 00:00:00 2001 From: Baraa Hamodi Date: Wed, 15 Jul 2015 13:39:12 -0700 Subject: [PATCH 007/118] Use secure links. --- CHANGELOG.md | 2 +- CONTRIBUTING.md | 2 +- README.md | 2 +- docs/src/docs/01-getting-started.md | 4 ++-- docs/src/docs/99-core-concepts.md | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad72ea3..df04b53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -65,7 +65,7 @@ Nuclear.Store({ ## 1.0.5 (June 4, 2015) - **[NEW]** Configured linting using [eslint](http://eslint.org/). Linting is now part of the [contributing process](https://github.com/optimizely/nuclear-js/blob/master/CONTRIBUTING.md). Eslint can be run using: `grunt eslint` -- **[NEW]** Implemented new developer docs landing page. This website is still in beta. You can view it here: http://optimizely.github.io/nuclear-js/ +- **[NEW]** Implemented new developer docs landing page. This website is still in beta. You can view it here: https://optimizely.github.io/nuclear-js/ - **[FIXED]** Removed accidentally checked in node_modules directory. - **[FIXED]** Addressed all the lint warnings and errors in the codebase using the new rules in `.eslintrc` - **[FIXED]** Updated documentation. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 329e171..d6e1474 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,7 +3,7 @@ Welcome to NuclearJS! If you are reading this, it probably means that you are interested in contributing to this awesome open source project. To make the process of contributing more straightforward, we have prepared this guideline for you. -To learn more about NuclearJS, check out our new [developer website!](http://optimizely.github.io/nuclear-js/) +To learn more about NuclearJS, check out our new [developer website!](https://optimizely.github.io/nuclear-js/) ## Development Setup diff --git a/README.md b/README.md index d099c7e..02450f7 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Traditional Flux architecture built with ImmutableJS data structures. ## Documentation -[http://optimizely.github.io/nuclear-js/](http://optimizely.github.io/nuclear-js/) +[https://optimizely.github.io/nuclear-js/](https://optimizely.github.io/nuclear-js/) ## Design Philosophy diff --git a/docs/src/docs/01-getting-started.md b/docs/src/docs/01-getting-started.md index 8b51a59..965f560 100644 --- a/docs/src/docs/01-getting-started.md +++ b/docs/src/docs/01-getting-started.md @@ -34,8 +34,8 @@ In this tutorial we'll create a Nuclear flux system to show a list of products a 1. Although the example code is written using ES6, this is totally optional. NuclearJS fully supports ES5 out of the box. 2. Nuclear stores work best when using ImmutableJS data structures. You will see `toImmutable` quite often, this is simply sugar -to convert plain JavaScript arrays into [`Immutable.List`](http://facebook.github.io/immutable-js/docs/#/List) and objects to -[`Immutable.Map`](http://facebook.github.io/immutable-js/docs/#/Map). The use of `toImmutable` is optional, you are free to use +to convert plain JavaScript arrays into [`Immutable.List`](https://facebook.github.io/immutable-js/docs/#/List) and objects to +[`Immutable.Map`](https://facebook.github.io/immutable-js/docs/#/Map). The use of `toImmutable` is optional, you are free to use any ImmutableJS data structure with no penalty. diff --git a/docs/src/docs/99-core-concepts.md b/docs/src/docs/99-core-concepts.md index da3ec3a..248b95d 100644 --- a/docs/src/docs/99-core-concepts.md +++ b/docs/src/docs/99-core-concepts.md @@ -82,7 +82,7 @@ var totalGetter = [ Notice that you can use getters as dependencies to other getters. This is an extremely powerful abstraction, and one that you'll undoubtedly want to become familiar with in your nuclear journey. -But you need to know one thing about getter transform functions - they MUST be pure functions (that is, a given set input values results in a [deterministic](http://en.wikipedia.org/wiki/Deterministic_algorithm) output). By making the transform functions pure, you can test Getters easier, compose them easier, and nuclear can [memoize](http://en.wikipedia.org/wiki/Memoization) calls to them, making Getter dependency resolution very performant. +But you need to know one thing about getter transform functions - they MUST be pure functions (that is, a given set input values results in a [deterministic](https://en.wikipedia.org/wiki/Deterministic_algorithm) output). By making the transform functions pure, you can test Getters easier, compose them easier, and nuclear can [memoize](https://en.wikipedia.org/wiki/Memoization) calls to them, making Getter dependency resolution very performant. __For the astute reader__ - You probably already noticed if you have experience in functional languages, but because Getters are simply arrays full of strings and pure functions, they are serializable. Since JS can stringify pure functions, your getters are nothing more than data that could be stored, sent over the wire, etc. From b3badd8b7afe258a2d03d848bf5d96c3a2d867cc Mon Sep 17 00:00:00 2001 From: Baraa Hamodi Date: Wed, 15 Jul 2015 16:00:54 -0700 Subject: [PATCH 008/118] Undo design philosophy bulletin point --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0186ffd..0cfb33f 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Traditional Flux architecture built with ImmutableJS data structures. ## Design Philosophy -- **Simple Over Easy** - The purpose of NuclearJS isn't to write the most expressive TodoMVC anyone's ever seen. The goal of NuclearJS is to provide a way to model data that is easy to read and decouple at very large scale. +- **Simple Over Easy** - The purpose of NuclearJS isn't to write the most expressive TodoMVC anyone's ever seen. The goal of NuclearJS is to provide a way to model data that is easy to reason about and decouple at very large scale. - **Immutable** - A means for less defensive programming, more predictability and better performance. From c69f1c7693f1701461300b098f03247313860af6 Mon Sep 17 00:00:00 2001 From: Kirill Date: Thu, 16 Jul 2015 13:45:22 +0300 Subject: [PATCH 009/118] resolved lint errors --- src/reactor.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/reactor.js b/src/reactor.js index 7fb6ea1..ee29040 100644 --- a/src/reactor.js +++ b/src/reactor.js @@ -247,15 +247,17 @@ class Reactor { // let each core handle the message this.__stores.forEach((store, id) => { - var currState = state.get(id), newState, dispatchError - + var currState = state.get(id) + var newState + var dispatchError + try { newState = store.handle(currState, actionType, payload) } catch(e) { dispatchError = e } - - if (this.debug && (newState === undefined || dispatchError)) { + + if (this.debug && (newState === undefined || dispatchError)) { var error = dispatchError || 'Store handler must return a value, did you forget a return statement' logging.dispatchError(error) throw new Error(error) From f74b4ba146353cc465974f3e5f8765c25e2afcbd Mon Sep 17 00:00:00 2001 From: Kirill Date: Fri, 17 Jul 2015 12:31:01 +0300 Subject: [PATCH 010/118] test for pull request #123: when the store's action handler throws, the error has to be logged (and hence the console group will be closed correctly) --- tests/reactor-tests.js | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tests/reactor-tests.js b/tests/reactor-tests.js index 7f825e8..4167b2b 100644 --- a/tests/reactor-tests.js +++ b/tests/reactor-tests.js @@ -4,6 +4,7 @@ var List = require('immutable').List var Reactor = require('../src/main').Reactor var Store = require('../src/main').Store var toImmutable = require('../src/immutable-helpers').toImmutable +var logging = require('../src/logging') describe('Reactor', () => { @@ -451,6 +452,40 @@ describe('Reactor', () => { }) }) + describe('when debug is true and a store has a handler for an action but throws', () => { + var reactor + + beforeEach(() => { + spyOn(logging, 'dispatchError') + var throwingStore = new Store({ + getInitialState() { + return 1 + }, + initialize() { + this.on('set', (_, val) => {throw new Error('Error during action handling')}) + }, + }) + + reactor = new Reactor({ + debug: true, + }) + reactor.registerStores({ + test: throwingStore, + }) + }) + + afterEach(() => { + reactor.reset() + }) + + it('should log and throw an error', function() { + expect(function() { + reactor.dispatch('set', 'foo') + }).toThrow() + expect(logging.dispatchError).toHaveBeenCalled() + }) + }) + describe('#registerStores', () => { var reactor From 2b31a2658018549cc200ed0929cf0b980d50af71 Mon Sep 17 00:00:00 2001 From: Roman Onufryk Date: Fri, 17 Jul 2015 17:10:48 +0200 Subject: [PATCH 011/118] Typo fix --- docs/src/docs/03-creating-stores.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/docs/03-creating-stores.md b/docs/src/docs/03-creating-stores.md index a616bd1..ac9b936 100644 --- a/docs/src/docs/03-creating-stores.md +++ b/docs/src/docs/03-creating-stores.md @@ -21,7 +21,7 @@ then returns new state. Handlers have the following signature: handler(currentState: any, payload: any) ``` -In Nucler, state can only be an ImmutableJS data type, such as an `Immutable.Map` or an `Immutable.List`, or a JavaScript primitive. +In Nuclear, state can only be an ImmutableJS data type, such as an `Immutable.Map` or an `Immutable.List`, or a JavaScript primitive. Because stores in Nuclear don't hold state — they simply receive state, transform it, and return new state — there is no need to worry about stores knowing about other stores. That means no confusing `store.waitsFor` and no cross-pollution of data. In Nuclear, the sole responsibility of a store is to return a portion From fad1ef92b0e9409345e66b2748fd9b57f934ed31 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 18 Jul 2015 14:39:43 -0700 Subject: [PATCH 012/118] Raise error if trying to dispatch from observer --- src/reactor.js | 15 ++++++++++++ tests/reactor-tests.js | 53 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/src/reactor.js b/src/reactor.js index ee29040..efcc36c 100644 --- a/src/reactor.js +++ b/src/reactor.js @@ -52,6 +52,9 @@ class Reactor { this.__batchDepth = 0 // number of dispatches in the top most batch cycle this.__batchDispatchCount = 0 + + // keep track if we are currently dispatching + this.__isDispatching = false } /** @@ -105,6 +108,14 @@ class Reactor { * @param {object|undefined} payload */ dispatch(actionType, payload) { + if (this.__batchDepth === 0) { + if (this.__isDispatching) { + this.__isDispatching = false + throw new Error('Dispatch may not be called while a dispatch is in progress') + } + this.__isDispatching = true + } + var prevState = this.state this.state = this.__handleAction(prevState, actionType, payload) @@ -112,6 +123,7 @@ class Reactor { this.__batchDispatchCount++ } else if (this.state !== prevState) { this.__notify() + this.__isDispatching = false } } @@ -285,7 +297,10 @@ class Reactor { if (this.__batchDepth <= 0) { if (this.__batchDispatchCount > 0) { + // set to true to catch if dispatch called from observer + this.__isDispatching = true this.__notify() + this.__isDispatching = false } this.__batchDispatchCount = 0 } diff --git a/tests/reactor-tests.js b/tests/reactor-tests.js index 4167b2b..8449a42 100644 --- a/tests/reactor-tests.js +++ b/tests/reactor-tests.js @@ -179,6 +179,25 @@ describe('Reactor', () => { expect(mockFn.calls.count()).toEqual(0) }) + + it('should raise an error if already dispatching another action', () => { + reactor.observe([], state => reactor.dispatch('noop', {})) + + expect(() => checkoutActions.setTaxPercent(5)).toThrow( + new Error('Dispatch may not be called while a dispatch is in progress')) + }) + + it('should keep working after it raised for dispatching while dispatching', () => { + var unWatchFn = reactor.observe([], state => reactor.dispatch('noop', {})) + + expect(() => checkoutActions.setTaxPercent(5)).toThrow( + new Error('Dispatch may not be called while a dispatch is in progress')) + + unWatchFn() + + expect(() => checkoutActions.setTaxPercent(5)).not.toThrow( + new Error('Dispatch may not be called while a dispatch is in progress')) + }) }) // when dispatching a relevant action describe('#observe', () => { @@ -1013,5 +1032,39 @@ describe('Reactor', () => { expect(observeSpy.calls.count()).toBe(1) expect(firstCallArg).toEqual(['one', 'two', 'three']) }) + + it('should not allow dispatch to be called from an observer', () => { + reactor.observe([], state => reactor.dispatch('noop', {})) + + expect(() => { + reactor.batch(() => { + reactor.dispatch('add', 'one') + reactor.dispatch('add', 'two') + }) + }).toThrow( + new Error('Dispatch may not be called while a dispatch is in progress')) + }) + + it('should keep working after it raised for dispatching while dispatching', () => { + var unWatchFn = reactor.observe([], state => reactor.dispatch('noop', {})) + + expect(() => { + reactor.batch(() => { + reactor.dispatch('add', 'one') + reactor.dispatch('add', 'two') + }) + }).toThrow( + new Error('Dispatch may not be called while a dispatch is in progress')) + + unWatchFn() + + expect(() => { + reactor.batch(() => { + reactor.dispatch('add', 'one') + reactor.dispatch('add', 'two') + }) + }).not.toThrow( + new Error('Dispatch may not be called while a dispatch is in progress')) + }) }) }) From d0d967acc19e01186e9bd67d81694c43a6828165 Mon Sep 17 00:00:00 2001 From: Baraa Hamodi Date: Sat, 18 Jul 2015 22:41:09 -0700 Subject: [PATCH 013/118] Random general cleanup... --- CHANGELOG.md | 10 +++++----- docs/README.md | 6 +++--- docs/TODO.md | 3 ++- docs/grunt/build-site.js | 2 -- docs/src/components/doc-sidebar.js | 2 -- docs/src/components/usage-example.js | 12 ++++++------ docs/src/docs/01-getting-started.md | 10 +++++----- docs/src/docs/02-creating-actions.md | 2 +- docs/src/docs/03-creating-stores.md | 6 +++--- docs/src/docs/05-hooking-up-to-react.md | 8 ++++---- .../docs/06-async-actions-and-optimistic-updates.md | 4 ++-- docs/src/docs/07-api.md | 8 ++++---- docs/src/docs/99-core-concepts.md | 12 ++++++------ docs/src/pages/index.js | 6 +++--- src/change-observer.js | 2 +- src/evaluator.js | 2 +- src/immutable-helpers.js | 4 ++-- src/reactor.js | 2 +- src/store.js | 8 ++++---- src/utils.js | 4 ++-- 20 files changed, 55 insertions(+), 58 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index df04b53..bb69051 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ #### `Reactor#serialize()` -Returns a plain javascript object representing the application state. By defualt this maps over all stores and returns `toJS(storeState)`. +Returns a plain JavaScript object representing the application state. By default this maps over all stores and returns `toJS(storeState)`. ```js reactor.loadState(reactor.serialize()) @@ -16,7 +16,7 @@ reactor.loadState(reactor.serialize()) #### `Reactor#loadState( state )` -Takes a plain javascript object and merges into the reactor state, using `store.deserialize` +Takes a plain JavaScript object and merges into the reactor state, using `store.deserialize` This can be useful if you need to load data already on the page. @@ -29,7 +29,7 @@ reactor.loadState({ #### `Store#serialize` -Serialization method for the store's data, by default its implemented as `Nuclear.toJS' which converts ImmutableJS objects to plain javascript. +Serialization method for the store's data, by default its implemented as `Nuclear.toJS' which converts ImmutableJS objects to plain JavaScript. This is overridable for your specific data needs. ```js @@ -48,7 +48,7 @@ Nuclear.Store({ #### `Store#deserialize` -Serialization method for the store's data, by default its implemented as `Nuclear.toImmutable' which converts plain javascript objects to ImmutableJS data structures. +Serialization method for the store's data, by default its implemented as `Nuclear.toImmutable' which converts plain JavaScript objects to ImmutableJS data structures. This is overridable for your specific data needs. ```js @@ -79,7 +79,7 @@ Nuclear.Store({ ## 1.0.1 (April 27, 2015) -- **[NEW]** Expose `createReactMixin` functionality on Nuclear singleton. +- **[NEW]** Expose `createReactMixin` functionality on NuclearJS singleton. - **[FIXED]** Fix `new Store()` from throwing error when not passed a config object. ## 1.0.0 (April 25, 2015) diff --git a/docs/README.md b/docs/README.md index e9bdeec..8cd1e99 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,14 +1,14 @@ # NuclearJS Docs -Doc site statically generated using `React` + `NuclearJS`. +Documentation site statically generated using `React` + `NuclearJS`. -##### For development +### For development ```sh grunt dev ``` -##### To deploy to gh-pages +### To deploy to gh-pages ```sh grunt publish diff --git a/docs/TODO.md b/docs/TODO.md index 3d5b63c..1c8a52e 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -1,4 +1,4 @@ -# Documentation site todo +# Documentation site TODO List - [x] links to docs, API, github - [x] build pipeline for docs from MD files @@ -8,3 +8,4 @@ - [ ] scaffold design patterns/examples area - [ ] mobile side navbar - [ ] build pipeline for examples, create example component, possibly with code editing +- [ ] add active state to Docs side bar. (currently doesn't show which tab is active/selected) diff --git a/docs/grunt/build-site.js b/docs/grunt/build-site.js index 2497342..359758b 100644 --- a/docs/grunt/build-site.js +++ b/docs/grunt/build-site.js @@ -112,9 +112,7 @@ function parseDocs(globPattern, opts, cb) { }) } - // Util Functions - function filenameOnly(filepath) { return path.basename(filepath, path.extname(filepath)) } diff --git a/docs/src/components/doc-sidebar.js b/docs/src/components/doc-sidebar.js index 09b7186..e1293b8 100644 --- a/docs/src/components/doc-sidebar.js +++ b/docs/src/components/doc-sidebar.js @@ -4,8 +4,6 @@ import { BASE_URL } from '../globals' export default React.createClass({ //propTypes: { //navStructure: React.PropTypes.objectOf(React.PropTypes.shape({ - - //})) //}, diff --git a/docs/src/components/usage-example.js b/docs/src/components/usage-example.js index ab80e3d..4580f3e 100644 --- a/docs/src/components/usage-example.js +++ b/docs/src/components/usage-example.js @@ -102,7 +102,7 @@ var item0Price = reactor.evaluate(['items', 0, 'price']) // Evaluate by getter var filteredItems = reactor.evaluate(filteredItemsGetter) -// Evaluate and coerce to plain javascript +// Evaluate and coerce to plain JavaScript var itemsPOJO = reactor.evaluateToJS(filteredItemsGetter) // Observation @@ -140,7 +140,7 @@ export default React.createClass({

    - initialize() - Sets up any action handlers, by specifying the action type and a function that transforms + initialize() - Sets up any action handlers, by specifying the action type and a function that transforms

    (storeState, action) => (newStoreState)

    @@ -213,15 +213,15 @@ export default React.createClass({

    - Nuclear maintains a very non-magical approach to dispatching actions. Simply call reactor.dispatch with the actionType and payload. + NuclearJS maintains a very non-magical approach to dispatching actions. Simply call reactor.dispatch with the actionType and payload.

    - All action handling is done synchronously, leaving the state of the system very predicatable after every action. + All action handling is done synchronously, leaving the state of the system very predictable after every action.

    - Because actions are simply functions, it is very easy to compose actions together using plain javascript. + Because actions are simply functions, it is very easy to compose actions together using plain JavaScript.

    @@ -239,7 +239,7 @@ export default React.createClass({

    - Nuclear also provides imperative mechanisms for evaluating and observing state. + NuclearJS also provides imperative mechanisms for evaluating and observing state.

    diff --git a/docs/src/docs/01-getting-started.md b/docs/src/docs/01-getting-started.md index 965f560..bc7cdee 100644 --- a/docs/src/docs/01-getting-started.md +++ b/docs/src/docs/01-getting-started.md @@ -17,7 +17,7 @@ npm install --save nuclear-js ## Overview -In this tutorial we'll create a Nuclear flux system to show a list of products and add them to a shopping cart. Here's the plan: +In this tutorial we'll create a NuclearJS flux system to show a list of products and add them to a shopping cart. Here's the plan: 1. Create a **Reactor** @@ -33,7 +33,7 @@ In this tutorial we'll create a Nuclear flux system to show a list of products a 1. Although the example code is written using ES6, this is totally optional. NuclearJS fully supports ES5 out of the box. -2. Nuclear stores work best when using ImmutableJS data structures. You will see `toImmutable` quite often, this is simply sugar +2. NuclearJS stores work best when using ImmutableJS data structures. You will see `toImmutable` quite often, this is simply sugar to convert plain JavaScript arrays into [`Immutable.List`](https://facebook.github.io/immutable-js/docs/#/List) and objects to [`Immutable.Map`](https://facebook.github.io/immutable-js/docs/#/Map). The use of `toImmutable` is optional, you are free to use any ImmutableJS data structure with no penalty. @@ -41,11 +41,11 @@ any ImmutableJS data structure with no penalty. ## Creating a `Reactor` -To get started, we'll create a Nuclear `Reactor`. In Nuclear, the `Reactor` is the brains of the system and in some ways analogous +To get started, we'll create a NuclearJS `Reactor`. In Nuclear, the `Reactor` is the brains of the system and in some ways analogous to the traditional Flux `dispatcher` (though it works differently under the hood and provides a few extra features, which we'll cover later). -Generally you'll only have one reactor for your application, however they are instanceable for server-side rendering. +Generally you'll only have one reactor for your application, however they are instance-able for server-side rendering. The reactor has two main jobs: @@ -67,7 +67,7 @@ export default reactor ``` _* If you pass a `debug: true` option when instantiating a reactor, you'll get great debugging tools that print to your browser console. -This is completely optional, but very useful for keeping tracking of dispatched actions and subsequest changes in state._ +This is completely optional, but very useful for keeping tracking of dispatched actions and subsequent changes in state._ Now that we have our reactor, let's create some actions. diff --git a/docs/src/docs/02-creating-actions.md b/docs/src/docs/02-creating-actions.md index b953490..9aacd0e 100644 --- a/docs/src/docs/02-creating-actions.md +++ b/docs/src/docs/02-creating-actions.md @@ -60,7 +60,7 @@ We've now created two actions that we can use to send data into the system. While synchronous actions are great, often you'll need to perform an asynchronous operation before dispatching an action. Nuclear fully supports creating actions asynchronously, as we're doing in `fetchProducts`. This is a common pattern you'll use as your application grows, -and Nuclear has no opinion on how you perform your operations: callbacks, Promises, Generators, ES7 async functions — they'll all work just fine! +and NuclearJS has no opinion on how you perform your operations: callbacks, Promises, Generators, ES7 async functions — they'll all work just fine! If you'd like to jump ahead, you can read more about [async actions](./04-async-actions-and-optimistic-updates.html). diff --git a/docs/src/docs/03-creating-stores.md b/docs/src/docs/03-creating-stores.md index ac9b936..e3b53d3 100644 --- a/docs/src/docs/03-creating-stores.md +++ b/docs/src/docs/03-creating-stores.md @@ -9,7 +9,7 @@ In Flux, stores are used for managing application state, but they don't represen More than simply managing ORM-style objects, **stores manage the state for a particular domain within the application**. -Unlike many other Flux libraries, Nuclear stores hold no state. Instead, they provide a collection of functions that transform current state into new state. +Unlike many other Flux libraries, NuclearJS stores hold no state. Instead, they provide a collection of functions that transform current state into new state. Stores provide a `getInitialState` method, which returns the initial state value that a store will manage, and an `initialize` hook, which is used to define what actions a store will respond to by attaching handlers. @@ -23,7 +23,7 @@ handler(currentState: any, payload: any) In Nuclear, state can only be an ImmutableJS data type, such as an `Immutable.Map` or an `Immutable.List`, or a JavaScript primitive. -Because stores in Nuclear don't hold state — they simply receive state, transform it, and return new state — there is no need to worry about stores knowing +Because stores in NuclearJS don't hold state — they simply receive state, transform it, and return new state — there is no need to worry about stores knowing about other stores. That means no confusing `store.waitsFor` and no cross-pollution of data. In Nuclear, the sole responsibility of a store is to return a portion of existing or transformed application state. The responsibility of reading application state falls on **Getters**, which we'll cover later. @@ -224,7 +224,7 @@ However, if stores are limited in scope, how can you read substantive data from It's actually quite simple: **composition**. -Nuclear allows you to combine data from stores in a non-destructive manner, check it out: +NuclearJS allows you to combine data from stores in a non-destructive manner, check it out: ```javascript reactor.evaluate([ diff --git a/docs/src/docs/05-hooking-up-to-react.md b/docs/src/docs/05-hooking-up-to-react.md index adeb352..d07c64e 100644 --- a/docs/src/docs/05-hooking-up-to-react.md +++ b/docs/src/docs/05-hooking-up-to-react.md @@ -7,13 +7,13 @@ section: "Guide" ### Binding application state to components -Every Nuclear Reactor comes with `reactor.ReactMixin` to easily create an always-in-sync binding between any KeyPath or Getter value +Every NuclearJS Reactor comes with `reactor.ReactMixin` to easily create an always-in-sync binding between any KeyPath or Getter value and a React component's state. The ability to observe any piece of composite data is immensely powerful and trivializes a lot of what other frameworks work hard to solve. To use simply include the `reactor.ReactMixin` and implement the `getDataBindings()` function that returns an object of state properties -to `KeyPath` or `Getter`. Nuclear will take care of the initial sync, observation and destroying the subscription when on `componentWillUnmount`. +to `KeyPath` or `Getter`. NuclearJS will take care of the initial sync, observation and destroying the subscription when on `componentWillUnmount`. **First let's expand our main file to initiate the fetch for products.** @@ -140,9 +140,9 @@ export default React.createClass({ ## Recap -Once you have a functioning Nuclear Reactor, hooking it up to a React application is very easy using the `reactor.ReactMixin` + `getDataBindings()` method. +Once you have a functioning NuclearJS Reactor, hooking it up to a React application is very easy using the `reactor.ReactMixin` + `getDataBindings()` method. -Nuclear will automatically sync the value of a getter to your component via `this.setState` whenever the underlying getter value changes. Meaning you never +NuclearJS will automatically sync the value of a getter to your component via `this.setState` whenever the underlying getter value changes. Meaning you never have to explicitly call `this.setState` to re-render a component. In the next section we will cover hooking up actions to our react components. diff --git a/docs/src/docs/06-async-actions-and-optimistic-updates.md b/docs/src/docs/06-async-actions-and-optimistic-updates.md index 6e075c1..fc987a2 100644 --- a/docs/src/docs/06-async-actions-and-optimistic-updates.md +++ b/docs/src/docs/06-async-actions-and-optimistic-updates.md @@ -163,7 +163,7 @@ This ends our getting started example, for a more in depth look all of the above For additional documentation and resources checkout the following: - [API Documentation](./07-api.html) -- [Flux Chat Example](https://github.com/optimizely/nuclear-js/tree/master/examples/flux-chat) - classic facebook flux chat example written in NuclearJS -- [Rest API Example](https://github.com/optimizely/nuclear-js/tree/master/examples/rest-api) - shows how to deal with fetching data from an API using NuclearJS conventions +- [Flux Chat Example](https://github.com/optimizely/nuclear-js/tree/master/examples/flux-chat) - A classic Facebook flux chat example written in NuclearJS. +- [Rest API Example](https://github.com/optimizely/nuclear-js/tree/master/examples/rest-api) - Shows how to deal with fetching data from an API using NuclearJS conventions. More coming soon... diff --git a/docs/src/docs/07-api.md b/docs/src/docs/07-api.md index 9ef0bf0..aaa0129 100644 --- a/docs/src/docs/07-api.md +++ b/docs/src/docs/07-api.md @@ -89,7 +89,7 @@ reactor.observe([ _added in 1.1_ -Returns a plain javascript object representing the application state. By defualt this maps over all stores and returns `toJS(storeState)`. +Returns a plain JavaScript object representing the application state. By default this maps over all stores and returns `toJS(storeState)`. ```javascript reactor.loadState(reactor.serialize()) @@ -99,7 +99,7 @@ reactor.loadState(reactor.serialize()) _added in 1.1_ -Takes a plain javascript object and merges into the reactor state, using `store.deserialize` +Takes a plain JavaScript object and merges into the reactor state, using `store.deserialize` This can be useful if you need to load data already on the page. @@ -201,7 +201,7 @@ Responsible for setting up action handlers for the store using `this.on(actionTy _added in 1.1_ -Serialization method for the store's data, by default its implemented as `Nuclear.toJS' which converts ImmutableJS objects to plain javascript. +Serialization method for the store's data, by default its implemented as `Nuclear.toJS' which converts ImmutableJS objects to plain JavaScript. This is overridable for your specific data needs. ```javascript @@ -222,7 +222,7 @@ Nuclear.Store({ _added in 1.1_ -Serialization method for the store's data, by default its implemented as `Nuclear.toImmutable' which converts plain javascript objects to ImmutableJS data structures. +Serialization method for the store's data, by default its implemented as `Nuclear.toImmutable' which converts plain JavaScript objects to ImmutableJS data structures. This is overridable for your specific data needs. ```javascript diff --git a/docs/src/docs/99-core-concepts.md b/docs/src/docs/99-core-concepts.md index 248b95d..eb61546 100644 --- a/docs/src/docs/99-core-concepts.md +++ b/docs/src/docs/99-core-concepts.md @@ -5,11 +5,11 @@ section: "Guide" ## Core Concepts -The easiest way to think about how NuclearJS is modelling the state of your system is to imagine it all as a single map (or JavaScript object). If you are familiar with Om then the concept of a singular App State is very familiar already. +The easiest way to think about how NuclearJS is modeling the state of your system is to imagine it all as a single map (or JavaScript object). If you are familiar with Om then the concept of a singular App State is very familiar already. Each entry in this top level map contains a portion of the entire app state for a specific domain and are managed by **stores**. -Imagine modelling a shopping cart. Our app state would look like: +Imagine modeling a shopping cart. Our app state would look like: ```javascript { @@ -31,7 +31,7 @@ live in our app state because those are all examples of **computable state**, an #### Reactor -In Nuclear a Reactor is the container that holds your app state, it's where you register stores, dispatch actions and read the current state of your system. Reactor's are the only stateful part of Nuclear and have only 3 API methods you REALLY need to know: `dispatch`, `get`, and `observe`. Don't worry, extensive API docs will be provided for all of these methods. +In NuclearJS a Reactor is the container that holds your app state, it's where you register stores, dispatch actions and read the current state of your system. Reactor's are the only stateful part of NuclearJS and have only 3 API methods you REALLY need to know: `dispatch`, `get`, and `observe`. Don't worry, extensive API docs will be provided for all of these methods. #### Stores @@ -80,9 +80,9 @@ var totalGetter = [ ] ``` -Notice that you can use getters as dependencies to other getters. This is an extremely powerful abstraction, and one that you'll undoubtedly want to become familiar with in your nuclear journey. +Notice that you can use getters as dependencies to other getters. This is an extremely powerful abstraction, and one that you'll undoubtedly want to become familiar with in your NuclearJS journey. -But you need to know one thing about getter transform functions - they MUST be pure functions (that is, a given set input values results in a [deterministic](https://en.wikipedia.org/wiki/Deterministic_algorithm) output). By making the transform functions pure, you can test Getters easier, compose them easier, and nuclear can [memoize](https://en.wikipedia.org/wiki/Memoization) calls to them, making Getter dependency resolution very performant. +But you need to know one thing about getter transform functions - they MUST be pure functions (that is, a given set input values results in a [deterministic](https://en.wikipedia.org/wiki/Deterministic_algorithm) output). By making the transform functions pure, you can test Getters easier, compose them easier, and NuclearJS can [memoize](https://en.wikipedia.org/wiki/Memoization) calls to them, making Getter dependency resolution very efficient. __For the astute reader__ - You probably already noticed if you have experience in functional languages, but because Getters are simply arrays full of strings and pure functions, they are serializable. Since JS can stringify pure functions, your getters are nothing more than data that could be stored, sent over the wire, etc. @@ -260,7 +260,7 @@ var ShoppingCart = React.createClass({ mixins: [reactor.ReactMixin], // simply implement this function to keep a component's state - // in sync with a Nuclear Reactor + // in sync with a NuclearJS Reactor getDataBindings() { return { // can reference a reactor KeyPath diff --git a/docs/src/pages/index.js b/docs/src/pages/index.js index f24927e..a3f4b88 100644 --- a/docs/src/pages/index.js +++ b/docs/src/pages/index.js @@ -42,7 +42,7 @@ export default React.createClass({

    - Powerful functional dataflow + Powerful functional dataflow

    Compose and transform your data together statelessly and efficiently using a functional lens concept called Getters. @@ -58,7 +58,7 @@ export default React.createClass({ Any Getter can be observed by a view to be notified whenever its derived value changes.

    - Nuclear includes tools to integrate with libraries such as React and VueJS out of the box. + NuclearJS includes tools to integrate with libraries such as React and VueJS out of the box.

    @@ -68,7 +68,7 @@ export default React.createClass({ Thanks to immutable data, change detection can be efficiently performed at any level of granularity by a constant time reference equality (===) check.

    - Since Getters use pure functions, Nuclear utilizes memoization to only recompute parts of the dataflow that might change. + Since Getters use pure functions, NuclearJS utilizes memoization to only recompute parts of the dataflow that might change.

    diff --git a/src/change-observer.js b/src/change-observer.js index 82c734c..5f1ce84 100644 --- a/src/change-observer.js +++ b/src/change-observer.js @@ -54,7 +54,7 @@ class ChangeObserver { } /** - * Specify an getter and a change handler fn + * Specify a getter and a change handler function * Handler function is called whenever the value of the getter changes * @param {Getter} getter * @param {function} handler diff --git a/src/evaluator.js b/src/evaluator.js index 4ed9abb..852d4f4 100644 --- a/src/evaluator.js +++ b/src/evaluator.js @@ -140,7 +140,7 @@ class Evaluator { * @param {Getter} */ untrack(getter) { - // TODO: untrack all depedencies + // TODO: untrack all dependencies } reset() { diff --git a/src/immutable-helpers.js b/src/immutable-helpers.js index 9b40a92..4076d26 100644 --- a/src/immutable-helpers.js +++ b/src/immutable-helpers.js @@ -15,7 +15,7 @@ function isImmutable(obj) { /** * Returns true if the value is an ImmutableJS data structure - * or a javascript primitive that is immutable (stirng, number, etc) + * or a JavaScript primitive that is immutable (string, number, etc) * @param {*} obj * @return {boolean} */ @@ -31,7 +31,7 @@ function isImmutableValue(obj) { * Can be called on any type */ function toJS(arg) { - // arg instanceof Immutable.Sequence is unreleable + // arg instanceof Immutable.Sequence is unreliable return (isImmutable(arg)) ? arg.toJS() : arg diff --git a/src/reactor.js b/src/reactor.js index efcc36c..8fdf232 100644 --- a/src/reactor.js +++ b/src/reactor.js @@ -14,7 +14,7 @@ var each = require('./utils').each /** - * In Nuclear Reactors are where state is stored. Reactors + * State is stored in NuclearJS Reactors. Reactors * contain a 'state' object which is an Immutable.Map * * The only way Reactors can change state is by reacting to diff --git a/src/store.js b/src/store.js index 5568ca2..65f1c10 100644 --- a/src/store.js +++ b/src/store.js @@ -25,7 +25,7 @@ class Store { } /** - * This method is overriden by extending classses to setup message handlers + * This method is overridden by extending classes to setup message handlers * via `this.on` and to set up the initial state * * Anything returned from this function will be coerced into an ImmutableJS value @@ -58,7 +58,7 @@ class Store { /** * Pure function taking the current state of store and returning - * the new state after a Nuclear reactor has been reset + * the new state after a NuclearJS reactor has been reset * * Overridable */ @@ -74,7 +74,7 @@ class Store { } /** - * Serializes store state to plain JSON serializable javascript + * Serializes store state to plain JSON serializable JavaScript * Overridable * @param {*} * @return {*} @@ -84,7 +84,7 @@ class Store { } /** - * Deserializes plain javascript to store state + * Deserializes plain JavaScript to store state * Overridable * @param {*} * @return {*} diff --git a/src/utils.js b/src/utils.js index a65dfc5..1ee2c25 100644 --- a/src/utils.js +++ b/src/utils.js @@ -16,7 +16,7 @@ exports.isArray = Array.isArray /* istanbul ignore next */|| function(val) { return objectToString(val) === '[object Array]' } -// taken from underscore source to account for browser descrepency +// taken from underscore source to account for browser discrepancy /* istanbul ignore if */ if (typeof /./ !== 'function' && typeof Int8Array !== 'object') { /** @@ -39,7 +39,7 @@ if (typeof /./ !== 'function' && typeof Int8Array !== 'object') { } /** - * Checks if the passed in value is af type Object + * Checks if the passed in value is of type Object * @param {*} val * @return {boolean} */ From ccae1b8c92df7e5bbefa2bf0630c357b375bf127 Mon Sep 17 00:00:00 2001 From: sinewyk Date: Sat, 18 Jul 2015 01:10:54 +0200 Subject: [PATCH 014/118] universal example flavored some of the code with es6 to showcase how to use the HoC as a decorator package styles in uses https://github.com/jordangarcia/nuclear-js-react-addons --- examples/isomorphic-flux-chat/.babelrc | 4 + examples/isomorphic-flux-chat/.eslintrc | 6 ++ examples/isomorphic-flux-chat/.gitignore | 1 + examples/isomorphic-flux-chat/README.md | 33 +++++++ examples/isomorphic-flux-chat/client.js | 27 +++++ .../components/ChatApp.jsx | 33 +++++++ .../components/MessageComposer.jsx | 63 ++++++++++++ .../components/MessageListItem.jsx | 47 +++++++++ .../components/MessageSection.jsx | 64 ++++++++++++ .../components/ThreadListItem.jsx | 65 ++++++++++++ .../components/ThreadSection.jsx | 63 ++++++++++++ examples/isomorphic-flux-chat/css/chatapp.css | 98 +++++++++++++++++++ examples/isomorphic-flux-chat/index.html | 15 +++ .../isomorphic-flux-chat/mock/messages.js | 65 ++++++++++++ .../modules/chat/action-types.js | 6 ++ .../modules/chat/actions.js | 39 ++++++++ .../modules/chat/getters.js | 43 ++++++++ .../modules/chat/index.js | 15 +++ .../chat/stores/current-thread-id-store.js | 19 ++++ .../modules/chat/stores/thread-store.js | 70 +++++++++++++ examples/isomorphic-flux-chat/package.json | 29 ++++++ examples/isomorphic-flux-chat/server.js | 76 ++++++++++++++ .../isomorphic-flux-chat/webpack.config.js | 69 +++++++++++++ 23 files changed, 950 insertions(+) create mode 100644 examples/isomorphic-flux-chat/.babelrc create mode 100644 examples/isomorphic-flux-chat/.eslintrc create mode 100644 examples/isomorphic-flux-chat/.gitignore create mode 100644 examples/isomorphic-flux-chat/README.md create mode 100644 examples/isomorphic-flux-chat/client.js create mode 100644 examples/isomorphic-flux-chat/components/ChatApp.jsx create mode 100644 examples/isomorphic-flux-chat/components/MessageComposer.jsx create mode 100644 examples/isomorphic-flux-chat/components/MessageListItem.jsx create mode 100644 examples/isomorphic-flux-chat/components/MessageSection.jsx create mode 100644 examples/isomorphic-flux-chat/components/ThreadListItem.jsx create mode 100644 examples/isomorphic-flux-chat/components/ThreadSection.jsx create mode 100644 examples/isomorphic-flux-chat/css/chatapp.css create mode 100644 examples/isomorphic-flux-chat/index.html create mode 100644 examples/isomorphic-flux-chat/mock/messages.js create mode 100644 examples/isomorphic-flux-chat/modules/chat/action-types.js create mode 100644 examples/isomorphic-flux-chat/modules/chat/actions.js create mode 100644 examples/isomorphic-flux-chat/modules/chat/getters.js create mode 100644 examples/isomorphic-flux-chat/modules/chat/index.js create mode 100644 examples/isomorphic-flux-chat/modules/chat/stores/current-thread-id-store.js create mode 100644 examples/isomorphic-flux-chat/modules/chat/stores/thread-store.js create mode 100644 examples/isomorphic-flux-chat/package.json create mode 100644 examples/isomorphic-flux-chat/server.js create mode 100644 examples/isomorphic-flux-chat/webpack.config.js diff --git a/examples/isomorphic-flux-chat/.babelrc b/examples/isomorphic-flux-chat/.babelrc new file mode 100644 index 0000000..15d27ad --- /dev/null +++ b/examples/isomorphic-flux-chat/.babelrc @@ -0,0 +1,4 @@ +{ + "stage": 0, + "loose": "all" +} diff --git a/examples/isomorphic-flux-chat/.eslintrc b/examples/isomorphic-flux-chat/.eslintrc new file mode 100644 index 0000000..339f75b --- /dev/null +++ b/examples/isomorphic-flux-chat/.eslintrc @@ -0,0 +1,6 @@ +{ + "extends": "./../../.eslintrc", + "ecmaFeatures": { + "jsx": true + } +} diff --git a/examples/isomorphic-flux-chat/.gitignore b/examples/isomorphic-flux-chat/.gitignore new file mode 100644 index 0000000..68b6768 --- /dev/null +++ b/examples/isomorphic-flux-chat/.gitignore @@ -0,0 +1 @@ +*bundle.js diff --git a/examples/isomorphic-flux-chat/README.md b/examples/isomorphic-flux-chat/README.md new file mode 100644 index 0000000..cfead23 --- /dev/null +++ b/examples/isomorphic-flux-chat/README.md @@ -0,0 +1,33 @@ +## Isomorphic Flux Chat Example + +This is the Facebook [flux-chat](https://github.com/facebook/flux/tree/master/examples/flux-chat) +example re-written in NuclearJS to demonstate the differences in the libraries as well as to show how Getters are used. + +## Running + +You must have [npm](https://www.npmjs.org/) installed on your computer. +From the root project directory run these commands from the command line: + +`npm install` + +This will install all dependencies. + +To build the project, first run this command: + +`npm run build` it will build the project. + +Then run `npm start`. It will run the server on the port 1337, if you want another port use `npm start -- --port=9000` for example. + +That's it. The client and server share everything except the entry file which is different for the client (`./js/main.js`) and server (`./server.js`). + +Then open a browser and go to `http://localhost:1337` and you can expect the page while disabling javascript =). + +Don't hesitate to run `npm run watch` and `npm start` in parallel to hack around. + +## Considerations + +Do not forget that React.render* functions are synchronous, it's up to the developers to actually make sure that the reactor is already filled before they call the render function. + +One could for example make all actions return a promise or be callback based. + +Then in the request server side, just wait for the data to be fetched somehow, and then render. diff --git a/examples/isomorphic-flux-chat/client.js b/examples/isomorphic-flux-chat/client.js new file mode 100644 index 0000000..9a886a8 --- /dev/null +++ b/examples/isomorphic-flux-chat/client.js @@ -0,0 +1,27 @@ +var mockData = require('./mock/messages') +var Nuclear = require('nuclear-js') +var NuclearAddons = require('nuclear-js-react-addons') +var reactor = new Nuclear.Reactor({ + debug: process.env.NODE_ENV, +}) +window.reactor = reactor +var Chat = require('./modules/chat') + +var ChatApp = require('./components/ChatApp.jsx') +ChatApp = NuclearAddons.provideReactor(ChatApp) + +Chat.register(reactor) + +// @todo: refactor to use new nuclear methods when 1.1 lands ? +if (window.window.reactor_state !== null) { + reactor.__state = Nuclear.Immutable.fromJS(window.reactor_state) +} else { + Chat.actions.receiveAll(reactor, mockData) +} + +var React = require('react') +window.React = React // export for http://fb.me/react-devtools + +React.render(, + document.getElementById('react') +) diff --git a/examples/isomorphic-flux-chat/components/ChatApp.jsx b/examples/isomorphic-flux-chat/components/ChatApp.jsx new file mode 100644 index 0000000..6b5e380 --- /dev/null +++ b/examples/isomorphic-flux-chat/components/ChatApp.jsx @@ -0,0 +1,33 @@ +/** + * This file is provided by Facebook for testing and evaluation purposes + * only. Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +var React = require('react') +var MessageSection = require('./MessageSection.jsx') +var ThreadSection = require('./ThreadSection.jsx') + +/** + * Styles + */ +require('../css/chatapp.css') + +var ChatApp = React.createClass({ + render: function() { + return ( +
    + + +
    + ) + }, +}) + +module.exports = ChatApp diff --git a/examples/isomorphic-flux-chat/components/MessageComposer.jsx b/examples/isomorphic-flux-chat/components/MessageComposer.jsx new file mode 100644 index 0000000..f6d1424 --- /dev/null +++ b/examples/isomorphic-flux-chat/components/MessageComposer.jsx @@ -0,0 +1,63 @@ +/** + * This file is provided by Facebook for testing and evaluation purposes + * only. Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +var Chat = require('../modules/chat') +var React = require('react') +var NuclearMixin = require('nuclear-js-react-addons/nuclearMixin') + +var ENTER_KEY_CODE = 13 + +// Difference with classic suggested architecture: +// use the nuclear react mixin to have access to the +// reactor in the context, that you can then use to +// pass as first arguments of your actions + +var MessageComposer = React.createClass({ + mixins: [NuclearMixin], + + propTypes: { + threadID: React.PropTypes.string.isRequired, + }, + + getInitialState: function() { + return {text: ''} + }, + + render: function() { + return ( +