diff --git a/.eslintrc b/.eslintrc
index 636f541..da2653e 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -2,7 +2,8 @@
"env": {
"node": true,
"browser": true,
- "amd": true
+ "amd": true,
+ "es6": true
},
"ecmaFeatures": {
"arrowFunctions": true,
@@ -10,6 +11,7 @@
"defaultParams": true,
"destructuring": true,
"forOf": true,
+ "modules": true,
"objectLiteralShorthandMethods": true,
"objectLiteralShorthandProperties": true,
"spread": true,
@@ -58,7 +60,7 @@
"use-isnan": 1,
"valid-jsdoc": 0,
"valid-typeof": 1,
- "block-scoped-var": 1,
+ "block-scoped-var": 0,
"complexity": 0,
"consistent-return": 0,
"curly": 1,
diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml
new file mode 100644
index 0000000..2740f65
--- /dev/null
+++ b/.github/workflows/run-tests.yml
@@ -0,0 +1,37 @@
+# .github/workflows/nodejs-ci.yml
+name: Run Tests for Nuclear JS
+
+on:
+ push:
+ branches:
+ - master
+ pull_request:
+ branches:
+ - master
+
+jobs:
+ build-and-test:
+ runs-on: ubuntu-latest
+
+ steps:
+ # Checkout the code
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ # Set up Node.js
+ - name: Set up Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20.18.1
+
+ # Install grunt-cli globally
+ - name: Install grunt-cli
+ run: npm install -g grunt-cli
+
+ # Install dependencies
+ - name: Install dependencies
+ run: npm install
+
+ # Run tests
+ - name: Run tests
+ run: grunt ci
diff --git a/.gitignore b/.gitignore
index 25fbf5a..6862f64 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
node_modules/
coverage/
+package-lock.json
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ad72ea3..8e77c92 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,14 +1,87 @@
-## 1.1.0 (proposed)
+## 1.4.0 (September 21, 2016)
+
+
+- **[NEW]** Added ability to switch out the default caching strategy for caching getter values. Also expose an LRU cache that can be swapped in for the basic cache
+- **[NEW]** Add ability to supply your own logger and override the default console group logger in NuclearJS
+- **[UPGRADE]** Upgrade `immutable` to `3.8.1`
+
+
+### Cache Configuration
+
+```
+import * as Nuclear from 'nuclear-js';
+
+const MAX_ITEMS = 1000 // (optional, default = 1000) how many items to keep in the LRU cache before evicting
+const EVICT_COUNT = 10 // (optional, default = 1) how many items to throw out when the cache fills up
+
+new Nuclear.Reactor({
+ debug: false,
+ cache: new Nuclear.LRUCache(MAX_ITEMS, EVICT_COUNT),
+});
+```
+
+### Using your own Logger
+
+```
+import * as Nuclear from 'nuclear-js';
+
+new Nuclear.Reactor({
+ logger: {
+ dispatchStart(reactorState, actionType, payload) {
+ console.log(`dispatch: actionType=${actionTypes}`, payload)
+ },
+ dispatchError(reactorState, error) {
+ // useful if you need to close a console.group if an error is thrown during dispatch
+ },
+ dispatchEnd(reactorState, state, dirtyStores, previousState) {
+ const prevStateChanges = previousState.filter((val, key) => dirtyStores.contains(key)).toJS()
+ const stateChanges = state.filter((val, key) => dirtyStores.contains(key)).toJS()
+
+ console.log('prev state: ', prevStateChanges)
+ console.log('new state: ', stateChanges)
+ },
+ },
+});
+```
+
+
+## 1.3.0 (December 31, 2015)
+
+- **[NEW]** Store hot-reloading via `reactor.replaceStores(stores)` which replaces the implementation of a store without resetting its underlying state value. See [hot reloading example](https://github.com/optimizely/nuclear-js/tree/master/examples/hot-reloading).
+- **[NEW]** Support for more granular options for logging and triggering invariants in the NuclearJS runtime. See [API Docs](https://github.com/optimizely/nuclear-js/blob/master/docs/src/docs/07-api.md) for details.
+
+## 1.2.1 (November 5, 2015)
+
+- **[FIXED]** Observers of the entire app state not triggering on actions from a late registered store
+
+## 1.2.0 (November 1, 2015)
+
+- **[NEW]** Exposed new API methods: `batchStart` and `batchStop`.
+- **[NEW]** Changed the transpiler to Babel.
+- **[FIXED]** Completely refactored `Reactor`, `Evaluator` and `ChangeObserver`.
+- **[FIXED]** Fixed all issues related to hash code collisions.
+- **[FIXED]** Refactored how change observation works to be much more efficient.
+
+## 1.1.2 (October 5, 2015)
+
+- **[FIXED]** Fix for observer iteration when removed during notify. [Issue #151](https://github.com/optimizely/nuclear-js/issues/151)
+
+## 1.1.1 (July 26, 2015)
+
+- **[ADDED]** Bowser support via bower.json
+
+## 1.1.0 (July 23, 2015)
- **[NEW]** added `Reactor#serialize`, `Reactor#loadState`, `Store#serialize` and `Store#deserialize` methods
- **[NEW]** added `Reactor#batch` to allow batch dispatches before notify observers
+- **[NEW]** throw error when trying to dispatch within a dispatch
- **[FIXED]** fix Evaluator locking if getter evaluation errors
### API Additions
#### `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 +89,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 +102,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 +121,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
@@ -65,7 +138,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.
@@ -79,7 +152,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/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/Gruntfile.js b/Gruntfile.js
index 8b7fb23..38d551e 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -1,6 +1,20 @@
-module.exports = function(grunt) {
- require('load-grunt-config')(grunt)
- // load npm tasks
- grunt.loadNpmTasks('grunt-karma')
- grunt.loadNpmTasks('grunt-karma-coveralls')
-}
+module.exports = function (grunt) {
+ // Load grunt configurations
+ require('load-grunt-config')(grunt, {
+ configPath: require('path').join(__dirname, 'grunt'),
+ init: true,
+ });
+
+ // Load npm tasks
+ grunt.loadNpmTasks('grunt-karma');
+ grunt.loadNpmTasks('grunt-karma-coveralls');
+
+ // Default task for Chrome and Firefox
+ grunt.registerTask('test', ['karma:chrome', 'karma:firefox']);
+
+ // Separate task for Chrome only
+ grunt.registerTask('test:chrome', ['karma:chrome']);
+
+ // Separate task for Firefox only
+ grunt.registerTask('test:firefox', ['karma:firefox']);
+};
diff --git a/LICENSE.md b/LICENSE.md
index e1e0e9a..c5e70a5 100644
--- a/LICENSE.md
+++ b/LICENSE.md
@@ -1,6 +1,6 @@
The MIT License (MIT)
-Copyright (c) 2015 Optimizely
+Copyright (c) 2014-Present, Optimizely
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index efc82d5..f1bbbea 100644
--- a/README.md
+++ b/README.md
@@ -1,1037 +1,67 @@
-# NuclearJS
+ NuclearJS
-[](https://travis-ci.org/optimizely/nuclear-js)
[](https://coveralls.io/r/optimizely/nuclear-js?branch=master)
[](https://gitter.im/optimizely/nuclear-js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
-[](https://saucelabs.com/u/nuclearjs)
-
Traditional Flux architecture built with ImmutableJS data structures.
-[Why you should use NuclearJS](http://optimizely.github.io/nuclear-js/).
-
-## 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.
+## Documentation
-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.
+[https://optimizely.github.io/nuclear-js/](https://optimizely.github.io/nuclear-js/)
## 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 reason about 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
```
-## 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 `[
- {threadListItems}
-
- {thread.get('threadName')}
-
-
- )
- })
- return (
- {item.get('quantity')}
- {item.get('name')}
- {item.get('price')}
-
-
-
-
- {itemRows}
- Quantity:
- Name:
- Price:
-
-
- subtotal:
- {this.state.subtotal}
-
-
- tax @ {this.state.taxPercent}%
- {this.state.taxPercent}
-
-
- total:
- {this.state.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
-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.
-
-You can read more of the implementation here: [src/evaluator.js](./src/evaluator.js)
+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 of its dependencies change.
## 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 (
-
-
- Quantity:
- Name:
- Price:
-
-
- {{ item.quantity }}
- {{ item.name }}
- {{ item.price | currency }}
-
-
- subtotal:
- {{ subtotal }}
-
-
- tax @ {{ taxPercent }}%
- {{ tax }}
-
-
-total:
- {{ total }}
-
- {threadListItems}
-
- =Tr)return ee(t,_,c,s,l);if(h&&!l&&2===_.length&&Qt(_[1^f]))return _[1^f];if(h&&l&&1===_.length&&Qt(l))return l;var v=t&&t===this.ownerID,y=h?l?c:c^a:c|a,d=h?l?se(_,f,l,v):ce(_,f,v):ae(_,f,l,v);return v?(this.bitmap=y,this.nodes=d,this):new Lt(t,y,d)},Tt.prototype.get=function(t,e,r,n){void 0===e&&(e=et(r));var i=(0===t?e:e>>>t)&hr,o=this.nodes[i];return o?o.get(t+ar,e,r,n):n},Tt.prototype.update=function(t,e,r,n,i,o,u){void 0===r&&(r=et(n));var s=(0===e?r:r>>>e)&hr,a=i===fr,c=this.nodes,h=c[s];if(a&&!h)return this;var f=Yt(h,t,e+ar,r,n,i,o,u);if(f===h)return this;var _=this.count;if(h){if(!f&&(_--,Jr>_))return te(t,c,_,s)}else _++;var p=t&&t===this.ownerID,l=se(c,s,f,p);return p?(this.count=_,this.nodes=l,this):new Tt(t,_,l)},Jt.prototype.get=function(t,e,r,n){for(var i=this.entries,o=0,u=i.length;u>o;o++)if(X(r,i[o][0]))return i[o][1];return n},Jt.prototype.update=function(t,e,n,o,u,s,a){void 0===n&&(n=et(o));var c=u===fr;if(n!==this.keyHash)return c?this:(r(a),r(s),Zt(this,t,e,n,[o,u]));for(var h=this.entries,f=0,_=h.length;_>f&&!X(o,h[f][0]);f++);var p=_>f;if(p?h[f][1]===u:c)return this;if(r(a),(c||!p)&&r(s),c&&2===_)return new Wt(t,this.keyHash,h[1^f]);var l=t&&t===this.ownerID,v=l?h:i(h);return p?c?f===_-1?v.pop():v[f]=v.pop():v[f]=[o,u]:v.push([o,u]),l?(this.entries=v,this):new Jt(t,this.keyHash,v)},Wt.prototype.get=function(t,e,r,n){return X(r,this.entry[0])?this.entry[1]:n},Wt.prototype.update=function(t,e,n,i,o,u,s){var a=o===fr,c=X(i,this.entry[0]);return(c?o===this.entry[1]:a)?this:(r(s),a?void r(u):c?t&&t===this.ownerID?(this.entry[1]=o,this):new Wt(t,this.keyHash,[i,o]):(r(u),Zt(this,t,e,et(i),[i,o])))},Bt.prototype.iterate=Jt.prototype.iterate=function(t,e){for(var r=this.entries,n=0,i=r.length-1;i>=n;n++)if(t(r[e?i-n:n])===!1)return!1},Lt.prototype.iterate=Tt.prototype.iterate=function(t,e){for(var r=this.nodes,n=0,i=r.length-1;i>=n;n++){var o=r[e?i-n:n];if(o&&o.iterate(t,e)===!1)return!1}},Wt.prototype.iterate=function(t,e){return t(this.entry)},t(Vt,w),Vt.prototype.next=function(){for(var t=this._type,e=this._stack;e;){var r,n=e.node,i=e.index++;if(n.entry){if(0===i)return Gt(t,n.entry)}else if(n.entries){if(r=n.entries.length-1,r>=i)return Gt(t,n.entries[this._reverse?r-i:i])}else if(r=n.nodes.length-1,r>=i){var o=n.nodes[this._reverse?r-i:i];if(o){if(o.entry)return Gt(t,o.entry);e=this._stack=Ht(o,e)}continue}e=this._stack=this._stack.__prev}return I()};var Br,Lr=cr/4,Tr=cr/2,Jr=cr/4;t(he,F),he.of=function(){return this(arguments)},he.prototype.toString=function(){return this.__toString("List [","]")},he.prototype.get=function(t,e){if(t=u(this,t),0>t||t>=this.size)return e;t+=this._origin;var r=me(this,t);return r&&r.array[t&hr]},he.prototype.set=function(t,e){return ye(this,t,e)},he.prototype.remove=function(t){return this.has(t)?0===t?this.shift():t===this.size-1?this.pop():this.splice(t,1):this},he.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=this._origin=this._capacity=0,this._level=ar,this._root=this._tail=null,this.__hash=void 0,this.__altered=!0,this):ve()},he.prototype.push=function(){var t=arguments,e=this.size;return this.withMutations(function(r){be(r,0,e+t.length);for(var n=0;n=1<f?new Bt([],n):_;if(_&&l>f&&ui&&(c=c.removeBefore(n,s,u-h)),c&&l
- initialize()
- Sets up any action handlers, by specifying the action type and a function that transforms
-
(storeState, action) => (newStoreState)
+ initialize()
- Sets up any action handlers, by specifying the action type and a function that transforms
+ (storeState, actionPayload) => (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 8b51a59..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,19 +33,19 @@ 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 +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. ## 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..fc6f79b 100644 --- a/docs/src/docs/02-creating-actions.md +++ b/docs/src/docs/02-creating-actions.md @@ -60,9 +60,9 @@ 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). +If you'd like to jump ahead, you can read more about [async actions](./06-async-actions-and-optimistic-updates.html). Now let's build a few stores. diff --git a/docs/src/docs/03-creating-stores.md b/docs/src/docs/03-creating-stores.md index a616bd1..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. @@ -21,9 +21,9 @@ 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 +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 0e9964d..fc987a2 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) - 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 4c31abf..cd3c70f 100644 --- a/docs/src/docs/07-api.md +++ b/docs/src/docs/07-api.md @@ -5,15 +5,64 @@ 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 enabled logging for dispatches and throw Errors in various circumstances described below. + +**config.options** (added in 1.3) + +If `config.debug` is true then all of the options below will be enabled. + +`logDispatches` (default=`false`) console.logs for every action. If disabled `logAppState` and `logDirtyStores` will be ignored, as no dispatch logging is occurring. + +`logAppState` (default=`false`) console.logs a snapshot of the entire app state after every dispatch. Disabling this can improve performance. + +`logDirtyStores` (default=`false`) console.logs what stores have changed after each dispatched action. + +`throwOnUndefinedActionType` (default=`false`) if true, throws an Error when dispatch is called with an undefined action type. + +`throwOnUndefinedStoreReturnValue` (default=`false`) if true, throws an Error if a store handler or `getInitialState()` ever returns `undefined`. + +`throwOnNonImmutableStore` (default=`false`) if true, throws an Error if a store returns a non-immutable value. Javascript primitive such as `String`, `Boolean` and `Number` count as immutable. + +`throwOnDispatchInDispatch` (default=`false`) if true, throws an Error if a dispatch occurs in a change observer. + +**Example** + +```javascript +var reactor = new Nuclear.Reactor({ + debug: true, + options: { + // do not log entire app state + logAppState: false, + // allow dispatch in dispatch + throwOnDispatchInDispatch: false, + }, +}) +``` + + +#### `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#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 +84,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 +103,67 @@ reactor.observe([ ]) ``` -### `Reactor#registerStores(stores)` +#### `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#batchStart()` + +_added in 1.2_ + +Sets the reactor in batch mode, where dispatches don't cause observer notification until `batchEnd()` is called. + +```javascript +// the following is equivalent to the `reactor.batch` example +reactor.batchStart() +reactor.dispatch('addUser', { name: 'jordan' }) +reactor.dispatch('addUser', { name: 'james' }) +reactor.batchEnd() +``` + +#### `Reactor#batchEnd()` + +_added in 1.2_ + +Signifies the end of reactor batching and will notify all observers of the changes that happened since `batchStart` + +#### `Reactor#serialize()` + +_added in 1.1_ + +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()) +``` + +#### `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 +174,24 @@ reactor.registerStores({ }) ``` -### `Reactor#reset()` +#### `Reactor#replaceStores(stores)` + +`stores` - an object of storeId => store instance + +Replace the implementation only of specified stores without resetting to their initial state. This is useful when doing store hot reloading. + +```javascript +reactor.replaceStores({ + '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` +#### `Reactor#ReactMixin` Exposes the ReactMixin to do automatic data binding. @@ -106,26 +228,16 @@ var ThreadSection = React.createClass({
Quantity: | +Name: | +Price: | +
subtotal: | +{this.state.subtotal} | +|
tax @ {this.state.taxPercent}% | +{this.state.taxPercent} | +|
total: | +{this.state.total} | +
Quantity: | +Name: | +Price: | +
{{ item.quantity }} | +{{ item.name }} | +{{ item.price | currency }} | +
subtotal: | +{{ subtotal }} | +|
tax @ {{ taxPercent }}% | +{{ tax }} | +|
total: | +{{ total }} | +
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.
(===)
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/examples/flux-chat/README.md b/examples/flux-chat/README.md index afa8f69..c1d492e 100644 --- a/examples/flux-chat/README.md +++ b/examples/flux-chat/README.md @@ -23,7 +23,7 @@ open the app. 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) +All of the above code lives in [examples/flux-chat](/examples/flux-chat) ##### `flux.js` @@ -187,7 +187,7 @@ At this point defined how our application manages state over time by creating an Getters can take 2 forms: - 1. A KeyPath such as `['messages']` which equates to a `state.getIn(['messages'])` on the app state `Immutable.Map`. + 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` diff --git a/examples/flux-chat/js/mock-data.js b/examples/flux-chat/js/mock-data.js index 5182ad5..e360b7b 100644 --- a/examples/flux-chat/js/mock-data.js +++ b/examples/flux-chat/js/mock-data.js @@ -6,7 +6,7 @@ module.exports = [ authorName: 'Bill', text: 'Hey Jing, want to give a Flux talk at ForwardJS?', isRead: false, - timestamp: Date.now() - 99999 + timestamp: Date.now() - 99999, }, { id: 'm_2', @@ -15,7 +15,7 @@ module.exports = [ authorName: 'Bill', text: 'Seems like a pretty cool conference.', isRead: false, - timestamp: Date.now() - 89999 + timestamp: Date.now() - 89999, }, { id: 'm_3', @@ -24,7 +24,7 @@ module.exports = [ authorName: 'Jing', text: 'Sounds good. Will they be serving dessert?', isRead: false, - timestamp: Date.now() - 79999 + timestamp: Date.now() - 79999, }, { id: 'm_4', @@ -33,7 +33,7 @@ module.exports = [ authorName: 'Bill', text: 'Hey Dave, want to get a beer after the conference?', isRead: false, - timestamp: Date.now() - 69999 + timestamp: Date.now() - 69999, }, { id: 'm_5', @@ -42,7 +42,7 @@ module.exports = [ authorName: 'Dave', text: 'Totally! Meet you at the hotel bar.', isRead: false, - timestamp: Date.now() - 59999 + timestamp: Date.now() - 59999, }, { id: 'm_6', @@ -51,7 +51,7 @@ module.exports = [ authorName: 'Bill', text: 'Hey Brian, are you going to be talking about functional stuff?', isRead: false, - timestamp: Date.now() - 49999 + timestamp: Date.now() - 49999, }, { id: 'm_7', @@ -60,6 +60,6 @@ module.exports = [ authorName: 'Brian', text: 'At ForwardJS? Yeah, of course. See you there!', isRead: false, - timestamp: Date.now() - 39999 - } + timestamp: Date.now() - 39999, + }, ] diff --git a/examples/flux-chat/js/modules/chat/actions.js b/examples/flux-chat/js/modules/chat/actions.js index dd04a9b..d630a57 100644 --- a/examples/flux-chat/js/modules/chat/actions.js +++ b/examples/flux-chat/js/modules/chat/actions.js @@ -22,12 +22,12 @@ exports.createMessage = function(text, threadID) { var id = 'm_' + timestamp var threadName = flux.evaluate([ getters.threadsMap, - threadsMap => threadsMap.getIn([threadID, 'threadName']) + threadsMap => threadsMap.getIn([threadID, 'threadName']), ]) var authorName = 'Jordan' flux.dispatch(actionTypes.ADD_MESSAGE, { - message: { id, threadID, threadName, authorName, timestamp, text } + message: { id, threadID, threadName, authorName, timestamp, text }, }) } diff --git a/examples/flux-chat/js/modules/chat/getters.js b/examples/flux-chat/js/modules/chat/getters.js index b73cfbd..a81eafb 100644 --- a/examples/flux-chat/js/modules/chat/getters.js +++ b/examples/flux-chat/js/modules/chat/getters.js @@ -1,16 +1,16 @@ // 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 underyling stores / data transformation that is taking place +// to the underlying stores / data transformation that is taking place exports.threadsMap = ['threads'] exports.threads = [ exports.threadsMap, - threadsMap => threadsMap.toList() + threadsMap => threadsMap.toList(), ] exports.currentThread = [ ['currentThreadID'], exports.threadsMap, - (currentThreadID, threadsMap) => threadsMap.get(currentThreadID) + (currentThreadID, threadsMap) => threadsMap.get(currentThreadID), ] exports.latestThread = [ @@ -21,13 +21,13 @@ exports.latestThread = [ thread.get('messages').last().get('timestamp') }) .last() - } + }, ] exports.currentThreadID = [ exports.currentThread, - thread => thread ? thread.get('threadID') : null + thread => thread ? thread.get('threadID') : null, ] exports.unreadCount = [ @@ -39,5 +39,5 @@ exports.unreadCount = [ } return accum }, 0) - } + }, ] diff --git a/examples/flux-chat/js/modules/chat/stores/current-thread-id-store.js b/examples/flux-chat/js/modules/chat/stores/current-thread-id-store.js index 4f22daa..3e74f31 100644 --- a/examples/flux-chat/js/modules/chat/stores/current-thread-id-store.js +++ b/examples/flux-chat/js/modules/chat/stores/current-thread-id-store.js @@ -1,5 +1,4 @@ var Nuclear = require('nuclear-js') -var toImmutable = Nuclear.toImmutable var actionTypes = require('../action-types') module.exports = new Nuclear.Store({ @@ -11,7 +10,7 @@ module.exports = new Nuclear.Store({ initialize() { // all action handlers are pure functions that take the current state and payload this.on(actionTypes.CLICK_THREAD, setCurrentThreadID) - } + }, }) function setCurrentThreadID(state, { threadID }) { diff --git a/examples/flux-chat/js/modules/chat/stores/thread-store.js b/examples/flux-chat/js/modules/chat/stores/thread-store.js index edf74c6..75f12ea 100644 --- a/examples/flux-chat/js/modules/chat/stores/thread-store.js +++ b/examples/flux-chat/js/modules/chat/stores/thread-store.js @@ -13,7 +13,7 @@ module.exports = new Nuclear.Store({ // 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) - } + }, }) /** diff --git a/examples/flux-chat/package.json b/examples/flux-chat/package.json index 8a2e109..69f8ef6 100644 --- a/examples/flux-chat/package.json +++ b/examples/flux-chat/package.json @@ -7,7 +7,7 @@ "license": "ISC", "dependencies": { "keymirror": "^0.1.1", - "react": "^0.13.0", + "react": "^0.14.0", "nuclear-js": "^0.5.0-rc4" }, "devDependencies": { diff --git a/examples/flux-chat/webpack.config.js b/examples/flux-chat/webpack.config.js index bfa0359..14088ac 100644 --- a/examples/flux-chat/webpack.config.js +++ b/examples/flux-chat/webpack.config.js @@ -7,15 +7,15 @@ module.exports = { output: { path: './', - filename: "[name].js", + filename: '[name].js', }, module: { loaders: [ { test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader'}, // required for react jsx - { test: /\.react.js$/, loader: "jsx-loader" }, - ] + { test: /\.react.js$/, loader: 'jsx-loader' }, + ], }, resolve: { diff --git a/examples/hot-reloading/.babelrc b/examples/hot-reloading/.babelrc new file mode 100644 index 0000000..d5ee687 --- /dev/null +++ b/examples/hot-reloading/.babelrc @@ -0,0 +1,7 @@ +{ + presets: ['es2015', 'react'], + plugins: ['transform-decorators-legacy', 'syntax-decorators'], + ignore: [ + '**/nuclear-js-react-addons/**', + ] +} diff --git a/examples/hot-reloading/.gitignore b/examples/hot-reloading/.gitignore new file mode 100644 index 0000000..de4d1f0 --- /dev/null +++ b/examples/hot-reloading/.gitignore @@ -0,0 +1,2 @@ +dist +node_modules diff --git a/examples/hot-reloading/README.md b/examples/hot-reloading/README.md new file mode 100644 index 0000000..c4db313 --- /dev/null +++ b/examples/hot-reloading/README.md @@ -0,0 +1,38 @@ +NuclearJS Hot Reloading +=== + +NuclearJS supports hot reloading of stores. Using the webpack Hot Module Replacement simply code like this to wherever your stores are registered. + + +```js +import { Reactor } from 'nuclear-js' +import * as stores from './stores' + +const reactor = new Reactor({ + debug: true, +}) +reactor.registerStores(stores) + +if (module.hot) { + // Enable webpack hot module replacement for stores + module.hot.accept('./stores', () => { + reactor.replaceStores(require('./stores')) + }) +} + +export default reactor +``` + +## Running Example + +``` +npm install +npm start +``` + +Go to [http://localhost:3000](http://localhost:3000) + +## Inpsiration & Thanks + +Big thanks to [redux](https://github.com/rackt/redux) and [react-redux](https://github.com/rackt/react-redux) for proving out this architecture +and creating simple APIs to accomplish hot reloading. diff --git a/examples/hot-reloading/index.html b/examples/hot-reloading/index.html new file mode 100644 index 0000000..0c9b694 --- /dev/null +++ b/examples/hot-reloading/index.html @@ -0,0 +1,12 @@ + + + ++ Clicked: {counter} times + {' '} + + {' '} + +
+ ) + } +} + +Counter.propTypes = { + increment: PropTypes.func.isRequired, + decrement: PropTypes.func.isRequired, + counter: PropTypes.number.isRequired +} + +export default Counter diff --git a/examples/hot-reloading/src/containers/app.js b/examples/hot-reloading/src/containers/app.js new file mode 100644 index 0000000..3797aac --- /dev/null +++ b/examples/hot-reloading/src/containers/app.js @@ -0,0 +1,18 @@ +import React, { Component } from 'react' +import { connect } from 'nuclear-js-react-addons' +import Counter from '../components/Counter' +import { increment, decrement } from '../actions/counter' + +@connect(props => ({ + counter: ['counter'] +})) +export default class AppContainer extends Component { + render() { + let { reactor, counter } = this.props + returnLine | Hits | Source |
---|---|---|
1 | 1 | var keyMirror = require('keymirror') |
2 | ||
3 | 1 | module.exports = keyMirror({ |
4 | API_FETCH_SUCCESS: null, | |
5 | API_FETCH_START: null, | |
6 | API_FETCH_FAIL: null, | |
7 | ||
8 | API_SAVE_SUCCESS: null, | |
9 | API_SAVE_START: null, | |
10 | API_SAVE_FAIL: null, | |
11 | ||
12 | API_DELETE_SUCCESS: null, | |
13 | API_DELETE_START: null, | |
14 | API_DELETE_FAIL: null, | |
15 | }) | |
16 |
Line | Hits | Source |
---|---|---|
1 | 1 | var Promise = require('es6-promise').Promise |
2 | 1 | var Flux = require('../../flux') |
3 | 1 | var actionTypes = require('./action-types') |
4 | ||
5 | /** | |
6 | * @param {Object} model | |
7 | * @param {String} model.entity | |
8 | * @param {Function} model.save | |
9 | * @param {Function} model.fetch | |
10 | * @param {Function} model.fetchAll | |
11 | * @param {Function} model.delete | |
12 | * @return {Object} | |
13 | */ | |
14 | 1 | module.exports = function(model) { |
15 | 21 | var entity = model.entity; |
16 | 21 | var apiActions = {} |
17 | ||
18 | 21 | apiActions.fetch = function(params) { |
19 | 6 | Flux.dispatch(actionTypes.API_FETCH_START, { |
20 | model: model, | |
21 | method: 'fetch', | |
22 | params: params, | |
23 | }) | |
24 | 6 | return model.fetch(params).then( |
25 | onFetchSuccess.bind(null, model, params), | |
26 | onFetchFail.bind(null, model, params) | |
27 | ) | |
28 | } | |
29 | ||
30 | 21 | apiActions.fetchAll = function(params) { |
31 | 8 | Flux.dispatch(actionTypes.API_FETCH_START, { |
32 | model: model, | |
33 | method: 'fetchAll', | |
34 | params: params, | |
35 | }) | |
36 | 8 | return model.fetchAll(params).then( |
37 | onFetchSuccess.bind(null, model, params), | |
38 | onFetchFail.bind(null, model, params) | |
39 | ) | |
40 | } | |
41 | ||
42 | 21 | apiActions.save = function(params) { |
43 | 5 | Flux.dispatch(actionTypes.API_SAVE_START, { |
44 | params: params, | |
45 | }) | |
46 | 5 | return model.save(params).then( |
47 | onSaveSuccess.bind(null, model, params), | |
48 | onSaveFail.bind(null, model, params) | |
49 | ) | |
50 | } | |
51 | ||
52 | 21 | apiActions['delete'] = function(params) { |
53 | 4 | Flux.dispatch(actionTypes.API_DELETE_START, { |
54 | params: params, | |
55 | }) | |
56 | 4 | return model['delete'](params).then( |
57 | onDeleteSuccess.bind(null, model, params), | |
58 | onDeleteFail.bind(null, model, params) | |
59 | ) | |
60 | } | |
61 | ||
62 | 21 | return apiActions |
63 | } | |
64 | ||
65 | /** | |
66 | * Handler for API fetch success, dispatches flux action to store the fetched | |
67 | * result in the api cache | |
68 | * @param {Model} model | |
69 | * @param {*} params used to call the `model.fetch(params)` | |
70 | * @param {Object} result | |
71 | * @return {Object} | |
72 | */ | |
73 | 1 | function onFetchSuccess(model, params, result) { |
74 | 10 | Flux.dispatch(actionTypes.API_FETCH_SUCCESS, { |
75 | model: model, | |
76 | params: params, | |
77 | result: result, | |
78 | }) | |
79 | 10 | return result |
80 | } | |
81 | ||
82 | /** | |
83 | * Handler for API fetch success, dispatches flux action to store the fetched | |
84 | * result in the api cache | |
85 | * @param {Model} model | |
86 | * @param {*} params used to call the `model.fetch(params)` | |
87 | * @param {*} reason | |
88 | * @return {Object} | |
89 | */ | |
90 | 1 | function onFetchFail(model, params, reason) { |
91 | 4 | Flux.dispatch(actionTypes.API_FETCH_FAIL, { |
92 | model: model, | |
93 | params: params, | |
94 | reason: reason, | |
95 | }) | |
96 | 4 | return Promise.reject(reason) |
97 | } | |
98 | ||
99 | /** | |
100 | * Handler for API save success, dispatches flux action to update the store with the | |
101 | * saved instance | |
102 | * @param {Model} model | |
103 | * @param {*} params used to call the `model.save(params)` | |
104 | * @param {Object} result | |
105 | * @return {Object} | |
106 | */ | |
107 | 1 | function onSaveSuccess(model, params, result) { |
108 | 3 | Flux.dispatch(actionTypes.API_SAVE_SUCCESS, { |
109 | model: model, | |
110 | params: params, | |
111 | result: result, | |
112 | }) | |
113 | 3 | return result |
114 | } | |
115 | ||
116 | /** | |
117 | * Handler for API save success, dispatches flux action to update the store with the | |
118 | * saved instance | |
119 | * @param {Model} model | |
120 | * @param {*} params used to call the `model.save(params)` | |
121 | * @param {*} reason | |
122 | * @return {Object} | |
123 | */ | |
124 | 1 | function onSaveFail(model, params, reason) { |
125 | 2 | Flux.dispatch(actionTypes.API_SAVE_FAIL, { |
126 | model: model, | |
127 | params: params, | |
128 | reason: reason, | |
129 | }) | |
130 | 2 | return Promise.reject(reason) |
131 | } | |
132 | ||
133 | /** | |
134 | * Handler for API delete success, dispatches flux action to remove the instance from the stores | |
135 | * @param {Model} model | |
136 | * @param {*} params used to call the `model.delete(params)` | |
137 | * @param {Object} result | |
138 | * @return {Object} | |
139 | */ | |
140 | 1 | function onDeleteSuccess(model, params, result) { |
141 | 2 | Flux.dispatch(actionTypes.API_DELETE_SUCCESS, { |
142 | model: model, | |
143 | params: params, | |
144 | result: result, | |
145 | }) | |
146 | 2 | return result |
147 | } | |
148 | ||
149 | /** | |
150 | * Handler for API delete fail | |
151 | * @param {Model} model | |
152 | * @param {*} params used to call the `model.delete(params)` | |
153 | * @param {Object} result | |
154 | * @return {Object} | |
155 | */ | |
156 | 1 | function onDeleteFail(model, params, reason) { |
157 | 2 | Flux.dispatch(actionTypes.API_DELETE_FAIL, { |
158 | model: model, | |
159 | params: params, | |
160 | reason: reason, | |
161 | }) | |
162 | 2 | return Promise.reject(reason) |
163 | } | |
164 | ||
165 |
Line | Hits | Source |
---|---|---|
1 | 1 | var toImmutable = require('nuclear-js').toImmutable |
2 | 1 | var Flux = require('../../flux') |
3 | ||
4 | // register stores with Flux system | |
5 | 1 | Flux.registerStores({ |
6 | restApiCache: require('./stores/rest-api-cache-store'), | |
7 | }) | |
8 | ||
9 | 1 | exports.createApiActions = require('./create-api-actions') |
10 | ||
11 | /** | |
12 | * Creates a getter to the restApiCache store for a particular entity | |
13 | * This decouples the implementation details of the RestApi module's caching | |
14 | * to consumers of the cached data | |
15 | * @param {Model} model | |
16 | */ | |
17 | 1 | exports.createEntityMapGetter = function(model) { |
18 | 2 | return [ |
19 | ['restApiCache', model.entity], | |
20 | /** | |
21 | * @return {Immutable.Map} | |
22 | */ | |
23 | function(entityMap) { | |
24 | // protect the entityMap here from being undefined, there are cases | |
25 | // where an entity type isn't loaded yet, so we need to always to | |
26 | // return an Immutable.Map for getters downstream | |
27 | 2 | if (!entityMap) { |
28 | 1 | return toImmutable({}) |
29 | } else { | |
30 | 1 | return entityMap |
31 | } | |
32 | } | |
33 | ] | |
34 | } | |
35 | ||
36 | /** | |
37 | * Creates a function that creates a getter that looks up the entity in the restApiCache by ID | |
38 | * @param {Model} model | |
39 | */ | |
40 | 1 | exports.createByIdGetter = function(model) { |
41 | 1 | return function(id) { |
42 | 1 | return ['restApiCache', model.entity, id] |
43 | } | |
44 | } | |
45 |
Line | Hits | Source |
---|---|---|
1 | /** | |
2 | * Stores cached entities for the Rest API | |
3 | */ | |
4 | 1 | var _ = require('lodash'); |
5 | 1 | var Nuclear = require('nuclear-js') |
6 | 1 | var toImmutable = Nuclear.toImmutable |
7 | 1 | var actionTypes = require('../action-types') |
8 | ||
9 | 1 | module.exports = new Nuclear.Store({ |
10 | getInitialState: function() { | |
11 | 25 | return toImmutable({}) |
12 | }, | |
13 | ||
14 | initialize: function() { | |
15 | 1 | this.on(actionTypes.API_FETCH_SUCCESS, loadData) |
16 | 1 | this.on(actionTypes.API_SAVE_SUCCESS, loadData) |
17 | 1 | this.on(actionTypes.API_DELETE_SUCCESS, removeData) |
18 | }, | |
19 | }) | |
20 | ||
21 | /** | |
22 | * @param {Immutable.Map} state | |
23 | * @param {Object} payload | |
24 | * @param {Model} payload.model | |
25 | * @param {any} payload.params | |
26 | * @param {Object|Array} payload.result | |
27 | */ | |
28 | 1 | function loadData(state, payload) { |
29 | 15 | var entity = payload.model.entity |
30 | 15 | var data = payload.result |
31 | ||
32 | 15 | if (!data) { |
33 | // no-op if no real data was returned | |
34 | 1 | return state |
35 | } | |
36 | ||
37 | 14 | if (!_.isArray(data)) { |
38 | 6 | data = [data] |
39 | } | |
40 | ||
41 | 14 | return state.withMutations(function(state) { |
42 | 14 | data.forEach(function(entry) { |
43 | 24 | state.setIn([entity, entry.id], toImmutable(entry)) |
44 | }) | |
45 | }) | |
46 | } | |
47 | ||
48 | /** | |
49 | * @param {Immutable.Map} state | |
50 | * @param {Object} payload | |
51 | * @param {Model} payload.model | |
52 | * @param {any} payload.params | |
53 | * @param {Object|Array} payload.result | |
54 | */ | |
55 | 1 | function removeData(state, payload) { |
56 | 2 | var entity = payload.model.entity |
57 | // we assume that params is the instance with an `id` property | |
58 | 2 | var id = payload.params.id |
59 | ||
60 | 2 | return state.removeIn([entity, id]); |
61 | } | |
62 |