Comparing version 0.7.6 to 0.8.1
{ | ||
"name": "imvvm", | ||
"version": "0.7.6", | ||
"version": "0.8.1", | ||
"homepage": "https://github.com/entrendipity/imvvm", | ||
@@ -21,4 +21,9 @@ "authors": [ | ||
"react", | ||
"routing", | ||
"pushState", | ||
"imvvm" | ||
], | ||
"dependencies": { | ||
"page": ">=1.3.7" | ||
}, | ||
"license": "MIT", | ||
@@ -25,0 +30,0 @@ "ignore": [ |
## 0.6.17 | ||
- `this.extend` dreprecated. Replaced with `IMVVM.extend`. Will not be available in version `0.7.0+` | ||
- `this.extend` deprecated. Replaced with `IMVVM.extend`. Will not be available in version `0.7.0+` | ||
- The following functions are no longer exposed to the View | ||
@@ -11,2 +11,3 @@ + getDomainDataContext | ||
- Updated reference implementation. | ||
## 0.6.18-19 | ||
@@ -49,6 +50,34 @@ | ||
## 0.7.5 | ||
- missed version | ||
- skipped version number | ||
## 0.7.6 | ||
- Bug fix: callback not firing when transient state exists | ||
## 0.8.0 | ||
- skipped version number | ||
## 0.8.1 | ||
- Add pushState router | ||
- Split mixin => IMVVM.mixin.main, IMVVM.mixin.pushState ***(Breaking Change) Change mixin from IMVVM.mixin to IMVVM.mixin.main*** | ||
- Added `getRoutes` function | ||
- Added `path` to Domain Data Context | ||
- Added `forceReplace`: true || false property to Domain Data Context | ||
- Added `pushState`: true || false to Domain Data Context | ||
- Added readonly `pageNotFound` to Domain Data Context | ||
- Added `kind:uid`: For future use | ||
- Automatic pushState for 'a' tags if pushState mixed in | ||
- Added ad-hoc `enableUndo`: true || false to Domain Data Context | ||
- rename core.js => stateController.js | ||
- rename imvvm.js => core.js | ||
- rename imvvmDomainViewModel => domainViewModel | ||
- rename imvvmViewModel => viewModel | ||
- rename imvvmModel => model | ||
- Bug fix: Domain Data Context watchedState did not link if it wasn't in appState.state during initialization | ||
- Added `dataContextWillInitialize` to DomainViewModel and ViewModels | ||
- setState callbacks now return an error as the first argument. i.e. callback(err, appContext) ***(Breaking Change) Add `error` argument to first param of setState callbacks*** | ||
- setState can now be called with no arguments eg. this.setState() | ||
- setState takes extra optional argument `forget` of Type Boolean. Only applicable in DomainViewModel and ViewModels |
1484
dist/imvvm.js
!function(e){if("object"==typeof exports)module.exports=e();else if("function"==typeof define&&define.amd)define(e);else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.IMVVM=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(_dereq_,module,exports){ | ||
'use strict' | ||
var IMVVM = _dereq_('./src/imvvm.js'); | ||
var IMVVM = _dereq_('./src/core.js'); | ||
module.exports = IMVVM; | ||
},{"./src/imvvm.js":3}],2:[function(_dereq_,module,exports){ | ||
var utils = _dereq_('./utils'); | ||
var extend = utils.extend; | ||
},{"./src/core.js":3}],2:[function(_dereq_,module,exports){ | ||
exports.getInitialState = function(appNamespace, domainModel, stateChangedHandler, enableUndo) { | ||
;(function(){ | ||
if(typeof stateChangedHandler !== 'function'){ | ||
throw new TypeError('stateChangedHandler must be a function!'); | ||
} | ||
/** | ||
* Perform initial dispatch. | ||
*/ | ||
enableUndo === void(0) ? true : enableUndo; | ||
var dispatch = true; | ||
var ApplicationDataContext, | ||
appState = {}, | ||
dataContexts = {}, | ||
domain, | ||
links = {}, | ||
watchedDataContexts = {}, | ||
dataContext, | ||
transientState = {}, | ||
processedState = {}, | ||
watchedState, | ||
watchedItem, | ||
watchedProp, | ||
watchedDataContext, | ||
link, | ||
calledBack = false; | ||
/** | ||
* Base path. | ||
*/ | ||
var appStateChangedHandler = function(caller, newState, newAppState, callback) { | ||
var base = ''; | ||
var nextState = {}, | ||
prevState = void(0), | ||
redoState = void(0), | ||
newStateKeys, | ||
keyIdx, | ||
transientStateKeysLen, | ||
dataContext, | ||
linkedDataContext, | ||
processedStateKeys = [], | ||
processedStateKeysLen, | ||
watchedField, | ||
subscribers, | ||
subscriber; | ||
/** | ||
* Running flag. | ||
*/ | ||
if(typeof newAppState === 'function'){ | ||
callback = newAppState; | ||
newAppState = {}; | ||
} | ||
var running; | ||
newState = newState || {}; | ||
newStateKeys = Object.keys(newState); | ||
/** | ||
* Register `path` with callback `fn()`, | ||
* or route `path`, or `page.start()`. | ||
* | ||
* page(fn); | ||
* page('*', fn); | ||
* page('/user/:id', load, user); | ||
* page('/user/' + user.id, { some: 'thing' }); | ||
* page('/user/' + user.id); | ||
* page(); | ||
* | ||
* @param {String|Function} path | ||
* @param {Function} fn... | ||
* @api public | ||
*/ | ||
//Check to see if appState is a ready made state object. If so | ||
//pass it straight to the stateChangedHandler. If a callback was passed in | ||
//it would be assigned to newState | ||
if(Object.getPrototypeOf(newState).constructor.classType === "DomainViewModel") { | ||
function page(path, fn) { | ||
// <callback> | ||
if ('function' == typeof path) { | ||
return page('*', path); | ||
} | ||
nextState = extend(newState); | ||
prevState = newState.previousState; | ||
redoState = newAppState; | ||
//Need to reset ViewModel with instance object so that setState is associated with | ||
//the current ViewModel. This reason this ccurs is that when a ViewModel is created it | ||
//assigns a setState function to all instance fields and binds itself to it. When undo is called | ||
//the data is rolled back but viewModels are not recreated and therefore the setState functions | ||
//are associated with outdated viewModels and returns unexpected output. So to realign the ViewModels | ||
//with setState we recreate them. | ||
for(dataContext in domain){ | ||
nextState[dataContext] = new dataContexts[dataContext](nextState[dataContext]); | ||
} | ||
// route <path> to <callback ...> | ||
if ('function' == typeof fn) { | ||
var route = new Route(path); | ||
for (var i = 1; i < arguments.length; ++i) { | ||
page.callbacks.push(route.middleware(arguments[i])); | ||
} | ||
// show <path> with [state] | ||
} else if ('string' == typeof path) { | ||
page.show(path, fn); | ||
// start [options] | ||
} else { | ||
page.start(path); | ||
} | ||
} | ||
//relink | ||
for(dataContext in domain){ | ||
if(domain.hasOwnProperty(dataContext)){ | ||
for(linkedDataContext in links[dataContext]){ | ||
if(links[dataContext].hasOwnProperty(linkedDataContext)){ | ||
nextState[dataContext].state[links[dataContext][linkedDataContext]] = | ||
(linkedDataContext in domain) ? extend(nextState[linkedDataContext].state) : | ||
nextState[linkedDataContext]; | ||
} | ||
} | ||
} | ||
} | ||
/** | ||
* Callback functions. | ||
*/ | ||
if(typeof callback === 'function'){ | ||
appState = new ApplicationDataContext(nextState, prevState, redoState, enableUndo); | ||
callback(appState); | ||
return; | ||
} | ||
page.callbacks = []; | ||
} else { | ||
/** | ||
* Get or set basepath to `path`. | ||
* | ||
* @param {String} path | ||
* @api public | ||
*/ | ||
if(!!newStateKeys.length){ | ||
if(caller === appNamespace){ | ||
nextState = extend(newState); | ||
} else { | ||
nextState[caller] = extend(newState); | ||
} | ||
} | ||
transientState = extend(nextState, transientState, newAppState); | ||
transientStateKeys = Object.keys(transientState); | ||
if(transientStateKeys.length === 0){ | ||
return; | ||
} | ||
page.base = function(path){ | ||
if (0 == arguments.length) return base; | ||
base = path; | ||
}; | ||
transientStateKeysLen = transientStateKeys.length - 1; | ||
/** | ||
* Bind with the given `options`. | ||
* | ||
* Options: | ||
* | ||
* - `click` bind to click events [true] | ||
* - `popstate` bind to popstate [true] | ||
* - `dispatch` perform initial dispatch [true] | ||
* | ||
* @param {Object} options | ||
* @api public | ||
*/ | ||
for (keyIdx = transientStateKeysLen; keyIdx >= 0; keyIdx--) { | ||
if(transientStateKeys[keyIdx] in domain){ | ||
nextState[transientStateKeys[keyIdx]] = extend(appState[transientStateKeys[keyIdx]], transientState[transientStateKeys[keyIdx]]); | ||
nextState[transientStateKeys[keyIdx]] = new dataContexts[transientStateKeys[keyIdx]](nextState[transientStateKeys[keyIdx]]); | ||
} else { | ||
nextState[transientStateKeys[keyIdx]] = transientState[transientStateKeys[keyIdx]]; | ||
} | ||
}; | ||
page.start = function(options){ | ||
options = options || {}; | ||
if (running) return; | ||
running = true; | ||
if (false === options.dispatch) dispatch = false; | ||
if (false !== options.popstate) window.addEventListener('popstate', onpopstate, false); | ||
if (false !== options.click) window.addEventListener('click', onclick, false); | ||
if (!dispatch) return; | ||
var url = location.pathname + location.search + location.hash; | ||
page.replace(url, null, true, dispatch); | ||
}; | ||
processedState = extend(processedState, nextState); | ||
/** | ||
* Unbind click and popstate event handlers. | ||
* | ||
* @api public | ||
*/ | ||
//Triggers | ||
nextState = extend(appState, processedState); | ||
page.stop = function(){ | ||
running = false; | ||
removeEventListener('click', onclick, false); | ||
removeEventListener('popstate', onpopstate, false); | ||
}; | ||
transientState = {}; | ||
for (keyIdx = transientStateKeysLen; keyIdx >= 0; keyIdx--) { | ||
if(transientStateKeys[keyIdx] in watchedDataContexts){ | ||
for(watchedField in watchedDataContexts[transientStateKeys[keyIdx]]){ | ||
if(watchedDataContexts[transientStateKeys[keyIdx]].hasOwnProperty(watchedField)){ | ||
if(newStateKeys.indexOf(watchedField) !== -1){ | ||
subscribers = watchedDataContexts[transientStateKeys[keyIdx]][watchedField]; | ||
for(subscriber in subscribers){ | ||
if(subscribers.hasOwnProperty(subscriber)){ | ||
/** | ||
* Show `path` with optional `state` object. | ||
* | ||
* @param {String} path | ||
* @param {Object} state | ||
* @param {Boolean} dispatch | ||
* @return {Context} | ||
* @api public | ||
*/ | ||
//Cross reference dataContext link Phase | ||
if(subscriber in links){ | ||
for(dataContext in links[subscriber]){ | ||
if(links[subscriber].hasOwnProperty(dataContext)){ | ||
nextState[subscriber].state[links[subscriber][dataContext]] = | ||
(dataContext in domain) ? extend(nextState[dataContext].state) : | ||
nextState[dataContext]; | ||
} | ||
if(dataContext in links){ | ||
for(dataContext2 in links[dataContext]){ | ||
if(links[dataContext].hasOwnProperty(dataContext2)){ | ||
nextState[dataContext].state[links[dataContext][dataContext2]] = | ||
(dataContext2 in domain) ? extend(nextState[dataContext2].state) : | ||
nextState[dataContext2]; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
transientState = extend(transientState, subscribers[subscriber].call(appState[subscriber], | ||
nextState[transientStateKeys[keyIdx]][watchedField], | ||
appState[transientStateKeys[keyIdx]][watchedField], watchedField, transientStateKeys[keyIdx])); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
}; | ||
if(!!Object.keys(transientState).length){ | ||
appStateChangedHandler(void(0), {}, transientState, callback); | ||
return; | ||
} | ||
page.show = function(path, state, dispatch){ | ||
var ctx = new Context(path, state); | ||
if (false !== dispatch) page.dispatch(ctx); | ||
if (!ctx.unhandled) ctx.pushState(); | ||
return ctx; | ||
}; | ||
//Link Phase | ||
processedStateKeys = Object.keys(processedState); | ||
processedStateKeysLen = processedStateKeys.length - 1; | ||
for (keyIdx = processedStateKeysLen; keyIdx >= 0; keyIdx--) { | ||
if(caller === appNamespace && (appNamespace in links)){ | ||
if(processedStateKeys[keyIdx] in links[appNamespace]){ | ||
for(dataContext in links[appNamespace][processedStateKeys[keyIdx]]){ | ||
if(links[appNamespace][processedStateKeys[keyIdx]].hasOwnProperty(dataContext)){ | ||
nextState[dataContext].state[links[appNamespace][processedStateKeys[keyIdx]][dataContext]] = | ||
nextState[processedStateKeys[keyIdx]]; | ||
} | ||
if(dataContext in links){ | ||
for(dataContext2 in links[dataContext]){ | ||
if(links[dataContext].hasOwnProperty(dataContext2)){ | ||
nextState[dataContext].state[links[dataContext][dataContext2]] = | ||
(dataContext2 in domain) ? extend(nextState[dataContext2].state) : | ||
nextState[dataContext2]; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} else { | ||
if(processedStateKeys[keyIdx] in links){ | ||
for(dataContext in links[processedStateKeys[keyIdx]]){ | ||
if(links[processedStateKeys[keyIdx]].hasOwnProperty(dataContext)){ | ||
nextState[processedStateKeys[keyIdx]].state[links[processedStateKeys[keyIdx]][dataContext]] = | ||
(dataContext in domain) ? extend(nextState[dataContext].state) : | ||
nextState[dataContext]; | ||
} | ||
if(dataContext in links){ | ||
for(dataContext2 in links[dataContext]){ | ||
if(links[dataContext].hasOwnProperty(dataContext2)){ | ||
nextState[dataContext].state[links[dataContext][dataContext2]] = | ||
(dataContext2 in domain) ? extend(nextState[dataContext2].state) : | ||
nextState[dataContext2]; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
/** | ||
* Replace `path` with optional `state` object. | ||
* | ||
* @param {String} path | ||
* @param {Object} state | ||
* @return {Context} | ||
* @api public | ||
*/ | ||
prevState = calledBack ? appState.previousState : appState; | ||
page.replace = function(path, state, init, dispatch){ | ||
var ctx = new Context(path, state); | ||
ctx.init = init; | ||
if (null == dispatch) dispatch = true; | ||
if (dispatch) page.dispatch(ctx); | ||
ctx.save(); | ||
return ctx; | ||
}; | ||
} | ||
/** | ||
* Dispatch the given `ctx`. | ||
* | ||
* @param {Object} ctx | ||
* @api private | ||
*/ | ||
if(!!prevState){ | ||
Object.freeze(prevState); | ||
} | ||
page.dispatch = function(ctx){ | ||
var i = 0; | ||
appState = new ApplicationDataContext(nextState, prevState, redoState, enableUndo); | ||
Object.freeze(appState); | ||
Object.freeze(appState.state); | ||
function next() { | ||
var fn = page.callbacks[i++]; | ||
if (!fn) return unhandled(ctx); | ||
fn(ctx, next); | ||
} | ||
if(typeof callback === 'function'){ | ||
calledBack = true; | ||
callback(appState); | ||
return; | ||
} | ||
//All the work is done! -> Notify the View | ||
stateChangedHandler(appState); | ||
calledBack = false; | ||
transientState = {}; | ||
processedState = {}; | ||
next(); | ||
}; | ||
}; | ||
/** | ||
* Unhandled `ctx`. When it's not the initial | ||
* popstate then redirect. If you wish to handle | ||
* 404s on your own use `page('*', callback)`. | ||
* | ||
* @param {Context} ctx | ||
* @api private | ||
*/ | ||
//Initialize Application Data Context | ||
ApplicationDataContext = domainModel.call(this, appStateChangedHandler.bind(this, appNamespace)); | ||
appState = new ApplicationDataContext(void(0), void(0), void(0), enableUndo); | ||
appState.state = appState.state || {}; | ||
function unhandled(ctx) { | ||
var current = window.location.pathname + window.location.search; | ||
if (current == ctx.canonicalPath) return; | ||
page.stop(); | ||
ctx.unhandled = true; | ||
window.location = ctx.canonicalPath; | ||
} | ||
domain = appState.constructor.originalSpec.getDomainDataContext(); | ||
delete appState.constructor.originalSpec.getDomainDataContext; | ||
/** | ||
* Initialize a new "request" `Context` | ||
* with the given `path` and optional initial `state`. | ||
* | ||
* @param {String} path | ||
* @param {Object} state | ||
* @api public | ||
*/ | ||
//Initialize all dataContexts | ||
for(dataContext in domain){ | ||
if(domain.hasOwnProperty(dataContext)){ | ||
dataContexts[dataContext] = domain[dataContext].call(this, appStateChangedHandler.bind(this, dataContext)); | ||
appState.state[dataContext] = new dataContexts[dataContext](appState.state[dataContext], true); | ||
} | ||
function Context(path, state) { | ||
if ('/' == path[0] && 0 != path.indexOf(base)) path = base + path; | ||
var i = path.indexOf('?'); | ||
this.canonicalPath = path; | ||
this.path = path.replace(base, '') || '/'; | ||
this.title = document.title; | ||
this.state = state || {}; | ||
this.state.path = path; | ||
this.querystring = ~i ? path.slice(i + 1) : ''; | ||
this.pathname = ~i ? path.slice(0, i) : path; | ||
this.params = []; | ||
// fragment | ||
this.hash = ''; | ||
if (!~this.path.indexOf('#')) return; | ||
var parts = this.path.split('#'); | ||
this.path = parts[0]; | ||
this.hash = parts[1] || ''; | ||
this.querystring = this.querystring.split('#')[0]; | ||
} | ||
//Store links | ||
for(dataContext in domain){ | ||
if(domain.hasOwnProperty(dataContext)){ | ||
if('getWatchedState' in appState[dataContext].constructor.originalSpec){ | ||
watchedState = appState[dataContext].constructor.originalSpec.getWatchedState(); | ||
for(watchedItem in watchedState){ | ||
if(watchedState.hasOwnProperty(watchedItem)){ | ||
if(watchedItem in domain || watchedItem in appState.state){ | ||
if('alias' in watchedState[watchedItem]){ | ||
if(!(dataContext in links)){ | ||
links[dataContext] = {}; | ||
} | ||
links[dataContext][watchedItem] = watchedState[watchedItem].alias; | ||
/** | ||
* Expose `Context`. | ||
*/ | ||
if(!(watchedItem in domain)){ | ||
if(!(appNamespace in links)){ | ||
links[appNamespace] = {}; | ||
} | ||
if(!(dataContext in links[appNamespace])){ | ||
links[appNamespace][watchedItem] = {}; | ||
} | ||
links[appNamespace][watchedItem][dataContext] = watchedState[watchedItem].alias; | ||
} | ||
} | ||
for(watchedProp in watchedState[watchedItem].fields){ | ||
if(watchedState[watchedItem].fields.hasOwnProperty(watchedProp)){ | ||
if(watchedItem in domain){ | ||
watchedDataContext = {}; | ||
if(!(watchedItem in watchedDataContexts)){ | ||
watchedDataContexts[watchedItem] = {}; | ||
} | ||
watchedDataContext[watchedProp] = {}; | ||
watchedDataContext[watchedProp][dataContext] = watchedState[watchedItem].fields[watchedProp]; | ||
watchedDataContexts[watchedItem] = watchedDataContext; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
page.Context = Context; | ||
//apply links | ||
for(dataContext in domain){ | ||
if(domain.hasOwnProperty(dataContext)){ | ||
for(link in links[dataContext]){ | ||
if(links[dataContext].hasOwnProperty(link)){ | ||
appState[dataContext].state[links[dataContext][link]] = (link in domain) ? extend(appState[link].state): appState[link]; | ||
} | ||
} | ||
} | ||
} | ||
/** | ||
* Push state. | ||
* | ||
* @api private | ||
*/ | ||
//reinitialize with all data in place | ||
for(dataContext in domain){ | ||
if(domain.hasOwnProperty(dataContext)){ | ||
appState.state[dataContext] = new dataContexts[dataContext](appState.state[dataContext]); | ||
Context.prototype.pushState = function(){ | ||
history.pushState(this.state, this.title, this.canonicalPath); | ||
}; | ||
/** | ||
* Save the context state. | ||
* | ||
* @api public | ||
*/ | ||
Context.prototype.save = function(){ | ||
history.replaceState(this.state, this.title, this.canonicalPath); | ||
}; | ||
/** | ||
* Initialize `Route` with the given HTTP `path`, | ||
* and an array of `callbacks` and `options`. | ||
* | ||
* Options: | ||
* | ||
* - `sensitive` enable case-sensitive routes | ||
* - `strict` enable strict matching for trailing slashes | ||
* | ||
* @param {String} path | ||
* @param {Object} options. | ||
* @api private | ||
*/ | ||
function Route(path, options) { | ||
options = options || {}; | ||
this.path = path; | ||
this.method = 'GET'; | ||
this.regexp = pathtoRegexp(path | ||
, this.keys = [] | ||
, options.sensitive | ||
, options.strict); | ||
} | ||
/** | ||
* Expose `Route`. | ||
*/ | ||
page.Route = Route; | ||
/** | ||
* Return route middleware with | ||
* the given callback `fn()`. | ||
* | ||
* @param {Function} fn | ||
* @return {Function} | ||
* @api public | ||
*/ | ||
Route.prototype.middleware = function(fn){ | ||
var self = this; | ||
return function(ctx, next){ | ||
if (self.match(ctx.path, ctx.params)) return fn(ctx, next); | ||
next(); | ||
}; | ||
}; | ||
/** | ||
* Check if this route matches `path`, if so | ||
* populate `params`. | ||
* | ||
* @param {String} path | ||
* @param {Array} params | ||
* @return {Boolean} | ||
* @api private | ||
*/ | ||
Route.prototype.match = function(path, params){ | ||
var keys = this.keys | ||
, qsIndex = path.indexOf('?') | ||
, pathname = ~qsIndex ? path.slice(0, qsIndex) : path | ||
, m = this.regexp.exec(pathname); | ||
if (!m) return false; | ||
for (var i = 1, len = m.length; i < len; ++i) { | ||
var key = keys[i - 1]; | ||
var val = 'string' == typeof m[i] | ||
? decodeURIComponent(m[i]) | ||
: m[i]; | ||
if (key) { | ||
params[key.name] = undefined !== params[key.name] | ||
? params[key.name] | ||
: val; | ||
} else { | ||
params.push(val); | ||
} | ||
} | ||
return true; | ||
}; | ||
/** | ||
* Normalize the given path string, | ||
* returning a regular expression. | ||
* | ||
* An empty array should be passed, | ||
* which will contain the placeholder | ||
* key names. For example "/user/:id" will | ||
* then contain ["id"]. | ||
* | ||
* @param {String|RegExp|Array} path | ||
* @param {Array} keys | ||
* @param {Boolean} sensitive | ||
* @param {Boolean} strict | ||
* @return {RegExp} | ||
* @api private | ||
*/ | ||
function pathtoRegexp(path, keys, sensitive, strict) { | ||
if (path instanceof RegExp) return path; | ||
if (path instanceof Array) path = '(' + path.join('|') + ')'; | ||
path = path | ||
.concat(strict ? '' : '/?') | ||
.replace(/\/\(/g, '(?:/') | ||
.replace(/(\/)?(\.)?:(\w+)(?:(\(.*?\)))?(\?)?/g, function(_, slash, format, key, capture, optional){ | ||
keys.push({ name: key, optional: !! optional }); | ||
slash = slash || ''; | ||
return '' | ||
+ (optional ? '' : slash) | ||
+ '(?:' | ||
+ (optional ? slash : '') | ||
+ (format || '') + (capture || (format && '([^/.]+?)' || '([^/]+?)')) + ')' | ||
+ (optional || ''); | ||
}) | ||
.replace(/([\/.])/g, '\\$1') | ||
.replace(/\*/g, '(.*)'); | ||
return new RegExp('^' + path + '$', sensitive ? '' : 'i'); | ||
} | ||
appState = new ApplicationDataContext(appState, void(0), void(0), enableUndo); | ||
Object.freeze(appState.state); | ||
Object.freeze(appState); | ||
return appState; | ||
}; | ||
/** | ||
* Handle "populate" events. | ||
*/ | ||
},{"./utils":8}],3:[function(_dereq_,module,exports){ | ||
function onpopstate(e) { | ||
if (e.state) { | ||
var path = e.state.path; | ||
page.replace(path, e.state); | ||
} | ||
} | ||
var model = _dereq_('./imvvmModel'); | ||
var viewModel = _dereq_('./imvvmViewModel'); | ||
var domainModel = _dereq_('./imvvmDomainViewModel'); | ||
/** | ||
* Handle "click" events. | ||
*/ | ||
function onclick(e) { | ||
if (1 != which(e)) return; | ||
if (e.metaKey || e.ctrlKey || e.shiftKey) return; | ||
if (e.defaultPrevented) return; | ||
// ensure link | ||
var el = e.target; | ||
while (el && 'A' != el.nodeName) el = el.parentNode; | ||
if (!el || 'A' != el.nodeName) return; | ||
// ensure non-hash for the same path | ||
var link = el.getAttribute('href'); | ||
if (el.pathname == location.pathname && (el.hash || '#' == link)) return; | ||
// check target | ||
if (el.target) return; | ||
// x-origin | ||
if (!sameOrigin(el.href)) return; | ||
// rebuild path | ||
var path = el.pathname + el.search + (el.hash || ''); | ||
// same page | ||
var orig = path + el.hash; | ||
path = path.replace(base, ''); | ||
if (base && orig == path) return; | ||
e.preventDefault(); | ||
page.show(orig); | ||
} | ||
/** | ||
* Event button. | ||
*/ | ||
function which(e) { | ||
e = e || window.event; | ||
return null == e.which | ||
? e.button | ||
: e.which; | ||
} | ||
/** | ||
* Check if `href` is the same origin. | ||
*/ | ||
function sameOrigin(href) { | ||
var origin = location.protocol + '//' + location.hostname; | ||
if (location.port) origin += ':' + location.port; | ||
return 0 == href.indexOf(origin); | ||
} | ||
/** | ||
* Expose `page`. | ||
*/ | ||
if ('undefined' == typeof module) { | ||
window.page = page; | ||
} else { | ||
module.exports = page; | ||
} | ||
})(); | ||
},{}],3:[function(_dereq_,module,exports){ | ||
var model = _dereq_('./model'); | ||
var viewModel = _dereq_('./viewModel'); | ||
var domainModel = _dereq_('./domainViewModel'); | ||
var mixin = _dereq_('./mixin'); | ||
var page = _dereq_('page'); | ||
var utils = _dereq_('./utils'); | ||
@@ -348,3 +479,3 @@ var extend = utils.extend; | ||
var Constructor = function(){}; | ||
Constructor.prototype = new ctor(); | ||
Constructor.prototype = new ctor(); | ||
Constructor.prototype.constructor = Constructor; | ||
@@ -361,3 +492,3 @@ | ||
Constructor.ConvenienceConstructor = ConvenienceConstructor; | ||
ConvenienceConstructor.originalSpec = spec; | ||
@@ -406,2 +537,4 @@ | ||
this.originalSpec[key].enumerable = false; | ||
} else if (this.originalSpec[key].kind === 'uid') { | ||
//Don't do anything as yet | ||
} else if (this.originalSpec[key].kind === 'instance' || | ||
@@ -412,3 +545,3 @@ this.originalSpec[key].kind === 'array') { //'instance' || 'array' | ||
throw new TypeError('"'+this.originalSpec[key].kind +'" '+ | ||
'is not a valid "kind" value. Valid values are "pseudo","instance" and "array". Please review field "' + key + '".'); | ||
'is not a valid "kind" value. Please review field "' + key + '".'); | ||
} | ||
@@ -420,3 +553,4 @@ delete this.originalSpec[key].kind; | ||
} else { | ||
if(key !== 'getInitialState' && key !== 'getWatchedState'){ | ||
if(key !== 'getInitialState' && key !== 'getWatchedState' && | ||
key !== 'getRoutes'){ | ||
proto[key] = this.originalSpec[key]; | ||
@@ -434,3 +568,3 @@ } | ||
this.originalSpec.__processedSpec__ = { | ||
this.originalSpec.__processedSpec__ = { | ||
descriptor: descriptor, | ||
@@ -450,3 +584,3 @@ proto: proto, | ||
var IMVVM = { | ||
module.exports = { | ||
createModel: IMVVMClass.createClass.bind(this, ModelBase, 'Model'), | ||
@@ -456,16 +590,17 @@ createViewModel: IMVVMClass.createClass.bind(this, ViewModelBase, 'ViewModel'), | ||
mixin: mixin, | ||
extend: extend | ||
extend: extend, | ||
page: page | ||
}; | ||
module.exports = IMVVM; | ||
},{"./domainViewModel":4,"./mixin":5,"./model":6,"./utils":8,"./viewModel":9,"page":2}],4:[function(_dereq_,module,exports){ | ||
},{"./imvvmDomainViewModel":4,"./imvvmModel":5,"./imvvmViewModel":6,"./mixin":7,"./utils":8}],4:[function(_dereq_,module,exports){ | ||
var utils = _dereq_('./utils'); | ||
var extend = utils.extend; | ||
var IMVVMDomainViewModel = { | ||
var DomainViewModel = { | ||
Mixin: { | ||
construct: function(stateChangedHandler){ | ||
var prevAdhocUndo = false; | ||
var previousPageNotFound = false; | ||
var desc = this.getDescriptor(); | ||
@@ -483,18 +618,80 @@ desc.proto.setState = stateChangedHandler; | ||
}; | ||
var DomainViewModelClass = function(nextState, prevState, redoState, enableUndo, | ||
routingEnabled, pushStateChanged, internal, forget) { | ||
var dataContext = function(nextState, prevState, redoState, enableUndo) { | ||
var freezeFields = desc.freezeFields, | ||
domainModel = Object.create(desc.proto, desc.descriptor), | ||
fld; | ||
if(!!enableUndo){ | ||
if(!!prevState){ | ||
Object.defineProperty(domainModel, 'previousState', { | ||
domainViewModel = Object.create(desc.proto, desc.descriptor), | ||
fld, | ||
init = nextState === void(0), | ||
adhocUndo, | ||
forceReplace, | ||
pushState, | ||
pageNotFound; | ||
pushStateChanged = routingEnabled ? pushStateChanged : false; | ||
if(!init){ | ||
if(routingEnabled){ | ||
forceReplace = nextState.forceReplace === void(0) ? false : | ||
nextState.forceReplace; | ||
pushState = nextState.pushState === void(0) ? true : | ||
nextState.pushState; | ||
pageNotFound = nextState.pageNotFound === void(0) ? false : | ||
nextState.pageNotFound; | ||
Object.defineProperty(domainViewModel, 'pageNotFound', { | ||
configurable: false, | ||
enumerable: false, | ||
writable: false, | ||
value: pageNotFound | ||
}); | ||
Object.defineProperty(domainViewModel, 'forceReplace', { | ||
configurable: false, | ||
enumerable: true, | ||
writable: false, | ||
value: forceReplace | ||
}); | ||
Object.defineProperty(domainViewModel, 'pushState', { | ||
configurable: false, | ||
enumerable: true, | ||
writable: false, | ||
value: pushState | ||
}); | ||
if(!('path' in domainViewModel) && ('path' in nextState)){ | ||
Object.defineProperty(domainViewModel, 'path', { | ||
configurable: false, | ||
enumerable: true, | ||
writable: false, | ||
value: nextState.path | ||
}); | ||
} | ||
} | ||
if(nextState.enableUndo === void(0)){ | ||
adhocUndo = false; | ||
} else { | ||
enableUndo = nextState.enableUndo; | ||
adhocUndo = nextState.enableUndo; | ||
if(!nextState.enableUndo){ | ||
routingEnabled = false; | ||
} | ||
} | ||
} | ||
//need routingEnabled flag because it depends on prevState | ||
if(enableUndo || routingEnabled){ | ||
if(!!prevState && (!pushStateChanged || adhocUndo || pageNotFound) && | ||
!previousAdhoc && internal && !forget){ | ||
previousAdhoc = adhocUndo; | ||
previousPageNotFound = pageNotFound; | ||
Object.defineProperty(domainViewModel, 'previousState', { | ||
configurable: false, | ||
enumerable: false, | ||
writable: false, | ||
value: prevState | ||
}); | ||
Object.defineProperty(domainModel, 'canRevert', { | ||
Object.defineProperty(domainViewModel, 'canRevert', { | ||
configurable: false, | ||
@@ -506,3 +703,3 @@ enumerable: false, | ||
} else { | ||
Object.defineProperty(domainModel, 'canRevert', { | ||
Object.defineProperty(domainViewModel, 'canRevert', { | ||
configurable: false, | ||
@@ -514,4 +711,5 @@ enumerable: false, | ||
} | ||
if(!!redoState && 'state' in redoState){ | ||
Object.defineProperty(domainModel, 'nextState', { | ||
if(!!redoState && ('state' in redoState) && !previousAdhoc && | ||
!previousPageNotFound){ | ||
Object.defineProperty(domainViewModel, 'nextState', { | ||
configurable: false, | ||
@@ -522,3 +720,3 @@ enumerable: false, | ||
}); | ||
Object.defineProperty(domainModel, 'canAdvance', { | ||
Object.defineProperty(domainViewModel, 'canAdvance', { | ||
configurable: false, | ||
@@ -530,3 +728,5 @@ enumerable: false, | ||
} else { | ||
Object.defineProperty(domainModel, 'canAdvance', { | ||
previousAdhoc = adhocUndo; | ||
previousPageNotFound = pageNotFound; | ||
Object.defineProperty(domainViewModel, 'canAdvance', { | ||
configurable: false, | ||
@@ -540,10 +740,20 @@ enumerable: false, | ||
if(nextState === void(0)){ | ||
if(init){ | ||
//Add state prop so that it can be referenced from within getInitialState | ||
nextState = ('getInitialState' in desc.originalSpec) ? desc.originalSpec.getInitialState.call(domainModel) : {}; | ||
nextState = ('getInitialState' in desc.originalSpec) ? | ||
desc.originalSpec.getInitialState.call(domainViewModel) : {}; | ||
if('path' in nextState){ | ||
Object.defineProperty(domainViewModel, 'path', { | ||
configurable: false, | ||
enumerable: true, | ||
writable: false, | ||
value: nextState.path | ||
}); | ||
} | ||
} else if('state' in nextState){ | ||
delete nextState.state; | ||
//Need to have 'state' prop in domainModel before can extend domainModel to get correct state | ||
Object.defineProperty(domainModel, 'state', { | ||
//Need to have 'state' prop in domainViewModel before can extend domainViewModel to get correct state | ||
Object.defineProperty(domainViewModel, 'state', { | ||
configurable: true, | ||
@@ -554,3 +764,3 @@ enumerable: false, | ||
}); | ||
nextState = extend(nextState, domainModel); | ||
nextState = extend(nextState, domainViewModel); | ||
} | ||
@@ -568,3 +778,3 @@ | ||
Object.defineProperty(domainModel, 'state', { | ||
Object.defineProperty(domainViewModel, 'state', { | ||
configurable: false, | ||
@@ -576,6 +786,5 @@ enumerable: false, | ||
return domainModel; | ||
return domainViewModel; | ||
}; | ||
return dataContext; | ||
return DomainViewModelClass; | ||
} | ||
@@ -585,10 +794,93 @@ } | ||
module.exports = IMVVMDomainViewModel; | ||
module.exports = DomainViewModel; | ||
},{"./utils":8}],5:[function(_dereq_,module,exports){ | ||
var core = _dereq_('./stateController'); | ||
var NAMESPACE = '__IMVVM__'; | ||
var mixin = { | ||
main: { | ||
stateChangedHandler: function(dataContext){ | ||
this.setState({domainDataContext: dataContext}); | ||
}, | ||
getInitialState: function(){ | ||
var dataContext = core.getInitialState(NAMESPACE, this.props.domainModel, | ||
this.stateChangedHandler, this.props.enableUndo); | ||
return {domainDataContext: dataContext}; | ||
} | ||
}, | ||
pushState: { | ||
componentDidMount: function(){ | ||
$(this.getDOMNode()).click(this.onclick); | ||
}, | ||
componentWillUnmount: function(){ | ||
$(this.getDOMNode()).unbind('click'); | ||
}, | ||
/** | ||
* Event button. | ||
*/ | ||
which: function(e) { | ||
e = e || window.event; | ||
return null == e.which | ||
? e.button | ||
: e.which; | ||
}, | ||
/** | ||
* Check if `href` is the same origin. | ||
*/ | ||
sameOrigin: function(href) { | ||
var origin = location.protocol + '//' + location.hostname; | ||
if (location.port) origin += ':' + location.port; | ||
return 0 == href.indexOf(origin); | ||
}, | ||
/** | ||
* Handle "click" events for routing from <a> | ||
*/ | ||
onclick: function (e) { | ||
if (1 != this.which(e)) return; | ||
if (e.metaKey || e.ctrlKey || e.shiftKey) return; | ||
if (e.defaultPrevented) return; | ||
// ensure link | ||
var el = e.target; | ||
while (el && 'A' != el.nodeName) el = el.parentNode; | ||
if (!el || 'A' != el.nodeName) return; | ||
// ensure non-hash for the same path | ||
var link = el.getAttribute('href'); | ||
if (el.pathname == location.pathname && (el.hash || '#' == link)) return; | ||
// check target | ||
if (el.target) return; | ||
// x-origin | ||
if (!this.sameOrigin(el.href)) return; | ||
// rebuild path | ||
var path = el.pathname + el.search + (el.hash || ''); | ||
// same page | ||
var orig = path + el.hash; | ||
e.preventDefault(); | ||
path = path.replace(IMVVM.page.base(), ''); | ||
if (IMVVM.page.base() && orig == path || | ||
el.href === el.baseURI + el.search + (el.hash || '')) { | ||
return; | ||
} | ||
IMVVM.page.show(orig); | ||
} | ||
} | ||
}; | ||
module.exports = mixin; | ||
},{"./stateController":7}],6:[function(_dereq_,module,exports){ | ||
var utils = _dereq_('./utils'); | ||
var extend = utils.extend; | ||
var IMVVMModel = { | ||
var Model = { | ||
Mixin: { | ||
@@ -599,3 +891,3 @@ construct: function(stateChangedHandler){ | ||
var dataContext = function(nextState, extendState, initialize) { | ||
var ModelClass = function(nextState, extendState, initialize) { | ||
var freezeFields = desc.freezeFields, | ||
@@ -670,3 +962,3 @@ fld, | ||
}; | ||
return dataContext; | ||
return ModelClass; | ||
} | ||
@@ -676,10 +968,484 @@ } | ||
module.exports = IMVVMModel; | ||
module.exports = Model; | ||
},{"./utils":8}],6:[function(_dereq_,module,exports){ | ||
},{"./utils":8}],7:[function(_dereq_,module,exports){ | ||
var page = _dereq_('page'); | ||
var utils = _dereq_('./utils'); | ||
var extend = utils.extend; | ||
exports.getInitialState = function(appNamespace, domainModel, stateChangedHandler, enableUndo) { | ||
if(typeof stateChangedHandler !== 'function'){ | ||
throw new TypeError('stateChangedHandler must be a function.'); | ||
} | ||
if(enableUndo === void(0)){ | ||
enableUndo = false; | ||
} | ||
var ApplicationDataContext, | ||
appState = {}, | ||
dataContexts = {}, | ||
domain, | ||
links = {}, | ||
watchedDataContexts = {}, | ||
dataContext, | ||
transientState = {}, | ||
processedState = {}, | ||
watchedState, | ||
watchedItem, | ||
watchedProp, | ||
watchedDataContext, | ||
link, | ||
calledBack = false, | ||
routingEnabled = false, | ||
routeHash = {}, | ||
routeMapping = {}, | ||
routePath, | ||
external = false, | ||
internal = false, | ||
dataContextWillInitialize = false; | ||
var appStateChangedHandler = function(caller, newState, newAppState, forget, callback) { | ||
var nextState = {}, | ||
prevState = void(0), | ||
redoState = void(0), | ||
newStateKeys, | ||
keyIdx, | ||
transientStateKeysLen, | ||
dataContext, | ||
linkedDataContext, | ||
processedStateKeys = [], | ||
processedStateKeysLen, | ||
watchedField, | ||
subscribers, | ||
subscriber, | ||
pushStateChanged = false; | ||
if(arguments.length < 2){ | ||
stateChangedHandler(appState); | ||
return; | ||
} | ||
if(typeof forget === 'function'){ | ||
callback = forget; | ||
forget = false; | ||
} else if(typeof newAppState === 'function'){ | ||
callback = newAppState; | ||
newAppState = {}; | ||
} else if (typeof newAppState === 'boolean'){ | ||
forget = newAppState; | ||
newAppState = {}; | ||
} | ||
if(forget === void(0)){ | ||
forget = false; | ||
} | ||
newState = newState || {}; | ||
newStateKeys = Object.keys(newState); | ||
//Check to see if appState is a ready made state object. If so | ||
//pass it straight to the stateChangedHandler. If a callback was passed in | ||
//it would be assigned to newState | ||
if(Object.getPrototypeOf(newState).constructor.classType === "DomainViewModel") { | ||
nextState = extend(newState); | ||
prevState = newState.previousState; | ||
redoState = newAppState; | ||
//Need to reset ViewModel with instance object so that setState is associated with | ||
//the current ViewModel. This reason this ccurs is that when a ViewModel is created it | ||
//assigns a setState function to all instance fields and binds itself to it. When undo is called | ||
//the data is rolled back but viewModels are not recreated and therefore the setState functions | ||
//are associated with outdated viewModels and returns unexpected output. So to realign the ViewModels | ||
//with setState we recreate them. | ||
for(dataContext in domain){ | ||
nextState[dataContext] = new dataContexts[dataContext](nextState[dataContext]); | ||
} | ||
//relink | ||
for(dataContext in domain){ | ||
if(domain.hasOwnProperty(dataContext)){ | ||
for(linkedDataContext in links[dataContext]){ | ||
if(links[dataContext].hasOwnProperty(linkedDataContext)){ | ||
nextState[dataContext].state[links[dataContext][linkedDataContext]] = | ||
(linkedDataContext in domain) ? extend(nextState[linkedDataContext].state) : | ||
nextState[linkedDataContext]; | ||
} | ||
} | ||
} | ||
} | ||
if(typeof callback === 'function'){ | ||
try { | ||
appState = new ApplicationDataContext(nextState, prevState, redoState, | ||
enableUndo, routingEnabled, nextState.path !== appState.path, | ||
!external || nextState.pageNotFound, forget); | ||
calledBack = true; | ||
callback(void(0), appState); | ||
} catch(e) { | ||
callback(e); | ||
} | ||
return; | ||
} | ||
} else { | ||
if(!!newStateKeys.length){ | ||
if(caller === appNamespace){ | ||
nextState = extend(newState); | ||
} else { | ||
nextState[caller] = extend(newState); | ||
} | ||
} | ||
transientState = extend(nextState, transientState, newAppState); | ||
transientStateKeys = Object.keys(transientState); | ||
if(transientStateKeys.length === 0){ | ||
return; | ||
} | ||
transientStateKeysLen = transientStateKeys.length - 1; | ||
for (keyIdx = transientStateKeysLen; keyIdx >= 0; keyIdx--) { | ||
if(transientStateKeys[keyIdx] in domain){ | ||
nextState[transientStateKeys[keyIdx]] = extend(appState[transientStateKeys[keyIdx]], transientState[transientStateKeys[keyIdx]]); | ||
nextState[transientStateKeys[keyIdx]] = new dataContexts[transientStateKeys[keyIdx]](nextState[transientStateKeys[keyIdx]]); | ||
} else { | ||
nextState[transientStateKeys[keyIdx]] = transientState[transientStateKeys[keyIdx]]; | ||
} | ||
}; | ||
processedState = extend(processedState, nextState); | ||
//Triggers | ||
nextState = extend(appState, processedState); | ||
transientState = {}; | ||
for (keyIdx = transientStateKeysLen; keyIdx >= 0; keyIdx--) { | ||
if(transientStateKeys[keyIdx] in watchedDataContexts){ | ||
for(watchedField in watchedDataContexts[transientStateKeys[keyIdx]]){ | ||
if(watchedDataContexts[transientStateKeys[keyIdx]].hasOwnProperty(watchedField)){ | ||
if(newStateKeys.indexOf(watchedField) !== -1){ | ||
subscribers = watchedDataContexts[transientStateKeys[keyIdx]][watchedField]; | ||
for(subscriber in subscribers){ | ||
if(subscribers.hasOwnProperty(subscriber)){ | ||
//Cross reference dataContext link Phase | ||
if(subscriber in links){ | ||
for(dataContext in links[subscriber]){ | ||
if(links[subscriber].hasOwnProperty(dataContext)){ | ||
nextState[subscriber].state[links[subscriber][dataContext]] = | ||
(dataContext in domain) ? extend(nextState[dataContext].state) : | ||
nextState[dataContext]; | ||
} | ||
if(dataContext in links){ | ||
for(dataContext2 in links[dataContext]){ | ||
if(links[dataContext].hasOwnProperty(dataContext2)){ | ||
nextState[dataContext].state[links[dataContext][dataContext2]] = | ||
(dataContext2 in domain) ? extend(nextState[dataContext2].state) : | ||
nextState[dataContext2]; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
transientState = extend(transientState, | ||
subscribers[subscriber].call(appState[subscriber], | ||
nextState[transientStateKeys[keyIdx]][watchedField], | ||
appState[transientStateKeys[keyIdx]][watchedField], | ||
watchedField, transientStateKeys[keyIdx], | ||
nextState.path, appState.path)); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
}; | ||
if(!!Object.keys(transientState).length){ | ||
appStateChangedHandler(void(0), {}, transientState, forget, callback); | ||
return; | ||
} | ||
//Link Phase | ||
processedStateKeys = Object.keys(processedState); | ||
processedStateKeysLen = processedStateKeys.length - 1; | ||
for (keyIdx = processedStateKeysLen; keyIdx >= 0; keyIdx--) { | ||
if(caller === appNamespace && (appNamespace in links)){ | ||
if(processedStateKeys[keyIdx] in links[appNamespace]){ | ||
for(dataContext in links[appNamespace][processedStateKeys[keyIdx]]){ | ||
if(links[appNamespace][processedStateKeys[keyIdx]].hasOwnProperty(dataContext)){ | ||
nextState[dataContext].state[links[appNamespace][processedStateKeys[keyIdx]][dataContext]] = | ||
nextState[processedStateKeys[keyIdx]]; | ||
} | ||
if(dataContext in links){ | ||
for(dataContext2 in links[dataContext]){ | ||
if(links[dataContext].hasOwnProperty(dataContext2)){ | ||
nextState[dataContext].state[links[dataContext][dataContext2]] = | ||
(dataContext2 in domain) ? extend(nextState[dataContext2].state) : | ||
nextState[dataContext2]; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} else { | ||
if(processedStateKeys[keyIdx] in links){ | ||
for(dataContext in links[processedStateKeys[keyIdx]]){ | ||
if(links[processedStateKeys[keyIdx]].hasOwnProperty(dataContext)){ | ||
nextState[processedStateKeys[keyIdx]].state[links[processedStateKeys[keyIdx]][dataContext]] = | ||
nextState[dataContext]; | ||
} | ||
if(dataContext in links){ | ||
for(dataContext2 in links[dataContext]){ | ||
if(links[dataContext].hasOwnProperty(dataContext2)){ | ||
nextState[dataContext].state[links[dataContext][dataContext2]] = | ||
nextState[dataContext2]; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
if(appState.canRevert && calledBack){ | ||
prevState = appState.previousState; | ||
} else { | ||
prevState = appState; | ||
} | ||
} | ||
if(!!prevState){ | ||
Object.freeze(prevState); | ||
} | ||
//check the paths to see of there has been an path change | ||
pushStateChanged = nextState.path !== appState.path; | ||
try { | ||
appState = new ApplicationDataContext(nextState, prevState, redoState, | ||
enableUndo, routingEnabled, pushStateChanged, | ||
!external || nextState.pageNotFound, forget); | ||
Object.freeze(appState); | ||
Object.freeze(appState.state); | ||
if(typeof callback === 'function'){ | ||
calledBack = true; | ||
callback(void(0), appState); | ||
return; | ||
} | ||
} catch(e) { | ||
if(typeof callback === 'function'){ | ||
callback(e); | ||
return; | ||
} | ||
} finally { | ||
calledBack = false; | ||
transientState = {}; | ||
processedState = {}; | ||
} | ||
//All the work is done! -> Notify the View | ||
stateChangedHandler(appState); | ||
// Internal call routing | ||
if(routingEnabled && appState.pushState){ | ||
if(('path' in appState) && !external){ | ||
internal = true; | ||
if(pushStateChanged && !appState.forceReplace){ | ||
page(appState.path); | ||
} else { | ||
page.replace(appState.path); | ||
} | ||
} | ||
external = false; | ||
} | ||
}; | ||
//Initialize Application Data Context | ||
ApplicationDataContext = domainModel.call(this, appStateChangedHandler.bind(this, appNamespace)); | ||
appState = new ApplicationDataContext(void(0), void(0), void(0), enableUndo, routingEnabled); | ||
appState.state = appState.state || {}; | ||
domain = appState.constructor.originalSpec.getDomainDataContext(); | ||
delete appState.constructor.originalSpec.getDomainDataContext; | ||
//Initialize all dataContexts | ||
for(dataContext in domain){ | ||
if(domain.hasOwnProperty(dataContext)){ | ||
dataContexts[dataContext] = domain[dataContext].call(this, appStateChangedHandler.bind(this, dataContext)); | ||
appState.state[dataContext] = new dataContexts[dataContext](appState.state[dataContext], true); | ||
} | ||
} | ||
//Store links | ||
for(dataContext in domain){ | ||
if(domain.hasOwnProperty(dataContext)){ | ||
if('getWatchedState' in appState[dataContext].constructor.originalSpec){ | ||
watchedState = appState[dataContext].constructor.originalSpec.getWatchedState(); | ||
for(watchedItem in watchedState){ | ||
if(watchedState.hasOwnProperty(watchedItem)){ | ||
if(watchedItem in domain || watchedItem in appState){ | ||
if('alias' in watchedState[watchedItem]){ | ||
if(!(dataContext in links)){ | ||
links[dataContext] = {}; | ||
} | ||
links[dataContext][watchedItem] = watchedState[watchedItem].alias; | ||
if(!(watchedItem in domain)){ | ||
if(!(appNamespace in links)){ | ||
links[appNamespace] = {}; | ||
} | ||
if(!(dataContext in links[appNamespace])){ | ||
links[appNamespace][watchedItem] = {}; | ||
} | ||
links[appNamespace][watchedItem][dataContext] = watchedState[watchedItem].alias; | ||
} | ||
} | ||
for(watchedProp in watchedState[watchedItem].fields){ | ||
if(watchedState[watchedItem].fields.hasOwnProperty(watchedProp)){ | ||
if(watchedItem in domain){ | ||
watchedDataContext = {}; | ||
if(!(watchedItem in watchedDataContexts)){ | ||
watchedDataContexts[watchedItem] = {}; | ||
} | ||
watchedDataContext[watchedProp] = {}; | ||
watchedDataContext[watchedProp][dataContext] = | ||
watchedState[watchedItem].fields[watchedProp]; | ||
watchedDataContexts[watchedItem] = watchedDataContext; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
//apply links | ||
for(dataContext in domain){ | ||
if(domain.hasOwnProperty(dataContext)){ | ||
for(link in links[dataContext]){ | ||
if(links[dataContext].hasOwnProperty(link)){ | ||
appState[dataContext].state[links[dataContext][link]] = appState[link]; | ||
} | ||
} | ||
} | ||
} | ||
//reinitialize with all data in place | ||
for(dataContext in domain){ | ||
if(domain.hasOwnProperty(dataContext)){ | ||
appState.state[dataContext] = | ||
new dataContexts[dataContext](appState.state[dataContext]); | ||
if('getRoutes' in appState[dataContext].constructor.originalSpec){ | ||
routingEnabled = true; | ||
routeHash = appState[dataContext].constructor.originalSpec.getRoutes(); | ||
for(routePath in routeHash){ | ||
if(routeHash.hasOwnProperty(routePath)){ | ||
routeMapping[routeHash[routePath].path] = routeHash[routePath].handler; | ||
page(routeHash[routePath].path, function(dataContextName, route, | ||
pathKey, ctx){ | ||
external = true; | ||
if(!internal) { | ||
if(appState.canRevert && appState.pageNotFound){ | ||
ctx.rollbackRequest = true; | ||
ctx.revert = function(){ | ||
this.revert.bind(this); | ||
this.setState({},{path:ctx.path}); | ||
}.bind(appState); | ||
} | ||
routeMapping[route].call(appState[dataContextName], ctx.params, | ||
ctx.path, pathKey, ctx); | ||
} | ||
internal = false; | ||
}.bind(this, dataContext, routeHash[routePath].path, routePath)); | ||
} | ||
} | ||
delete appState[dataContext].constructor.originalSpec.getRoutes; | ||
} | ||
} | ||
} | ||
appState = new ApplicationDataContext(appState, void(0), void(0), | ||
enableUndo, routingEnabled); | ||
if(routingEnabled){ | ||
//Setup 'pageNotFound' route | ||
page(function(ctx){ | ||
external = true; | ||
appState.setState({'pageNotFound':true}); | ||
internal = false; | ||
}); | ||
//Initilize first path | ||
internal = true; | ||
page.replace(appState.path); | ||
page.start({click: false, dispatch: false}); | ||
external = false; | ||
} | ||
if('dataContextWillInitialize' in appState.constructor.originalSpec){ | ||
dataContextWillInitialize = true; | ||
appState.constructor.originalSpec.dataContextWillInitialize.call(appState); | ||
delete appState.constructor.originalSpec.dataContextWillInitialize; | ||
} | ||
for(dataContext in domain){ | ||
if(domain.hasOwnProperty(dataContext)){ | ||
if('dataContextWillInitialize' in appState[dataContext].constructor.originalSpec){ | ||
dataContextWillInitialize = true; | ||
appState[dataContext].constructor.originalSpec.dataContextWillInitialize.call(appState[dataContext]); | ||
delete appState[dataContext].constructor.originalSpec.dataContextWillInitialize; | ||
} | ||
} | ||
} | ||
if(!dataContextWillInitialize){ | ||
Object.freeze(appState.state); | ||
Object.freeze(appState); | ||
return appState; | ||
} | ||
}; | ||
},{"./utils":8,"page":2}],8:[function(_dereq_,module,exports){ | ||
var utils = { | ||
extend: function () { | ||
var newObj = {}; | ||
for (var i = 0; i < arguments.length; i++) { | ||
var obj = arguments[i]; | ||
for (var key in obj) { | ||
if (obj.hasOwnProperty(key)) { | ||
newObj[key] = obj[key]; | ||
} | ||
} | ||
} | ||
return newObj; | ||
}, | ||
mixInto: function(constructor, methodBag) { | ||
var methodName; | ||
for (methodName in methodBag) { | ||
if (!methodBag.hasOwnProperty(methodName)) { | ||
continue; | ||
} | ||
constructor.prototype[methodName] = methodBag[methodName]; | ||
} | ||
} | ||
}; | ||
module.exports = utils; | ||
},{}],9:[function(_dereq_,module,exports){ | ||
var utils = _dereq_('./utils'); | ||
var extend = utils.extend; | ||
var IMVVMViewModel = { | ||
var ViewModel = { | ||
Mixin: { | ||
@@ -691,3 +1457,3 @@ construct: function(stateChangedHandler){ | ||
var dataContext = function(nextState, initialize) { | ||
var ViewModelClass = function(nextState, initialize) { | ||
@@ -697,3 +1463,3 @@ //nextState has already been extended with prevState in core | ||
nextState = ('state' in nextState ? nextState.state : nextState); | ||
var freezeFields = desc.freezeFields, | ||
@@ -715,3 +1481,3 @@ fld, | ||
extend(nextState, desc.originalSpec.getInitialState.call(viewModel)) : nextState; | ||
Object.defineProperty(viewModel, 'state', { | ||
@@ -723,3 +1489,2 @@ configurable: true, | ||
}); | ||
} | ||
@@ -733,3 +1498,3 @@ | ||
tempModel = Object.create(tempDesc.proto, tempDesc.descriptor); | ||
tempModel.__stateChangedHandler = (function(fld){ | ||
@@ -765,7 +1530,6 @@ return viewModel[fld].__stateChangedHandler; | ||
}); | ||
return Object.freeze(viewModel); | ||
}; | ||
return dataContext; | ||
return ViewModelClass; | ||
} | ||
@@ -775,54 +1539,6 @@ } | ||
module.exports = IMVVMViewModel; | ||
module.exports = ViewModel; | ||
},{"./utils":8}],7:[function(_dereq_,module,exports){ | ||
var core = _dereq_('./core'); | ||
var NAMESPACE = '__IMVVM__'; | ||
var mixin = { | ||
stateChangedHandler: function(dataContext){ | ||
this.setState({domainDataContext: dataContext}) | ||
}, | ||
getInitialState: function(){ | ||
var dataContext = core.getInitialState(NAMESPACE, this.props.domainModel, | ||
this.stateChangedHandler, this.props.enableUndo); | ||
return {domainDataContext: dataContext}; | ||
} | ||
}; | ||
module.exports = mixin; | ||
},{"./core":2}],8:[function(_dereq_,module,exports){ | ||
var utils = { | ||
extend: function () { | ||
var newObj = {}; | ||
for (var i = 0; i < arguments.length; i++) { | ||
var obj = arguments[i]; | ||
for (var key in obj) { | ||
if (obj.hasOwnProperty(key)) { | ||
newObj[key] = obj[key]; | ||
} | ||
} | ||
} | ||
return newObj; | ||
}, | ||
mixInto: function(constructor, methodBag) { | ||
var methodName; | ||
for (methodName in methodBag) { | ||
if (!methodBag.hasOwnProperty(methodName)) { | ||
continue; | ||
} | ||
constructor.prototype[methodName] = methodBag[methodName]; | ||
} | ||
} | ||
}; | ||
module.exports = utils; | ||
},{}]},{},[1]) | ||
},{"./utils":8}]},{},[1]) | ||
(1) | ||
}); |
@@ -1,1 +0,1 @@ | ||
!function(b){if("object"==typeof exports){module.exports=b()}else{if("function"==typeof define&&define.amd){define(b)}else{var a;"undefined"!=typeof window?a=window:"undefined"!=typeof global?a=global:"undefined"!=typeof self&&(a=self),a.IMVVM=b()}}}(function(){var d,b,a;return(function c(f,k,h){function g(n,l){if(!k[n]){if(!f[n]){var i=typeof require=="function"&&require;if(!l&&i){return i(n,!0)}if(e){return e(n,!0)}throw new Error("Cannot find module '"+n+"'")}var m=k[n]={exports:{}};f[n][0].call(m.exports,function(o){var p=f[n][1][o];return g(p?p:o)},m,m.exports,c,f,k,h)}return k[n].exports}var e=typeof require=="function"&&require;for(var j=0;j<h.length;j++){g(h[j])}return g})({1:[function(h,g,f){var e=h("./src/imvvm.js");g.exports=e},{"./src/imvvm.js":3}],2:[function(h,g,f){var e=h("./utils");var i=e.extend;f.getInitialState=function(m,n,w,l){if(typeof w!=="function"){throw new TypeError("stateChangedHandler must be a function!")}l===void (0)?true:l;var B,v={},s={},C,k={},z={},u,r={},p={},y,x,A,t,q,j=false;var o=function(F,K,M,R){var T={},P=void (0),H=void (0),N,I,G,E,J,L=[],Q,O,D,S;if(typeof M==="function"){R=M;M={}}K=K||{};N=Object.keys(K);if(Object.getPrototypeOf(K).constructor.classType==="DomainViewModel"){T=i(K);P=K.previousState;H=M;for(E in C){T[E]=new s[E](T[E])}for(E in C){if(C.hasOwnProperty(E)){for(J in k[E]){if(k[E].hasOwnProperty(J)){T[E].state[k[E][J]]=(J in C)?i(T[J].state):T[J]}}}}if(typeof R==="function"){v=new B(T,P,H,l);R(v);return}}else{if(!!N.length){if(F===m){T=i(K)}else{T[F]=i(K)}}r=i(T,r,M);transientStateKeys=Object.keys(r);if(transientStateKeys.length===0){return}G=transientStateKeys.length-1;for(I=G;I>=0;I--){if(transientStateKeys[I] in C){T[transientStateKeys[I]]=i(v[transientStateKeys[I]],r[transientStateKeys[I]]);T[transientStateKeys[I]]=new s[transientStateKeys[I]](T[transientStateKeys[I]])}else{T[transientStateKeys[I]]=r[transientStateKeys[I]]}}p=i(p,T);T=i(v,p);r={};for(I=G;I>=0;I--){if(transientStateKeys[I] in z){for(O in z[transientStateKeys[I]]){if(z[transientStateKeys[I]].hasOwnProperty(O)){if(N.indexOf(O)!==-1){D=z[transientStateKeys[I]][O];for(S in D){if(D.hasOwnProperty(S)){if(S in k){for(E in k[S]){if(k[S].hasOwnProperty(E)){T[S].state[k[S][E]]=(E in C)?i(T[E].state):T[E]}if(E in k){for(dataContext2 in k[E]){if(k[E].hasOwnProperty(dataContext2)){T[E].state[k[E][dataContext2]]=(dataContext2 in C)?i(T[dataContext2].state):T[dataContext2]}}}}}r=i(r,D[S].call(v[S],T[transientStateKeys[I]][O],v[transientStateKeys[I]][O],O,transientStateKeys[I]))}}}}}}}if(!!Object.keys(r).length){o(void (0),{},r,R);return}L=Object.keys(p);Q=L.length-1;for(I=Q;I>=0;I--){if(F===m&&(m in k)){if(L[I] in k[m]){for(E in k[m][L[I]]){if(k[m][L[I]].hasOwnProperty(E)){T[E].state[k[m][L[I]][E]]=T[L[I]]}if(E in k){for(dataContext2 in k[E]){if(k[E].hasOwnProperty(dataContext2)){T[E].state[k[E][dataContext2]]=(dataContext2 in C)?i(T[dataContext2].state):T[dataContext2]}}}}}}else{if(L[I] in k){for(E in k[L[I]]){if(k[L[I]].hasOwnProperty(E)){T[L[I]].state[k[L[I]][E]]=(E in C)?i(T[E].state):T[E]}if(E in k){for(dataContext2 in k[E]){if(k[E].hasOwnProperty(dataContext2)){T[E].state[k[E][dataContext2]]=(dataContext2 in C)?i(T[dataContext2].state):T[dataContext2]}}}}}}}P=j?v.previousState:v}if(!!P){Object.freeze(P)}v=new B(T,P,H,l);Object.freeze(v);Object.freeze(v.state);if(typeof R==="function"){j=true;R(v);return}w(v);j=false;r={};p={}};B=n.call(this,o.bind(this,m));v=new B(void (0),void (0),void (0),l);v.state=v.state||{};C=v.constructor.originalSpec.getDomainDataContext();delete v.constructor.originalSpec.getDomainDataContext;for(u in C){if(C.hasOwnProperty(u)){s[u]=C[u].call(this,o.bind(this,u));v.state[u]=new s[u](v.state[u],true)}}for(u in C){if(C.hasOwnProperty(u)){if("getWatchedState" in v[u].constructor.originalSpec){y=v[u].constructor.originalSpec.getWatchedState();for(x in y){if(y.hasOwnProperty(x)){if(x in C||x in v.state){if("alias" in y[x]){if(!(u in k)){k[u]={}}k[u][x]=y[x].alias;if(!(x in C)){if(!(m in k)){k[m]={}}if(!(u in k[m])){k[m][x]={}}k[m][x][u]=y[x].alias}}for(A in y[x].fields){if(y[x].fields.hasOwnProperty(A)){if(x in C){t={};if(!(x in z)){z[x]={}}t[A]={};t[A][u]=y[x].fields[A];z[x]=t}}}}}}}}}for(u in C){if(C.hasOwnProperty(u)){for(q in k[u]){if(k[u].hasOwnProperty(q)){v[u].state[k[u][q]]=(q in C)?i(v[q].state):v[q]}}}}for(u in C){if(C.hasOwnProperty(u)){v.state[u]=new s[u](v.state[u])}}v=new B(v,void (0),void (0),l);Object.freeze(v.state);Object.freeze(v);return v}},{"./utils":8}],3:[function(f,g,j){var k=f("./imvvmModel");var p=f("./imvvmViewModel");var i=f("./imvvmDomainViewModel");var r=f("./mixin");var q=f("./utils");var o=q.extend;var m=q.mixInto;var l=function(){};var h=function(){};var n=function(){};m(l,k.Mixin);m(h,p.Mixin);m(n,i.Mixin);var e={createClass:function(u,v,t){var y=function(){};y.prototype=new u();y.prototype.constructor=y;var x=y;var w=function(z){var A=new x();return A.construct.apply(w,arguments)};w.componentConstructor=y;y.ConvenienceConstructor=w;w.originalSpec=t;w.type=y;y.prototype.type=y;w.classType=v;y.prototype.classType=v;w.getDescriptor=function(){var E={},D=this.prototype,B={},z=[],A={},C;if("__processedSpec__" in this.originalSpec){return this.originalSpec.__processedSpec__}for(C in this.originalSpec){if(this.originalSpec.hasOwnProperty(C)){if("get" in this.originalSpec[C]||"set" in this.originalSpec[C]){this.originalSpec[C].enumerable=true;if("viewModel" in this.originalSpec[C]){B[C]=this.originalSpec[C].viewModel;delete this.originalSpec[C].viewModel;delete this.originalSpec[C].set}else{if("aliasFor" in this.originalSpec[C]){A[this.originalSpec[C].aliasFor]=C;delete this.originalSpec[C].aliasFor}if("kind" in this.originalSpec[C]){if(this.originalSpec[C].kind==="pseudo"){this.originalSpec[C].enumerable=false}else{if(this.originalSpec[C].kind==="instance"||this.originalSpec[C].kind==="array"){z.push({fieldName:C,kind:this.originalSpec[C].kind})}else{throw new TypeError('"'+this.originalSpec[C].kind+'" is not a valid "kind" value. Valid values are "pseudo","instance" and "array". Please review field "'+C+'".')}}delete this.originalSpec[C].kind}}E[C]=this.originalSpec[C]}else{if(C!=="getInitialState"&&C!=="getWatchedState"){D[C]=this.originalSpec[C]}}}}if(!!Object.keys(B).length){this.originalSpec.getDomainDataContext=function(){return B}}this.originalSpec.__processedSpec__={descriptor:E,proto:D,originalSpec:this.originalSpec||{},freezeFields:z,aliases:A};return this.originalSpec.__processedSpec__};return w}};var s={createModel:e.createClass.bind(this,l,"Model"),createViewModel:e.createClass.bind(this,h,"ViewModel"),createDomainViewModel:e.createClass.bind(this,n,"DomainViewModel"),mixin:r,extend:o};g.exports=s},{"./imvvmDomainViewModel":4,"./imvvmModel":5,"./imvvmViewModel":6,"./mixin":7,"./utils":8}],4:[function(i,h,g){var f=i("./utils");var j=f.extend;var e={Mixin:{construct:function(l){var m=this.getDescriptor();m.proto.setState=l;m.proto.revert=function(){this.setState(this.previousState,!!this.previousState?this:void (0))};m.proto.advance=function(){if(this.canAdvance){this.setState(this.nextState,this.nextState.nextState)}};var k=function(p,t,o,s){var r=m.freezeFields,q=Object.create(m.proto,m.descriptor),n;if(!!s){if(!!t){Object.defineProperty(q,"previousState",{configurable:false,enumerable:false,writable:false,value:t});Object.defineProperty(q,"canRevert",{configurable:false,enumerable:false,writable:false,value:true})}else{Object.defineProperty(q,"canRevert",{configurable:false,enumerable:false,writable:false,value:false})}if(!!o&&"state" in o){Object.defineProperty(q,"nextState",{configurable:false,enumerable:false,writable:false,value:o});Object.defineProperty(q,"canAdvance",{configurable:false,enumerable:false,writable:false,value:true})}else{Object.defineProperty(q,"canAdvance",{configurable:false,enumerable:false,writable:false,value:false})}}if(p===void (0)){p=("getInitialState" in m.originalSpec)?m.originalSpec.getInitialState.call(q):{}}else{if("state" in p){delete p.state;Object.defineProperty(q,"state",{configurable:true,enumerable:false,writable:true,value:p});p=j(p,q)}}for(n=r.length-1;n>=0;n--){if(r[n].kind==="array"){p[r[n].fieldName]=p[r[n].fieldName]||[];Object.freeze(p[r[n].fieldName])}else{throw new TypeError('kind:"instance" can only be specified in a ViewModel.')}}Object.defineProperty(q,"state",{configurable:false,enumerable:false,writable:false,value:p});return q};return k}}};h.exports=e},{"./utils":8}],5:[function(h,g,f){var e=h("./utils");var j=e.extend;var i={Mixin:{construct:function(l){var m=this.getDescriptor();var k=function(r,p,n){var s=m.freezeFields,q,o=Object.create(m.proto,m.descriptor);if(r===void (0)){n=true}else{if(typeof r==="boolean"){n=r;r=void (0)}else{if(typeof p==="boolean"){n=p;p=void (0)}}}r=j(r,p);Object.defineProperty(o,"state",{configurable:true,enumerable:false,writable:true,value:r});r=j(r,o);if(n){for(var t in m.aliases){if(m.aliases.hasOwnProperty(t)&&t in r){r[m.aliases[t]]=r[t];delete r[t]}}Object.defineProperty(o,"state",{configurable:true,enumerable:false,writable:true,value:r});r=j(r,o);if("getInitialState" in m.originalSpec){r=j(r,m.originalSpec.getInitialState.call(o))}}for(q=s.length-1;q>=0;q--){if(s[q].kind==="array"){r[s[q].fieldName]=r[s[q].fieldName]||[];Object.freeze(r[s[q].fieldName])}else{throw new TypeError('kind:"instance" can only be specified in a ViewModel.')}}Object.defineProperty(o,"state",{configurable:false,enumerable:false,writable:false,value:r});o.__stateChangedHandler=(function(){return l})();return Object.freeze(o)};return k}}};g.exports=i},{"./utils":8}],6:[function(i,h,g){var f=i("./utils");var j=f.extend;var e={Mixin:{construct:function(l){var m=this.getDescriptor(this);m.proto.setState=l;var k=function(r,n){r=r||{};r=("state" in r?r.state:r);var s=m.freezeFields,q,p=Object.create(m.proto,m.descriptor),t,o;Object.defineProperty(p,"state",{configurable:true,enumerable:false,writable:true,value:r});if(n){r=("getInitialState" in m.originalSpec)?j(r,m.originalSpec.getInitialState.call(p)):r;Object.defineProperty(p,"state",{configurable:true,enumerable:false,writable:true,value:r})}for(q=s.length-1;q>=0;q--){if(s[q].kind==="instance"){if(p[s[q].fieldName]){t=p[s[q].fieldName].constructor.originalSpec.__processedSpec__;o=Object.create(t.proto,t.descriptor);o.__stateChangedHandler=(function(u){return p[u].__stateChangedHandler})(s[q].fieldName);Object.defineProperty(o,"state",{configurable:true,enumerable:false,writable:true,value:p[s[q].fieldName].state});o.__proto__.setState=function(u,v){this.__stateChangedHandler.call(p,j(this.state,u),v)};Object.freeze(p[s[q].fieldName])}}else{r[s[q].fieldName]=r[s[q].fieldName]||[];Object.freeze(r[s[q].fieldName])}}Object.defineProperty(p,"state",{configurable:false,enumerable:false,writable:false,value:r});return Object.freeze(p)};return k}}};h.exports=e},{"./utils":8}],7:[function(j,i,h){var f=j("./core");var e="__IMVVM__";var g={stateChangedHandler:function(k){this.setState({domainDataContext:k})},getInitialState:function(){var k=f.getInitialState(e,this.props.domainModel,this.stateChangedHandler,this.props.enableUndo);return{domainDataContext:k}}};i.exports=g},{"./core":2}],8:[function(h,g,f){var e={extend:function(){var j={};for(var l=0;l<arguments.length;l++){var m=arguments[l];for(var k in m){if(m.hasOwnProperty(k)){j[k]=m[k]}}}return j},mixInto:function(j,k){var i;for(i in k){if(!k.hasOwnProperty(i)){continue}j.prototype[i]=k[i]}}};g.exports=e},{}]},{},[1])(1)}); | ||
!function(b){if("object"==typeof exports){module.exports=b()}else{if("function"==typeof define&&define.amd){define(b)}else{var a;"undefined"!=typeof window?a=window:"undefined"!=typeof global?a=global:"undefined"!=typeof self&&(a=self),a.IMVVM=b()}}}(function(){var d,b,a;return(function c(f,k,h){function g(n,l){if(!k[n]){if(!f[n]){var i=typeof require=="function"&&require;if(!l&&i){return i(n,!0)}if(e){return e(n,!0)}throw new Error("Cannot find module '"+n+"'")}var m=k[n]={exports:{}};f[n][0].call(m.exports,function(o){var p=f[n][1][o];return g(p?p:o)},m,m.exports,c,f,k,h)}return k[n].exports}var e=typeof require=="function"&&require;for(var j=0;j<h.length;j++){g(h[j])}return g})({1:[function(h,g,f){var e=h("./src/core.js");g.exports=e},{"./src/core.js":3}],2:[function(g,f,e){(function(){var o=true;var h="";var l;function p(w,v){if("function"==typeof w){return p("*",w)}if("function"==typeof v){var t=new m(w);for(var u=1;u<arguments.length;++u){p.callbacks.push(t.middleware(arguments[u]))}}else{if("string"==typeof w){p.show(w,v)}else{p.start(w)}}}p.callbacks=[];p.base=function(t){if(0==arguments.length){return h}h=t};p.start=function(u){u=u||{};if(l){return}l=true;if(false===u.dispatch){o=false}if(false!==u.popstate){window.addEventListener("popstate",q,false)}if(false!==u.click){window.addEventListener("click",s,false)}if(!o){return}var t=location.pathname+location.search+location.hash;p.replace(t,null,true,o)};p.stop=function(){l=false;removeEventListener("click",s,false);removeEventListener("popstate",q,false)};p.show=function(w,v,u){var t=new r(w,v);if(false!==u){p.dispatch(t)}if(!t.unhandled){t.pushState()}return t};p.replace=function(w,v,x,u){var t=new r(w,v);t.init=x;if(null==u){u=true}if(u){p.dispatch(t)}t.save();return t};p.dispatch=function(t){var u=0;function v(){var w=p.callbacks[u++];if(!w){return j(t)}w(t,v)}v()};function j(t){var u=window.location.pathname+window.location.search;if(u==t.canonicalPath){return}p.stop();t.unhandled=true;window.location=t.canonicalPath}function r(w,u){if("/"==w[0]&&0!=w.indexOf(h)){w=h+w}var t=w.indexOf("?");this.canonicalPath=w;this.path=w.replace(h,"")||"/";this.title=document.title;this.state=u||{};this.state.path=w;this.querystring=~t?w.slice(t+1):"";this.pathname=~t?w.slice(0,t):w;this.params=[];this.hash="";if(!~this.path.indexOf("#")){return}var v=this.path.split("#");this.path=v[0];this.hash=v[1]||"";this.querystring=this.querystring.split("#")[0]}p.Context=r;r.prototype.pushState=function(){history.pushState(this.state,this.title,this.canonicalPath)};r.prototype.save=function(){history.replaceState(this.state,this.title,this.canonicalPath)};function m(u,t){t=t||{};this.path=u;this.method="GET";this.regexp=n(u,this.keys=[],t.sensitive,t.strict)}p.Route=m;m.prototype.middleware=function(u){var t=this;return function(v,w){if(t.match(v.path,v.params)){return u(v,w)}w()}};m.prototype.match=function(B,w){var A=this.keys,C=B.indexOf("?"),t=~C?B.slice(0,C):B,v=this.regexp.exec(t);if(!v){return false}for(var x=1,y=v.length;x<y;++x){var z=A[x-1];var u="string"==typeof v[x]?decodeURIComponent(v[x]):v[x];if(z){w[z.name]=undefined!==w[z.name]?w[z.name]:u}else{w.push(u)}}return true};function n(w,v,u,t){if(w instanceof RegExp){return w}if(w instanceof Array){w="("+w.join("|")+")"}w=w.concat(t?"":"/?").replace(/\/\(/g,"(?:/").replace(/(\/)?(\.)?:(\w+)(?:(\(.*?\)))?(\?)?/g,function(z,B,C,A,x,y){v.push({name:A,optional:!!y});B=B||"";return""+(y?"":B)+"(?:"+(y?B:"")+(C||"")+(x||(C&&"([^/.]+?)"||"([^/]+?)"))+")"+(y||"")}).replace(/([\/.])/g,"\\$1").replace(/\*/g,"(.*)");return new RegExp("^"+w+"$",u?"":"i")}function q(u){if(u.state){var t=u.state.path;p.replace(t,u.state)}}function s(w){if(1!=i(w)){return}if(w.metaKey||w.ctrlKey||w.shiftKey){return}if(w.defaultPrevented){return}var t=w.target;while(t&&"A"!=t.nodeName){t=t.parentNode}if(!t||"A"!=t.nodeName){return}var u=t.getAttribute("href");if(t.pathname==location.pathname&&(t.hash||"#"==u)){return}if(t.target){return}if(!k(t.href)){return}var v=t.pathname+t.search+(t.hash||"");var x=v+t.hash;v=v.replace(h,"");if(h&&x==v){return}w.preventDefault();p.show(x)}function i(t){t=t||window.event;return null==t.which?t.button:t.which}function k(u){var t=location.protocol+"//"+location.hostname;if(location.port){t+=":"+location.port}return 0==u.indexOf(t)}if("undefined"==typeof f){window.page=p}else{f.exports=p}})()},{}],3:[function(f,g,j){var k=f("./model");var r=f("./viewModel");var i=f("./domainViewModel");var s=f("./mixin");var p=f("page");var q=f("./utils");var o=q.extend;var m=q.mixInto;var l=function(){};var h=function(){};var n=function(){};m(l,k.Mixin);m(h,r.Mixin);m(n,i.Mixin);var e={createClass:function(u,v,t){var y=function(){};y.prototype=new u();y.prototype.constructor=y;var x=y;var w=function(z){var A=new x();return A.construct.apply(w,arguments)};w.componentConstructor=y;y.ConvenienceConstructor=w;w.originalSpec=t;w.type=y;y.prototype.type=y;w.classType=v;y.prototype.classType=v;w.getDescriptor=function(){var E={},D=this.prototype,B={},z=[],A={},C;if("__processedSpec__" in this.originalSpec){return this.originalSpec.__processedSpec__}for(C in this.originalSpec){if(this.originalSpec.hasOwnProperty(C)){if("get" in this.originalSpec[C]||"set" in this.originalSpec[C]){this.originalSpec[C].enumerable=true;if("viewModel" in this.originalSpec[C]){B[C]=this.originalSpec[C].viewModel;delete this.originalSpec[C].viewModel;delete this.originalSpec[C].set}else{if("aliasFor" in this.originalSpec[C]){A[this.originalSpec[C].aliasFor]=C;delete this.originalSpec[C].aliasFor}if("kind" in this.originalSpec[C]){if(this.originalSpec[C].kind==="pseudo"){this.originalSpec[C].enumerable=false}else{if(this.originalSpec[C].kind==="uid"){}else{if(this.originalSpec[C].kind==="instance"||this.originalSpec[C].kind==="array"){z.push({fieldName:C,kind:this.originalSpec[C].kind})}else{throw new TypeError('"'+this.originalSpec[C].kind+'" is not a valid "kind" value. Please review field "'+C+'".')}}}delete this.originalSpec[C].kind}}E[C]=this.originalSpec[C]}else{if(C!=="getInitialState"&&C!=="getWatchedState"&&C!=="getRoutes"){D[C]=this.originalSpec[C]}}}}if(!!Object.keys(B).length){this.originalSpec.getDomainDataContext=function(){return B}}this.originalSpec.__processedSpec__={descriptor:E,proto:D,originalSpec:this.originalSpec||{},freezeFields:z,aliases:A};return this.originalSpec.__processedSpec__};return w}};g.exports={createModel:e.createClass.bind(this,l,"Model"),createViewModel:e.createClass.bind(this,h,"ViewModel"),createDomainViewModel:e.createClass.bind(this,n,"DomainViewModel"),mixin:s,extend:o,page:p}},{"./domainViewModel":4,"./mixin":5,"./model":6,"./utils":8,"./viewModel":9,page:2}],4:[function(i,h,g){var f=i("./utils");var j=f.extend;var e={Mixin:{construct:function(l){var n=false;var k=false;var o=this.getDescriptor();o.proto.setState=l;o.proto.revert=function(){this.setState(this.previousState,!!this.previousState?this:void (0))};o.proto.advance=function(){if(this.canAdvance){this.setState(this.nextState,this.nextState.nextState)}};var m=function(D,B,v,r,E,w,x,q){var u=o.freezeFields,A=Object.create(o.proto,o.descriptor),t,C=D===void (0),p,y,z,s;w=E?w:false;if(!C){if(E){y=D.forceReplace===void (0)?false:D.forceReplace;z=D.pushState===void (0)?true:D.pushState;s=D.pageNotFound===void (0)?false:D.pageNotFound;Object.defineProperty(A,"pageNotFound",{configurable:false,enumerable:false,writable:false,value:s});Object.defineProperty(A,"forceReplace",{configurable:false,enumerable:true,writable:false,value:y});Object.defineProperty(A,"pushState",{configurable:false,enumerable:true,writable:false,value:z});if(!("path" in A)&&("path" in D)){Object.defineProperty(A,"path",{configurable:false,enumerable:true,writable:false,value:D.path})}}if(D.enableUndo===void (0)){p=false}else{r=D.enableUndo;p=D.enableUndo;if(!D.enableUndo){E=false}}}if(r||E){if(!!B&&(!w||p||s)&&!previousAdhoc&&x&&!q){previousAdhoc=p;k=s;Object.defineProperty(A,"previousState",{configurable:false,enumerable:false,writable:false,value:B});Object.defineProperty(A,"canRevert",{configurable:false,enumerable:false,writable:false,value:true})}else{Object.defineProperty(A,"canRevert",{configurable:false,enumerable:false,writable:false,value:false})}if(!!v&&("state" in v)&&!previousAdhoc&&!k){Object.defineProperty(A,"nextState",{configurable:false,enumerable:false,writable:false,value:v});Object.defineProperty(A,"canAdvance",{configurable:false,enumerable:false,writable:false,value:true})}else{previousAdhoc=p;k=s;Object.defineProperty(A,"canAdvance",{configurable:false,enumerable:false,writable:false,value:false})}}if(C){D=("getInitialState" in o.originalSpec)?o.originalSpec.getInitialState.call(A):{};if("path" in D){Object.defineProperty(A,"path",{configurable:false,enumerable:true,writable:false,value:D.path})}}else{if("state" in D){delete D.state;Object.defineProperty(A,"state",{configurable:true,enumerable:false,writable:true,value:D});D=j(D,A)}}for(t=u.length-1;t>=0;t--){if(u[t].kind==="array"){D[u[t].fieldName]=D[u[t].fieldName]||[];Object.freeze(D[u[t].fieldName])}else{throw new TypeError('kind:"instance" can only be specified in a ViewModel.')}}Object.defineProperty(A,"state",{configurable:false,enumerable:false,writable:false,value:D});return A};return m}}};h.exports=e},{"./utils":8}],5:[function(j,i,h){var f=j("./stateController");var e="__IMVVM__";var g={main:{stateChangedHandler:function(k){this.setState({domainDataContext:k})},getInitialState:function(){var k=f.getInitialState(e,this.props.domainModel,this.stateChangedHandler,this.props.enableUndo);return{domainDataContext:k}}},pushState:{componentDidMount:function(){$(this.getDOMNode()).click(this.onclick)},componentWillUnmount:function(){$(this.getDOMNode()).unbind("click")},which:function(k){k=k||window.event;return null==k.which?k.button:k.which},sameOrigin:function(l){var k=location.protocol+"//"+location.hostname;if(location.port){k+=":"+location.port}return 0==l.indexOf(k)},onclick:function(n){if(1!=this.which(n)){return}if(n.metaKey||n.ctrlKey||n.shiftKey){return}if(n.defaultPrevented){return}var k=n.target;while(k&&"A"!=k.nodeName){k=k.parentNode}if(!k||"A"!=k.nodeName){return}var l=k.getAttribute("href");if(k.pathname==location.pathname&&(k.hash||"#"==l)){return}if(k.target){return}if(!this.sameOrigin(k.href)){return}var m=k.pathname+k.search+(k.hash||"");var o=m+k.hash;n.preventDefault();m=m.replace(IMVVM.page.base(),"");if(IMVVM.page.base()&&o==m||k.href===k.baseURI+k.search+(k.hash||"")){return}IMVVM.page.show(o)}}};i.exports=g},{"./stateController":7}],6:[function(h,g,f){var e=h("./utils");var j=e.extend;var i={Mixin:{construct:function(k){var m=this.getDescriptor();var l=function(r,p,n){var s=m.freezeFields,q,o=Object.create(m.proto,m.descriptor);if(r===void (0)){n=true}else{if(typeof r==="boolean"){n=r;r=void (0)}else{if(typeof p==="boolean"){n=p;p=void (0)}}}r=j(r,p);Object.defineProperty(o,"state",{configurable:true,enumerable:false,writable:true,value:r});r=j(r,o);if(n){for(var t in m.aliases){if(m.aliases.hasOwnProperty(t)&&t in r){r[m.aliases[t]]=r[t];delete r[t]}}Object.defineProperty(o,"state",{configurable:true,enumerable:false,writable:true,value:r});r=j(r,o);if("getInitialState" in m.originalSpec){r=j(r,m.originalSpec.getInitialState.call(o))}}for(q=s.length-1;q>=0;q--){if(s[q].kind==="array"){r[s[q].fieldName]=r[s[q].fieldName]||[];Object.freeze(r[s[q].fieldName])}else{throw new TypeError('kind:"instance" can only be specified in a ViewModel.')}}Object.defineProperty(o,"state",{configurable:false,enumerable:false,writable:false,value:r});o.__stateChangedHandler=(function(){return k})();return Object.freeze(o)};return l}}};g.exports=i},{"./utils":8}],7:[function(h,g,f){var i=h("page");var e=h("./utils");var j=e.extend;f.getInitialState=function(o,p,A,n){if(typeof A!=="function"){throw new TypeError("stateChangedHandler must be a function.")}if(n===void (0)){n=false}var J,z={},v={},K,m={},E={},x,u={},s={},D,C,G,w,t,l=false,I=false,B={},H={},k,r=false,y=false,F=false;var q=function(aa,L,M,ab,Q){var T={},R=void (0),ae=void (0),V,S,O,U,W,ad=[],Y,Z,P,X,N=false;if(arguments.length<2){A(z);return}if(typeof ab==="function"){Q=ab;ab=false}else{if(typeof M==="function"){Q=M;M={}}else{if(typeof M==="boolean"){ab=M;M={}}}}if(ab===void (0)){ab=false}L=L||{};V=Object.keys(L);if(Object.getPrototypeOf(L).constructor.classType==="DomainViewModel"){T=j(L);R=L.previousState;ae=M;for(U in K){T[U]=new v[U](T[U])}for(U in K){if(K.hasOwnProperty(U)){for(W in m[U]){if(m[U].hasOwnProperty(W)){T[U].state[m[U][W]]=(W in K)?j(T[W].state):T[W]}}}}if(typeof Q==="function"){try{z=new J(T,R,ae,n,I,T.path!==z.path,!r||T.pageNotFound,ab);l=true;Q(void (0),z)}catch(ac){Q(ac)}return}}else{if(!!V.length){if(aa===o){T=j(L)}else{T[aa]=j(L)}}u=j(T,u,M);transientStateKeys=Object.keys(u);if(transientStateKeys.length===0){return}O=transientStateKeys.length-1;for(S=O;S>=0;S--){if(transientStateKeys[S] in K){T[transientStateKeys[S]]=j(z[transientStateKeys[S]],u[transientStateKeys[S]]);T[transientStateKeys[S]]=new v[transientStateKeys[S]](T[transientStateKeys[S]])}else{T[transientStateKeys[S]]=u[transientStateKeys[S]]}}s=j(s,T);T=j(z,s);u={};for(S=O;S>=0;S--){if(transientStateKeys[S] in E){for(Z in E[transientStateKeys[S]]){if(E[transientStateKeys[S]].hasOwnProperty(Z)){if(V.indexOf(Z)!==-1){P=E[transientStateKeys[S]][Z];for(X in P){if(P.hasOwnProperty(X)){if(X in m){for(U in m[X]){if(m[X].hasOwnProperty(U)){T[X].state[m[X][U]]=(U in K)?j(T[U].state):T[U]}if(U in m){for(dataContext2 in m[U]){if(m[U].hasOwnProperty(dataContext2)){T[U].state[m[U][dataContext2]]=(dataContext2 in K)?j(T[dataContext2].state):T[dataContext2]}}}}}u=j(u,P[X].call(z[X],T[transientStateKeys[S]][Z],z[transientStateKeys[S]][Z],Z,transientStateKeys[S],T.path,z.path))}}}}}}}if(!!Object.keys(u).length){q(void (0),{},u,ab,Q);return}ad=Object.keys(s);Y=ad.length-1;for(S=Y;S>=0;S--){if(aa===o&&(o in m)){if(ad[S] in m[o]){for(U in m[o][ad[S]]){if(m[o][ad[S]].hasOwnProperty(U)){T[U].state[m[o][ad[S]][U]]=T[ad[S]]}if(U in m){for(dataContext2 in m[U]){if(m[U].hasOwnProperty(dataContext2)){T[U].state[m[U][dataContext2]]=(dataContext2 in K)?j(T[dataContext2].state):T[dataContext2]}}}}}}else{if(ad[S] in m){for(U in m[ad[S]]){if(m[ad[S]].hasOwnProperty(U)){T[ad[S]].state[m[ad[S]][U]]=T[U]}if(U in m){for(dataContext2 in m[U]){if(m[U].hasOwnProperty(dataContext2)){T[U].state[m[U][dataContext2]]=T[dataContext2]}}}}}}}if(z.canRevert&&l){R=z.previousState}else{R=z}}if(!!R){Object.freeze(R)}N=T.path!==z.path;try{z=new J(T,R,ae,n,I,N,!r||T.pageNotFound,ab);Object.freeze(z);Object.freeze(z.state);if(typeof Q==="function"){l=true;Q(void (0),z);return}}catch(ac){if(typeof Q==="function"){Q(ac);return}}finally{l=false;u={};s={}}A(z);if(I&&z.pushState){if(("path" in z)&&!r){y=true;if(N&&!z.forceReplace){i(z.path)}else{i.replace(z.path)}}r=false}};J=p.call(this,q.bind(this,o));z=new J(void (0),void (0),void (0),n,I);z.state=z.state||{};K=z.constructor.originalSpec.getDomainDataContext();delete z.constructor.originalSpec.getDomainDataContext;for(x in K){if(K.hasOwnProperty(x)){v[x]=K[x].call(this,q.bind(this,x));z.state[x]=new v[x](z.state[x],true)}}for(x in K){if(K.hasOwnProperty(x)){if("getWatchedState" in z[x].constructor.originalSpec){D=z[x].constructor.originalSpec.getWatchedState();for(C in D){if(D.hasOwnProperty(C)){if(C in K||C in z){if("alias" in D[C]){if(!(x in m)){m[x]={}}m[x][C]=D[C].alias;if(!(C in K)){if(!(o in m)){m[o]={}}if(!(x in m[o])){m[o][C]={}}m[o][C][x]=D[C].alias}}for(G in D[C].fields){if(D[C].fields.hasOwnProperty(G)){if(C in K){w={};if(!(C in E)){E[C]={}}w[G]={};w[G][x]=D[C].fields[G];E[C]=w}}}}}}}}}for(x in K){if(K.hasOwnProperty(x)){for(t in m[x]){if(m[x].hasOwnProperty(t)){z[x].state[m[x][t]]=z[t]}}}}for(x in K){if(K.hasOwnProperty(x)){z.state[x]=new v[x](z.state[x]);if("getRoutes" in z[x].constructor.originalSpec){I=true;B=z[x].constructor.originalSpec.getRoutes();for(k in B){if(B.hasOwnProperty(k)){H[B[k].path]=B[k].handler;i(B[k].path,function(O,N,L,M){r=true;if(!y){if(z.canRevert&&z.pageNotFound){M.rollbackRequest=true;M.revert=function(){this.revert.bind(this);this.setState({},{path:M.path})}.bind(z)}H[N].call(z[O],M.params,M.path,L,M)}y=false}.bind(this,x,B[k].path,k))}}delete z[x].constructor.originalSpec.getRoutes}}}z=new J(z,void (0),void (0),n,I);if(I){i(function(L){r=true;z.setState({pageNotFound:true});y=false});y=true;i.replace(z.path);i.start({click:false,dispatch:false});r=false}if("dataContextWillInitialize" in z.constructor.originalSpec){F=true;z.constructor.originalSpec.dataContextWillInitialize.call(z);delete z.constructor.originalSpec.dataContextWillInitialize}for(x in K){if(K.hasOwnProperty(x)){if("dataContextWillInitialize" in z[x].constructor.originalSpec){F=true;z[x].constructor.originalSpec.dataContextWillInitialize.call(z[x]);delete z[x].constructor.originalSpec.dataContextWillInitialize}}}if(!F){Object.freeze(z.state);Object.freeze(z);return z}}},{"./utils":8,page:2}],8:[function(h,g,f){var e={extend:function(){var j={};for(var l=0;l<arguments.length;l++){var m=arguments[l];for(var k in m){if(m.hasOwnProperty(k)){j[k]=m[k]}}}return j},mixInto:function(j,k){var i;for(i in k){if(!k.hasOwnProperty(i)){continue}j.prototype[i]=k[i]}}};g.exports=e},{}],9:[function(i,h,f){var e=i("./utils");var j=e.extend;var g={Mixin:{construct:function(k){var m=this.getDescriptor(this);m.proto.setState=k;var l=function(r,n){r=r||{};r=("state" in r?r.state:r);var s=m.freezeFields,q,p=Object.create(m.proto,m.descriptor),t,o;Object.defineProperty(p,"state",{configurable:true,enumerable:false,writable:true,value:r});if(n){r=("getInitialState" in m.originalSpec)?j(r,m.originalSpec.getInitialState.call(p)):r;Object.defineProperty(p,"state",{configurable:true,enumerable:false,writable:true,value:r})}for(q=s.length-1;q>=0;q--){if(s[q].kind==="instance"){if(p[s[q].fieldName]){t=p[s[q].fieldName].constructor.originalSpec.__processedSpec__;o=Object.create(t.proto,t.descriptor);o.__stateChangedHandler=(function(u){return p[u].__stateChangedHandler})(s[q].fieldName);Object.defineProperty(o,"state",{configurable:true,enumerable:false,writable:true,value:p[s[q].fieldName].state});o.__proto__.setState=function(u,v){this.__stateChangedHandler.call(p,j(this.state,u),v)};Object.freeze(p[s[q].fieldName])}}else{r[s[q].fieldName]=r[s[q].fieldName]||[];Object.freeze(r[s[q].fieldName])}}Object.defineProperty(p,"state",{configurable:false,enumerable:false,writable:false,value:r});return Object.freeze(p)};return l}}};h.exports=g},{"./utils":8}]},{},[1])(1)}); |
'use strict' | ||
var IMVVM = require('./src/imvvm.js'); | ||
var IMVVM = require('./src/core.js'); | ||
module.exports = IMVVM; | ||
module.exports = IMVVM; |
{ | ||
"name": "imvvm", | ||
"description": "Immutable MVVM for React", | ||
"version": "0.7.6", | ||
"version": "0.8.1", | ||
"keywords": [ | ||
@@ -11,2 +11,4 @@ "react", | ||
"react", | ||
"routing", | ||
"pushState", | ||
"imvvm" | ||
@@ -28,2 +30,5 @@ ], | ||
"main": "main.js", | ||
"dependencies": { | ||
"page": ">=1.3.7" | ||
}, | ||
"devDependencies": { | ||
@@ -30,0 +35,0 @@ "browserify": "^3.33.0", |
931
README.md
IMVVM | ||
===== | ||
Immutable MVVM for React | ||
## Introduction | ||
IMVVM is designed to complement the [React](http://facebook.github.io/react/) library. A Facebook and Instagram collaboration. | ||
IMVVM helps implement the Model-View-ViewModel pattern in [React](http://facebook.github.io/react/) applications. It's role is to provide the framework to create Models and ViewModels, with React acting as the View. It is designed to complement React. The IMVVM API gets its inspiration from the React library. So it feels like React. This makes it easy to understand and enables you to be productive in a short time frame. | ||
React is: | ||
Documentation can be found [here.](https://github.com/entrendipity/imvvm/wiki) | ||
> A Javascript Library for building User Interfaces | ||
To further quote the React website, "lots of people use React as the 'V' in MVC". Well I thought, why not use React as the 'V' in MVVM, and keeping inline with React's philosophy on immutability, lets make it use immutable data. So I created IMVVM. | ||
This is my take on MVVM and immutability using javascript. IMVVM tries to resemble a React application. So it not only feels like your developing in the same environment, but if your coming from React, you should be able to pick up IMVVM quickly. | ||
Anyway, download it, run the example application and try it out. Maybe avoid production environments for the moment. Be sure to raise issues in the Issues Register as you encounter them and please feel free to create pull requests. | ||
Thanks. | ||
___TODO:___ | ||
Write some tests. I know, they should have already been done... | ||
## Usage | ||
IMVVM can be loaded as: | ||
- standalone. `IMVVM` is exposed as a global variable | ||
```javscript | ||
<script src="imvvm.min.js"></script> | ||
``` | ||
- a Node.js module | ||
``` | ||
$ npm install imvvm | ||
``` | ||
- a Bower module | ||
``` | ||
$ bower install imvvm | ||
``` | ||
- a RequireJS module | ||
``` | ||
require(['./imvvm.min.js'], function (IMVVM) { | ||
// Do something with IMVVM | ||
}); | ||
``` | ||
## Getting Started | ||
###Install and start the example application (which is intended to act as a reference implementation). | ||
The example application is a good starting place when figuring out how things work. To get it running, navigate to the `./example` directory and run the following commands. | ||
``` | ||
$ npm install | ||
$ cd app | ||
$ bower install | ||
$ bower install imvvm | ||
$ cd .. | ||
$ grunt serve | ||
``` | ||
### Create a Model | ||
```javascript | ||
var PersonModel = IMVVM.createModel({ | ||
getInitialState: function(){ | ||
return { | ||
age: calculateAge(this.dob), | ||
id: this.id ? this.id : uuid() | ||
}; | ||
}, | ||
id: { | ||
get: function(){ | ||
return this.state.id; | ||
} | ||
}, | ||
name: { | ||
get: function(){ | ||
return this.state.firstName; | ||
}, | ||
set: function(newValue){ | ||
this.setState({'name': newValue}); | ||
} | ||
}, | ||
dob: { | ||
get: function(){ | ||
return this.state.dob; | ||
}, | ||
set: function(newValue){ | ||
var age; | ||
if(newValue.length === 10){ | ||
age = calculateAge(newValue); | ||
} | ||
if(newValue.length === 0){ | ||
age = 'Enter your Birthday'; | ||
} | ||
this.setState({ | ||
dob: newValue, | ||
age: age | ||
}); | ||
} | ||
}, | ||
gender: { | ||
get: function(){ | ||
return this.state.gender; | ||
}, | ||
set: function(newValue){ | ||
this.setState({'gender': newValue}); | ||
} | ||
}, | ||
age: { | ||
get: function(){ | ||
return this.state.age; | ||
} | ||
}, | ||
}); | ||
``` | ||
#####getInitialState() | ||
The `getInitialState` function initializes any fields that require a value during initialization. Such values include, default values and calculated fields. This function has access to the state which was passed into the constructor, and is able to reference this state via `this.state.property`. | ||
#####name | ||
`name` is a simple field descriptor. It defines a getter and setter. The getter returns state that resides in the state's property of the same name. The setter will transition the model to the next state, when a value is assigned to it. It uses the value and passes it, as an object, to the setState function. This will notify any ViewModels that have registered a stateChangedHandler with this model. | ||
#####age | ||
`age` is a calculated field. It is set by setting `dob`. `dob` uses a calculateAge function to set the value of age and passes both the dob and age values to setState. Please note that `age` needed to be initialized in the getInitialState() function. | ||
### Create a ViewModel | ||
```javascript | ||
var personStateChangedHandler = function(nextState){ | ||
var persons = {}; | ||
persons.collection = this.collection.map(function(person){ | ||
if(person.id === nextState.id){ | ||
persons.selectedPerson = new Person(nextState); | ||
return persons.selectedPerson; | ||
} | ||
return person; | ||
}.bind(this)); | ||
this.setState(persons); | ||
}; | ||
var Person = function(){ | ||
return new PersonModel(personStateChangedHandler).apply(this, arguments); | ||
}; | ||
var PersonsViewModel = IMVVM.createViewModel({ | ||
getInitialState: function(){ | ||
var nextState = {}; | ||
nextState.collection = DataService.getData().map(function(person, idx){ | ||
if (idx === 0){ | ||
nextState.selectedPerson = new Person(person, true); | ||
return nextState.selectedPerson; | ||
} | ||
return new Person(person, true); | ||
}.bind(this)); | ||
return nextState; | ||
}, | ||
getWatchedState: function() { | ||
return { | ||
'hobbies': { | ||
alias: 'hobbiesContext', | ||
}, | ||
'online': { | ||
alias: 'imOnline' | ||
} | ||
}; | ||
}, | ||
imOnline: { | ||
kind:'pseudo', | ||
get: function(){ | ||
return this.state.imOnline; | ||
} | ||
}, | ||
selectedPerson: { | ||
kind: 'instance', | ||
get: function() { return this.state.selectedPerson; } | ||
}, | ||
collection: { | ||
kind: 'array', | ||
get: function(){ return this.state.collection; }, | ||
}, | ||
selectedHobby: { | ||
kind: 'pseudo', | ||
get: function() { | ||
return this.state.hobbiesContext.current ? this.state.hobbiesContext.current.name: void(0); | ||
} | ||
}, | ||
selectPerson: function(id){ | ||
for (var i = this.collection.length - 1; i >= 0; i--) { | ||
if(this.selectedPerson.id !== id && this.collection[i].id === id){ | ||
this.setState({ selectedPerson: new Person(this.collection[i]) }); | ||
break; | ||
} | ||
} | ||
}, | ||
addPerson: function(value){ | ||
var nextState = {}; | ||
var name; | ||
if(value && value.length > 0){ | ||
name = value.split(' '); | ||
nextState.selectedPerson = new Person({ | ||
firstName: name[0], | ||
lastName: name.slice(1).join(' ') | ||
}, true); | ||
nextState.collection = this.collection.slice(0); | ||
nextState.collection = nextState.collection.concat(nextState.selectedPerson); | ||
this.setState(nextState); | ||
} | ||
}, | ||
deletePerson: function(uid){ | ||
var nextState = {}; | ||
nextState.collection = this.collection.filter(function(person){ | ||
return person.id !== uid; | ||
}); | ||
nextState.selectedPerson = void(0); | ||
if(nextState.collection.length > 0){ | ||
if (this.selectedPerson.id === uid){ | ||
nextState.selectedPerson = new Person(nextState.collection[0]); | ||
} else { | ||
nextState.selectedPerson = new Person(this.selectedPerson); | ||
} | ||
} | ||
this.setState(nextState); | ||
}, | ||
}); | ||
``` | ||
#####getInitialState() | ||
The `getInitialState` function initializes any fields that require a value and | ||
can initialize models to establish the data for the Viewmodel. Unlike Models, the ViewModel does not have access to `state`. | ||
#####getWatchedState() | ||
The `getWatchedState` function establises links between ViewModels. In the code above, `getWatchedState` is referencing the `hobbies` data context with the alias of hobbiesContext. This enables the ViewModel to refer to `hobbies` as `hobbiesContext` (i.e. `this.state.hobbiesContext...`). Enabling fields to be displayed from other ViewModels and will be automatically update thier values. | ||
We can also reference fields from the DomainViewModel. We simply specify the field name and supply an alias. Then it can be referred to from the state object. | ||
#####imOnline | ||
The `imOnline` field is referencing the linked DomainModel field `online` with an alias of `imOnline`. It simply forwards the online value it recieves from the DomainViewModel to the View and has no influence over its output. It is for this reason that the field descriptor has a `kind` decorator of `pseudo`. Fields that obtain their value from other sources and have no influence on the output value, should be flagged with the decorator `kind:'pseudo'`. | ||
#####selectedPerson | ||
`selectedPerson` is another field that has a `kind` decorator. The `selectedPerson` field is of kind `instance`. Which simply means that it will return a model instance. | ||
#####collection | ||
`collection` has a `kind` decorator of `array`. Yes, you guessed it, because it returns an array. | ||
_The reason that the kind decorator is used is because IMVVM does some extra processing with these types._ | ||
#####personStateChangedHandler | ||
`personStateChangedHandler` will be invoked anytime `setState` is called within the model that it is registered to. This function processes changes in the ViewModel and notifies the DomainModel of any changes so that the DomainModel can transition to the next state. | ||
#####Person | ||
`Person` is a wrapper around the `PersonModel` constructor. It makes `PersonModel` easier to invoke and acts like a Model Factory of sorts. The parameters passed to `Person` are passed directly to the `PersonModel` constructor, but not before registering the stateChangedHandler `personStateChangedHandler`, to ensure that all state changes in PersonModel notify interested Models/ViewModels and is handled approapriately. | ||
#####selectPerson, addPerson & deletePerson | ||
These functions are exposed to the View and enable tasks to be performed in the ViewModel. However, the View will also interact with any Models the ViewModel exposes to it. In this instance the ViewModel exposes the PersonModel via the `selectedPerson` property. See [A word on how to update a Model from the View](#a-word-on-how-to-update-a-model-from-the-view) | ||
I've added a code snippet of HobbiesViewModel, from the example application to help explain a little more about `getWatchedState`. | ||
__HobbiesViewModel snippet__ | ||
```javascript | ||
var onPersonChangedHandler = function(nextState, prevState, field, context){ | ||
if(this.current !== void(0) && context === 'persons' && | ||
nextState.id !== prevState.id){ | ||
return { hobbies: { current: void(0) }, busy: false }; | ||
} | ||
}; | ||
var HobbiesViewModel = IMVVM.createViewModel({ | ||
getWatchedState: function() { | ||
return { | ||
'persons': { | ||
alias: 'personsContext', | ||
fields: { | ||
'selectedPerson': onPersonChangedHandler | ||
} | ||
}, | ||
'busy': { | ||
alias: 'busy' | ||
} | ||
}; | ||
}, | ||
... | ||
selectHobby: function(id){ | ||
for (var i = this.hobbies.length - 1; i >= 0; i--) { | ||
if ((this.current === void(0) || this.current.id !== id) && this.hobbies[i].id === id){ | ||
this.setState({current: new Hobby(this.hobbies[i])}, {busy: true}); | ||
/* | ||
//OR use a callback | ||
this.setState({current: new Hobby(this.hobbies[i])}, function(){ | ||
this.setState(void(0), {busy: true}); | ||
}.bind(this)); | ||
*/ | ||
break; | ||
} | ||
} | ||
}, | ||
... | ||
}); | ||
``` | ||
#####getWatchedState() | ||
Notice in this instance of the `getWatchedState` function, the `persons` object has an extra property called `fields`. `fields` the ViewModel should be notified when `persons.selectedPerson` changes, and if it does change trigger the `onPersonChanged` handler. | ||
Both the `alias` and `fields` properties are optional. However at least one must be present. | ||
#####onPersonChangedHandler | ||
`onPersonChangedHandler` will be triggered whenever any of the fields that it is assigned to changes. It is important to note that if there is a change, `onPersonChangedHandler` must return a state object. If no change has occurred, return either void(0) or don't return anything. It should also be noted that the returned state object has a property with the data context, `hobbies`, and the associated next state object. This is necessary so that any data context or domain property can be updated from this ViewModel by specfying it in the returned object. | ||
#####selectHobby | ||
The `selectHobby` function is nothing special. What is different about it, is the `setState` function. A ViewModel's `setState` function has an extra parameter which enables the ViewModel to update state in other data contexts. The second parameter takes a state object, not dissimilar to the state object that is returned from `onPersonChangedHandler`. The second parameter accepts an object that specifies the data context or domain property and its associated state. | ||
For instance `this.setState({current: new Hobby(this.hobbies[i])}, {busy: true});`. The first parameter is the next state for the `hobbies` data context, the second parameter specifies that `busy`, a property in the domain data context, should be changed to `true`. This second parameter also accepts other data contexts (e.g. `{persons: {selectedPerson: new Person(personState)}}`). | ||
Also noted within comments, is that this can be achieved within a callback, ensuring to pass `void(0)` as the first parameter to `setState`. | ||
***n.b. You may have noticed that not all fields have setters, however, the values are still able to be updated. This is what `setState` does. You only need to supply a setter if you would like the View to update the field directly.*** | ||
### Create a DomainViewModel | ||
```javascript | ||
var DomainViewModel = IMVVM.createDomainViewModel({ | ||
getInitialState: function(){ | ||
return { | ||
online: true, | ||
busy: false | ||
}; | ||
}, | ||
persons: { | ||
viewModel: PersonsViewModel, | ||
get: function(){ | ||
return this.state.persons; | ||
} | ||
}, | ||
hobbies: { | ||
viewModel: HobbiesViewModel, | ||
get: function(){ | ||
return this.state.hobbies; | ||
} | ||
}, | ||
personCount: { | ||
kind:'pseudo', | ||
get: function(){ | ||
return this.persons ? this.persons.collection.length : 0; | ||
} | ||
}, | ||
busy: { | ||
get: function(){ | ||
return this.state.busy; | ||
}, | ||
}, | ||
online: { | ||
get: function(){ | ||
return this.state.online; | ||
}, | ||
set: function(newValue){ | ||
this.setState({'online': newValue }); | ||
} | ||
}, | ||
}); | ||
``` | ||
#####getInitialState() | ||
The 'getInitialState' is the same as for the ViewModel. | ||
#####persons | ||
The `persons` property sets up a data context called 'persons'. It has a special decorator called `viewModel`, which specifies the associated ViewModel for this data context. | ||
___ | ||
>___This is probably a good time to explain a little about what gets exposed to the View.___ | ||
> | ||
>When a Model, ViewModel or DomainViewModel is instantiated, IMVVM takes all the defined functions and places them on the Object's prototype. It also takes all the field descriptors and assigns them as properties for the newly created object. | ||
> | ||
>So all functions and fields defined within a Model, ViewModel or DomainModel, are visible to other objects. This means that the View has visibility and access to these functions and fields, including any implementation specific functions, which probably should be kept separate from the View. | ||
> | ||
>What would be preferred, is to only expose the properties and functions the View needs. To accomplish this, define variables, properties or functions outside of your Models, ViewModels and DomainViewModels, and simply reference them. That way the View API is kept nice and clean and your implementation is kept separate. | ||
> | ||
>Refer to the reference implementation (i.e. example application), which uses this technique. | ||
___ | ||
### Hook up the View | ||
Once you have created your Models. ViewModels and DomainViewModel, you're ready to hook it up the the View. All you need to do is specify the mixin and IMVVM will attach a `domainDataContext` to the state object that will be kept in sync with you're ViewModel. | ||
```javascript | ||
var ApplicationView = React.createClass({ | ||
mixins: [IMVVM.mixin], | ||
render: function(){ | ||
return ( | ||
<div> | ||
<NavBarView appContext={this.state.domainDataContext} /> | ||
<SideBarView appContext={this.state.domainDataContext} /> | ||
<DetailsView appContext={this.state.domainDataContext} /> | ||
</div> | ||
); | ||
} | ||
}); | ||
``` | ||
The `domainDataContext` has all the properties that were specified in the DomainViewModel. So this `domainDataContext` will have the following properties: | ||
- busy | ||
- online | ||
- personCount | ||
- persons (dataContext) | ||
+ imOnline | ||
+ selectedPerson | ||
* [model properties and functions...] | ||
+ collection | ||
+ selectedHobby | ||
+ selectPerson | ||
+ addPerson | ||
+ deletePerson | ||
- hobbies (dataContext) | ||
+ [viewModel properties and functions...] | ||
### Render the View | ||
Then you just render the View, specifying a `domainModel` prop, that references the 'DomainViewModel'. | ||
```javascript | ||
React.renderComponent(<ApplicationView domainModel={DomainViewModel}/>, | ||
document.getElementById('container')); | ||
``` | ||
#####A word on how to update a Model from the View | ||
As mentioned above, the `PersonsViewModel` has a `persons` data context. This data context exposes a `Person` model instance via the `selectedPerson` property. | ||
To update the `Person` instance, simply assign values to the associated properties on the `selectedPerson` property. | ||
__FormView example__ | ||
```javascript | ||
var FormView = React.createClass({ | ||
updateName: function(e){ | ||
this.props.appContext.persons.selectedPerson.name = e.target.value; | ||
}, | ||
updateGender: function(e){ | ||
this.props.appContext.persons.selectedPerson.gender = e.target.value; | ||
}, | ||
updateDOB: function(e){ | ||
this.props.appContext.persons.selectedPerson.dob = e.target.value; | ||
}, | ||
render: function() { | ||
var current = this.props.appContext.persons.selectedPerson; | ||
return ( | ||
<div key={current.id}> | ||
<form role="form"> | ||
<div> | ||
<label>Name</label> | ||
<div> | ||
<input type="text" value={current.name} | ||
onChange={this.updateName} /> | ||
</div> | ||
</div> | ||
<div> | ||
<label>Gender</label> | ||
<div> | ||
<div> | ||
<label> | ||
<input type="radio" onChange={this.updateGender} | ||
value="male" | ||
checked={current.gender === 'male'} /> | ||
Male | ||
</label> | ||
</div> | ||
<div> | ||
<label> | ||
<input type="radio" onChange={this.updateGender} | ||
value="female" | ||
checked={current.gender === 'female'} /> | ||
Female | ||
</label> | ||
</div> | ||
</div> | ||
</div> | ||
<div> | ||
<label>Birthday</label> | ||
<div> | ||
<input type="text" | ||
placeholder="yyyy-mm-dd" | ||
value={current.dob} | ||
onChange={this.updateDOB} /> | ||
</div> | ||
</div> | ||
<div className="form-group"> | ||
<label>Age</label> | ||
<div> | ||
<div> | ||
{current.age} | ||
</div> | ||
</div> | ||
</div> | ||
</form> | ||
</div> | ||
); | ||
} | ||
}); | ||
``` | ||
##API | ||
###Class | ||
####Constructors | ||
___ | ||
#####function createDomainViewModel(object specification) | ||
Creates a DomainViewModel object. | ||
*parameters* | ||
__specification__ - see [Specification](#specification) | ||
___ | ||
#####function createViewModel(object specification) | ||
Creates a ViewModel object. | ||
*parameters* | ||
__specification__ - see [Specification](#specification) | ||
___ | ||
#####function createModel(object specification) | ||
Creates a Model object. | ||
*parameters* | ||
__specification__ - see [Specification](#specification) | ||
###Instance | ||
####Functions | ||
___ | ||
#####void setState(object nextState[, oject nextDomainState, function callback]) | ||
Transition Data Context to the next state. | ||
*parameters* | ||
__nextState__ | ||
Object containing the next state values. | ||
__nextDomainState__ ***n.b. This parameter is only available in ViewModels*** | ||
Object containing the next state values which are to be passed to the Domain Data Context. | ||
___example___ | ||
```javascript | ||
//From PersonsViewModel | ||
{ | ||
hobbies: { current: void(0) }, | ||
busy: false | ||
} | ||
``` | ||
__callback__ | ||
Used to do sequential setState calls and returns latest Domain Data Context state. | ||
___example___ ***n.b. This example is not part of the reference implementation.It demonstrates how a callback can be used. Here the callback updates an amount property on a currentTarget object, which had been selected by the calling function `selectTarget` and passed back as `returnedAppContext`. Notice that you do not need to bind to `this` using this approach.*** | ||
```javascript | ||
handleChange: function(target, e){ | ||
this.props.appContext.positionsContext.selectTarget(target, function(returnedAppContext){ | ||
returnedAppContext.positionsContext.currentTarget.amount = Utils.unformat(e.target.value); | ||
}); | ||
}, | ||
``` | ||
_Available in:_ DomainViewModel, ViewModel, Model | ||
___ | ||
#####object revert() | ||
`revert` transitions state to the previous state. Requires [`enableUndo`](#enableundo-boolean-default--false) to be `true` to enable this functionality. | ||
_Available in:_ DomainViewModel | ||
___ | ||
#####object advance() | ||
`advance` transitions state to the next state. Requires [`enableUndo`](#enableundo-boolean-default--false) to be `true` to enable this functionality. | ||
_Available in:_ DomainViewModel | ||
####Properties | ||
___ | ||
#####state | ||
Holds object state. | ||
_Available in:_ DomainViewModel, ViewModel, Model | ||
___ | ||
#####previousState | ||
Holds Domain Data Context previous state. | ||
_Available in:_ DomainViewModel | ||
___ | ||
#####nextState | ||
Holds Domain Data Context next state. | ||
_Available in:_ DomainViewModel | ||
___ | ||
#####canRevert | ||
`true` - can revert to previous state. | ||
`false` - can not revert to previous state. | ||
_Available in:_ DomainViewModel | ||
___ | ||
#####canAdvance | ||
`true` - can advance to next state. | ||
`false` - can not advance to next state. | ||
_Available in:_ DomainViewModel | ||
###Specification | ||
####Functions | ||
___ | ||
#####object getInitialState() | ||
Called when initalized in order to initialize state. Returns a state object; | ||
_Available in:_ DomainViewModel, ViewModel, Model | ||
_Optional:_ true | ||
___ | ||
#####object getWatchedState() | ||
Provides watched items to domain. Returns a WatchedState object definition. | ||
######Watching ViewModel state | ||
dataContext\: \{ | ||
[alias]\: preferredName, | ||
[fields]\: \{ | ||
dataContextField\: ViewModelStateChangedHandler | ||
\} | ||
\} | ||
___n.b. `alias` and `fields` are optional, however at least one must be present. This is intentional. `alias` is an indicator that the ViewModel should attach the specified dataContext to the `state` object. If `alias` is not specified then it will not be attached.___ | ||
######Watching Domain Data Context state | ||
field\: \{ | ||
alias\: preferredName | ||
\} | ||
___n.b. `alias` is not optional, in this instance, as only fields can be referenced to the Domain dataContext.___ | ||
___example___ | ||
```javascript | ||
getWatchedState: function() { | ||
return { | ||
'persons': { | ||
alias: 'personsContext', | ||
fields: { | ||
'selected': onPersonChange | ||
} | ||
}, | ||
'busy': { | ||
alias: 'busy' | ||
} | ||
} | ||
}, | ||
``` | ||
_Available in:_ ViewModel | ||
_Optional:_ true | ||
___ | ||
#####object ViewModelStateChangedHandler(object nextState, object previousState, string field, string dataContext) | ||
*arguments* | ||
__nextState__ | ||
Next state of the watched field. | ||
__previousState__ | ||
Previous state of the watched field. | ||
__field__ | ||
Watched field name. | ||
__dataContext__ | ||
Watched Data Context name. | ||
___example___ | ||
```javascript | ||
onPersonChange: function(nextState, prevState, field, context){ | ||
if(this.current !== void(0) && context === 'persons' && | ||
nextState.id !== prevState.id){ | ||
return { hobbies: { current: void(0) }, busy: false }; | ||
} | ||
} | ||
``` | ||
_Available in:_ ViewModel | ||
___ | ||
#####void ModelStateChangedHandler(object nextState[, function callback]) | ||
*arguments* | ||
__nextState__ | ||
Next state of the model. | ||
__callback__ | ||
Returns latest Domain Data Context state. | ||
```javascript | ||
personStateChangedHandler: function(nextState){ | ||
var persons = {}; | ||
persons.collection = this.collection.map(function(person){ | ||
if(person.id === nextState.id){ | ||
persons.selected = new Person(nextState, person, true); | ||
return persons.selected; | ||
} | ||
return person; | ||
}.bind(this)); | ||
this.setState(persons); | ||
}, | ||
``` | ||
_Available in:_ ViewModel | ||
___ | ||
#####ModelInstance ModelName([function stateChangedHandler])([object nextState, object extendState, boolean initialize]) | ||
Returns a new model instance. This function is usually wrapped in another function for easier usage. | ||
*parameters* | ||
__stateChangedHandler__ | ||
State changed handler which is triggered whenever setState is called from within the model. | ||
___n.b. Only use stateChangedHandlers in ViewModels. Do not use stateChangedHandlers in Models. You can still use Model within Models. Below is an example of how you would initialize Model instances inside other Models.___ | ||
```javascript | ||
var Hobby: function(){ | ||
return new HobbyModel().apply(this, arguments); | ||
}, | ||
``` | ||
__nextState__ | ||
Object containing the next state values for the model. | ||
__extendState__ | ||
Object containing more next state values for the model. This parameter is for convenience, so that there is no need to call extend to update a model. The examples below both update `personObj.firstName` to 'Fred'. | ||
```javascript | ||
var nextState = this.extend(personObj, {firstName: 'Fred'}); | ||
persons.selectedPerson = new Person(nextState); | ||
``` | ||
```javascript | ||
persons.selectedPerson = new Person(personObj, {firstName: 'Fred'}); | ||
``` | ||
__initialize__ | ||
Boolean value indicating the model instance should be initialized. (Calls getInitialState()) | ||
_Available in:_ ViewModel, Model | ||
___Usage example___ | ||
___n.b. This is suggested usage and not part of the API___ | ||
```javascript | ||
var Person: function(){ | ||
return new PersonModel(personStateChangedHandler).apply(this, arguments); | ||
}, | ||
``` | ||
####Field | ||
___ | ||
#####fieldName : [Descriptor](#descriptor) | ||
_Available in:_ DomainViewModel, ViewModel, Model | ||
####Descriptor | ||
___ | ||
#####Functions | ||
######get() | ||
_Available in:_ DomainViewModel, ViewModel, Model | ||
######void set(newValue) | ||
_Available in:_ DomainViewModel, ViewModel, Model | ||
#####Decorators | ||
######kind: ['instance' || 'array' || 'pseudo'] | ||
**instance** | ||
Specifies that the getter returns a model instance. This needs to be specified if the View interacts with the returned model object. i.e. It will update values on the model. | ||
_Available in:_ ViewModel | ||
**array** | ||
Specifies that the field holds an array. This will initialize the array, if it hasn't already been initialized. | ||
_Available in:_ DomainViewModel, ViewModel, Model | ||
**pseudo** | ||
Specifies that the value for this field is obtained from other properties. | ||
_Available in:_ DomainViewModel, ViewModel, Model | ||
######aliasFor: string originalKey | ||
**aliasFor** | ||
Transforms key from original data source to specified field. | ||
__example__ | ||
In the original data source, `job` was is property name. However, `aliasFor` will expose `job` as `occupation` to the View. This is a one way transformation. If it is required to persist the data as `job`, that will need to be transformed back within the application. | ||
```javascript | ||
occupation: { | ||
aliasFor: 'job', | ||
get: function(){ | ||
return this.state.occupation; | ||
}, | ||
set: function(newValue){ | ||
this.setState({'occupation': newValue }); | ||
} | ||
}, | ||
``` | ||
_Available in:_ Model | ||
######viewModel: ViewModel Class | ||
**viewModel** | ||
Identifies the ViewModel to be used for the defined data context. | ||
_Available in:_ DomainViewModel | ||
###Mixin | ||
####IMVVM.mixin | ||
___ | ||
#####mixins: [IMVVM.mixin] | ||
Adds domainDataContext to state object. | ||
```javascript | ||
var ApplicationView = React.createClass({ | ||
mixins: [IMVVM.mixin], | ||
render: function(){ | ||
return <DetailsView appContext={this.state.domainDataContext} />; | ||
} | ||
}); | ||
``` | ||
####props | ||
#####domainModel= DomainViewModel Class | ||
#####[enableUndo]= boolean: default = false | ||
**Point the View to the DomainViewModel.** | ||
```javascript | ||
React.renderComponent(<ApplicationView domainModel={DomainViewModel}/>, | ||
document.getElementById('container')); | ||
``` | ||
###Static methods | ||
####IMVVM.extend() | ||
___ | ||
#####object extend(object currentState[, object... nextState]) | ||
Creates a shallow copy of currentState. Adds/replaces properties with properties of subsequent objects. | ||
*parameters* | ||
__currentState__ | ||
Old state object. | ||
__nextState__ | ||
Next state objects. | ||
___Usage example___ | ||
```javascript | ||
var nextState = IMVVM.extend(currentState, nextState); | ||
``` | ||
_Available in:_ DomainViewModel, ViewModel, Model | ||
## Browser Support | ||
Most ECMAScript 5 compliant browsers. | ||
__IE8 and below are not supported.__ | ||
## Author | ||
Entrendipity - [Follow @entrendipity](https://twitter.com/intent/follow?screen_name=entrendipity) | ||
Entrendipity - [Follow @entrendipity](https://twitter.com/intent/follow?screen_name=entrendipity) | ||
Frank Panetta - [Follow @fattenap](https://twitter.com/intent/follow?screen_name=fattenap) | ||
@@ -936,0 +11,0 @@ |
401
src/core.js
var model = require('./model'); | ||
var viewModel = require('./viewModel'); | ||
var domainModel = require('./domainViewModel'); | ||
var mixin = require('./mixin'); | ||
var page = require('page'); | ||
var utils = require('./utils'); | ||
var extend = utils.extend; | ||
var mixInto = utils.mixInto; | ||
exports.getInitialState = function(appNamespace, domainModel, stateChangedHandler, enableUndo) { | ||
var ModelBase = function(){}; | ||
var ViewModelBase = function(){}; | ||
var DomainViewModelBase = function(){}; | ||
if(typeof stateChangedHandler !== 'function'){ | ||
throw new TypeError('stateChangedHandler must be a function!'); | ||
} | ||
mixInto(ModelBase, model.Mixin); | ||
mixInto(ViewModelBase, viewModel.Mixin); | ||
mixInto(DomainViewModelBase, domainModel.Mixin); | ||
enableUndo === void(0) ? true : enableUndo; | ||
var IMVVMClass = { | ||
createClass: function(ctor, classType, spec){ | ||
var ApplicationDataContext, | ||
appState = {}, | ||
dataContexts = {}, | ||
domain, | ||
links = {}, | ||
watchedDataContexts = {}, | ||
dataContext, | ||
transientState = {}, | ||
processedState = {}, | ||
watchedState, | ||
watchedItem, | ||
watchedProp, | ||
watchedDataContext, | ||
link, | ||
calledBack = false; | ||
var Constructor = function(){}; | ||
Constructor.prototype = new ctor(); | ||
Constructor.prototype.constructor = Constructor; | ||
var appStateChangedHandler = function(caller, newState, newAppState, callback) { | ||
var DescriptorConstructor = Constructor; | ||
var nextState = {}, | ||
prevState = void(0), | ||
redoState = void(0), | ||
newStateKeys, | ||
keyIdx, | ||
transientStateKeysLen, | ||
dataContext, | ||
linkedDataContext, | ||
processedStateKeys = [], | ||
processedStateKeysLen, | ||
watchedField, | ||
subscribers, | ||
subscriber; | ||
var ConvenienceConstructor = function(stateChangedHandler) { | ||
var descriptor = new DescriptorConstructor(); | ||
return descriptor.construct.apply(ConvenienceConstructor, arguments); | ||
}; | ||
if(typeof newAppState === 'function'){ | ||
callback = newAppState; | ||
newAppState = {}; | ||
} | ||
ConvenienceConstructor.componentConstructor = Constructor; | ||
Constructor.ConvenienceConstructor = ConvenienceConstructor; | ||
newState = newState || {}; | ||
newStateKeys = Object.keys(newState); | ||
ConvenienceConstructor.originalSpec = spec; | ||
//Check to see if appState is a ready made state object. If so | ||
//pass it straight to the stateChangedHandler. If a callback was passed in | ||
//it would be assigned to newState | ||
if(Object.getPrototypeOf(newState).constructor.classType === "DomainViewModel") { | ||
// Expose the convience constructor on the prototype so that it can be | ||
// easily accessed on descriptors. E.g. <Foo />.type === Foo.type and for | ||
// static methods like <Foo />.type.staticMethod(); | ||
// This should not be named constructor since this may not be the function | ||
// that created the descriptor, and it may not even be a constructor. | ||
ConvenienceConstructor.type = Constructor; | ||
Constructor.prototype.type = Constructor; | ||
nextState = extend(newState); | ||
prevState = newState.previousState; | ||
redoState = newAppState; | ||
//Need to reset ViewModel with instance object so that setState is associated with | ||
//the current ViewModel. This reason this ccurs is that when a ViewModel is created it | ||
//assigns a setState function to all instance fields and binds itself to it. When undo is called | ||
//the data is rolled back but viewModels are not recreated and therefore the setState functions | ||
//are associated with outdated viewModels and returns unexpected output. So to realign the ViewModels | ||
//with setState we recreate them. | ||
for(dataContext in domain){ | ||
nextState[dataContext] = new dataContexts[dataContext](nextState[dataContext]); | ||
} | ||
ConvenienceConstructor.classType = classType; | ||
Constructor.prototype.classType = classType; | ||
//relink | ||
for(dataContext in domain){ | ||
if(domain.hasOwnProperty(dataContext)){ | ||
for(linkedDataContext in links[dataContext]){ | ||
if(links[dataContext].hasOwnProperty(linkedDataContext)){ | ||
nextState[dataContext].state[links[dataContext][linkedDataContext]] = | ||
(linkedDataContext in domain) ? extend(nextState[linkedDataContext].state) : | ||
nextState[linkedDataContext]; | ||
} | ||
} | ||
} | ||
} | ||
ConvenienceConstructor.getDescriptor = function(){ | ||
var descriptor = {}, | ||
proto = this.prototype, | ||
viewModels = {}, | ||
autoFreeze = [], | ||
aliases = {}, | ||
key; | ||
if(typeof callback === 'function'){ | ||
appState = new ApplicationDataContext(nextState, prevState, redoState, enableUndo); | ||
callback(appState); | ||
return; | ||
} | ||
if('__processedSpec__' in this.originalSpec){ | ||
return this.originalSpec.__processedSpec__; | ||
} | ||
} else { | ||
for(key in this.originalSpec){ | ||
if(this.originalSpec.hasOwnProperty(key)){ | ||
if('get' in this.originalSpec[key] || 'set' in this.originalSpec[key]){ | ||
//assume it is a descriptor | ||
this.originalSpec[key].enumerable = true; | ||
if('viewModel' in this.originalSpec[key]) { | ||
viewModels[key] = this.originalSpec[key].viewModel; | ||
delete this.originalSpec[key].viewModel; | ||
delete this.originalSpec[key].set; | ||
} else { | ||
if('aliasFor' in this.originalSpec[key]){ | ||
aliases[this.originalSpec[key].aliasFor] = key; | ||
delete this.originalSpec[key].aliasFor; | ||
} | ||
if(!!newStateKeys.length){ | ||
if(caller === appNamespace){ | ||
nextState = extend(newState); | ||
} else { | ||
nextState[caller] = extend(newState); | ||
} | ||
} | ||
transientState = extend(nextState, transientState, newAppState); | ||
transientStateKeys = Object.keys(transientState); | ||
if(transientStateKeys.length === 0){ | ||
return; | ||
} | ||
if('kind' in this.originalSpec[key]){ | ||
if(this.originalSpec[key].kind === 'pseudo'){ | ||
this.originalSpec[key].enumerable = false; | ||
} else if (this.originalSpec[key].kind === 'uid') { | ||
//Don't do anything as yet | ||
} else if (this.originalSpec[key].kind === 'instance' || | ||
this.originalSpec[key].kind === 'array') { //'instance' || 'array' | ||
autoFreeze.push({fieldName: key, kind: this.originalSpec[key].kind}); | ||
} else { | ||
throw new TypeError('"'+this.originalSpec[key].kind +'" '+ | ||
'is not a valid "kind" value. Please review field "' + key + '".'); | ||
} | ||
delete this.originalSpec[key].kind; | ||
} | ||
} | ||
descriptor[key] = this.originalSpec[key]; | ||
} else { | ||
if(key !== 'getInitialState' && key !== 'getWatchedState' && | ||
key !== 'getRoutes'){ | ||
proto[key] = this.originalSpec[key]; | ||
} | ||
} | ||
} | ||
} | ||
transientStateKeysLen = transientStateKeys.length - 1; | ||
if(!!Object.keys(viewModels).length){ | ||
this.originalSpec.getDomainDataContext = function(){ | ||
return viewModels; | ||
} | ||
} | ||
for (keyIdx = transientStateKeysLen; keyIdx >= 0; keyIdx--) { | ||
if(transientStateKeys[keyIdx] in domain){ | ||
nextState[transientStateKeys[keyIdx]] = extend(appState[transientStateKeys[keyIdx]], transientState[transientStateKeys[keyIdx]]); | ||
nextState[transientStateKeys[keyIdx]] = new dataContexts[transientStateKeys[keyIdx]](nextState[transientStateKeys[keyIdx]]); | ||
} else { | ||
nextState[transientStateKeys[keyIdx]] = transientState[transientStateKeys[keyIdx]]; | ||
} | ||
}; | ||
this.originalSpec.__processedSpec__ = { | ||
descriptor: descriptor, | ||
proto: proto, | ||
originalSpec: this.originalSpec || {}, | ||
freezeFields: autoFreeze, | ||
aliases: aliases | ||
}; | ||
processedState = extend(processedState, nextState); | ||
return this.originalSpec.__processedSpec__; | ||
}; | ||
//Triggers | ||
nextState = extend(appState, processedState); | ||
return ConvenienceConstructor; | ||
}, | ||
}; | ||
transientState = {}; | ||
for (keyIdx = transientStateKeysLen; keyIdx >= 0; keyIdx--) { | ||
if(transientStateKeys[keyIdx] in watchedDataContexts){ | ||
for(watchedField in watchedDataContexts[transientStateKeys[keyIdx]]){ | ||
if(watchedDataContexts[transientStateKeys[keyIdx]].hasOwnProperty(watchedField)){ | ||
if(newStateKeys.indexOf(watchedField) !== -1){ | ||
subscribers = watchedDataContexts[transientStateKeys[keyIdx]][watchedField]; | ||
for(subscriber in subscribers){ | ||
if(subscribers.hasOwnProperty(subscriber)){ | ||
//Cross reference dataContext link Phase | ||
if(subscriber in links){ | ||
for(dataContext in links[subscriber]){ | ||
if(links[subscriber].hasOwnProperty(dataContext)){ | ||
nextState[subscriber].state[links[subscriber][dataContext]] = | ||
(dataContext in domain) ? extend(nextState[dataContext].state) : | ||
nextState[dataContext]; | ||
} | ||
if(dataContext in links){ | ||
for(dataContext2 in links[dataContext]){ | ||
if(links[dataContext].hasOwnProperty(dataContext2)){ | ||
nextState[dataContext].state[links[dataContext][dataContext2]] = | ||
(dataContext2 in domain) ? extend(nextState[dataContext2].state) : | ||
nextState[dataContext2]; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
transientState = extend(transientState, subscribers[subscriber].call(appState[subscriber], | ||
nextState[transientStateKeys[keyIdx]][watchedField], | ||
appState[transientStateKeys[keyIdx]][watchedField], watchedField, transientStateKeys[keyIdx])); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
}; | ||
if(!!Object.keys(transientState).length){ | ||
appStateChangedHandler(void(0), {}, transientState, callback); | ||
return; | ||
} | ||
//Link Phase | ||
processedStateKeys = Object.keys(processedState); | ||
processedStateKeysLen = processedStateKeys.length - 1; | ||
for (keyIdx = processedStateKeysLen; keyIdx >= 0; keyIdx--) { | ||
if(caller === appNamespace && (appNamespace in links)){ | ||
if(processedStateKeys[keyIdx] in links[appNamespace]){ | ||
for(dataContext in links[appNamespace][processedStateKeys[keyIdx]]){ | ||
if(links[appNamespace][processedStateKeys[keyIdx]].hasOwnProperty(dataContext)){ | ||
nextState[dataContext].state[links[appNamespace][processedStateKeys[keyIdx]][dataContext]] = | ||
nextState[processedStateKeys[keyIdx]]; | ||
} | ||
if(dataContext in links){ | ||
for(dataContext2 in links[dataContext]){ | ||
if(links[dataContext].hasOwnProperty(dataContext2)){ | ||
nextState[dataContext].state[links[dataContext][dataContext2]] = | ||
(dataContext2 in domain) ? extend(nextState[dataContext2].state) : | ||
nextState[dataContext2]; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} else { | ||
if(processedStateKeys[keyIdx] in links){ | ||
for(dataContext in links[processedStateKeys[keyIdx]]){ | ||
if(links[processedStateKeys[keyIdx]].hasOwnProperty(dataContext)){ | ||
nextState[processedStateKeys[keyIdx]].state[links[processedStateKeys[keyIdx]][dataContext]] = | ||
(dataContext in domain) ? extend(nextState[dataContext].state) : | ||
nextState[dataContext]; | ||
} | ||
if(dataContext in links){ | ||
for(dataContext2 in links[dataContext]){ | ||
if(links[dataContext].hasOwnProperty(dataContext2)){ | ||
nextState[dataContext].state[links[dataContext][dataContext2]] = | ||
(dataContext2 in domain) ? extend(nextState[dataContext2].state) : | ||
nextState[dataContext2]; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
prevState = calledBack ? appState.previousState : appState; | ||
} | ||
if(!!prevState){ | ||
Object.freeze(prevState); | ||
} | ||
appState = new ApplicationDataContext(nextState, prevState, redoState, enableUndo); | ||
Object.freeze(appState); | ||
Object.freeze(appState.state); | ||
if(typeof callback === 'function'){ | ||
calledBack = true; | ||
callback(appState); | ||
return; | ||
} | ||
//All the work is done! -> Notify the View | ||
stateChangedHandler(appState); | ||
calledBack = false; | ||
transientState = {}; | ||
processedState = {}; | ||
}; | ||
//Initialize Application Data Context | ||
ApplicationDataContext = domainModel.call(this, appStateChangedHandler.bind(this, appNamespace)); | ||
appState = new ApplicationDataContext(void(0), void(0), void(0), enableUndo); | ||
appState.state = appState.state || {}; | ||
domain = appState.constructor.originalSpec.getDomainDataContext(); | ||
delete appState.constructor.originalSpec.getDomainDataContext; | ||
//Initialize all dataContexts | ||
for(dataContext in domain){ | ||
if(domain.hasOwnProperty(dataContext)){ | ||
dataContexts[dataContext] = domain[dataContext].call(this, appStateChangedHandler.bind(this, dataContext)); | ||
appState.state[dataContext] = new dataContexts[dataContext](appState.state[dataContext], true); | ||
} | ||
} | ||
//Store links | ||
for(dataContext in domain){ | ||
if(domain.hasOwnProperty(dataContext)){ | ||
if('getWatchedState' in appState[dataContext].constructor.originalSpec){ | ||
watchedState = appState[dataContext].constructor.originalSpec.getWatchedState(); | ||
for(watchedItem in watchedState){ | ||
if(watchedState.hasOwnProperty(watchedItem)){ | ||
if(watchedItem in domain || watchedItem in appState.state){ | ||
if('alias' in watchedState[watchedItem]){ | ||
if(!(dataContext in links)){ | ||
links[dataContext] = {}; | ||
} | ||
links[dataContext][watchedItem] = watchedState[watchedItem].alias; | ||
if(!(watchedItem in domain)){ | ||
if(!(appNamespace in links)){ | ||
links[appNamespace] = {}; | ||
} | ||
if(!(dataContext in links[appNamespace])){ | ||
links[appNamespace][watchedItem] = {}; | ||
} | ||
links[appNamespace][watchedItem][dataContext] = watchedState[watchedItem].alias; | ||
} | ||
} | ||
for(watchedProp in watchedState[watchedItem].fields){ | ||
if(watchedState[watchedItem].fields.hasOwnProperty(watchedProp)){ | ||
if(watchedItem in domain){ | ||
watchedDataContext = {}; | ||
if(!(watchedItem in watchedDataContexts)){ | ||
watchedDataContexts[watchedItem] = {}; | ||
} | ||
watchedDataContext[watchedProp] = {}; | ||
watchedDataContext[watchedProp][dataContext] = watchedState[watchedItem].fields[watchedProp]; | ||
watchedDataContexts[watchedItem] = watchedDataContext; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
//apply links | ||
for(dataContext in domain){ | ||
if(domain.hasOwnProperty(dataContext)){ | ||
for(link in links[dataContext]){ | ||
if(links[dataContext].hasOwnProperty(link)){ | ||
appState[dataContext].state[links[dataContext][link]] = (link in domain) ? extend(appState[link].state): appState[link]; | ||
} | ||
} | ||
} | ||
} | ||
//reinitialize with all data in place | ||
for(dataContext in domain){ | ||
if(domain.hasOwnProperty(dataContext)){ | ||
appState.state[dataContext] = new dataContexts[dataContext](appState.state[dataContext]); | ||
} | ||
} | ||
appState = new ApplicationDataContext(appState, void(0), void(0), enableUndo); | ||
Object.freeze(appState.state); | ||
Object.freeze(appState); | ||
return appState; | ||
module.exports = { | ||
createModel: IMVVMClass.createClass.bind(this, ModelBase, 'Model'), | ||
createViewModel: IMVVMClass.createClass.bind(this, ViewModelBase, 'ViewModel'), | ||
createDomainViewModel: IMVVMClass.createClass.bind(this, DomainViewModelBase, 'DomainViewModel'), | ||
mixin: mixin, | ||
extend: extend, | ||
page: page | ||
}; |
var core = require('./core'); | ||
var core = require('./stateController'); | ||
var NAMESPACE = '__IMVVM__'; | ||
var mixin = { | ||
stateChangedHandler: function(dataContext){ | ||
this.setState({domainDataContext: dataContext}) | ||
}, | ||
getInitialState: function(){ | ||
var dataContext = core.getInitialState(NAMESPACE, this.props.domainModel, | ||
this.stateChangedHandler, this.props.enableUndo); | ||
return {domainDataContext: dataContext}; | ||
main: { | ||
stateChangedHandler: function(dataContext){ | ||
this.setState({domainDataContext: dataContext}); | ||
}, | ||
getInitialState: function(){ | ||
var dataContext = core.getInitialState(NAMESPACE, this.props.domainModel, | ||
this.stateChangedHandler, this.props.enableUndo); | ||
return {domainDataContext: dataContext}; | ||
} | ||
}, | ||
pushState: { | ||
componentDidMount: function(){ | ||
$(this.getDOMNode()).click(this.onclick); | ||
}, | ||
componentWillUnmount: function(){ | ||
$(this.getDOMNode()).unbind('click'); | ||
}, | ||
/** | ||
* Event button. | ||
*/ | ||
which: function(e) { | ||
e = e || window.event; | ||
return null == e.which | ||
? e.button | ||
: e.which; | ||
}, | ||
/** | ||
* Check if `href` is the same origin. | ||
*/ | ||
sameOrigin: function(href) { | ||
var origin = location.protocol + '//' + location.hostname; | ||
if (location.port) origin += ':' + location.port; | ||
return 0 == href.indexOf(origin); | ||
}, | ||
/** | ||
* Handle "click" events for routing from <a> | ||
*/ | ||
onclick: function (e) { | ||
if (1 != this.which(e)) return; | ||
if (e.metaKey || e.ctrlKey || e.shiftKey) return; | ||
if (e.defaultPrevented) return; | ||
// ensure link | ||
var el = e.target; | ||
while (el && 'A' != el.nodeName) el = el.parentNode; | ||
if (!el || 'A' != el.nodeName) return; | ||
// ensure non-hash for the same path | ||
var link = el.getAttribute('href'); | ||
if (el.pathname == location.pathname && (el.hash || '#' == link)) return; | ||
// check target | ||
if (el.target) return; | ||
// x-origin | ||
if (!this.sameOrigin(el.href)) return; | ||
// rebuild path | ||
var path = el.pathname + el.search + (el.hash || ''); | ||
// same page | ||
var orig = path + el.hash; | ||
e.preventDefault(); | ||
path = path.replace(IMVVM.page.base(), ''); | ||
if (IMVVM.page.base() && orig == path || | ||
el.href === el.baseURI + el.search + (el.hash || '')) { | ||
return; | ||
} | ||
IMVVM.page.show(orig); | ||
} | ||
} | ||
@@ -14,0 +79,0 @@ }; |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Native code
Supply chain riskContains native code (e.g., compiled binaries or shared libraries). Including native code can obscure malicious behavior.
Found 1 instance in 1 package
104819
1
18
2270
22
+ Addedpage@>=1.3.7
+ Addedisarray@0.0.1(transitive)
+ Addedpage@1.11.6(transitive)
+ Addedpath-to-regexp@1.2.1(transitive)