Skip to content

Volland/moreartyjs

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Introduction

Morearty.js is a thin layer on top of React providing better state management facilities in the manner of Om but written in pure JavaScript without any external dependencies.

In its core Morearty implements immutable Map and Vector data structures which hold the state of an application. That state is described by a single Binding object and all state transitions are performed through it. When an application component needs to delegate a part of its state to a sub-component, it can create a sub-binding which points to a nested location within the global state and is fully synchronized with the original binding. This way every component knows only what it should know and the entire state is effectively encapsulated. Morearty detects any changes automatically and triggers re-rendering. Each component gets a correctly defined shouldComponentUpdate method that compares the component's state using straightforward JavaScript strict equals operator ===. This is possible due to immutable nature of underlying data structures. So, only the components whose state was altered are re-rendered.

Download

Current version is 0.1.0. Browser, AMD, Node.js environments are supported. You can get production (30kb) and development (70kb) versions. Or just npm install morearty. Loading with Require.js is preferable.

API documentation

Auto-generated API documentation is available here.

Usage

To start using Morearty.js add the script to the page or load it with your favorite AMD loader, e.g. Require.js, and create Morearty context using createContext method:

var Ctx = Morearty.createContext(React, {
  // your initial application state
  nowShowing: 'all',
  items: [{
    title: 'My first task',
    completed: false,
    editing: false
  }]
});

All further activities will be performed through this context which exposes a set of modules, mainly:

  • Data.Map - immutable persistent Map based on hash trie with Java-like hashcode implementation;
  • Data.Vector - immutable Vector currently based on array-copying;
  • Callback - callback related utilities.

Next, create Bootstrap component which will initialize the context and pass it to your application:

var Bootstrap = Ctx.createClass({
  componentWillMount: function () {
    Ctx.init(this);
  },

  render: function () {
    return App({ state: Ctx.state() });
  }
});

When you create components this way, they acquire correctly defined shouldComponentUpdate method which uses component's binding (if any) to determine if its state was changed. By default state is transferred to sub-components in state attribute and can be retrieved using getState method.

TodoMVC

To continue this introduction TodoMVC implementation based on Morearty.js will be used (repository, application). You should have some previous React knowledge to follow painlessly, only Morearty-specific parts will be described.

App component

Having defined Bootstrap module let's now create main application module App:

var NOW_SHOWING = Object.freeze({ ALL: 'all', ACTIVE: 'active', COMPLETED: 'completed' });

var App = Ctx.createClass({
  componentDidMount: function () {
    var state = this.getState();
    Router({
      '/': state.assoc.bind(state, 'nowShowing', NOW_SHOWING.ALL),
      '/active': state.assoc.bind(state, 'nowShowing', NOW_SHOWING.ACTIVE),
      '/completed': state.assoc.bind(state, 'nowShowing', NOW_SHOWING.COMPLETED)
    }).init();
  },

  render: function () {
    var state = this.getState();
    var _ = Ctx.React.DOM;
    return _.section({ id: 'todoapp' },
      Header({ state: state }),
      TodoList({ state: state }),
      Footer({ state: state })
    );
  }
});

Notice that App uses getState method to retrieve its state binding and delegate it to its children.

Header component

var Header = Ctx.createClass({
  componentDidMount: function () {
    this.refs.newTodo.getDOMNode().focus();
  },

  onAddTodo: function (event) {
    var title = event.target.value;
    if (title) {
      this.getState().update('items', function (todos) {
        return todos.append(Ctx.Data.Map.fill(
          'title', title,
          'completed', false,
          'editing', false
        ));
      });
      event.target.value = '';
    }
  },

  render: function () {
    var _ = Ctx.React.DOM;
    return _.header({ id: 'header' },
      _.h1(null, 'todos'),
      _.input({
        id: 'new-todo',
        ref: 'newTodo',
        placeholder: 'What needs to be done?',
        onKeyPress: Ctx.Callback.onEnter(this.onAddTodo)
      })
    );
  }
});

In onAddTodo method component state is updated by appending new TODO item to the list.

TodoList component

var TodoList = Ctx.createClass({
  onToggleAll: function (event) {
    var completed = event.target.checked;
    this.getState().update('items', function (items) {
      return items.map(function (item) {
        return item.assoc('completed', completed);
      });
    });
  },

  render: function () {
    var state = this.getState();
    var nowShowing = state.val('nowShowing');
    var itemsBinding = state.sub('items');
    var items = itemsBinding.val();

    var isShown = function (item) {
      switch (nowShowing) {
        case NOW_SHOWING.ALL:
          return true;
        case NOW_SHOWING.ACTIVE:
          return !item.get('completed');
        case NOW_SHOWING.COMPLETED:
          return item.get('completed');
      }
    };

    var renderTodo = function (item, index) {
      return isShown(item) ? TodoItem({ state: itemsBinding.sub(index) }) : null;
    };

    var allCompleted = !items.find(function (item) {
      return !item.get('completed');
    });

    var _ = Ctx.React.DOM;
    return _.section({ id: 'main' },
      items.size() ? _.input({ id: 'toggle-all', type: 'checkbox', checked: allCompleted, onChange: this.onToggleAll }) : null,
      _.ul({ id: 'todo-list' },
        items.map(renderTodo).toArray()
      )
    );
  }
});

onToggleAll callback sets completed property on all items. Note how state is transferred to the children: only the relevant sub-state is passed using sub method which creates a sub-binding pointing deeper into global state. So, TODO item can only access and modify its own cell, and the rest of application state is protected from incidental modification. val method allows to retrieve the value stored in the binding or in its sub-path.

TodoItem

var TodoItem = Ctx.createClass({
  componentDidUpdate: function () {
    if (this.getState().val('editing')) {
      var node = this.refs.editField.getDOMNode();
      node.focus();
      node.setSelectionRange(node.value.length, node.value.length);
    }
  },

  onToggleCompleted: function (event) {
    this.getState().assoc('completed', event.target.checked);
    return false;
  },

  onToggleEditing: function () {
    this.getState().update('editing', Ctx.Util.not);
    return false;
  },

  onEnter: function (event) {
    this.getState().atomically()
      .assoc('title', event.target.value)
      .assoc('editing', false)
      .commit();
    return false;
  },

  render: function () {
    var state = this.getState();
    var item = state.val();

    var liClass = Ctx.React.addons.classSet({
      completed: item.get('completed'),
      editing: item.get('editing')
    });
    var title = item.get('title');

    var _ = Ctx.React.DOM;
    return _.li({ className: liClass },
      _.div({ className: 'view' },
        _.input({
          className: 'toggle',
          type: 'checkbox',
          checked: item.get('completed'),
          onChange: this.onToggleCompleted
        }),
        _.label({ onClick: this.onToggleEditing }, title),
        _.button({ className: 'destroy', onClick: state.dissoc.bind(state, '') })
      ),
      _.input({
        className: 'edit',
        ref: 'editField',
        value: title,
        onChange: Ctx.Callback.assoc(state, 'title'),
        onKeyPress: Ctx.Callback.onEnter(this.onEnter),
        onBlur: this.onToggleEditing
      })
    )
  }
});

Here component title is written to the global state using assoc helper when text in changed. To delete the item no callback needs to be passed from the parent: item component just calls Binding's dissoc method which removes it from the list of items. In onEnter method transaction is used to prevent re-rendering between state transitions. It effectively notifies global listeners once on commit.

Footer component

var Footer = Ctx.createClass({
  onClearCompleted: function () {
    this.getState().update('items', function (items) {
      return items.filter(function (item) {
        return !item.get('completed');
      });
    });
  },

  render: function () {
    var state = this.getState();
    var nowShowing = state.val('nowShowing');

    var items = state.val('items');
    var completedItems = items.filter(function (item) {
      return item.get('completed');
    });
    var completedItemsCount = completedItems.size();

    var _ = Ctx.React.DOM;
    return _.footer({ id: 'footer' },
      _.span({ id: 'todo-count' }, items.size() - completedItemsCount + ' items left'),
      _.ul({ id: 'filters' },
        _.li(null, _.a({ className: nowShowing === NOW_SHOWING.ALL ? 'selected' : '', href: '#/' }, 'All')),
        _.li(null, _.a({ className: nowShowing === NOW_SHOWING.ACTIVE ? 'selected' : '', href: '#/active' }, 'Active')),
        _.li(null, _.a({ className: nowShowing === NOW_SHOWING.COMPLETED ? 'selected' : '', href: '#/completed' }, 'Completed'))
      ),
      completedItemsCount ?
        _.button({ id: 'clear-completed', onClick: this.onClearCompleted },
            'Clear completed (' + completedItemsCount + ')'
        ) :
        null
    );
  }
});

Nothing special here so let's jump straight to...

Starting the application

Ctx.React.renderComponent(
  Bootstrap(),
  document.getElementById('root')
);

Just usual React routine here.

Principal differences from React

You can compare this Morearty-based TodoMVC implementation to the official React version. Main highlights are:

  • No callbacks are passed to sub-components. This becomes especially useful when you find yourself trying to transfer a callback to a component's grand-children (you may never know how you DOM may be restructured after a redesign). There is nothing inherently wrong in passing callbacks to sub-components, but in many cases this can be avoided.
  • No hacks in code simulating immutable state and other tricks (look at the comments withing React version sources).
  • Reasoning about the application is much simpler!
  • Each component gets shouldComponentUpdate method, no need to define it manually (but you can if you like).
  • Less code.

Other features

  • Util and Data.Util modules with a bunch of useful functions;
  • History module well-integrated with Binding allowing to painlessly implement undo/redo;
  • Callback module;
  • binding listeners support: you can listen to state changes and react accordingly;
  • and more.

Current status

Version 0.1.0 is ready. Test coverage is almost 100% with more than 400 test cases. Map performance is very good: approximately 3-times faster then Mori's implementation for additions and retrievals. Vector modification is 2-3 times slower than Mori's, but has significantly faster iteration. This is due to underlying array-copying based implementation.

Future goals by priority

  1. Improve the documentation, provide more examples.
  2. Gather community feedback to find areas for improvement.
  3. Stabilize API and code.
  4. Battle-test the library on more projects.
  5. Rewrite Vector using more efficient persistent immutable data structure keeping its contract intact.

About

Morearty.js - better state management for React in pure JavaScript

Resources

License

Code of conduct

Security policy

Stars

Watchers

Forks

Packages

No packages published