ng-reflux

1.0.0 • Public • Published

ng-reflux

An Angular service providing a simplified RefluxJS implementation. This implementation is based heavily on Mikael Brassman's original gist idea for RefluxJS, and combines a small sub-set of the async features from the current RefluxJS implementation.

A simple library for unidirectional dataflow architecture inspired by ReactJS Flux.

You can read an overview of Flux here, however the gist of it is to introduce a more functional programming style architecture by eschewing MVC like pattern and adopting a single data flow pattern.

╔═════════╗       ╔════════╗       ╔═════════════════╗
║ Actions ║──────>║ Stores ║──────>║ View Components ║
╚═════════╝       ╚════════╝       ╚═════════════════╝
     ^                                      │
     └──────────────────────────────────────┘

The pattern is composed of actions and data stores, where actions initiate new data to pass through data stores before coming back to the view components again. If a view component has an event that needs to make a change in the application's data stores, they need to do so by signaling to the stores through the actions available.

Content

Comparing ngReflux, RefluxJS and Flux

ngReflux includes a subset of RefluxJS features. Notably, here is what ngReflux provides:

  • No outside dependencies on EventEmitter or Underscore.
  • The ability to create Actions and Stores
  • Unidirectional data-flow, with synchronous Stores and the ability to use Actions with Promises.
  • The singleton dispatcher is removed in favor for letting every action act as dispatcher instead.
  • Because actions are listenable, the stores may listen to them. Stores don't need to have big switch statements that do static type checking (of action types) with strings
  • Stores may listen to other stores, i.e. it is possible to create stores that can aggregate data further, similar to a map/reduce.
  • Action creators are not needed because RefluxJS actions are functions that will pass on the payload they receive to anyone listening to them

Back to top

Installation

You can currently install the package as a npm package. Bower and CDN support are coming in the future.

NPM

The following command installs ngReflux as an npm package:

npm install ng-reflux

ES5/ES6 Compatibility

ngReflux is currently written in ES5, but is being reworked in ES6 and built using browserify and babel. The distribution files are transpiled and usable in most evergreen browsers as well as IE10+.

Angular Compatibility

It is recommended to use ngReflux with Angular 1.3.x or above. An Angular 2 version is not currently being developed; but may be considered.

Back to top

ngReflux Usage

The library is implemented as a set of services provided by the ng.reflux module, which can be included in your main Angular app's module dependencies.

import "ng-reflux";

angular.module('app', ['ng.reflux'])
//...

The ng.reflux module provides an ngReflux service which has an API for creating Actions and Stores that can be used in your Angular directives and controllers to implement a Flux style architecture. Typically, you'll create your Stores and Actions as Services themselves, to be injected for use in your View Components (ie., Directives and/or Controllers).

Note: You can also inject the standalone EventEmitter service provided with ng-reflux as well if you need an EventEmitter.

Back to top

Actions

Actions serve as function objects that can be listened to and emit an event, passing any arguments to the listener's callback to initiate a change in a Store.

Creating Actions

You can create a single Action by calling ngReflux#createAction. Actions are simply functions that are wired to emit events and pass on their payload to any handlers listening to those events. Stores are the typical components that listen to actions, while your view components will trigger them to initiate a change in the Stores.

    let doThing = ngReflux.createAction();  // create an Action
    doThing("some data", 12);               // initiate an Action, passing data
    doThing.trigger("some data", 12);       // same as above

Back to top

Creating Multiple Actions

However, you'll typically not create single actions, but multiple actions. Actions represent the basic behavior that view components can initiate to tell the Stores what to change. For creating multiple actions at once, use ngReflux#createActions.

angular.module('app',['ng.reflux'])
  .factory('TodoActions', ['ngReflux', function(ngReflux) {
    let todoActions = ngReflux.createActions([
        'addTodo',
        'removeTodo',
        'toggleTodo'
    ]); 
    return todoActions;
  });

ngReflux#createActions takes an array of string names representing the actions to create and returns an object whose property names are Actions that match those names in the array.

To use Actions, simply inject them into your view components.

angular.module('app')
  .controller('MyController', function(TodoActions) {
    this.addTodo = function(todo) {
      TodoActions.addTodo(todo);
    };
  });

Calling an Action, which is an observable, triggers it to emit the data passed to it to any callback registered as a listener for that Action. This triggering is a synchronous operation.

Back to top

Asynchronous Actions

You can create asynchronous Actions by passing the async: true option to the ngReflux#createAction() method, or call ngReflux#createActions with an object, where the property names are the action names and the value is the options object.

// Create a single, asynchronous action
let doThingAsync = ngReflux.createAction({ async: true });

// Create multiple asynchronous or synchronous Actions
let myActions = ngReflux.createActions({
  'doThing': {},                    // synchronous
  'doThingAsync': { async: true }   // asynchronous
});

Asynchronous actions in ngReflux differ from the standard RefluxJS library by simplifying the API and limiting it to using Promises to handle asynchronous responses. Passing { async: true } when creating an Action will setup two sub-actions named completed and failed that will get triggered when the main action, which should return a Promise, completes successfully or fails/rejects, respectively.

Typically, the asynchronous action is done when the Action is triggered by listening to the Action itself.

// this creates 'load', 'load.completed' and 'load.failed'
var Actions = ngReflux.createActions({
    "load": { async: true }
});

// when 'load' is triggered, call async operation and trigger related actions
Actions.load.listen( function() {
    // By default, the listener is bound to the action
    // so we can access child actions using 'this'
    someAsyncOperation()
        .then( this.completed )
        .catch( this.failed );
});

There are currently no Action hooks like preEmit or shouldEmit as seen in RefluxJS; but those are being considered as well for a future release.

Back to top

Stores

You can create a data Store by passing a definiton object to ngReflux#createStore. You may set up all action listeners in the init function and register them by calling the store's own listenTo function.

// Some previously defined Action
let statusUpdate = ngReflux.createAction();

// Creates a DataStore
let statusStore = ngReflux.createStore({

    // Initial setup
    init() {

        // Register statusUpdate action
        this.listenTo(statusUpdate, this.output);
    },

    // Callback
    output(flag) {
        var status = flag ? 'ONLINE' : 'OFFLINE';

        // Pass on a "change" event to other listeners
        this.trigger(status);
    }
});

In the above example, whenever the action is called, the store's output callback will be called with whatever parameters were sent in the action. E.g. if the action is called as statusUpdate(true) then the flag argument in output function is true.

Back to top

Mixins in Stores

You can create and add mixins to your Stores in ngReflux.

let MyMixin = { foo() { console.log('bar!'); } }
let Store = ngReflux.createStore({
    mixins: [MyMixin]
});
Store.foo(); // outputs "bar!" to console

Methods from mixins are available as well as the methods declared in the Store. So it's possible to access a store's this from mixin, or methods of mixin from methods of store:

let MyMixin = { 
    mixinMethod() { console.log(this.foo); } 
};
let Store = ngReflux.createStore({
    mixins: [MyMixin],
    foo: 'bar!',
    storeMethod() {
        this.mixinMethod(); // outputs "bar!" to console
    }
});

A nice feature of mixins is that if a store is using multiple mixins and several mixins define an init() method, all of the init() methods are guaranteed to be called. Any mixin init() methods are called in the order provided and before the Store's init() method is called.

Back to top

Listening to Many Actions at Once

Since it is a very common pattern to listen to all actions from a ngReflux#createActions call in a store init() method, the store has a listenToMany function that takes an object of listenables.

let actions = ngReflux.createActions(["fireBall","magicMissile"]);

let Store = ngReflux.createStore({
    init() {
        this.listenToMany(actions);
    },
    onFireBall(){
        // whoooosh!
    },
    onMagicMissile(){
        // bzzzzapp!
    }
});

This will add listeners to all actions actionName who have a corresponding onActionName (or actionName if you prefer) method in the store. Thus if the actions object should also have included an iceShard spell, that would simply be ignored, as there is no corresponding handler in the Store.

Back to top

The listenables shorthand

To make things more convenient still, if you give an object of actions to the listenables property of the store definition, that will be automatically passed to listenToMany. So the above example can be simplified even further:

let actions = ngReflux.createActions(["fireBall","magicMissile"]);

let Store = ngReflux.createStore({
    listenables: actions,
    onFireBall(){
        // whoooosh!
    },
    onMagicMissile(){
        // bzzzzapp!
    }
});

The listenables property can also be an array of such objects, in which case all of them will be sent to listenToMany. This allows you to do convenient things like this:

MyStore.$inject = ['DarkSpells', 'LightSpells'];
function MyStore(DarkSpells, LightSpells) {
  return ngReflux.createStore({
    listenables: [ DarkSpells, LightSpells ]
    // ...
  });
}

Back to top

Listenables and Asynchronous Actions

If options.async is set for an Action, as in the example below, you can use onActionSubaction to add a listener to the child action. For example:

let Actions = Reflux.createActions({
    "load": { async: true }
});

// Perform some asynchronous action when the action is triggered
Actions.load.listen((data) => {
  someAsyncAction(data)
    .then(this.completed)
    .catch(this.failed);
});

let Store = ngReflux.createStore({
    listenables: Actions,
    onLoad: function() {
        // do something when Actions.load is triggered;
    },
    onLoadCompleted: function() {
        // do something when the Promise returned from Actions.load resolves
    },
    onLoadFailed: function() {
        // do something when Promise returned from Actions.load fails or rejects
    }
});

Back to top

Using ngReflux in an Angular App

You can use ngReflux service in any Angular Directive, Controller or even Service simply by injecting it via Angular's DI mechanism. However, in preparing for the coming of Angular 2 and it's use of "Components", I typically structure my views using only Directives with embedded Controllers, which is how Angular 2 Components will end up functioning. In this way, I can make up a view using just Directives, and inject my Stores and Actions into the controller to manage state.

The goal of Flux style architectures is to keep state management in one place. State, here, typically refers to application state or data models that provide state to multiple components in a view. If a Directive has nested Directives, you can pass state to the child directives via properties, much like React/Flux does; and still use Actions to trigger changes in Stores. The parent Component will receive updates from the Stores they listen to and pass the new state down to child Directives via properties - allowing us to take advantage of Angular's binding mechanism.

╔═════════╗       ╔════════╗ (state)   ╔═════════════════╗
║ Actions ║──────>║ Stores ║──────────>║ View Components ║
╚═════════╝       ╚════════╝           ╚═════════════════╝
     ^                                      │  │   │
     └──────────────────┰───────────────────┘  │   └───────────┐ (prop=state)
                        │                      v               v
                        │       ╔═════════════════╗   ╔═════════════════╗
                        └───────║ Child Component ║   ║ Child Component ║
                                ╚═════════════════╝   ╚═════════════════╝

An Angular view can be made up of multiple such components. Components can use a single Store for managing application state, or use multiple Stores. ngReflux is flexible in how you pair up components to Stores and Actions.

Creating Actions as Services

Actions are created as Services to be included by the Stores and Components that need to listen to them or trigger them.

angular.app('app').factory('TodoActions', TodoActions);

TodoActions.$inject = ['ngReflux'];
function TodoActions(ngReflux) {
    let actions =   ngReflux.createActions([
        'addTodo',
        'removeTodo',
        'toggleTodo'
    ]);
    return actions;
}

We use the ngReflux#createActions method to create multiple actions at once. We create an Action for all the possible behavior we want to provide to our components. Actions, in this way, serve as a kind of API between Components and Stores. All the Actions are for changing state in the Store. As we'll see, the Components actually get their data (read only) from the Store directly.

Creating Stores as Services

Stores are created as Services to be included by the Components that want to listen on them for data and changes.

angular.app('app').factory('TodoStore', TodoStore);

TodoStore.$inject = ['ngReflux', 'TodoActions'];
function TodoStore(ngReflux, TodoActions) {
    return ngReflux.createStore({
        listenables: [ TodoActions ],
        init() { 
            this.lastid = 0;
            this.todos = [];
        },
        onAddTodo(description) {
            this.todos.push({
                id: this.lastid += 1,
                description: description,
                added: (new Date()).getTime(),
                done: false
            });
        },
        onRemoveTodo(id) {
            this.todos = this.todos.filter((t) => t.id != id);
        },
        onToggleTodo(id) {
            this.todos = this.todos.map((todo) => {
                todo.done = todo.id == id ? !todo.done : todo.done;
                return todo;
            });
        },
        getTodos() {
            return this.todos;
        }
    });
}

Here, the Store provides handlers for all the Actions in TodoActions that it is listening to and it provides a getTodos() method that view components can use to request the list of todos at any time.

Using Actions and Stores in Components

Now, let's make a todo-list Directive, in component style, that takes advantage of our TodoStore and TodoActions. The component will simply initialize its state, the list of todos, listen for any changes on the store, and request changes through Actions.

angular.module('app').directive('todoList', todoList);

function todoList() {
    return {
        scope: {},
        restrict: 'AE',
        controllerAs: 'todoList',
        bindToController: true,
        controller: ['TodoStore', 'TodoActions', TodoListController],
        templateUrl: 'todo-list.html'
    };            
}

class TodoListController {
    constructor(TodoStore, TodoActions) {
        this.actions = TodoActions;
        this.store = TodoStore;
        this.todos = this.store.getTodos();

        this.columns = ['Id','Description','Added','Done?', 'Actions'];

        // Listen for changes in state on our Store, update our todos
        this.store.listen((state) => {
            this.todos = this.store.getState();
        });
    }

    addTodo(description) {
        this.actions.addTodo(description);
    }

    removeTodo(id) {
        this.actions.removeTodo(id);
    }

    toggleTodo(id) {
        this.actions.toggleTodo(id);
    }
}

Our Directive becomes much easier to implement now that we have a Store managing the state for us, and Actions to interact with that Store to request mutations. Our controller methods are simply wrappers that call the appropriate Action to trigger the change in the Store. To get our data, we simply grab the Todos when our Controller is instantiated and listen for any changes to that data coming from the Store. Angular's binding takes care of the updating for us in the template.

And here's our simple todo list template for this directive, todo-list.html

<h1>Todo List</h1>
<table>
    <thead>
        <tr><th ng-repeat="column in todoList.columns">{{column}}</th></tr>
    </thead>
    <tbody>
        <tr ng-repeat="todo in todoList.todos">
            <td>{{todo.id}}</td>
            <td>{{todo.description}}</td>
            <td>{{todo.added}}</td>
            <td>
                <input type="checkbox" ng-click="todoList.toggleTodo(todo.id)" ng-checked="todo.done" />
            </td>
            <td>
                <a ng-click="todoList.removeTodo(todo.id)">&times;</a>
            </td>
        </tr>
    </tbody>
</table>

Advanced Concepts

TODO

Stores listening to Stores

TODO

Using Components vs Controllers and Directives

TODO

Using the State Mixin

TODO

Readme

Keywords

none

Package Sidebar

Install

npm i ng-reflux

Weekly Downloads

0

Version

1.0.0

License

MIT

Last publish

Collaborators

  • datchley