diff --git a/README.md b/README.md
index 073e426..b2a486a 100644
--- a/README.md
+++ b/README.md
@@ -1,52 +1,61 @@
-# react-backbone.glue
-This is React add-on designed to replace Backbone views with React in existing Backbone application.
+# nestedreact
+This is React add-on designed to simplify migration to React views in large Backbone applications.
It allows you:
-- To use React component in place of every Backbone View in your application.
-- To use your existing Backbone Views as subviews in React components.
+- To use React component in place of every Backbone View.
+- To use your existing Backbone Views from React components.
- To use your existing Backbone Models as React component state.
- Update React components on Backbone events.
+- Data-binding for models and collections
Thus, no refactoring of your application is required. You can start writing UI with React immediately replacing your Backbone Views one-by-one, while keeping your existing models.
-This extension works with raw Backbone. However, in order to take full advantage of React/Backbone
-architecture you are encouraged to upgrade to `Backbone.NestedTypes`. It will give you following
+# Breaking changes introduced in 0.3
+- `component.createView( props )` doesn't work any more, use `new component.View( props )` instead.
+- module and `npm` package name is now `nestedreact`.
+- Raw `backbone` is not supported any more. Upgrade to `NestedTypes` 1.1.5 or more is required. It will give you following
features to help managing complex application state:
+ - Proper nested models and collections implementation with deep changes detection. React components will
+ update UI on nested attribute changes.
+ - Dramatic improvement in model update performance compared to Backbone. Up to 40x faster in Chrome. Important for mobile devices.
+ - Type safety. Attributes are guaranteed to hold values of declared types all the time.
-- Proper nested models and collections implementation with deep changes detection. React components will
-update UI on nested attribute changes.
-- Dramatic improvement in model update performance compared to Backbone. Up to 40x faster in Chrome. Imprortant for mobile devices.
-- Type safety. Attributes are guaranteed to hold values of declared types all the time.
-
-For more information, visit
-http://volicon.github.io/backbone.nestedTypes/
-and
-https://github.com/Volicon/backbone.nestedTypes
+ For more information about `NestedTypes`, visit
+ http://volicon.github.io/backbone.nestedTypes/
+ and
+ https://github.com/Volicon/backbone.nestedTypes
# Usage
-It's packed as single UMD, thus grab the module which is appropriate for you. So far, there are two of them, one for raw backbone, and one for `backbone.nestedTypes`.
+It's packed as single UMD, thus grab the module or use `npm` to install.
+ `npm install --save nestedreact`
-First one depends on `react` and `backbone`, so if you're using Chaplin or Marionette you will
-probably need to pass appropriate module instead of `backbone`. Don't hesitate to replace module name in the beginning of the file, or use raw factory function from `src/glue.js`.
+Module export's modified React namespace (without touching original React), and its
+safe to use it as a replacement for `react`.
-If you're using `NestedTypes`, you need `NestedTypes` to require appropriate framework. Report a bug if something goes wrong, we like when someone share our passion for technology and quite fast in response.
+If you're using backbone-based frameworks such as `ChaplinJS` or `Marionette`,
+you need to do following things:
+- Make sure that frameworks includes `nestedtypes` instead of `backbone`.
+- On application start, tell `nestedreact` to use proper base class for View.
+ `require( 'nestedreact' ).useView( Chaplin.View )`
# Features
## Use React components as Backbone View
```javscript
-var backboneView = MyReactComponent.createView( props );
+var backboneView = new MyReactComponent.View( props );
```
## Use Backbone View in React component
```javscript
+var React = require( 'nestedreact' );
+
var MyComponent = React.createClass({
render : function(){
return (
-
- { this.state.get( 'count' ) }
+ { this.state.count }
);
},
onClick : function(){
- this.state.set( 'count', this.state.get( 'count' ) + 1 );
+ this.state.count = this.state.count + 1;
}
});
```
If Model is specified for the component,
-- `this.state` is backbone model. Usage of `setState` is not allowed.
+- `this.state` and `this.model` holds backbone model. Usage of `setState` is *not allowed*.
- React component will update itself whenever model emit `change` event.
- You can customize UI update events supplying `listenToState` property. For example, `listenToState : 'change:attr sync'`.
- You can disable UI updates on state change, supplying `listenToState : false` option.
@@ -86,8 +120,10 @@ If Model is specified for the component,
## Managing state with ad-hoc Backbone model
```javscript
+var React = require( 'nestedreact' );
+
var MyComponent = React.createClass({
- //Model : BackboneModel,
+ //Model : BackboneModel,
attributes : { // Model defaults
count : 0
@@ -96,43 +132,103 @@ var MyComponent = React.createClass({
render : function(){
return (
- { this.state.get( 'count' ) }
+ { this.state.count }
);
},
onClick : function(){
- this.state.set( 'count', this.state.get( 'count' ) + 1 );
+ this.state.count = this.state.count + 1;
}
});
```
-- New Model definition will be created, using `attributes` as Model.defaults.
+- New `NestedTypes` Model definition will be created, using `attributes` as Model.defaults.
- If Model property is specified, it will be used as base model and extended.
- `attributes` property from mixins will be properly merged.
-- if you're using `Backbone.NestedTypes` models, it's far superior to react state in every aspect. It handles updates much faster, it detects nested elements changes, and it has type specs for state elements in a way like react's `propTypes`.
+- Since `state` is `NestedTypes` model in this case,
+ - All attributes *must* be declared using `NestedTypes` standard type specs.
+ - `state` attributes allows direct assignments - treat it as regular object.
+ - Every `state` modification (including direct assignments and nested attributes changes) will
+ cause automagical react update.
## Passing Backbone objects as React components props
```javscript
var MyComponent = React.createClass({
- listenToProps : {
+ listenToProps : { // or just string with property names, separated by space
model : 'change'
},
render : function(){
return (
- { this.props.model.get( 'count' ) }
+ { this.props.model.count }
);
},
onClick : function(){
- this.props.model.set( 'count', this.props.model.get( 'count' ) + 1 );
+ this.props.model.count = this.props.model.count + 1;
}
});
```
You can update react component on backbone events from component props.
-
Event subscription is managed automatically. No props passed - no problems.
+
+## Data binding
+
+`nestedreact` supports data binding links compatible with standard React's `valueLink`.
+Links are "live" in a sense that they always point to actual value based on current model or collection state.
+It doesn't break anything in React, rather extends possible use cases.
+
+- `var link = model.getLink( 'attr' )` creates link for model attribute.
+- `var link = collection.getLink( model )` creates boolean link, toggling model in collection. True if model is contained in collection, assignments will add/remove given model. Useful for checkboxes.
+
+### Value access methods
+
+In addition to standard members `link.requestChange( x )` and `link.value`, links supports all popular property access styles:
+
+- jQuery property style: setter `link.val( x )`, getter `link.val()`
+- Backbone style: setter `link.set( x )`, getter `link.get()`
+- plain assugnments style: setter `link.value = x`, getter `link.value`
+- `link.toggle()` is a shortcut for `link.requestChange( !link.value )`
+
+Most efficient way to work with link is using `link.val()` function, that's how its internally implemented. `val` function is bound, and can be passed around safely.
+
+### Link transformations
+
+Attribute's link can be further transformed using extended link API. Link transformations allowing you to use new `stateless functions` component definition style introduced in React 0.14 in most cases.
+
+For links with any value type:
+
+- `link.equals( x )` creates boolean link which is true whenever link value is equal to x. Useful for radio groups.
+- `link.update( x => !x )` creates function transforming link value (toggling boolean value in this case). Useful for `onClick` event handlers.
+
+For link enclosing array:
+
+- `arrLink.contains( x )` creates boolean link which is true whenever x is contained in an array in link value. Useful for checkboxes. Avoid long arrays, currently operations has O(N^2) complexity.
+
+For link enclosings arrays and plain JS objects:
+- `arrOrObjLink.at( key )` creates link to array of object member with a given key. Can be applied multiple times to work with object hierarchies; on modifications, objects will be updated in purely functional way (modified parts will be shallow copied). Useful when working with plain JS objects in model attributes - updating them through links make changes visible to the model.
+- `arrOrObjLink.map( ( itemLink, key ) => )` iterates through object or array, wrapping its elements to links. Useful for JSX transofrmation.
+
+### Links and components state
+
+Link received through component props can be mapped as state member using the following declaration:
+```javascript
+attributes : {
+ selected : Nested.link( '^props.selectedLink' )
+}
+```
+It can be accessed as a part of state, however, in this case it's not true state. All read/write operations will be done with link itself, and local state won't be modified.
+
+Also, links can be used to declaratively expose real component state to upper conponents. In this example, link optionally received in props will be updated every time `this.state.selected` object is replaced. In this case, updates are one way, from bottom component to upper one, and stateful component will render itself when state is changed.
+
+```javascript
+attributes : {
+ selected : Item.has.watcher( '^props.selectedLink.val' )
+}
+```
+
+Technically, "watcher" - is just a callback function with a single argument receiving new attribute value, so links are not required here.
diff --git a/example/test.html b/example/test.html
new file mode 100644
index 0000000..4fe67bb
--- /dev/null
+++ b/example/test.html
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/nestedreact.js b/nestedreact.js
new file mode 100644
index 0000000..4e2892e
--- /dev/null
+++ b/nestedreact.js
@@ -0,0 +1,664 @@
+(function webpackUniversalModuleDefinition(root, factory) {
+ if(typeof exports === 'object' && typeof module === 'object')
+ module.exports = factory(require("react"), require("react-dom"), require("nestedtypes"));
+ else if(typeof define === 'function' && define.amd)
+ define(["react", "react-dom", "nestedtypes"], factory);
+ else if(typeof exports === 'object')
+ exports["React"] = factory(require("react"), require("react-dom"), require("nestedtypes"));
+ else
+ root["React"] = factory(root["React"], root["ReactDOM"], root["Nested"]);
+})(this, function(__WEBPACK_EXTERNAL_MODULE_1__, __WEBPACK_EXTERNAL_MODULE_2__, __WEBPACK_EXTERNAL_MODULE_3__) {
+return /******/ (function(modules) { // webpackBootstrap
+/******/ // The module cache
+/******/ var installedModules = {};
+/******/
+/******/ // The require function
+/******/ function __webpack_require__(moduleId) {
+/******/
+/******/ // Check if module is in cache
+/******/ if(installedModules[moduleId])
+/******/ return installedModules[moduleId].exports;
+/******/
+/******/ // Create a new module (and put it into the cache)
+/******/ var module = installedModules[moduleId] = {
+/******/ exports: {},
+/******/ id: moduleId,
+/******/ loaded: false
+/******/ };
+/******/
+/******/ // Execute the module function
+/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
+/******/
+/******/ // Flag the module as loaded
+/******/ module.loaded = true;
+/******/
+/******/ // Return the exports of the module
+/******/ return module.exports;
+/******/ }
+/******/
+/******/
+/******/ // expose the modules object (__webpack_modules__)
+/******/ __webpack_require__.m = modules;
+/******/
+/******/ // expose the module cache
+/******/ __webpack_require__.c = installedModules;
+/******/
+/******/ // __webpack_public_path__
+/******/ __webpack_require__.p = "";
+/******/
+/******/ // Load entry module and return exports
+/******/ return __webpack_require__(0);
+/******/ })
+/************************************************************************/
+/******/ ([
+/* 0 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var React = __webpack_require__( 1 ),
+ ReactDOM = __webpack_require__( 2 ),
+ Nested = __webpack_require__( 3 ),
+ $ = Nested.$;
+
+ // extend React namespace
+ var NestedReact = module.exports = Object.create( React );
+
+ // listenToProps, listenToState, model, attributes, Model
+ NestedReact.createClass = __webpack_require__( 4 );
+
+ var ComponentView = __webpack_require__( 5 );
+
+ // export hook to override base View class used...
+ NestedReact.useView = function( View ){
+ NestedReact._BaseView = ComponentView.use( View );
+ };
+
+ NestedReact.useView( Nested.View );
+
+ // React component for attaching views
+ NestedReact.subview = __webpack_require__( 6 );
+
+ NestedReact.tools = __webpack_require__( 7 );
+
+ // Extend react components to have backbone-style jquery accessors
+ var Component = React.createClass( { render : function(){} } ),
+ BaseComponent = Object.getPrototypeOf( Component.prototype );
+
+ Object.defineProperties( BaseComponent, {
+ el : { get : function(){ return ReactDOM.findDOMNode( this ); } },
+ $el : { get : function(){ return $( this.el ); } },
+ $ : { value : function( sel ){ return this.$el.find( sel ); } }
+ } );
+
+ var ValueLink = __webpack_require__( 8 );
+ var Link = Nested.Link = ValueLink.Link;
+ Nested.link = ValueLink.link;
+
+ var ModelProto = Nested.Model.prototype;
+
+ ModelProto.getLink = function( attr ){
+ var model = this;
+
+ return new Link( function( x ){
+ if( arguments.length ){
+ model[ attr ] = x;
+ }
+
+ return model[ attr ];
+ });
+ };
+
+ var CollectionProto = Nested.Collection.prototype;
+
+ CollectionProto.getLink = function( model ){
+ var collection = this;
+
+ return new Link( function( x ){
+ var prev = Boolean( collection.get( model ) );
+
+ if( arguments.length ){
+ var next = Boolean( x );
+ if( prev !== next ){
+ collection.toggle( model, x );
+ return next;
+ }
+ }
+
+ return prev;
+ });
+ };
+
+
+/***/ },
+/* 1 */
+/***/ function(module, exports) {
+
+ module.exports = __WEBPACK_EXTERNAL_MODULE_1__;
+
+/***/ },
+/* 2 */
+/***/ function(module, exports) {
+
+ module.exports = __WEBPACK_EXTERNAL_MODULE_2__;
+
+/***/ },
+/* 3 */
+/***/ function(module, exports) {
+
+ module.exports = __WEBPACK_EXTERNAL_MODULE_3__;
+
+/***/ },
+/* 4 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var React = __webpack_require__( 1 ),
+ Nested = __webpack_require__( 3 );
+
+ function forceUpdate(){ this.forceUpdate(); }
+
+ var Events = Object.assign( {
+ componentWillUnmount : function(){
+ this.stopListening();
+ }
+ }, Nested.Events );
+
+ var ListenToProps = {
+ componentDidMount : function(){
+ var props = this.props,
+ updateOn = this.listenToProps;
+
+ for( var prop in updateOn ){
+ var emitter = props[ prop ];
+ emitter && this.listenTo( emitter, updateOn[ prop ], forceUpdate );
+ }
+ }
+ };
+
+ var ListenToPropsArray = {
+ componentDidMount : function(){
+ var props = this.props,
+ updateOn = this.listenToProps;
+
+ for( var i = 0; i < updateOn.length; i++ ){
+ var emitter = props[ updateOn[ i ] ];
+ emitter && this.listenTo( emitter, emitter.triggerWhenChanged, forceUpdate );
+ }
+ }
+ };
+
+ var ModelState = {
+ listenToState : 'change',
+ model : null,
+
+ getInitialState : function(){
+ this.model = new this.Model();
+ // enable owner references in the model to access component props
+ this.model._owner = this;
+
+ return this.model;
+ },
+
+ // reference global store to fix model's store locator
+ getStore : function(){
+ this.model._defaultStore;
+ },
+
+ componentDidMount : function(){
+ var events = this.listenToState;
+ events && this.listenTo( this.model, events, forceUpdate );
+ },
+
+ componentWillUnmount : function(){
+ this.model._owner = null;
+ this.model.stopListening();
+ }
+ };
+
+ function createClass( spec ){
+ var mixins = spec.mixins || ( spec.mixins = [] );
+
+ var attributes = getModelAttributes( spec );
+ if( attributes ){
+ var BaseModel = spec.Model || Nested.Model;
+ spec.Model = BaseModel.extend( { defaults : attributes } );
+ }
+
+ if( spec.Model ) mixins.push( ModelState );
+
+ var listenToProps = spec.listenToProps;
+ if( listenToProps ){
+ if( typeof listenToProps === 'string' ){
+ spec.listenToProps = listenToProps.split( ' ' );
+ mixins.unshift( ListenToPropsArray );
+ }
+ else{
+ mixins.unshift( ListenToProps );
+ }
+ }
+
+ mixins.push( Events );
+
+ var component = React.createClass( spec );
+
+ // attach lazily evaluated backbone View class
+ var NestedReact = this;
+
+ Object.defineProperty( component, 'View', {
+ get : function(){
+ return this._View || ( this._View = NestedReact._BaseView.extend( { reactClass : component } ) );
+ }
+ });
+
+ return component;
+ }
+
+ function getModelAttributes( spec ){
+ var attributes = null;
+
+ for( var i = spec.mixins.length - 1; i >= 0; i-- ){
+ var mixin = spec.mixins[ i ];
+ if( mixin.attributes ){
+ attributes || ( attributes = {} );
+ Object.assign( attributes, mixin.attributes );
+ }
+ }
+
+ if( spec.attributes ){
+ if( attributes ){
+ Object.assign( attributes, spec.attributes );
+ }
+ else{
+ attributes = spec.attributes;
+ }
+ }
+
+ return attributes;
+ }
+
+ module.exports = createClass;
+
+
+/***/ },
+/* 5 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var React = __webpack_require__( 1 ),
+ ReactDOM = __webpack_require__( 2 );
+
+ module.exports.use = function( View ){
+ var dispose = View.prototype.dispose || function(){},
+ setElement = View.prototype.setElement;
+
+ var ComponentView = View.extend( {
+ reactClass : null,
+ props : {},
+ element : null,
+
+ initialize : function( props ){
+ // memorise arguments to pass to React
+ this.options = props || {};
+ this.element = React.createElement( this.reactClass, this.options );
+ },
+
+ setElement : function(){
+ this.unmountComponent();
+ return setElement.apply( this, arguments );
+ },
+
+ // cached instance of react component...
+ component : null,
+ prevState : null,
+
+ render : function(){
+ var component = ReactDOM.render( this.element, this.el );
+ this.component || this.mountComponent( component );
+ },
+
+ mountComponent : function( component ){
+ this.component = component;
+
+ if( this.prevState ){
+ component.model.set( this.prevState );
+ this.prevState = null;
+ }
+
+ component.trigger && this.listenTo( component, 'all', function(){
+ this.trigger.apply( this, arguments );
+ });
+ },
+
+ unmountComponent : function(){
+ var component = this.component;
+
+ if( component ){
+ this.prevState = component.model && component.model.attributes;
+
+ if( component.trigger ){
+ this.stopListening( component );
+ }
+
+ ReactDOM.unmountComponentAtNode( this.el );
+ this.component = null;
+ }
+ },
+
+ dispose : function(){
+ this.unmountComponent();
+ return dispose.apply( this, arguments );
+ }
+ } );
+
+ Object.defineProperty( ComponentView.prototype, 'model', {
+ get : function(){
+ this.component || this.render();
+ return this.component && this.component.model;
+ }
+ } );
+
+ return ComponentView;
+ };
+
+
+/***/ },
+/* 6 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var React = __webpack_require__( 1 ),
+ jsonNotEqual = __webpack_require__( 7 ).jsonNotEqual;
+
+ module.exports = React.createClass({
+ displayName : 'BackboneView',
+
+ propTypes : {
+ View : React.PropTypes.func.isRequired,
+ options : React.PropTypes.object
+ },
+
+ shouldComponentUpdate : function( next ){
+ var props = this.props;
+ return next.View !== props.View || jsonNotEqual( next.options, props.options );
+ },
+
+ render : function(){
+ return React.DOM.div({
+ ref : 'subview',
+ className : this.props.className
+ });
+ },
+
+ componentDidMount : function(){
+ this._mountView();
+ },
+ componentDidUpdate : function(){
+ this._dispose();
+ this._mountView();
+ },
+ componentWillUnmount : function(){
+ this._dispose();
+ },
+
+ _mountView: function () {
+ var el = this.refs.subview,
+ p = this.props;
+
+ var view = this.view = p.options ? new p.View( p.options ) : new p.View();
+
+ el.appendChild( view.el );
+ view.render();
+ },
+
+ _dispose : function(){
+ var view = this.view;
+ if( view ){
+ view.stopListening();
+ if( view.dispose ) view.dispose();
+ this.refs.subview.innerHTML = "";
+ this.view = null;
+ }
+ }
+ });
+
+/***/ },
+/* 7 */
+/***/ function(module, exports) {
+
+ // equality checking for deep JSON comparison of plain Array and Object
+ var ArrayProto = Array.prototype,
+ ObjectProto = Object.prototype;
+
+ exports.jsonNotEqual = jsonNotEqual;
+ function jsonNotEqual( objA, objB) {
+ if (objA === objB) {
+ return false;
+ }
+
+ if (typeof objA !== 'object' || !objA ||
+ typeof objB !== 'object' || !objB ) {
+ return true;
+ }
+
+ var protoA = Object.getPrototypeOf( objA ),
+ protoB = Object.getPrototypeOf( objB );
+
+ if( protoA !== protoB ) return true;
+
+ if( protoA === ArrayProto ) return arraysNotEqual( objA, objB );
+ if( protoA === ObjectProto ) return objectsNotEqual( objA, objB );
+
+ return true;
+ }
+
+ function objectsNotEqual( objA, objB ){
+ var keysA = Object.keys(objA);
+ var keysB = Object.keys(objB);
+
+ if (keysA.length !== keysB.length) {
+ return true;
+ }
+
+ // Test for A's keys different from B.
+ var bHasOwnProperty = Object.prototype.hasOwnProperty.bind(objB);
+
+ for (var i = 0; i < keysA.length; i++) {
+ var key = keysA[i];
+ if ( !bHasOwnProperty( key ) || jsonNotEqual( objA[ key ], objB[ key ] )) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ function arraysNotEqual( a, b ){
+ if( a.length !== b.length ) return true;
+
+ for( var i = 0; i < a.length; i++ ){
+ if( jsonNotEqual( a[ i ], b[ i ] ) ) return true;
+ }
+
+ return false;
+ }
+
+ // private array helpers
+ exports.contains = contains;
+ function contains( arr, el ){
+ for( var i = 0; i < arr.length; i++ ){
+ if( arr[ i ] === el ) return true;
+ }
+
+ return false;
+ };
+
+ exports.without = without;
+ function without( arr, el ){
+ var res = [];
+
+ for( var i = 0; i < arr.length; i++ ){
+ var current = arr[ i ];
+ current === el || res.push( current );
+ }
+
+ return res;
+ };
+
+ exports.clone = clone;
+ function clone( objOrArray ){
+ return objOrArray instanceof Array ? objOrArray.slice() : Object.assign( {}, objOrArray );
+ };
+
+
+/***/ },
+/* 8 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var Nested = __webpack_require__( 3 ),
+ tools = __webpack_require__( 7 ),
+ contains = tools.contains,
+ without = tools.without,
+ clone = tools.clone;
+
+ var Link = exports.Link = Object.extend( {
+ constructor : function( val ){
+ this.val = val;
+ },
+
+ val : function( x ){ return x; },
+
+ properties : {
+ value : {
+ get : function(){ return this.val(); },
+ set : function( x ){ this.val( x ); }
+ }
+ },
+
+ requestChange : function( x ){ this.val( x ); },
+ get : function(){ return this.val(); },
+ set : function( x ){ this.val( x ); },
+ toggle : function(){ this.val( !this.val() ); },
+
+ contains : function( element ){
+ var link = this;
+
+ return new Link( function( x ){
+ var arr = link.val(),
+ prev = contains( arr, element );
+
+ if( arguments.length ){
+ var next = Boolean( x );
+ if( prev !== next ){
+ link.val( x ? arr.concat( element ) : without( arr, element ) );
+ return next;
+ }
+ }
+
+ return prev;
+ } );
+ },
+
+ // create boolean link for value equality
+ equals : function( asTrue ){
+ var link = this;
+
+ return new Link( function( x ){
+ if( arguments.length ) link.val( x ? asTrue : null );
+
+ return link.val() === asTrue;
+ } );
+ },
+
+ // link to enclosed object or array member
+ at : function( key ){
+ var link = this;
+
+ return new Link( function( x ){
+ var arr = link.val(),
+ prev = arr[ key ];
+
+ if( arguments.length ){
+ if( prev !== x ){
+ arr = clone( arr );
+ arr[ key ] = x;
+ link.val( arr );
+ return x;
+ }
+ }
+
+ return prev;
+ } );
+ },
+
+ // iterates through enclosed object or array, generating set of links
+ map : function( fun ){
+ var arr = this.val();
+ return arr ? ( arr instanceof Array ? mapArray( this, arr, fun ) : mapObject( this, arr, fun ) ) : [];
+ },
+
+ // create function which updates the link
+ update : function( transform ){
+ var val = this.val;
+ return function(){
+ val( transform( val() ) )
+ }
+ }
+ });
+
+ function mapObject( link, object, fun ){
+ var res = [];
+
+ for( var i in object ){
+ if( object.hasOwnProperty( i ) ){
+ var y = fun( link.at( i ), i );
+ y === void 0 || ( res.push( y ) );
+ }
+ }
+
+ return res;
+ }
+
+ function mapArray( link, arr, fun ){
+ var res = [];
+
+ for( var i = 0; i < arr.length; i++ ){
+ var y = fun( link.at( i ), i );
+ y === void 0 || ( res.push( y ) );
+ }
+
+ return res;
+ }
+
+ exports.link = function( reference ){
+ var getMaster = Nested.parseReference( reference );
+
+ function setLink( value ){
+ var link = getMaster.call( this );
+ link && link.val( value );
+ }
+
+ function getLink(){
+ var link = getMaster.call( this );
+ return link && link.val();
+ }
+
+ var LinkAttribute = Nested.attribute.Type.extend( {
+ createPropertySpec : function(){
+ return {
+ // call to optimized set function for single argument. Doesn't work for backbone types.
+ set : setLink,
+
+ // attach get hook to the getter function, if present
+ get : getLink
+ }
+ },
+
+ set : setLink
+ } );
+
+ var options = Nested.attribute( { toJSON : false } );
+ options.Attribute = LinkAttribute;
+ return options;
+ };
+
+/***/ }
+/******/ ])
+});
+;
+//# sourceMappingURL=nestedreact.js.map
\ No newline at end of file
diff --git a/nestedreact.js.map b/nestedreact.js.map
new file mode 100644
index 0000000..78eff95
--- /dev/null
+++ b/nestedreact.js.map
@@ -0,0 +1 @@
+{"version":3,"sources":["webpack:///webpack/universalModuleDefinition","webpack:///webpack/bootstrap 53a793ad5a4068deea99","webpack:///./src/main.js","webpack:///external {\"commonjs\":\"react\",\"commonjs2\":\"react\",\"amd\":\"react\",\"root\":\"React\"}","webpack:///external {\"commonjs\":\"react-dom\",\"commonjs2\":\"react-dom\",\"amd\":\"react-dom\",\"root\":\"ReactDOM\"}","webpack:///external {\"commonjs\":\"nestedtypes\",\"commonjs2\":\"nestedtypes\",\"amd\":\"nestedtypes\",\"root\":\"Nested\"}","webpack:///./src/createClass.js","webpack:///./src/component-view.js","webpack:///./src/view-element.js","webpack:///./src/tools.js","webpack:///./src/value-link.js"],"names":[],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,CAAC;AACD,O;ACVA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA,uBAAe;AACf;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;;AAGA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;;;;;;;ACtCA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;;AAEA;AACA;AACA;AACA;;AAEA;;AAEA;AACA;;AAEA;;AAEA;AACA,yCAAwC,sBAAsB,EAAE;AAChE;;AAEA;AACA,YAAW,kBAAkB,qCAAqC,EAAE,EAAE;AACtE,YAAW,kBAAkB,qBAAqB,EAAE,EAAE;AACtD,YAAW,yBAAyB,6BAA6B,EAAE;AACnE,EAAC;;AAED;AACA;AACA;;AAEA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA,MAAK;AACL;;AAEA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA,MAAK;AACL;;;;;;;ACvEA,gD;;;;;;ACAA,gD;;;;;;ACAA,gD;;;;;;ACAA;AACA;;AAEA,wBAAuB,oBAAoB;;AAE3C;AACA;AACA;AACA;AACA,EAAC;;AAED;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA,wBAAuB,qBAAqB;AAC5C;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA,MAAK;;AAEL;AACA;AACA;AACA,MAAK;;AAEL;AACA;AACA;AACA,MAAK;;AAEL;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA,4CAA2C,wBAAwB;AACnE;;AAEA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;;AAEA;;AAEA;AACA;;AAEA;AACA;AACA,gFAA+E,yBAAyB;AACxG;AACA,MAAK;;AAEL;AACA;;AAEA;AACA;;AAEA,yCAAwC,QAAQ;AAChD;AACA;AACA,4CAA2C;AAC3C;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;;;;;;;AC5HA;AACA;;AAEA;AACA,4DAA2D;AAC3D;;AAEA;AACA;AACA,wBAAuB;AACvB;;AAEA;AACA;AACA;AACA;AACA,UAAS;;AAET;AACA;AACA;AACA,UAAS;;AAET;AACA;AACA;;AAEA;AACA;AACA;AACA,UAAS;;AAET;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA,cAAa;AACb,UAAS;;AAET;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA,UAAS;;AAET;AACA;AACA;AACA;AACA,MAAK;;AAEL;AACA;AACA;AACA;AACA;AACA,MAAK;;AAEL;AACA;;;;;;;AC1EA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA,MAAK;;AAEL;AACA;AACA;AACA,MAAK;;AAEL;AACA;AACA;AACA;AACA,UAAS;AACT,MAAK;;AAEL;AACA;AACA,MAAK;AACL;AACA;AACA;AACA,MAAK;AACL;AACA;AACA,MAAK;;AAEL;AACA;AACA;;AAEA;;AAEA;AACA;AACA,MAAK;;AAEL;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,EAAC,E;;;;;;ACrDD;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;;AAEA,oBAAmB,kBAAkB;AACrC;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA,oBAAmB,cAAc;AACjC;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA,oBAAmB,gBAAgB;AACnC;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA,oBAAmB,gBAAgB;AACnC;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA,gFAA+E;AAC/E;;;;;;;AClFA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,MAAK;;AAEL,yBAAwB,UAAU,EAAE;;AAEpC;AACA;AACA,8BAA6B,mBAAmB,EAAE;AAClD,iCAAgC,eAAe;AAC/C;AACA,MAAK;;AAEL,mCAAkC,eAAe,EAAE;AACnD,gCAA+B,mBAAmB,EAAE;AACpD,mCAAkC,eAAe,EAAE;AACnD,gCAA+B,yBAAyB,EAAE;;AAE1D;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA,UAAS;AACT,MAAK;;AAEL;AACA;AACA;;AAEA;AACA;;AAEA;AACA,UAAS;AACT,MAAK;;AAEL;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA,UAAS;AACT,MAAK;;AAEL;AACA;AACA;AACA;AACA,MAAK;;AAEL;AACA;AACA;AACA;AACA;AACA;AACA;AACA,EAAC;;AAED;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA,oBAAmB,gBAAgB;AACnC;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,UAAS;;AAET;AACA,MAAK;;AAEL,4CAA2C,iBAAiB;AAC5D;AACA;AACA,G","file":"./nestedreact.js","sourcesContent":["(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory(require(\"react\"), require(\"react-dom\"), require(\"nestedtypes\"));\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine([\"react\", \"react-dom\", \"nestedtypes\"], factory);\n\telse if(typeof exports === 'object')\n\t\texports[\"React\"] = factory(require(\"react\"), require(\"react-dom\"), require(\"nestedtypes\"));\n\telse\n\t\troot[\"React\"] = factory(root[\"React\"], root[\"ReactDOM\"], root[\"Nested\"]);\n})(this, function(__WEBPACK_EXTERNAL_MODULE_1__, __WEBPACK_EXTERNAL_MODULE_2__, __WEBPACK_EXTERNAL_MODULE_3__) {\nreturn \n\n\n/** WEBPACK FOOTER **\n ** webpack/universalModuleDefinition\n **/"," \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId])\n \t\t\treturn installedModules[moduleId].exports;\n\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\texports: {},\n \t\t\tid: moduleId,\n \t\t\tloaded: false\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.loaded = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(0);\n\n\n\n/** WEBPACK FOOTER **\n ** webpack/bootstrap 53a793ad5a4068deea99\n **/","var React = require( 'react' ),\n ReactDOM = require( 'react-dom' ),\n Nested = require( 'nestedtypes' ),\n $ = Nested.$;\n\n// extend React namespace\nvar NestedReact = module.exports = Object.create( React );\n\n// listenToProps, listenToState, model, attributes, Model\nNestedReact.createClass = require( './createClass' );\n\nvar ComponentView = require( './component-view' );\n\n// export hook to override base View class used...\nNestedReact.useView = function( View ){\n NestedReact._BaseView = ComponentView.use( View );\n};\n\nNestedReact.useView( Nested.View );\n\n// React component for attaching views\nNestedReact.subview = require( './view-element' );\n\nNestedReact.tools = require( './tools' );\n\n// Extend react components to have backbone-style jquery accessors\nvar Component = React.createClass( { render : function(){} } ),\n BaseComponent = Object.getPrototypeOf( Component.prototype );\n\nObject.defineProperties( BaseComponent, {\n el : { get : function(){ return ReactDOM.findDOMNode( this ); } },\n $el : { get : function(){ return $( this.el ); } },\n $ : { value : function( sel ){ return this.$el.find( sel ); } }\n} );\n\nvar ValueLink = require( './value-link' );\nvar Link = Nested.Link = ValueLink.Link;\nNested.link = ValueLink.link;\n\nvar ModelProto = Nested.Model.prototype;\n\nModelProto.getLink = function( attr ){\n var model = this;\n\n return new Link( function( x ){\n if( arguments.length ){\n model[ attr ] = x;\n }\n\n return model[ attr ];\n });\n};\n\nvar CollectionProto = Nested.Collection.prototype;\n\nCollectionProto.getLink = function( model ){\n var collection = this;\n\n return new Link( function( x ){\n var prev = Boolean( collection.get( model ) );\n\n if( arguments.length ){\n var next = Boolean( x );\n if( prev !== next ){\n collection.toggle( model, x );\n return next;\n }\n }\n\n return prev;\n });\n};\n\n\n\n/*****************\n ** WEBPACK FOOTER\n ** ./src/main.js\n ** module id = 0\n ** module chunks = 0\n **/","module.exports = __WEBPACK_EXTERNAL_MODULE_1__;\n\n\n/*****************\n ** WEBPACK FOOTER\n ** external {\"commonjs\":\"react\",\"commonjs2\":\"react\",\"amd\":\"react\",\"root\":\"React\"}\n ** module id = 1\n ** module chunks = 0\n **/","module.exports = __WEBPACK_EXTERNAL_MODULE_2__;\n\n\n/*****************\n ** WEBPACK FOOTER\n ** external {\"commonjs\":\"react-dom\",\"commonjs2\":\"react-dom\",\"amd\":\"react-dom\",\"root\":\"ReactDOM\"}\n ** module id = 2\n ** module chunks = 0\n **/","module.exports = __WEBPACK_EXTERNAL_MODULE_3__;\n\n\n/*****************\n ** WEBPACK FOOTER\n ** external {\"commonjs\":\"nestedtypes\",\"commonjs2\":\"nestedtypes\",\"amd\":\"nestedtypes\",\"root\":\"Nested\"}\n ** module id = 3\n ** module chunks = 0\n **/","var React = require( 'react' ),\n Nested = require( 'nestedtypes' );\n\nfunction forceUpdate(){ this.forceUpdate(); }\n\nvar Events = Object.assign( {\n componentWillUnmount : function(){\n this.stopListening();\n }\n}, Nested.Events );\n\nvar ListenToProps = {\n componentDidMount : function(){\n var props = this.props,\n updateOn = this.listenToProps;\n\n for( var prop in updateOn ){\n var emitter = props[ prop ];\n emitter && this.listenTo( emitter, updateOn[ prop ], forceUpdate );\n }\n }\n};\n\nvar ListenToPropsArray = {\n componentDidMount : function(){\n var props = this.props,\n updateOn = this.listenToProps;\n\n for( var i = 0; i < updateOn.length; i++ ){\n var emitter = props[ updateOn[ i ] ];\n emitter && this.listenTo( emitter, emitter.triggerWhenChanged, forceUpdate );\n }\n }\n};\n\nvar ModelState = {\n listenToState : 'change',\n model : null,\n\n getInitialState : function(){\n this.model = new this.Model();\n // enable owner references in the model to access component props\n this.model._owner = this;\n\n return this.model;\n },\n\n // reference global store to fix model's store locator\n getStore : function(){\n this.model._defaultStore;\n },\n\n componentDidMount : function(){\n var events = this.listenToState;\n events && this.listenTo( this.model, events, forceUpdate );\n },\n\n componentWillUnmount : function(){\n this.model._owner = null;\n this.model.stopListening();\n }\n};\n\nfunction createClass( spec ){\n var mixins = spec.mixins || ( spec.mixins = [] );\n\n var attributes = getModelAttributes( spec );\n if( attributes ){\n var BaseModel = spec.Model || Nested.Model;\n spec.Model = BaseModel.extend( { defaults : attributes } );\n }\n\n if( spec.Model ) mixins.push( ModelState );\n\n var listenToProps = spec.listenToProps;\n if( listenToProps ){\n if( typeof listenToProps === 'string' ){\n spec.listenToProps = listenToProps.split( ' ' );\n mixins.unshift( ListenToPropsArray );\n }\n else{\n mixins.unshift( ListenToProps );\n }\n }\n\n mixins.push( Events );\n\n var component = React.createClass( spec );\n\n // attach lazily evaluated backbone View class\n var NestedReact = this;\n\n Object.defineProperty( component, 'View', {\n get : function(){\n return this._View || ( this._View = NestedReact._BaseView.extend( { reactClass : component } ) );\n }\n });\n\n return component;\n}\n\nfunction getModelAttributes( spec ){\n var attributes = null;\n\n for( var i = spec.mixins.length - 1; i >= 0; i-- ){\n var mixin = spec.mixins[ i ];\n if( mixin.attributes ){\n attributes || ( attributes = {} );\n Object.assign( attributes, mixin.attributes );\n }\n }\n\n if( spec.attributes ){\n if( attributes ){\n Object.assign( attributes, spec.attributes );\n }\n else{\n attributes = spec.attributes;\n }\n }\n\n return attributes;\n}\n\nmodule.exports = createClass;\n\n\n\n/*****************\n ** WEBPACK FOOTER\n ** ./src/createClass.js\n ** module id = 4\n ** module chunks = 0\n **/","var React = require( 'react' ),\n ReactDOM = require( 'react-dom' );\n\nmodule.exports.use = function( View ){\n var dispose = View.prototype.dispose || function(){},\n setElement = View.prototype.setElement;\n\n var ComponentView = View.extend( {\n reactClass : null,\n props : {},\n element : null,\n\n initialize : function( props ){\n // memorise arguments to pass to React\n this.options = props || {};\n this.element = React.createElement( this.reactClass, this.options );\n },\n\n setElement : function(){\n this.unmountComponent();\n return setElement.apply( this, arguments );\n },\n\n // cached instance of react component...\n component : null,\n prevState : null,\n\n render : function(){\n var component = ReactDOM.render( this.element, this.el );\n this.component || this.mountComponent( component );\n },\n\n mountComponent : function( component ){\n this.component = component;\n\n if( this.prevState ){\n component.model.set( this.prevState );\n this.prevState = null;\n }\n\n component.trigger && this.listenTo( component, 'all', function(){\n this.trigger.apply( this, arguments );\n });\n },\n\n unmountComponent : function(){\n var component = this.component;\n\n if( component ){\n this.prevState = component.model && component.model.attributes;\n\n if( component.trigger ){\n this.stopListening( component );\n }\n\n ReactDOM.unmountComponentAtNode( this.el );\n this.component = null;\n }\n },\n\n dispose : function(){\n this.unmountComponent();\n return dispose.apply( this, arguments );\n }\n } );\n\n Object.defineProperty( ComponentView.prototype, 'model', {\n get : function(){\n this.component || this.render();\n return this.component && this.component.model;\n }\n } );\n\n return ComponentView;\n};\n\n\n\n/*****************\n ** WEBPACK FOOTER\n ** ./src/component-view.js\n ** module id = 5\n ** module chunks = 0\n **/","var React = require( 'react' ),\n jsonNotEqual = require( './tools' ).jsonNotEqual;\n\nmodule.exports = React.createClass({\n displayName : 'BackboneView',\n\n propTypes : {\n View : React.PropTypes.func.isRequired,\n options : React.PropTypes.object\n },\n\n shouldComponentUpdate : function( next ){\n var props = this.props;\n return next.View !== props.View || jsonNotEqual( next.options, props.options );\n },\n\n render : function(){\n return React.DOM.div({\n ref : 'subview',\n className : this.props.className\n });\n },\n\n componentDidMount : function(){\n this._mountView();\n },\n componentDidUpdate : function(){\n this._dispose();\n this._mountView();\n },\n componentWillUnmount : function(){\n this._dispose();\n },\n\n _mountView: function () {\n var el = this.refs.subview,\n p = this.props;\n\n var view = this.view = p.options ? new p.View( p.options ) : new p.View();\n\n el.appendChild( view.el );\n view.render();\n },\n\n _dispose : function(){\n var view = this.view;\n if( view ){\n view.stopListening();\n if( view.dispose ) view.dispose();\n this.refs.subview.innerHTML = \"\";\n this.view = null;\n }\n }\n});\n\n\n/*****************\n ** WEBPACK FOOTER\n ** ./src/view-element.js\n ** module id = 6\n ** module chunks = 0\n **/","// equality checking for deep JSON comparison of plain Array and Object\nvar ArrayProto = Array.prototype,\n ObjectProto = Object.prototype;\n\nexports.jsonNotEqual = jsonNotEqual;\nfunction jsonNotEqual( objA, objB) {\n if (objA === objB) {\n return false;\n }\n\n if (typeof objA !== 'object' || !objA ||\n typeof objB !== 'object' || !objB ) {\n return true;\n }\n\n var protoA = Object.getPrototypeOf( objA ),\n protoB = Object.getPrototypeOf( objB );\n\n if( protoA !== protoB ) return true;\n\n if( protoA === ArrayProto ) return arraysNotEqual( objA, objB );\n if( protoA === ObjectProto ) return objectsNotEqual( objA, objB );\n\n return true;\n}\n\nfunction objectsNotEqual( objA, objB ){\n var keysA = Object.keys(objA);\n var keysB = Object.keys(objB);\n\n if (keysA.length !== keysB.length) {\n return true;\n }\n\n // Test for A's keys different from B.\n var bHasOwnProperty = Object.prototype.hasOwnProperty.bind(objB);\n\n for (var i = 0; i < keysA.length; i++) {\n var key = keysA[i];\n if ( !bHasOwnProperty( key ) || jsonNotEqual( objA[ key ], objB[ key ] )) {\n return true;\n }\n }\n\n return false;\n}\n\nfunction arraysNotEqual( a, b ){\n if( a.length !== b.length ) return true;\n\n for( var i = 0; i < a.length; i++ ){\n if( jsonNotEqual( a[ i ], b[ i ] ) ) return true;\n }\n\n return false;\n}\n\n// private array helpers\nexports.contains = contains;\nfunction contains( arr, el ){\n for( var i = 0; i < arr.length; i++ ){\n if( arr[ i ] === el ) return true;\n }\n\n return false;\n};\n\nexports.without = without;\nfunction without( arr, el ){\n var res = [];\n\n for( var i = 0; i < arr.length; i++ ){\n var current = arr[ i ];\n current === el || res.push( current );\n }\n\n return res;\n};\n\nexports.clone = clone;\nfunction clone( objOrArray ){\n return objOrArray instanceof Array ? objOrArray.slice() : Object.assign( {}, objOrArray );\n};\n\n\n\n/*****************\n ** WEBPACK FOOTER\n ** ./src/tools.js\n ** module id = 7\n ** module chunks = 0\n **/","var Nested = require( 'nestedtypes' ),\n tools = require( './tools' ),\n contains = tools.contains,\n without = tools.without,\n clone = tools.clone;\n\nvar Link = exports.Link = Object.extend( {\n constructor : function( val ){\n this.val = val;\n },\n\n val : function( x ){ return x; },\n\n properties : {\n value : {\n get : function(){ return this.val(); },\n set : function( x ){ this.val( x ); }\n }\n },\n\n requestChange : function( x ){ this.val( x ); },\n get : function(){ return this.val(); },\n set : function( x ){ this.val( x ); },\n toggle : function(){ this.val( !this.val() ); },\n\n contains : function( element ){\n var link = this;\n\n return new Link( function( x ){\n var arr = link.val(),\n prev = contains( arr, element );\n\n if( arguments.length ){\n var next = Boolean( x );\n if( prev !== next ){\n link.val( x ? arr.concat( element ) : without( arr, element ) );\n return next;\n }\n }\n\n return prev;\n } );\n },\n\n // create boolean link for value equality\n equals : function( asTrue ){\n var link = this;\n\n return new Link( function( x ){\n if( arguments.length ) link.val( x ? asTrue : null );\n\n return link.val() === asTrue;\n } );\n },\n\n // link to enclosed object or array member\n at : function( key ){\n var link = this;\n\n return new Link( function( x ){\n var arr = link.val(),\n prev = arr[ key ];\n\n if( arguments.length ){\n if( prev !== x ){\n arr = clone( arr );\n arr[ key ] = x;\n link.val( arr );\n return x;\n }\n }\n\n return prev;\n } );\n },\n\n // iterates through enclosed object or array, generating set of links\n map : function( fun ){\n var arr = this.val();\n return arr ? ( arr instanceof Array ? mapArray( this, arr, fun ) : mapObject( this, arr, fun ) ) : [];\n },\n\n // create function which updates the link\n update : function( transform ){\n var val = this.val;\n return function(){\n val( transform( val() ) )\n }\n }\n});\n\nfunction mapObject( link, object, fun ){\n var res = [];\n\n for( var i in object ){\n if( object.hasOwnProperty( i ) ){\n var y = fun( link.at( i ), i );\n y === void 0 || ( res.push( y ) );\n }\n }\n\n return res;\n}\n\nfunction mapArray( link, arr, fun ){\n var res = [];\n\n for( var i = 0; i < arr.length; i++ ){\n var y = fun( link.at( i ), i );\n y === void 0 || ( res.push( y ) );\n }\n\n return res;\n}\n\nexports.link = function( reference ){\n var getMaster = Nested.parseReference( reference );\n\n function setLink( value ){\n var link = getMaster.call( this );\n link && link.val( value );\n }\n\n function getLink(){\n var link = getMaster.call( this );\n return link && link.val();\n }\n\n var LinkAttribute = Nested.attribute.Type.extend( {\n createPropertySpec : function(){\n return {\n // call to optimized set function for single argument. Doesn't work for backbone types.\n set : setLink,\n\n // attach get hook to the getter function, if present\n get : getLink\n }\n },\n\n set : setLink\n } );\n\n var options = Nested.attribute( { toJSON : false } );\n options.Attribute = LinkAttribute;\n return options;\n};\n\n\n/*****************\n ** WEBPACK FOOTER\n ** ./src/value-link.js\n ** module id = 8\n ** module chunks = 0\n **/"],"sourceRoot":""}
\ No newline at end of file
diff --git a/package.json b/package.json
index b7ea2c6..9c421a1 100644
--- a/package.json
+++ b/package.json
@@ -1,53 +1,37 @@
{
- "name": "react-backbone.glue",
- "main": "react-backbone.js",
- "description": "Essential tool for migration to React in large Backbone application",
+ "name": "nestedreact",
+ "main": "nestedreact.js",
+ "description": "Advanced models, state management, and data binding solution for React",
"homepage": "https://github.com/Volicon/react-backbone.glue",
"keywords": [
"backbone",
+ "nestedtypes",
"react"
],
-
"repository": {
"type": "git",
"url": "https://github.com/Volicon/react-backbone.glue.git"
},
-
"author": "Vlad Balin ",
"contributors": "",
-
"dependencies": {
- "backbone": ">=1.1.2",
- "react" : ">=0.13.0"
+ "react": "^0.14.0",
+ "react-dom": "^0.14.0",
+ "nestedtypes": "^1.1.5"
},
-
"devDependencies": {
- "uglify-js": "*"
+ "jquery": "^2.1.4",
+ "underscore": "^1.8.3",
+ "webpack": "^1.12.2"
},
-
"files": [
- "react-backbone.js",
- "react-nested.js",
- "react-backbone.min.js",
- "react-nested.min.js"
+ "nestedreact.js",
+ "nestedreact.js.map"
],
-
"license": "MIT",
- "version": "0.1.1",
-
+ "version": "0.3.0",
"scripts": {
- "make:backbone:win" : "type .\\umd\\copyright.js .\\umd\\backbone-head.js .\\src\\glue.js .\\umd\\tail.js > .\\react-backbone.js",
- "make:nested:win" : "type .\\umd\\copyright.js .\\umd\\nested-head.js .\\src\\glue.js .\\umd\\tail.js > .\\react-nested.js",
- "make:win": "npm run make:backbone:win & npm run make:nested:win",
-
- "minify:win": ".\\node_modules\\.bin\\uglifyjs react-nested.js --comments --compress --mangle --screw-ie8 > react-nested.min.js & .\\node_modules\\.bin\\uglifyjs .\\react-backbone.js --comments --compress --mangle --screw-ie8 > .\\react-backbone.min.js",
- "build:win": "npm run make:win && npm run minify:win",
-
- "make:backbone" : "cat ./umd/copyright.js ./umd/backbone-head.js ./src/glue.js ./umd/tail.js > ./react-backbone.js",
- "make:nested" : "cat ./umd/copyright.js ./umd/nested-head.js ./src/glue.js ./umd/tail.js > ./react-nested.js",
- "make": "npm run make:backbone & npm run make:nested",
-
- "minify": "./node_modules/.bin/uglifyjs react-nested.js --comments --compress --mangle --screw-ie8 > react-nested.min.js & ./node_modules/.bin/uglifyjs ./react-backbone.js --comments --compress --mangle --screw-ie8 > ./react-backbone.min.js",
- "build": "npm run make && npm run minify"
+ "build": "./node_modules/.bin/webpack",
+ "watch": "./node_modules/.bin/webpack --watch"
}
-}
\ No newline at end of file
+}
diff --git a/react-backbone.js b/react-backbone.js
deleted file mode 100644
index ef54c84..0000000
--- a/react-backbone.js
+++ /dev/null
@@ -1,251 +0,0 @@
-/**
- * React-Backbone.Glue 0.1.1
- * (c) 2015 Vlad Balin & Volicon
- * Released under MIT @license
- */
-(function( root, factory ){
- if( typeof exports === 'object' ){
- module.exports = factory( require( 'backbone' ), require( 'react' ) );
- }
- else if( typeof define === 'function' && define.amd ){
- define( [ 'backbone', 'react' ], factory );
- }
- else{
- root.React = factory( root.Backbone, root.React );
- }
-}( this, function reactBackboneGlue( Backbone, React ){
- // Object.assign polyfill
-
- Object.assign || ( Object.assign = function( target, firstSource ){
- if( target == null ){
- throw new TypeError( 'Cannot convert first argument to object' );
- }
-
- var to = Object( target );
- for( var i = 1; i < arguments.length; i++ ){
- var nextSource = arguments[ i ];
- if( nextSource == null ){
- continue;
- }
-
- var keysArray = Object.keys( Object( nextSource ) );
- for( var nextIndex = 0, len = keysArray.length; nextIndex < len; nextIndex++ ){
- var nextKey = keysArray[ nextIndex ];
- var desc = Object.getOwnPropertyDescriptor( nextSource, nextKey );
- if( desc !== void 0 && desc.enumerable ){
- to[ nextKey ] = nextSource[ nextKey ];
- }
- }
- }
- return to;
- });
-
- // Wrapper for forceUpdate to be used in backbone events handlers
- function forceUpdate(){
- this.forceUpdate();
- }
-
- var ListenToProps = {
- componentDidMount : function(){
- var props = this.props,
- updateOn = this.listenToProps;
-
- for( var prop in updateOn ){
- var emitter = props[ prop ];
- emitter && this.listenTo( emitter, updateOn[ prop ], forceUpdate );
- }
- },
-
- componentWillUnmount : function(){
- var props = this.props,
- updateOn = this.listenToProps;
-
- for( var prop in updateOn ){
- var emitter = props[ prop ];
- emitter && this.stopListening( emitter );
- }
- }
- };
-
- var ModelState = {
- listenToState : 'change',
-
- getInitialState : function(){
- return new this.Model();
- },
-
- componentDidMount : function(){
- var events = this.listenToState;
- events && this.listenTo( this.state, events, forceUpdate );
- },
-
- componentWillUnmount : function(){
- this.stopListening( this.state );
- this.state = null;
- }
- };
-
- function getModelAttributes( spec ){
- var attributes = null;
-
- for( var i = spec.mixins.length - 1; i >= 0; i-- ){
- var mixin = spec.mixins[ i ];
- if( mixin.attributes ){
- attributes || ( attributes = {} );
- Object.assign( attributes, mixin.attributes );
- }
- }
-
- if( spec.attributes ){
- if( attributes ){
- Object.assign( attributes, spec.attributes );
- }
- else{
- attributes = spec.attributes;
- }
- }
-
- return attributes;
- }
-
- var createClass = React.createClass;
-
- React.createClass = function( spec ){
- spec.mixins || ( spec.mixins = [] );
-
- var attributes = getModelAttributes( spec );
- if( attributes ){
- var BaseModel = spec.Model || Backbone.Model;
- spec.Model = BaseModel.extend({ defaults : attributes });
- }
-
- if( spec.Model ){
- spec.mixins.unshift( ModelState );
- }
-
- if( spec.listenToProps ){
- spec.mixins.unshift( ListenToProps );
- }
-
- if( spec.Model || spec.listenToProps ){
- spec.mixins.push( Backbone.Events );
- }
-
- var component = createClass.call( React, spec );
- component.createView = createView;
- return component;
- };
-
- var slice = Array.prototype.slice;
-
- function createView(){
- var args = slice.call( arguments );
- args.unshift( this );
- return new ReactView( args );
- }
-
- /**
- * React Backbone View Wrapper. Same as React.createElement
- * but returns Backbone.View
- *
- * Usage:
- * var View = React.createView( MyReactClass, {
- * prop1 : value1,
- * prop2 : value2,
- * ...
- * });
- */
- React.createView = function(){
- return new ReactView( arguments );
- };
-
- var ReactView = Backbone.View.extend({
- initialize : function( args ){
- // memorise arguments to pass to React
- this._args = args;
- },
-
- // cached react element...
- element : null,
-
- setElement : function(){
- // new element instance needs to be created on next render...
- if( this.element ){
- this.element = null;
- this.unmountComponent();
- }
-
- return Backbone.View.prototype.setElement.apply( this, arguments );
- },
-
- // cached instance of react component...
- component : null,
-
- unmountComponent : function(){
- if( this.component ){
- if( this.component.trigger ){
- this.stopListening( this.component );
- }
-
- React.unmountComponentAtNode( this.el );
- this.component = null;
- }
- },
-
- render : function(){
- if( !this.element ){
- this.element = React.createElement.apply( React, this._args );
- }
-
- var firstCall = !this.component;
- this.component = React.render( this.element, this.el );
-
- if( firstCall ){
- this.component.trigger && this.listenTo( this.component, 'all', function(){
- this.trigger.apply( this, arguments );
- });
- }
- },
-
- dispose : function(){
- this.unmountComponent();
- Backbone.View.prototype.dispose.apply( this, arguments );
- }
- });
-
- React.subview = React.createClass({
- displayName : 'BackboneView',
-
- propTypes : {
- View : React.PropTypes.func.isRequired,
- options : React.PropTypes.object
- },
-
- render : function(){
- return React.DOM.div({
- ref : 'subview',
- className : this.props.className
- });
- },
-
- componentDidMount : function(){
- var el = this.refs.subview.getDOMNode(),
- p = this.props;
-
- var view = this.view = p.options ? new p.View( p.options ) : new p.View();
- view.setElement( el );
- view.render();
- },
-
- componentDidUpdate : function(){
- this.view.render();
- },
-
- componentWillUnmount : function(){
- var view = this.view;
- if( view.dispose ) view.dispose();
- }
- });
-
- return React;
-} ));
\ No newline at end of file
diff --git a/react-backbone.min.js b/react-backbone.min.js
deleted file mode 100644
index 3174b76..0000000
--- a/react-backbone.min.js
+++ /dev/null
@@ -1,6 +0,0 @@
-/**
- * React-Backbone.Glue 0.1.1
- * (c) 2015 Vlad Balin & Volicon
- * Released under MIT @license
- */
-!function(t,e){"object"==typeof exports?module.exports=e(require("backbone"),require("react")):"function"==typeof define&&define.amd?define(["backbone","react"],e):t.React=e(t.Backbone,t.React)}(this,function(t,e){function n(){this.forceUpdate()}function i(t){for(var e=null,n=t.mixins.length-1;n>=0;n--){var i=t.mixins[n];i.attributes&&(e||(e={}),Object.assign(e,i.attributes))}return t.attributes&&(e?Object.assign(e,t.attributes):e=t.attributes),e}function o(){var t=u.call(arguments);return t.unshift(this),new c(t)}Object.assign||(Object.assign=function(t,e){if(null==t)throw new TypeError("Cannot convert first argument to object");for(var n=Object(t),i=1;ir;r++){var u=s[r],c=Object.getOwnPropertyDescriptor(o,u);void 0!==c&&c.enumerable&&(n[u]=o[u])}}return n});var s={componentDidMount:function(){var t=this.props,e=this.listenToProps;for(var i in e){var o=t[i];o&&this.listenTo(o,e[i],n)}},componentWillUnmount:function(){var t=this.props,e=this.listenToProps;for(var n in e){var i=t[n];i&&this.stopListening(i)}}},r={listenToState:"change",getInitialState:function(){return new this.Model},componentDidMount:function(){var t=this.listenToState;t&&this.listenTo(this.state,t,n)},componentWillUnmount:function(){this.stopListening(this.state),this.state=null}},a=e.createClass;e.createClass=function(n){n.mixins||(n.mixins=[]);var u=i(n);if(u){var c=n.Model||t.Model;n.Model=c.extend({defaults:u})}n.Model&&n.mixins.unshift(r),n.listenToProps&&n.mixins.unshift(s),(n.Model||n.listenToProps)&&n.mixins.push(t.Events);var l=a.call(e,n);return l.createView=o,l};var u=Array.prototype.slice;e.createView=function(){return new c(arguments)};var c=t.View.extend({initialize:function(t){this._args=t},element:null,setElement:function(){return this.element&&(this.element=null,this.unmountComponent()),t.View.prototype.setElement.apply(this,arguments)},component:null,unmountComponent:function(){this.component&&(this.component.trigger&&this.stopListening(this.component),e.unmountComponentAtNode(this.el),this.component=null)},render:function(){this.element||(this.element=e.createElement.apply(e,this._args));var t=!this.component;this.component=e.render(this.element,this.el),t&&this.component.trigger&&this.listenTo(this.component,"all",function(){this.trigger.apply(this,arguments)})},dispose:function(){this.unmountComponent(),t.View.prototype.dispose.apply(this,arguments)}});return e.subview=e.createClass({displayName:"BackboneView",propTypes:{View:e.PropTypes.func.isRequired,options:e.PropTypes.object},render:function(){return e.DOM.div({ref:"subview",className:this.props.className})},componentDidMount:function(){var t=this.refs.subview.getDOMNode(),e=this.props,n=this.view=e.options?new e.View(e.options):new e.View;n.setElement(t),n.render()},componentDidUpdate:function(){this.view.render()},componentWillUnmount:function(){var t=this.view;t.dispose&&t.dispose()}}),e});
diff --git a/react-nested.js b/react-nested.js
deleted file mode 100644
index 393dc3f..0000000
--- a/react-nested.js
+++ /dev/null
@@ -1,251 +0,0 @@
-/**
- * React-Backbone.Glue 0.1.1
- * (c) 2015 Vlad Balin & Volicon
- * Released under MIT @license
- */
-(function( root, factory ){
- if( typeof exports === 'object' ){
- module.exports = factory( require( 'nestedtypes' ), require( 'react' ) );
- }
- else if( typeof define === 'function' && define.amd ){
- define( [ 'nestedtypes', 'react' ], factory );
- }
- else{
- root.React = factory( root.Nested, root.React );
- }
-}( this, function reactBackboneGlue( Backbone, React ){
- // Object.assign polyfill
-
- Object.assign || ( Object.assign = function( target, firstSource ){
- if( target == null ){
- throw new TypeError( 'Cannot convert first argument to object' );
- }
-
- var to = Object( target );
- for( var i = 1; i < arguments.length; i++ ){
- var nextSource = arguments[ i ];
- if( nextSource == null ){
- continue;
- }
-
- var keysArray = Object.keys( Object( nextSource ) );
- for( var nextIndex = 0, len = keysArray.length; nextIndex < len; nextIndex++ ){
- var nextKey = keysArray[ nextIndex ];
- var desc = Object.getOwnPropertyDescriptor( nextSource, nextKey );
- if( desc !== void 0 && desc.enumerable ){
- to[ nextKey ] = nextSource[ nextKey ];
- }
- }
- }
- return to;
- });
-
- // Wrapper for forceUpdate to be used in backbone events handlers
- function forceUpdate(){
- this.forceUpdate();
- }
-
- var ListenToProps = {
- componentDidMount : function(){
- var props = this.props,
- updateOn = this.listenToProps;
-
- for( var prop in updateOn ){
- var emitter = props[ prop ];
- emitter && this.listenTo( emitter, updateOn[ prop ], forceUpdate );
- }
- },
-
- componentWillUnmount : function(){
- var props = this.props,
- updateOn = this.listenToProps;
-
- for( var prop in updateOn ){
- var emitter = props[ prop ];
- emitter && this.stopListening( emitter );
- }
- }
- };
-
- var ModelState = {
- listenToState : 'change',
-
- getInitialState : function(){
- return new this.Model();
- },
-
- componentDidMount : function(){
- var events = this.listenToState;
- events && this.listenTo( this.state, events, forceUpdate );
- },
-
- componentWillUnmount : function(){
- this.stopListening( this.state );
- this.state = null;
- }
- };
-
- function getModelAttributes( spec ){
- var attributes = null;
-
- for( var i = spec.mixins.length - 1; i >= 0; i-- ){
- var mixin = spec.mixins[ i ];
- if( mixin.attributes ){
- attributes || ( attributes = {} );
- Object.assign( attributes, mixin.attributes );
- }
- }
-
- if( spec.attributes ){
- if( attributes ){
- Object.assign( attributes, spec.attributes );
- }
- else{
- attributes = spec.attributes;
- }
- }
-
- return attributes;
- }
-
- var createClass = React.createClass;
-
- React.createClass = function( spec ){
- spec.mixins || ( spec.mixins = [] );
-
- var attributes = getModelAttributes( spec );
- if( attributes ){
- var BaseModel = spec.Model || Backbone.Model;
- spec.Model = BaseModel.extend({ defaults : attributes });
- }
-
- if( spec.Model ){
- spec.mixins.unshift( ModelState );
- }
-
- if( spec.listenToProps ){
- spec.mixins.unshift( ListenToProps );
- }
-
- if( spec.Model || spec.listenToProps ){
- spec.mixins.push( Backbone.Events );
- }
-
- var component = createClass.call( React, spec );
- component.createView = createView;
- return component;
- };
-
- var slice = Array.prototype.slice;
-
- function createView(){
- var args = slice.call( arguments );
- args.unshift( this );
- return new ReactView( args );
- }
-
- /**
- * React Backbone View Wrapper. Same as React.createElement
- * but returns Backbone.View
- *
- * Usage:
- * var View = React.createView( MyReactClass, {
- * prop1 : value1,
- * prop2 : value2,
- * ...
- * });
- */
- React.createView = function(){
- return new ReactView( arguments );
- };
-
- var ReactView = Backbone.View.extend({
- initialize : function( args ){
- // memorise arguments to pass to React
- this._args = args;
- },
-
- // cached react element...
- element : null,
-
- setElement : function(){
- // new element instance needs to be created on next render...
- if( this.element ){
- this.element = null;
- this.unmountComponent();
- }
-
- return Backbone.View.prototype.setElement.apply( this, arguments );
- },
-
- // cached instance of react component...
- component : null,
-
- unmountComponent : function(){
- if( this.component ){
- if( this.component.trigger ){
- this.stopListening( this.component );
- }
-
- React.unmountComponentAtNode( this.el );
- this.component = null;
- }
- },
-
- render : function(){
- if( !this.element ){
- this.element = React.createElement.apply( React, this._args );
- }
-
- var firstCall = !this.component;
- this.component = React.render( this.element, this.el );
-
- if( firstCall ){
- this.component.trigger && this.listenTo( this.component, 'all', function(){
- this.trigger.apply( this, arguments );
- });
- }
- },
-
- dispose : function(){
- this.unmountComponent();
- Backbone.View.prototype.dispose.apply( this, arguments );
- }
- });
-
- React.subview = React.createClass({
- displayName : 'BackboneView',
-
- propTypes : {
- View : React.PropTypes.func.isRequired,
- options : React.PropTypes.object
- },
-
- render : function(){
- return React.DOM.div({
- ref : 'subview',
- className : this.props.className
- });
- },
-
- componentDidMount : function(){
- var el = this.refs.subview.getDOMNode(),
- p = this.props;
-
- var view = this.view = p.options ? new p.View( p.options ) : new p.View();
- view.setElement( el );
- view.render();
- },
-
- componentDidUpdate : function(){
- this.view.render();
- },
-
- componentWillUnmount : function(){
- var view = this.view;
- if( view.dispose ) view.dispose();
- }
- });
-
- return React;
-} ));
\ No newline at end of file
diff --git a/react-nested.min.js b/react-nested.min.js
deleted file mode 100644
index 1dabf74..0000000
--- a/react-nested.min.js
+++ /dev/null
@@ -1,6 +0,0 @@
-/**
- * React-Backbone.Glue 0.1.1
- * (c) 2015 Vlad Balin & Volicon
- * Released under MIT @license
- */
-!function(t,e){"object"==typeof exports?module.exports=e(require("nestedtypes"),require("react")):"function"==typeof define&&define.amd?define(["nestedtypes","react"],e):t.React=e(t.Nested,t.React)}(this,function(t,e){function n(){this.forceUpdate()}function i(t){for(var e=null,n=t.mixins.length-1;n>=0;n--){var i=t.mixins[n];i.attributes&&(e||(e={}),Object.assign(e,i.attributes))}return t.attributes&&(e?Object.assign(e,t.attributes):e=t.attributes),e}function s(){var t=u.call(arguments);return t.unshift(this),new p(t)}Object.assign||(Object.assign=function(t,e){if(null==t)throw new TypeError("Cannot convert first argument to object");for(var n=Object(t),i=1;ir;r++){var u=o[r],p=Object.getOwnPropertyDescriptor(s,u);void 0!==p&&p.enumerable&&(n[u]=s[u])}}return n});var o={componentDidMount:function(){var t=this.props,e=this.listenToProps;for(var i in e){var s=t[i];s&&this.listenTo(s,e[i],n)}},componentWillUnmount:function(){var t=this.props,e=this.listenToProps;for(var n in e){var i=t[n];i&&this.stopListening(i)}}},r={listenToState:"change",getInitialState:function(){return new this.Model},componentDidMount:function(){var t=this.listenToState;t&&this.listenTo(this.state,t,n)},componentWillUnmount:function(){this.stopListening(this.state),this.state=null}},a=e.createClass;e.createClass=function(n){n.mixins||(n.mixins=[]);var u=i(n);if(u){var p=n.Model||t.Model;n.Model=p.extend({defaults:u})}n.Model&&n.mixins.unshift(r),n.listenToProps&&n.mixins.unshift(o),(n.Model||n.listenToProps)&&n.mixins.push(t.Events);var l=a.call(e,n);return l.createView=s,l};var u=Array.prototype.slice;e.createView=function(){return new p(arguments)};var p=t.View.extend({initialize:function(t){this._args=t},element:null,setElement:function(){return this.element&&(this.element=null,this.unmountComponent()),t.View.prototype.setElement.apply(this,arguments)},component:null,unmountComponent:function(){this.component&&(this.component.trigger&&this.stopListening(this.component),e.unmountComponentAtNode(this.el),this.component=null)},render:function(){this.element||(this.element=e.createElement.apply(e,this._args));var t=!this.component;this.component=e.render(this.element,this.el),t&&this.component.trigger&&this.listenTo(this.component,"all",function(){this.trigger.apply(this,arguments)})},dispose:function(){this.unmountComponent(),t.View.prototype.dispose.apply(this,arguments)}});return e.subview=e.createClass({displayName:"BackboneView",propTypes:{View:e.PropTypes.func.isRequired,options:e.PropTypes.object},render:function(){return e.DOM.div({ref:"subview",className:this.props.className})},componentDidMount:function(){var t=this.refs.subview.getDOMNode(),e=this.props,n=this.view=e.options?new e.View(e.options):new e.View;n.setElement(t),n.render()},componentDidUpdate:function(){this.view.render()},componentWillUnmount:function(){var t=this.view;t.dispose&&t.dispose()}}),e});
diff --git a/src/component-view.js b/src/component-view.js
new file mode 100644
index 0000000..a03af32
--- /dev/null
+++ b/src/component-view.js
@@ -0,0 +1,75 @@
+var React = require( 'react' ),
+ ReactDOM = require( 'react-dom' );
+
+module.exports.use = function( View ){
+ var dispose = View.prototype.dispose || function(){},
+ setElement = View.prototype.setElement;
+
+ var ComponentView = View.extend( {
+ reactClass : null,
+ props : {},
+ element : null,
+
+ initialize : function( props ){
+ // memorise arguments to pass to React
+ this.options = props || {};
+ this.element = React.createElement( this.reactClass, this.options );
+ },
+
+ setElement : function(){
+ this.unmountComponent();
+ return setElement.apply( this, arguments );
+ },
+
+ // cached instance of react component...
+ component : null,
+ prevState : null,
+
+ render : function(){
+ var component = ReactDOM.render( this.element, this.el );
+ this.component || this.mountComponent( component );
+ },
+
+ mountComponent : function( component ){
+ this.component = component;
+
+ if( this.prevState ){
+ component.model.set( this.prevState );
+ this.prevState = null;
+ }
+
+ component.trigger && this.listenTo( component, 'all', function(){
+ this.trigger.apply( this, arguments );
+ });
+ },
+
+ unmountComponent : function(){
+ var component = this.component;
+
+ if( component ){
+ this.prevState = component.model && component.model.attributes;
+
+ if( component.trigger ){
+ this.stopListening( component );
+ }
+
+ ReactDOM.unmountComponentAtNode( this.el );
+ this.component = null;
+ }
+ },
+
+ dispose : function(){
+ this.unmountComponent();
+ return dispose.apply( this, arguments );
+ }
+ } );
+
+ Object.defineProperty( ComponentView.prototype, 'model', {
+ get : function(){
+ this.component || this.render();
+ return this.component && this.component.model;
+ }
+ } );
+
+ return ComponentView;
+};
diff --git a/src/createClass.js b/src/createClass.js
new file mode 100644
index 0000000..5a37fad
--- /dev/null
+++ b/src/createClass.js
@@ -0,0 +1,125 @@
+var React = require( 'react' ),
+ Nested = require( 'nestedtypes' );
+
+function forceUpdate(){ this.forceUpdate(); }
+
+var Events = Object.assign( {
+ componentWillUnmount : function(){
+ this.stopListening();
+ }
+}, Nested.Events );
+
+var ListenToProps = {
+ componentDidMount : function(){
+ var props = this.props,
+ updateOn = this.listenToProps;
+
+ for( var prop in updateOn ){
+ var emitter = props[ prop ];
+ emitter && this.listenTo( emitter, updateOn[ prop ], forceUpdate );
+ }
+ }
+};
+
+var ListenToPropsArray = {
+ componentDidMount : function(){
+ var props = this.props,
+ updateOn = this.listenToProps;
+
+ for( var i = 0; i < updateOn.length; i++ ){
+ var emitter = props[ updateOn[ i ] ];
+ emitter && this.listenTo( emitter, emitter.triggerWhenChanged, forceUpdate );
+ }
+ }
+};
+
+var ModelState = {
+ listenToState : 'change',
+ model : null,
+
+ getInitialState : function(){
+ this.model = new this.Model();
+ // enable owner references in the model to access component props
+ this.model._owner = this;
+
+ return this.model;
+ },
+
+ // reference global store to fix model's store locator
+ getStore : function(){
+ this.model._defaultStore;
+ },
+
+ componentDidMount : function(){
+ var events = this.listenToState;
+ events && this.listenTo( this.model, events, forceUpdate );
+ },
+
+ componentWillUnmount : function(){
+ this.model._owner = null;
+ this.model.stopListening();
+ }
+};
+
+function createClass( spec ){
+ var mixins = spec.mixins || ( spec.mixins = [] );
+
+ var attributes = getModelAttributes( spec );
+ if( attributes ){
+ var BaseModel = spec.Model || Nested.Model;
+ spec.Model = BaseModel.extend( { defaults : attributes } );
+ }
+
+ if( spec.Model ) mixins.push( ModelState );
+
+ var listenToProps = spec.listenToProps;
+ if( listenToProps ){
+ if( typeof listenToProps === 'string' ){
+ spec.listenToProps = listenToProps.split( ' ' );
+ mixins.unshift( ListenToPropsArray );
+ }
+ else{
+ mixins.unshift( ListenToProps );
+ }
+ }
+
+ mixins.push( Events );
+
+ var component = React.createClass( spec );
+
+ // attach lazily evaluated backbone View class
+ var NestedReact = this;
+
+ Object.defineProperty( component, 'View', {
+ get : function(){
+ return this._View || ( this._View = NestedReact._BaseView.extend( { reactClass : component } ) );
+ }
+ });
+
+ return component;
+}
+
+function getModelAttributes( spec ){
+ var attributes = null;
+
+ for( var i = spec.mixins.length - 1; i >= 0; i-- ){
+ var mixin = spec.mixins[ i ];
+ if( mixin.attributes ){
+ attributes || ( attributes = {} );
+ Object.assign( attributes, mixin.attributes );
+ }
+ }
+
+ if( spec.attributes ){
+ if( attributes ){
+ Object.assign( attributes, spec.attributes );
+ }
+ else{
+ attributes = spec.attributes;
+ }
+ }
+
+ return attributes;
+}
+
+module.exports = createClass;
diff --git a/src/glue.js b/src/glue.js
deleted file mode 100644
index b0fb62c..0000000
--- a/src/glue.js
+++ /dev/null
@@ -1,236 +0,0 @@
-function reactBackboneGlue( Backbone, React ){
- // Object.assign polyfill
-
- Object.assign || ( Object.assign = function( target, firstSource ){
- if( target == null ){
- throw new TypeError( 'Cannot convert first argument to object' );
- }
-
- var to = Object( target );
- for( var i = 1; i < arguments.length; i++ ){
- var nextSource = arguments[ i ];
- if( nextSource == null ){
- continue;
- }
-
- var keysArray = Object.keys( Object( nextSource ) );
- for( var nextIndex = 0, len = keysArray.length; nextIndex < len; nextIndex++ ){
- var nextKey = keysArray[ nextIndex ];
- var desc = Object.getOwnPropertyDescriptor( nextSource, nextKey );
- if( desc !== void 0 && desc.enumerable ){
- to[ nextKey ] = nextSource[ nextKey ];
- }
- }
- }
- return to;
- });
-
- // Wrapper for forceUpdate to be used in backbone events handlers
- function forceUpdate(){
- this.forceUpdate();
- }
-
- var ListenToProps = {
- componentDidMount : function(){
- var props = this.props,
- updateOn = this.listenToProps;
-
- for( var prop in updateOn ){
- var emitter = props[ prop ];
- emitter && this.listenTo( emitter, updateOn[ prop ], forceUpdate );
- }
- },
-
- componentWillUnmount : function(){
- var props = this.props,
- updateOn = this.listenToProps;
-
- for( var prop in updateOn ){
- var emitter = props[ prop ];
- emitter && this.stopListening( emitter );
- }
- }
- };
-
- var ModelState = {
- listenToState : 'change',
-
- getInitialState : function(){
- return new this.Model();
- },
-
- componentDidMount : function(){
- var events = this.listenToState;
- events && this.listenTo( this.state, events, forceUpdate );
- },
-
- componentWillUnmount : function(){
- this.stopListening( this.state );
- this.state = null;
- }
- };
-
- function getModelAttributes( spec ){
- var attributes = null;
-
- for( var i = spec.mixins.length - 1; i >= 0; i-- ){
- var mixin = spec.mixins[ i ];
- if( mixin.attributes ){
- attributes || ( attributes = {} );
- Object.assign( attributes, mixin.attributes );
- }
- }
-
- if( spec.attributes ){
- if( attributes ){
- Object.assign( attributes, spec.attributes );
- }
- else{
- attributes = spec.attributes;
- }
- }
-
- return attributes;
- }
-
- var createClass = React.createClass;
-
- React.createClass = function( spec ){
- spec.mixins || ( spec.mixins = [] );
-
- var attributes = getModelAttributes( spec );
- if( attributes ){
- var BaseModel = spec.Model || Backbone.Model;
- spec.Model = BaseModel.extend({ defaults : attributes });
- }
-
- if( spec.Model ){
- spec.mixins.unshift( ModelState );
- }
-
- if( spec.listenToProps ){
- spec.mixins.unshift( ListenToProps );
- }
-
- if( spec.Model || spec.listenToProps ){
- spec.mixins.push( Backbone.Events );
- }
-
- var component = createClass.call( React, spec );
- component.createView = createView;
- return component;
- };
-
- var slice = Array.prototype.slice;
-
- function createView(){
- var args = slice.call( arguments );
- args.unshift( this );
- return new ReactView( args );
- }
-
- /**
- * React Backbone View Wrapper. Same as React.createElement
- * but returns Backbone.View
- *
- * Usage:
- * var View = React.createView( MyReactClass, {
- * prop1 : value1,
- * prop2 : value2,
- * ...
- * });
- */
- React.createView = function(){
- return new ReactView( arguments );
- };
-
- var ReactView = Backbone.View.extend({
- initialize : function( args ){
- // memorise arguments to pass to React
- this._args = args;
- },
-
- // cached react element...
- element : null,
-
- setElement : function(){
- // new element instance needs to be created on next render...
- if( this.element ){
- this.element = null;
- this.unmountComponent();
- }
-
- return Backbone.View.prototype.setElement.apply( this, arguments );
- },
-
- // cached instance of react component...
- component : null,
-
- unmountComponent : function(){
- if( this.component ){
- if( this.component.trigger ){
- this.stopListening( this.component );
- }
-
- React.unmountComponentAtNode( this.el );
- this.component = null;
- }
- },
-
- render : function(){
- if( !this.element ){
- this.element = React.createElement.apply( React, this._args );
- }
-
- var firstCall = !this.component;
- this.component = React.render( this.element, this.el );
-
- if( firstCall ){
- this.component.trigger && this.listenTo( this.component, 'all', function(){
- this.trigger.apply( this, arguments );
- });
- }
- },
-
- dispose : function(){
- this.unmountComponent();
- Backbone.View.prototype.dispose.apply( this, arguments );
- }
- });
-
- React.subview = React.createClass({
- displayName : 'BackboneView',
-
- propTypes : {
- View : React.PropTypes.func.isRequired,
- options : React.PropTypes.object
- },
-
- render : function(){
- return React.DOM.div({
- ref : 'subview',
- className : this.props.className
- });
- },
-
- componentDidMount : function(){
- var el = this.refs.subview.getDOMNode(),
- p = this.props;
-
- var view = this.view = p.options ? new p.View( p.options ) : new p.View();
- view.setElement( el );
- view.render();
- },
-
- componentDidUpdate : function(){
- this.view.render();
- },
-
- componentWillUnmount : function(){
- var view = this.view;
- if( view.dispose ) view.dispose();
- }
- });
-
- return React;
-}
\ No newline at end of file
diff --git a/src/main.js b/src/main.js
new file mode 100644
index 0000000..b5d1057
--- /dev/null
+++ b/src/main.js
@@ -0,0 +1,72 @@
+var React = require( 'react' ),
+ ReactDOM = require( 'react-dom' ),
+ Nested = require( 'nestedtypes' ),
+ $ = Nested.$;
+
+// extend React namespace
+var NestedReact = module.exports = Object.create( React );
+
+// listenToProps, listenToState, model, attributes, Model
+NestedReact.createClass = require( './createClass' );
+
+var ComponentView = require( './component-view' );
+
+// export hook to override base View class used...
+NestedReact.useView = function( View ){
+ NestedReact._BaseView = ComponentView.use( View );
+};
+
+NestedReact.useView( Nested.View );
+
+// React component for attaching views
+NestedReact.subview = require( './view-element' );
+
+NestedReact.tools = require( './tools' );
+
+// Extend react components to have backbone-style jquery accessors
+var Component = React.createClass( { render : function(){} } ),
+ BaseComponent = Object.getPrototypeOf( Component.prototype );
+
+Object.defineProperties( BaseComponent, {
+ el : { get : function(){ return ReactDOM.findDOMNode( this ); } },
+ $el : { get : function(){ return $( this.el ); } },
+ $ : { value : function( sel ){ return this.$el.find( sel ); } }
+} );
+
+var ValueLink = require( './value-link' );
+var Link = Nested.Link = ValueLink.Link;
+Nested.link = ValueLink.link;
+
+var ModelProto = Nested.Model.prototype;
+
+ModelProto.getLink = function( attr ){
+ var model = this;
+
+ return new Link( function( x ){
+ if( arguments.length ){
+ model[ attr ] = x;
+ }
+
+ return model[ attr ];
+ });
+};
+
+var CollectionProto = Nested.Collection.prototype;
+
+CollectionProto.getLink = function( model ){
+ var collection = this;
+
+ return new Link( function( x ){
+ var prev = Boolean( collection.get( model ) );
+
+ if( arguments.length ){
+ var next = Boolean( x );
+ if( prev !== next ){
+ collection.toggle( model, x );
+ return next;
+ }
+ }
+
+ return prev;
+ });
+};
diff --git a/src/tools.js b/src/tools.js
new file mode 100644
index 0000000..dbcfdc8
--- /dev/null
+++ b/src/tools.js
@@ -0,0 +1,83 @@
+// equality checking for deep JSON comparison of plain Array and Object
+var ArrayProto = Array.prototype,
+ ObjectProto = Object.prototype;
+
+exports.jsonNotEqual = jsonNotEqual;
+function jsonNotEqual( objA, objB) {
+ if (objA === objB) {
+ return false;
+ }
+
+ if (typeof objA !== 'object' || !objA ||
+ typeof objB !== 'object' || !objB ) {
+ return true;
+ }
+
+ var protoA = Object.getPrototypeOf( objA ),
+ protoB = Object.getPrototypeOf( objB );
+
+ if( protoA !== protoB ) return true;
+
+ if( protoA === ArrayProto ) return arraysNotEqual( objA, objB );
+ if( protoA === ObjectProto ) return objectsNotEqual( objA, objB );
+
+ return true;
+}
+
+function objectsNotEqual( objA, objB ){
+ var keysA = Object.keys(objA);
+ var keysB = Object.keys(objB);
+
+ if (keysA.length !== keysB.length) {
+ return true;
+ }
+
+ // Test for A's keys different from B.
+ var bHasOwnProperty = Object.prototype.hasOwnProperty.bind(objB);
+
+ for (var i = 0; i < keysA.length; i++) {
+ var key = keysA[i];
+ if ( !bHasOwnProperty( key ) || jsonNotEqual( objA[ key ], objB[ key ] )) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+function arraysNotEqual( a, b ){
+ if( a.length !== b.length ) return true;
+
+ for( var i = 0; i < a.length; i++ ){
+ if( jsonNotEqual( a[ i ], b[ i ] ) ) return true;
+ }
+
+ return false;
+}
+
+// private array helpers
+exports.contains = contains;
+function contains( arr, el ){
+ for( var i = 0; i < arr.length; i++ ){
+ if( arr[ i ] === el ) return true;
+ }
+
+ return false;
+};
+
+exports.without = without;
+function without( arr, el ){
+ var res = [];
+
+ for( var i = 0; i < arr.length; i++ ){
+ var current = arr[ i ];
+ current === el || res.push( current );
+ }
+
+ return res;
+};
+
+exports.clone = clone;
+function clone( objOrArray ){
+ return objOrArray instanceof Array ? objOrArray.slice() : Object.assign( {}, objOrArray );
+};
diff --git a/src/value-link.js b/src/value-link.js
new file mode 100644
index 0000000..5296385
--- /dev/null
+++ b/src/value-link.js
@@ -0,0 +1,146 @@
+var Nested = require( 'nestedtypes' ),
+ tools = require( './tools' ),
+ contains = tools.contains,
+ without = tools.without,
+ clone = tools.clone;
+
+var Link = exports.Link = Object.extend( {
+ constructor : function( val ){
+ this.val = val;
+ },
+
+ val : function( x ){ return x; },
+
+ properties : {
+ value : {
+ get : function(){ return this.val(); },
+ set : function( x ){ this.val( x ); }
+ }
+ },
+
+ requestChange : function( x ){ this.val( x ); },
+ get : function(){ return this.val(); },
+ set : function( x ){ this.val( x ); },
+ toggle : function(){ this.val( !this.val() ); },
+
+ contains : function( element ){
+ var link = this;
+
+ return new Link( function( x ){
+ var arr = link.val(),
+ prev = contains( arr, element );
+
+ if( arguments.length ){
+ var next = Boolean( x );
+ if( prev !== next ){
+ link.val( x ? arr.concat( element ) : without( arr, element ) );
+ return next;
+ }
+ }
+
+ return prev;
+ } );
+ },
+
+ // create boolean link for value equality
+ equals : function( asTrue ){
+ var link = this;
+
+ return new Link( function( x ){
+ if( arguments.length ) link.val( x ? asTrue : null );
+
+ return link.val() === asTrue;
+ } );
+ },
+
+ // link to enclosed object or array member
+ at : function( key ){
+ var link = this;
+
+ return new Link( function( x ){
+ var arr = link.val(),
+ prev = arr[ key ];
+
+ if( arguments.length ){
+ if( prev !== x ){
+ arr = clone( arr );
+ arr[ key ] = x;
+ link.val( arr );
+ return x;
+ }
+ }
+
+ return prev;
+ } );
+ },
+
+ // iterates through enclosed object or array, generating set of links
+ map : function( fun ){
+ var arr = this.val();
+ return arr ? ( arr instanceof Array ? mapArray( this, arr, fun ) : mapObject( this, arr, fun ) ) : [];
+ },
+
+ // create function which updates the link
+ update : function( transform ){
+ var val = this.val;
+ return function(){
+ val( transform( val() ) )
+ }
+ }
+});
+
+function mapObject( link, object, fun ){
+ var res = [];
+
+ for( var i in object ){
+ if( object.hasOwnProperty( i ) ){
+ var y = fun( link.at( i ), i );
+ y === void 0 || ( res.push( y ) );
+ }
+ }
+
+ return res;
+}
+
+function mapArray( link, arr, fun ){
+ var res = [];
+
+ for( var i = 0; i < arr.length; i++ ){
+ var y = fun( link.at( i ), i );
+ y === void 0 || ( res.push( y ) );
+ }
+
+ return res;
+}
+
+exports.link = function( reference ){
+ var getMaster = Nested.parseReference( reference );
+
+ function setLink( value ){
+ var link = getMaster.call( this );
+ link && link.val( value );
+ }
+
+ function getLink(){
+ var link = getMaster.call( this );
+ return link && link.val();
+ }
+
+ var LinkAttribute = Nested.attribute.Type.extend( {
+ createPropertySpec : function(){
+ return {
+ // call to optimized set function for single argument. Doesn't work for backbone types.
+ set : setLink,
+
+ // attach get hook to the getter function, if present
+ get : getLink
+ }
+ },
+
+ set : setLink
+ } );
+
+ var options = Nested.attribute( { toJSON : false } );
+ options.Attribute = LinkAttribute;
+ return options;
+};
\ No newline at end of file
diff --git a/src/view-element.js b/src/view-element.js
new file mode 100644
index 0000000..b079bc8
--- /dev/null
+++ b/src/view-element.js
@@ -0,0 +1,54 @@
+var React = require( 'react' ),
+ jsonNotEqual = require( './tools' ).jsonNotEqual;
+
+module.exports = React.createClass({
+ displayName : 'BackboneView',
+
+ propTypes : {
+ View : React.PropTypes.func.isRequired,
+ options : React.PropTypes.object
+ },
+
+ shouldComponentUpdate : function( next ){
+ var props = this.props;
+ return next.View !== props.View || jsonNotEqual( next.options, props.options );
+ },
+
+ render : function(){
+ return React.DOM.div({
+ ref : 'subview',
+ className : this.props.className
+ });
+ },
+
+ componentDidMount : function(){
+ this._mountView();
+ },
+ componentDidUpdate : function(){
+ this._dispose();
+ this._mountView();
+ },
+ componentWillUnmount : function(){
+ this._dispose();
+ },
+
+ _mountView: function () {
+ var el = this.refs.subview,
+ p = this.props;
+
+ var view = this.view = p.options ? new p.View( p.options ) : new p.View();
+
+ el.appendChild( view.el );
+ view.render();
+ },
+
+ _dispose : function(){
+ var view = this.view;
+ if( view ){
+ view.stopListening();
+ if( view.dispose ) view.dispose();
+ this.refs.subview.innerHTML = "";
+ this.view = null;
+ }
+ }
+});
\ No newline at end of file
diff --git a/umd/backbone-head.js b/umd/backbone-head.js
deleted file mode 100644
index 2e606e5..0000000
--- a/umd/backbone-head.js
+++ /dev/null
@@ -1,11 +0,0 @@
-(function( root, factory ){
- if( typeof exports === 'object' ){
- module.exports = factory( require( 'backbone' ), require( 'react' ) );
- }
- else if( typeof define === 'function' && define.amd ){
- define( [ 'backbone', 'react' ], factory );
- }
- else{
- root.React = factory( root.Backbone, root.React );
- }
-}( this,
\ No newline at end of file
diff --git a/umd/copyright.js b/umd/copyright.js
deleted file mode 100644
index ea7273c..0000000
--- a/umd/copyright.js
+++ /dev/null
@@ -1,5 +0,0 @@
-/**
- * React-Backbone.Glue 0.1.1
- * (c) 2015 Vlad Balin & Volicon
- * Released under MIT @license
- */
diff --git a/umd/nested-head.js b/umd/nested-head.js
deleted file mode 100644
index 416f351..0000000
--- a/umd/nested-head.js
+++ /dev/null
@@ -1,11 +0,0 @@
-(function( root, factory ){
- if( typeof exports === 'object' ){
- module.exports = factory( require( 'nestedtypes' ), require( 'react' ) );
- }
- else if( typeof define === 'function' && define.amd ){
- define( [ 'nestedtypes', 'react' ], factory );
- }
- else{
- root.React = factory( root.Nested, root.React );
- }
-}( this,
\ No newline at end of file
diff --git a/umd/tail.js b/umd/tail.js
deleted file mode 100644
index e0f7d5d..0000000
--- a/umd/tail.js
+++ /dev/null
@@ -1 +0,0 @@
- ));
\ No newline at end of file
diff --git a/webpack.config.js b/webpack.config.js
new file mode 100644
index 0000000..9aed0e6
--- /dev/null
+++ b/webpack.config.js
@@ -0,0 +1,36 @@
+module.exports = {
+ entry : "./src/main",
+
+ output : {
+ filename : './nestedreact.js',
+ library : "React",
+ libraryTarget : 'umd'
+ },
+
+ devtool : 'source-map',
+
+ externals : [
+ {
+ 'nestedtypes' : {
+ commonjs : 'nestedtypes',
+ commonjs2 : 'nestedtypes',
+ amd : 'nestedtypes',
+ root : 'Nested'
+ },
+
+ 'react' : {
+ commonjs : 'react',
+ commonjs2 : 'react',
+ amd : 'react',
+ root : 'React'
+ },
+
+ 'react-dom' : {
+ commonjs : 'react-dom',
+ commonjs2 : 'react-dom',
+ amd : 'react-dom',
+ root : 'ReactDOM'
+ }
+ }
+ ]
+};