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' + } + } + ] +};