cherrytree-for-knockout
Advanced tools
Comparing version 0.2.2 to 0.3.2
@@ -11,3 +11,5 @@ (function(factory) { | ||
})(function(ko) { | ||
var activeRoutes = ko.observableArray(), transitioning, router | ||
var transitioning, router, | ||
activeRoutes = ko.observableArray(), | ||
activeComponents = ko.observableArray() | ||
@@ -41,4 +43,10 @@ ko.components.register('route-blank', { | ||
delete ctx.$parentContext._routeCtx | ||
ctx.$route = ctx._routeCtx | ||
delete ctx._routeCtx | ||
ctx.$routeComponent = dataItemOrAccessor | ||
var idx = activeRoutes.peek().indexOf(ctx.$route) | ||
if (idx > -1) { | ||
activeComponents.splice(idx, 1, dataItemOrAccessor) | ||
} | ||
} | ||
@@ -58,3 +66,3 @@ return retval | ||
if (!bindingContext.$root.activeRoutes) { | ||
bindingContext.$root.activeRoutes = activeRoutes | ||
bindingContext.$root.activeRoutes = knockoutCherrytreeMiddleware.activeRoutes | ||
bindingContext.$leafRoute = function() { | ||
@@ -69,10 +77,8 @@ return activeRoutes()[activeRoutes().length - 1] | ||
update: function(element, valueAccessor, ab, vm, bindingContext) { | ||
var depth = 0, contextIter = bindingContext.$parentContext, | ||
var depth = 0, contextIter = bindingContext, | ||
routeComponent = ko.observable({ name: 'route-blank' }), | ||
prevRoute, routeClass | ||
while (contextIter) { | ||
if ('$route' in contextIter) { | ||
depth++ | ||
} | ||
while (contextIter.$parentContext && contextIter.$routeComponent !== contextIter.$parentContext.$routeComponent) { | ||
depth++ | ||
contextIter = contextIter.$parentContext | ||
@@ -89,4 +95,3 @@ } | ||
bindingContext.$route = route | ||
bindingContext._routeCtx = true | ||
bindingContext._routeCtx = route | ||
@@ -98,7 +103,4 @@ var res = route.resolutions() | ||
delete params.$route.resolutions | ||
extend(params, route.queryParams) | ||
if (route.queryParams) { | ||
extend(params, route.queryParams) | ||
} | ||
prevRoute = route | ||
@@ -127,2 +129,14 @@ routeComponent({ name: ko.bindingHandlers.routeView.prefix + route.name, params: params }) | ||
function mapQuery(queryParams, query) { | ||
return Object.keys(queryParams).reduce(function(q, k) { | ||
var val = queryParams[k]() | ||
if (val !== queryParams[k].default) { | ||
q[k] = val | ||
} else { | ||
delete q[k] | ||
} | ||
return q | ||
}, query || {}) | ||
} | ||
ko.bindingHandlers.routeHref = { | ||
@@ -143,6 +157,3 @@ update: function(element, valueAccessor, allBindings, viewModel, bindingContext) { | ||
if (query === true) { | ||
query = Object.keys(bindingContext.$route.queryParams).reduce(function(q, k) { | ||
q[k] = ko.unwrap(bindingContext.$route.queryParams[k]) | ||
return q | ||
}, {}) | ||
query = mapQuery(bindingContext.$route.queryParams) | ||
} | ||
@@ -162,17 +173,8 @@ } | ||
ko.computed(function bindToQueryString() { | ||
var routes = activeRoutes(), | ||
query = routes.reduce(function(q, route) { | ||
if (route.queryParams) { | ||
Object.keys(route.queryParams).forEach(function(key) { | ||
var val = route.queryParams[key]() | ||
if (val !== route.queryParams[key].default) { | ||
q[key] = val | ||
} else { | ||
delete q[key] | ||
} | ||
}) | ||
} | ||
return q | ||
}, extend({}, routes.length ? routes[routes.length - 1].query : {})) | ||
var routes = activeRoutes() | ||
if (!routes.length) return | ||
var lastRoute = routes[routes.length - 1] | ||
query = mapQuery(lastRoute.queryParams, extend({}, lastRoute.query)) | ||
if (transitioning) return | ||
@@ -186,16 +188,14 @@ if (transitioning !== false) { | ||
stringified = router.options.qs.stringify(query) | ||
router.location.setURL(url.split('?')[0] + (stringified ? '?' + stringified : '')) | ||
router.location.replaceURL(url.split('?')[0] + (stringified ? '?' + stringified : '')) | ||
}) | ||
function updateQueryParams(route, query) { | ||
if (route.queryParams) { | ||
Object.keys(route.queryParams).forEach(function(key) { | ||
var observable = route.queryParams[key] | ||
if (key in query) { | ||
observable(query[key]) | ||
} else { | ||
observable(Array.isArray(observable.default) ? observable.default.slice() : observable.default) | ||
} | ||
}) | ||
} | ||
Object.keys(route.queryParams).forEach(function(key) { | ||
var observable = route.queryParams[key] | ||
if (key in query) { | ||
observable(query[key]) | ||
} else { | ||
observable(Array.isArray(observable.default) ? observable.default.slice() : observable.default) | ||
} | ||
}) | ||
} | ||
@@ -206,2 +206,4 @@ | ||
if ('resolutions' in comp && !comp.resolutions()) return false | ||
return Object.keys(route.params).every(function(param) { | ||
@@ -212,4 +214,4 @@ return comp.params[param] === route.params[param] | ||
return function knockoutCherrytreeMiddleware(transition) { | ||
var resolutions = {}, routeResolvers = [], startIdx = 0, | ||
function knockoutCherrytreeMiddleware(transition) { | ||
var resolutions = {}, routeResolvers = [], queryParams = {}, startIdx = 0, | ||
filteredRoutes = transition.routes.filter(function(route) { | ||
@@ -233,4 +235,9 @@ return route.options && !!(route.options.template || route.options.resolve) | ||
query: transition.query, | ||
queryParams: queryParams, | ||
resolutions: ko.observable(), | ||
transitionTo: transition.redirectTo | ||
transitionTo: function(name, params, query) { | ||
return query === true ? | ||
transition.redirectTo(name, params, mapQuery(routeData.queryParams)) : | ||
transition.redirectTo.apply(transition, arguments) | ||
} | ||
} | ||
@@ -245,17 +252,18 @@ | ||
if (query) { | ||
routeData.queryParams = Object.keys(query).reduce(function(q, key) { | ||
Object.keys(query).forEach(function(key) { | ||
var queryVal = routeData.query[key], defaultVal = query[key] | ||
if (!Array.isArray(defaultVal)) { | ||
q[key] = ko.observable(queryVal !== undefined ? queryVal : defaultVal) | ||
q[key].default = defaultVal | ||
queryParams[key] = ko.observable(queryVal !== undefined ? queryVal : defaultVal) | ||
queryParams[key].default = defaultVal | ||
} else { | ||
if (queryVal) { | ||
q[key] = ko.observableArray(Array.isArray(queryVal) ? queryVal : [queryVal]) | ||
queryParams[key] = ko.observableArray(Array.isArray(queryVal) ? queryVal : [queryVal]) | ||
} else { | ||
q[key] = ko.observableArray(defaultVal) | ||
queryParams[key] = ko.observableArray(defaultVal) | ||
} | ||
q[key].default = defaultVal.splice() | ||
queryParams[key].default = defaultVal.splice() | ||
} | ||
return q | ||
}, {}) | ||
}) | ||
queryParams = extend({}, queryParams) | ||
} | ||
@@ -286,2 +294,4 @@ } | ||
// using a peek() to avoid an extra nofication | ||
activeComponents.peek().splice(startIdx) | ||
activeRoutes.splice.apply(activeRoutes, [startIdx, activeRoutes().length - startIdx].concat(newRoutes)) | ||
@@ -294,2 +304,16 @@ transitioning = false | ||
} | ||
knockoutCherrytreeMiddleware.activeRoutes = ko.pureComputed(function() { | ||
return activeRoutes().map(function(r, idx) { | ||
return { | ||
name: r.name, | ||
params: r.params, | ||
query: r.query, | ||
resolutions: r.resolutions, | ||
component: activeComponents()[idx] | ||
} | ||
}) | ||
}) | ||
return knockoutCherrytreeMiddleware | ||
}) |
{ | ||
"name": "cherrytree-for-knockout", | ||
"version": "0.2.2", | ||
"version": "0.3.2", | ||
"description": "Use knockout components with CherryTree hiearchial routing", | ||
@@ -34,4 +34,4 @@ "main": "cherrytree-for-knockout.js", | ||
"chai": "^3.2.0", | ||
"chai-dom": "^1.2.1", | ||
"cherrytree": "^2.0.0-rc3", | ||
"chai-dom": "^1.4.0", | ||
"cherrytree": "^2.0.0", | ||
"grunt": "^0.4.5", | ||
@@ -38,0 +38,0 @@ "grunt-connect": "^0.2.0", |
@@ -15,3 +15,3 @@ ## CherryTree for Knockout | ||
cherrytree-for-knockout is extremely lightweight in the microlib spirit at < 200 lines of code. It has one job and does it well. | ||
cherrytree-for-knockout is very lightweight, focused on one single responsibility, with under 350 lines of code. It has one job and does it well. | ||
@@ -73,2 +73,6 @@ ### Example | ||
As you work with your route components, you'll often want to refer to it in your view, which in simple cases using `$component` will meet your needs. However, as you begin to use more components nested within eachother, using `$parents` is cumbersome and fragile, `$routeComponent` is available and exposed to work as your replacement for `$root` in a traditional one uber view-model structure. | ||
However `$routeComponent` is not supported with asyncrounous components. In general I recommend always declaring your components as syncrounous, as `bindingContext` was designed for the syncrounous binding world, and for symettry with standard bindings that always work syncronously. | ||
### Two-way binding of Query Parameters | ||
@@ -75,0 +79,0 @@ |
@@ -19,3 +19,5 @@ describe('CherryTree for Knockout', function() { | ||
template: '<section class="forums"><h1>Viewing all forums</h1><div data-bind="routeView: true"></div></section>', | ||
viewModel: function() {}, | ||
viewModel: function() { | ||
this.forumsViewModel = true | ||
}, | ||
synchronous: true | ||
@@ -77,3 +79,14 @@ } | ||
<a class="sort" data-bind="click: function() { sort(sort() === \'asc\' ? \'desc\' : \'asc\') }, text: sort"></a>\ | ||
<div data-bind="routeView: true"></div>\ | ||
</div>' | ||
}, function() { | ||
route('unread', { | ||
template: '<p class="unread-tags" data-bind="text: \'Tagged \' + $route.queryParams.tags().join(\', \')"></p>' | ||
}) | ||
route('compose', { | ||
query: { | ||
title: undefined, | ||
}, | ||
template: '<form><input name="title" data-bind="value: $route.queryParams.title" /></form>' | ||
}) | ||
}) | ||
@@ -205,2 +218,3 @@ }) | ||
}, | ||
queryParams: {}, | ||
query: { | ||
@@ -213,19 +227,56 @@ unreadOnly: 'true' | ||
it('should expose activeRoutes() with the current active route name, query, param, and resolutions', function() { | ||
location.setURL('/forums/1') | ||
it('should expose activeRoutes() with the current active route details, and the instance when it resolves', function() { | ||
var activeRoutes = ko.contextFor(testEl).$root.activeRoutes | ||
ko.isObservable(activeRoutes).should.be.true | ||
ko.bindingHandlers.routeView.middleware.activeRoutes.should.equal(activeRoutes) | ||
activeRoutes().should.be.empty | ||
should.not.exist(activeRoutes()[0]) | ||
var snapshots = [] | ||
var sub = activeRoutes.subscribe(function(items) { | ||
snapshots.push(items.slice()) | ||
}) | ||
thread.resolve = { | ||
foo: function() { | ||
return Promise.resolve('bar') | ||
} | ||
} | ||
location.setURL('/forums/1') | ||
return pollUntilPassing(function() { | ||
activeRoutes().length.should.equal(2) | ||
snapshots.length.should.equal(3) | ||
snapshots.map(function(s) { return s.length }).should.deep.equal([2, 2, 2]) | ||
should.not.exist(snapshots[0][0].component) | ||
should.not.exist(snapshots[0][1].component) | ||
snapshots[1][0].component.forumsViewModel.should.be.true | ||
should.not.exist(snapshots[1][1].component) | ||
snapshots[2][0].component.forumsViewModel.should.be.true | ||
snapshots[2][1].component.title.should.equal('Viewing forum {0}') | ||
snapshots[0][0].should.contain.keys(['params', 'query', 'resolutions']) | ||
snapshots[0][1].params.forumId.should.equal('1') | ||
snapshots[0].map(function(r) { return r.name }).should.deep.equal(['forums', 'threads']) | ||
activeRoutes().map(function(r) { return r.name }).should.deep.equal(['forums', 'threads']) | ||
activeRoutes()[1].params.forumId.should.equal('1') | ||
activeRoutes()[1].should.contain.keys(['params', 'query']) | ||
snapshots[0][1].params.forumId.should.equal('1') | ||
snapshots[0][1].should.contain.keys(['params', 'query']) | ||
}).then(function() { | ||
location.setURL('/forums/1/threads/2') | ||
should.not.exist(activeRoutes()[2]) | ||
return pollUntilPassing(function() { | ||
activeRoutes().map(function(r) { return r.name }).should.deep.equal(['forums', 'threads', 'thread']) | ||
activeRoutes()[2].params.forumId.should.equal('1') | ||
snapshots.length.should.equal(5) | ||
snapshots[3].map(function(r) { return r.name }).should.deep.equal(['forums', 'threads', 'thread']) | ||
snapshots[3][2].params.forumId.should.equal('1') | ||
should.not.exist(snapshots[3][2].component) | ||
snapshots[3][2].resolutions().should.be.ok | ||
snapshots[4][2].component.title.should.equal('Viewing thread {0}') | ||
snapshots[4][2].resolutions().foo.should.equal('bar') | ||
}) | ||
}).then(function() { | ||
sub.dispose() | ||
}) | ||
@@ -236,7 +287,8 @@ }) | ||
location.setURL('/forums/1') | ||
ko.contextFor(testEl).$leafRoute.should.be.instanceof(Function) | ||
should.not.exist(ko.contextFor(testEl).$leafRoute()) | ||
var rootContext = ko.contextFor(testEl) | ||
rootContext.$leafRoute.should.be.instanceof(Function) | ||
should.not.exist(rootContext.$leafRoute()) | ||
return pollUntilPassing(function() { | ||
ko.contextFor(testEl).$leafRoute().name.should.equal('threads') | ||
rootContext.$leafRoute().name.should.equal('threads') | ||
ko.contextFor(testEl.querySelector('section.forums h1')).$leafRoute().name.should.equal('threads') | ||
@@ -246,3 +298,3 @@ }).then(function() { | ||
return pollUntilPassing(function() { | ||
ko.contextFor(testEl).$leafRoute().name.should.equal('thread') | ||
rootContext.$leafRoute().name.should.equal('thread') | ||
ko.contextFor(testEl.querySelector('section.forums h1')).$leafRoute().name.should.equal('thread') | ||
@@ -254,2 +306,23 @@ ko.contextFor(testEl.querySelector('section.thread p')).$leafRoute().name.should.equal('thread') | ||
it('should set $route to the current route, not a sibling or parent', function() { | ||
location.setURL('/forums/1') | ||
var rootContext = ko.contextFor(testEl) | ||
should.not.exist(rootContext.$route) | ||
return pollUntilPassing(function() { | ||
should.not.exist(rootContext.$route) | ||
ko.contextFor(testEl.querySelector('section.forums')).$route.name.should.equal('forums') | ||
ko.contextFor(testEl.querySelector('section.forums > div')).$route.name.should.equal('forums') | ||
}).then(function() { | ||
location.setURL('/forums/1/threads/2') | ||
return pollUntilPassing(function() { | ||
should.not.exist(rootContext.$route) | ||
ko.contextFor(testEl.querySelector('section.forums')).$route.name.should.equal('forums') | ||
ko.contextFor(testEl.querySelector('section.forums > div')).$route.name.should.equal('forums') | ||
ko.contextFor(testEl.querySelector('section.thread')).$route.name.should.equal('thread') | ||
ko.contextFor(testEl.querySelector('section.thread h4')).$route.name.should.equal('thread') | ||
}) | ||
}) | ||
}) | ||
it('should expose the route component as $routeComponent', function() { | ||
@@ -319,2 +392,28 @@ ko.components.register('some-component', { | ||
}) | ||
it('should rerender any component that did not complete its resolutions', function() { | ||
forum.resolve = { | ||
foo: function(transition) { | ||
return (transition.params.threadId || 0) % 2 === 1 ? | ||
transition.redirectTo('/forums/' + transition.params.forumId + '/threads/' + (transition.params.threadId - 1)) | ||
: 'bar' | ||
} | ||
} | ||
location.setURL('/forums/1/threads/10') | ||
return pollUntilPassing(function() { | ||
testEl.querySelector('section.thread h4').should.contain.text('thread 10') | ||
}).then(function() { | ||
testEl.querySelector('section.forums section.forum').foo = 'bar' | ||
location.setURL('/forums/2/threads/9') | ||
return pollUntilPassing(function() { | ||
testEl.querySelector('section.thread h4').should.contain.text('thread 8') | ||
}) | ||
}).then(function() { | ||
var forumEl = testEl.querySelector('section.forums section.forum') | ||
forumEl.should.not.have.property('foo') | ||
ko.contextFor(forumEl).$route.resolutions().foo.should.equal('bar') | ||
}) | ||
}) | ||
}) | ||
@@ -461,3 +560,3 @@ | ||
testEl.querySelectorAll('section.forums').length.should.equal(0) | ||
}).then(function() { | ||
}).then(function() { | ||
forumsDeferred.resolve([{ id: 1, name: 'Home forum' }]) | ||
@@ -678,2 +777,11 @@ return pollUntilPassing(function() { | ||
return pollUntilPassing(function() { testEl.querySelector('.inbox a.sort').click }).then(function() { | ||
// unfortunately MemoryLocation#replaceURL calls setURL so we have to disambiguate that case | ||
origSetURL = location.setURL.bind(location) | ||
location.replaceURL = sinon.spy(function (path, options) { | ||
if (location.path !== path) { | ||
origSetURL(path, options) | ||
} | ||
}) | ||
sinon.spy(location, 'setURL') | ||
var sort = testEl.querySelector('.inbox a.sort') | ||
@@ -685,5 +793,8 @@ if (typeof sort.click === 'function') { | ||
} | ||
return pollUntilPassing(function() { | ||
location.getURL().should.equal('/inbox?foo=bar&search=bob&sort=asc') | ||
testEl.querySelector('.inbox a.sort').should.have.text('asc') | ||
location.replaceURL.should.have.been.calledOnce | ||
location.setURL.should.have.not.been.called | ||
}) | ||
@@ -752,3 +863,44 @@ }).then(function() { | ||
}) | ||
it('should inherit query string parameters from parent routes', function() { | ||
location.setURL('/inbox/unread?tags=promotion&tags=lastweek') | ||
return pollUntilPassing(function() { | ||
var p = testEl.querySelector('p.unread-tags') | ||
p.should.have.text('Tagged promotion, lastweek') | ||
ko.contextFor(p).$route.queryParams.sort().should.equal('desc') | ||
}) | ||
}) | ||
it('should use the same observable instances for children, but not expose child queries to parents', function() { | ||
location.setURL('/inbox/compose?foo=bar&title=Hello') | ||
return pollUntilPassing(function() { | ||
testEl.querySelector('input[name="title"]').value.should.equal('Hello') | ||
}).then(function() { | ||
ko.contextFor(testEl.querySelector('input[name="title"]')).$route.queryParams.sort('asc') | ||
return pollUntilPassing(function() { | ||
location.getURL().should.equal('/inbox/compose?foo=bar&title=Hello&sort=asc') | ||
var sortToggle = testEl.querySelector('.inbox a.sort') | ||
sortToggle.should.have.text('asc') | ||
ko.contextFor(sortToggle).$route.queryParams.should.not.contain.key('title') | ||
}) | ||
}) | ||
}) | ||
it('should provide a wrapper around transitionTo to easily include the current bound querystring parameters', function() { | ||
location.setURL('/inbox?foo=bar&search=bob') | ||
return pollUntilPassing(function() { testEl.querySelector('.inbox a.sort').click }).then(function() { | ||
testEl.querySelector('.inbox a.sort').should.have.text('desc') | ||
location.setURL('/inbox?foo=baz&search=Jane&tags=unread&tags=priority') | ||
return pollUntilPassing(function() { | ||
location.getURL().should.equal('/inbox?foo=baz&search=Jane&tags=unread&tags=priority') | ||
}) | ||
}).then(function() { | ||
ko.contextFor(testEl.querySelector('a.sort')).$route.transitionTo('router-href-test', { someparam: 1 }, true) | ||
return pollUntilPassing(function() { | ||
location.getURL().should.equal('/href-test/1?search=Jane&tags=unread&tags=priority') | ||
}) | ||
}) | ||
}) | ||
}) | ||
}) |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
56792
1163
97