cherrytree-for-knockout
Advanced tools
Comparing version 0.4.1 to 0.5.2
@@ -7,3 +7,3 @@ { | ||
], | ||
"description": "Use knockout components with CherryTree hiearchial routing", | ||
"description": "Hiearchial Routing in Knockout with CherryTree", | ||
"main": "cherrytree-for-knockout.js", | ||
@@ -19,3 +19,2 @@ "moduleType": [ | ||
"routes", | ||
"components", | ||
"web-components", | ||
@@ -33,2 +32,6 @@ "routing", | ||
], | ||
"dependencies": { | ||
"cherrytree": "^2.0.0", | ||
"knockout": "^3.0.0" | ||
}, | ||
"devDependencies": { | ||
@@ -35,0 +38,0 @@ "chai-dom": "~1.0.0" |
@@ -11,16 +11,8 @@ (function(factory) { | ||
})(function(ko) { | ||
var transitioning, router, | ||
activeRoutes = ko.observableArray(), | ||
activeComponents = ko.observableArray() | ||
var transitioning, pendingQueryStringWrite, | ||
router, | ||
templates = {}, | ||
viewModels = {}, | ||
activeRoutes = ko.observableArray() | ||
ko.components.register('route-blank', { | ||
template: '<div></div>', | ||
synchronous: true | ||
}) | ||
ko.components.register('route-loading', { | ||
template: '<div class="route-loading"></div>', | ||
synchronous: true | ||
}) | ||
function extend(target, obj) { | ||
@@ -33,27 +25,4 @@ return Object.keys(obj).reduce(function(t, key) { | ||
var origCreatChildContext = ko.bindingContext.prototype.createChildContext | ||
// a bit of a hack, but since the component binding instantiates the component, | ||
// likely async too, and no way to walk down binding contexts. | ||
// we are extending the component context | ||
ko.bindingContext.prototype.createChildContext = function(dataItemOrAccessor, dataItemAlias, extendCallback) { | ||
return origCreatChildContext.call(this, dataItemOrAccessor, dataItemAlias, function(ctx) { | ||
var retval = typeof extendCallback === 'function' && extendCallback(ctx) | ||
if (ctx && ctx.$parentContext && ctx.$parentContext._routeCtx) { | ||
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) | ||
} | ||
} | ||
return retval | ||
}) | ||
} | ||
ko.bindingHandlers.routeView = { | ||
init: function(_, valueAccessor, __, ___, bindingContext) { | ||
init: function(element, valueAccessor, __, ___, bindingContext) { | ||
var r = valueAccessor() | ||
@@ -73,10 +42,6 @@ if (r && typeof r.map === 'function' && typeof r.use === 'function') { | ||
return { controlsDescendantBindings: true } | ||
}, | ||
update: function(element, valueAccessor, ab, vm, bindingContext) { | ||
var depth = 0, contextIter = bindingContext, | ||
routeComponent = ko.observable({ name: 'route-blank' }), | ||
prevRoute, routeClass | ||
prevRoute, routeClass | ||
while (contextIter.$parentContext && contextIter.$routeComponent !== contextIter.$parentContext.$routeComponent) { | ||
while (contextIter.$parentContext && contextIter.$route !== contextIter.$parentContext.$route) { | ||
depth++ | ||
@@ -86,42 +51,70 @@ contextIter = contextIter.$parentContext | ||
function setRouteName(name) { | ||
if (element.nodeType !== 8) { | ||
var newClass = name && name.replace(/[^a-zA-Z0-9]/g, '-').toLowerCase() | ||
if (newClass === routeClass) return | ||
if (routeClass) { | ||
element.classList.remove(routeClass) | ||
} | ||
routeClass = newClass | ||
element.classList.add(routeClass) | ||
} | ||
} | ||
function disposePrevRouteIfNeeded() { | ||
if (prevRoute && prevRoute.$root && typeof prevRoute.$root.dispose === 'function') { | ||
prevRoute.$root.dispose() | ||
} | ||
} | ||
ko.computed(function() { | ||
var route = activeRoutes()[depth] | ||
if (route == prevRoute) return | ||
disposePrevRouteIfNeeded() | ||
if (!route) { | ||
routeComponent({ name: 'route-blank' }) | ||
ko.utils.emptyDomNode(element) | ||
setRouteName() | ||
return | ||
} | ||
bindingContext._routeCtx = route | ||
var res = route.resolutions() | ||
if (res) { | ||
var params = extend({}, res) | ||
params.$route = extend({}, route) | ||
delete params.$route.resolutions | ||
extend(params, route.queryParams) | ||
if (viewModels[route.name]) { | ||
var params = extend({}, res), | ||
routeCopy = extend({}, route) | ||
delete routeCopy.resolutions | ||
extend(params, route.queryParams) | ||
route.$root = new viewModels[route.name](params, routeCopy, element) | ||
} | ||
prevRoute = route | ||
routeComponent({ name: ko.bindingHandlers.routeView.prefix + route.name, params: params }) | ||
ko.virtualElements.setDomNodeChildren(element, ko.utils.cloneNodes(templates[route.name])) | ||
var childCtx = bindingContext.createChildContext(route.$root, null, function(context) { | ||
context.$route = route | ||
}) | ||
setRouteName('route-' + route.name) | ||
ko.applyBindingsToDescendants(childCtx, element) | ||
if (route.$root) { | ||
knockoutCherrytreeMiddleware.activeRoutes.notifySubscribers(activeRoutes().slice()) | ||
} | ||
} else { | ||
routeComponent({ name: 'route-loading' }) | ||
setRouteName('route-loading') | ||
ko.virtualElements.setDomNodeChildren(element, [ko.bindingHandlers.routeView.routeLoading.cloneNode(true)]) | ||
} | ||
}, null, { disposeWhenNodeIsRemoved: element }).extend({ rateLimit: 5 }) | ||
ko.computed(function() { | ||
var newClass = routeComponent().name.replace(/[^a-zA-Z0-9]/g, '-').toLowerCase() | ||
if (newClass === routeClass) return | ||
if (routeClass) { | ||
element.classList.remove(routeClass) | ||
} | ||
ko.utils.domNodeDisposal.addDisposeCallback(element, disposePrevRouteIfNeeded) | ||
routeClass = newClass | ||
element.classList.add(routeClass) | ||
}, null, { disposeWhenNodeIsRemoved: element }) | ||
return ko.bindingHandlers.component.init(element, routeComponent, ab, vm, bindingContext) | ||
return { controlsDescendantBindings: true } | ||
} | ||
} | ||
ko.bindingHandlers.routeView.prefix = 'route:' | ||
ko.virtualElements.allowedBindings.routeView = true | ||
ko.bindingHandlers.routeView.routeLoading = document.createElement('div') | ||
ko.bindingHandlers.routeView.routeLoading.className = 'route-loading' | ||
function mapQuery(queryParams, query) { | ||
@@ -173,3 +166,4 @@ return Object.keys(queryParams).reduce(function(q, k) { | ||
var lastRoute = routes[routes.length - 1], | ||
query = mapQuery(lastRoute.queryParams, extend({}, lastRoute.query)) | ||
query = mapQuery(lastRoute.queryParams, extend({}, lastRoute.query)), | ||
stringified = router.options.qs.stringify(query) | ||
@@ -182,4 +176,4 @@ if (transitioning) return | ||
var url = router.location.getURL(), | ||
stringified = router.options.qs.stringify(query) | ||
var url = router.location.getURL() | ||
pendingQueryStringWrite = true | ||
router.location.replaceURL(url.split('?')[0] + (stringified ? '?' + stringified : '')) | ||
@@ -189,2 +183,6 @@ }) | ||
function updateQueryParams(route, query) { | ||
if (pendingQueryStringWrite) { | ||
pendingQueryStringWrite = false | ||
return | ||
} | ||
Object.keys(route.queryParams).forEach(function(key) { | ||
@@ -219,9 +217,19 @@ var observable = route.queryParams[key] | ||
Object.assign(resolutions, activeRoutes()[startIdx].resolutions()) | ||
updateQueryParams(activeRoutes()[startIdx], transition.query) | ||
startIdx++ | ||
} | ||
var lastSkipped = activeRoutes()[startIdx - 1] | ||
if (lastSkipped) { | ||
transition.query && updateQueryParams(lastSkipped, transition.query) | ||
queryParams = extend({}, lastSkipped.queryParams) | ||
} | ||
var newRoutes = filteredRoutes.slice(startIdx).map(function(route) { | ||
var routeData | ||
if (route.options.template) { | ||
if (!templates[route.name]) { | ||
templates[route.name] = ko.utils.parseHtmlFragment(route.options.template) | ||
viewModels[route.name] = route.options.viewModel | ||
} | ||
routeData = { | ||
@@ -240,7 +248,2 @@ name: route.name, | ||
var compName = ko.bindingHandlers.routeView.prefix + routeData.name | ||
if (!ko.components.isRegistered(compName)) { | ||
ko.components.register(compName, route.options) | ||
} | ||
var query = route.options.query | ||
@@ -289,4 +292,2 @@ if (query) { | ||
// using a peek() to avoid an extra nofication | ||
activeComponents.peek().splice(startIdx) | ||
activeRoutes.splice.apply(activeRoutes, [startIdx, activeRoutes().length - startIdx].concat(newRoutes)) | ||
@@ -301,11 +302,3 @@ 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 activeRoutes().slice() | ||
}) | ||
@@ -312,0 +305,0 @@ |
{ | ||
"name": "cherrytree-for-knockout", | ||
"version": "0.4.1", | ||
"description": "Use knockout components with CherryTree hiearchial routing", | ||
"version": "0.5.2", | ||
"description": "Hiearchial Routing in Knockout with CherryTree", | ||
"main": "cherrytree-for-knockout.js", | ||
@@ -29,3 +29,3 @@ "scripts": { | ||
"dependencies": { | ||
"knockout": "^3.3.0" | ||
"knockout": "^3.0.0" | ||
}, | ||
@@ -32,0 +32,0 @@ "devDependencies": { |
## CherryTree for Knockout | ||
### Component-based hiearchial routing for [Knockout](http://knockoutjs.com) via the [CherryTree](https://github.com/QubitProducts/cherrytree) router | ||
### Hiearchial routing for [Knockout](http://knockoutjs.com) via the [CherryTree](https://github.com/QubitProducts/cherrytree) router | ||
[![Build Status](https://secure.travis-ci.org/nathanboktae/cherrytree-for-knockout.png?branch=master)](https://travis-ci.org/nathanboktae/cherrytree-for-knockout) | ||
[![Build Status](https://secure.travis-ci.org/nathanboktae/cherrytree-for-knockout.png?branch=master)](https://travis-ci.org/nathanboktae/cherrytree-for-knockout) [![Join the chat at https://gitter.im/nathanboktae/cherrytree-for-knockout](https://badges.gitter.im/nathanboktae/cherrytree-for-knockout.svg)](https://gitter.im/nathanboktae/cherrytree-for-knockout?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) | ||
@@ -13,3 +13,3 @@ [![SauceLabs Test Status](https://saucelabs.com/browser-matrix/Cherrytree-ko.svg)](https://saucelabs.com/u/Cherrytree-ko) | ||
cherrytree-for-knockout helps you with all that legwork. You associate components with routes that will load and display where you want in the page (you define that, and anything outside the component for the route, like a breadcrumb bar, account dropdown that is on every page, etc is fully in your control). You can even specify data you need (any function that returns a promise) that will be provided to your component before initializes. | ||
cherrytree-for-knockout helps you with all that legwork. Inspired by Knockout components, You associate view models and templates with routes that will load and display where you want in the page (you define that, and anything outside the view model for the route, like a breadcrumb bar, account dropdown that is on every page, etc is fully in your control). You can even specify data you need (any function that returns a promise) that will be provided to your view model constructor. | ||
@@ -20,3 +20,3 @@ 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. | ||
Specify your components when you map your routes like so: | ||
Specify your template and optional viewModel constructor when you map your routes like so: | ||
@@ -46,4 +46,2 @@ ```javascript | ||
Notice that you do not have to explicitly register the component via `ko.components.register` - cherrytree-for-knockout will do that for you. | ||
Now for the HTML: | ||
@@ -54,3 +52,3 @@ | ||
<header> | ||
<ul data-bind="foreach: route.state && route.state.routes"> | ||
<ul data-bind="foreach: $root.activeRoutes()"> | ||
<li data-bind="routeHref: name, text: name"></li> | ||
@@ -70,12 +68,10 @@ </ul> | ||
Notice the `routeView` binding. This is where a component for a route will be rendered. In the top level `routeView` binding, you must provide the router instance. This will be available on the root view model as `router`. For nested `routeView`s, the parameter is currently ignored so `true` or `{}` will suffice. | ||
Notice the `routeView` binding. This is where your route will be rendered. In the top level `routeView` binding, you must provide the router instance. This will be available on the root view model as `router`. For nested `routeView`s, the parameter is currently ignored so `true` or `{}` will suffice. | ||
Above `main` there is a header which creates bindings based on the current route state. cherrytree-for-knockout will back the `state` property behind an observable, so when the current route changes, depedencies will update, so we can have a simple breadcrumb in this example. `routeHref` is a binding handler that will set the `href` for the route you specify via `router.generate` | ||
Above `main` there is a header which creates a breadcrumb of the active routes. `activeRoutes` is added onto the $root of your view model by `cherrytree-for-knockout`. `routeHref` is a binding handler that will set the `href` for the route you specify via `router.generate` | ||
Below that is a signout button with a click handler, showing that cherrytree-for-knockout plugs into your existing app how you wish, and ultimately your are still in control of your application's layout and workflow. | ||
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. | ||
When writing your view markup, you can access the route view model at `$route.$root`. | ||
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 | ||
@@ -92,7 +88,6 @@ | ||
viewModel: function(params) { | ||
this.sort = params.sort | ||
this.toggleSort = () => params.sort(params.sort() === 'asc' ? 'desc' : 'asc') | ||
} | ||
template: '<div class="inbox">\ | ||
<a class="sort" data-bind="click: toggleSort, text: sort"></a>\ | ||
<a class="sort" data-bind="click: $route.$root.toggleSort, text: $route.queryParams.sort"></a>\ | ||
</div>' | ||
@@ -99,0 +94,0 @@ } |
@@ -12,4 +12,3 @@ describe('CherryTree for Knockout', function() { | ||
this.title = 'please login' | ||
}, | ||
synchronous: true | ||
} | ||
} | ||
@@ -19,7 +18,10 @@ | ||
path: 'forums', | ||
template: '<section class="forums"><h1>Viewing all forums</h1><div data-bind="routeView: true"></div></section>', | ||
template: '<section class="forums"><h1>Viewing all forums</h1><!-- ko routeView: true --><!-- /ko --></section>', | ||
viewModel: function() { | ||
this.forumsViewModel = true | ||
this.dispose = function() { | ||
forums.disposeCount++ | ||
} | ||
}, | ||
synchronous: true | ||
disposeCount: 0 | ||
} | ||
@@ -32,4 +34,3 @@ | ||
this.title = 'Viewing forum {0}' | ||
}, | ||
synchronous: true | ||
} | ||
} | ||
@@ -39,9 +40,20 @@ | ||
path: 'threads/:threadId', | ||
template: '<section class="thread">\ | ||
<h4><a data-bind="text: title.replace(\'{0}\', $route.params.threadId), routeHref: \'threads\'"></a></h4>\ | ||
<p data-bind="text: JSON.stringify($route)"></p></section>', | ||
viewModel: function() { | ||
this.title = 'Viewing thread {0}' | ||
template: '\ | ||
<section class="thread">\ | ||
<nav><a data-bind="routeHref: \'threads\'"></a></nav>\ | ||
<h4 data-bind="text: title.replace(\'{0}\', $route.params.threadId)"></h4>\ | ||
<p data-bind="text: JSON.stringify($route)"></p>\ | ||
<ul data-bind="foreach: threads">\ | ||
<li data-bind="text: title"></li>\ | ||
</ul>\ | ||
</section>', | ||
viewModel: function(params, route) { | ||
route.should.contain.keys(['query', 'params', 'transitionTo']) | ||
this.title = params.forum ? 'Viewing threads for forum ' + params.forum.name : 'Viewing thread {0}' | ||
this.threads = params.threads || [] | ||
this.dispose = function() { | ||
thread.disposeCount++ | ||
} | ||
}, | ||
synchronous: true | ||
disposeCount: 0 | ||
} | ||
@@ -57,3 +69,2 @@ | ||
route('router-href-test', { | ||
synchronous: true, | ||
path: 'href-test/:someparam', | ||
@@ -71,3 +82,2 @@ query: { | ||
route('inbox', { | ||
synchronous: true, | ||
query: { | ||
@@ -146,3 +156,3 @@ sort: 'desc', | ||
it('should automatically register a component and render it', function() { | ||
it('should render a simple route when navigated to', function() { | ||
location.setURL('/login') | ||
@@ -154,3 +164,3 @@ return pollUntilPassing(function() { | ||
it('should render nested components with route params', function() { | ||
it('should render nested routes with route params', function() { | ||
location.setURL('/forums/1') | ||
@@ -196,16 +206,25 @@ return pollUntilPassing(function() { | ||
it('should not register components already registered', function() { | ||
ko.components.unregister('route:login') | ||
ko.components.register('route:login', { | ||
template: login.template, | ||
viewModel: function() { | ||
this.title = 'The login form!' | ||
}, | ||
synchronous: true | ||
}) | ||
it('should call dispose of view models that have a dispose function', function() { | ||
location.setURL('/forums/1/threads/2') | ||
return pollUntilPassing(function() { | ||
testEl.querySelector('section.forums section.forum section.thread').should.be.ok | ||
}).then(function() { | ||
forums.disposeCount.should.equal(0) | ||
thread.disposeCount.should.equal(0) | ||
location.setURL('/forums/1/threads/4') | ||
location.setURL('/login') | ||
return pollUntilPassing(function() { | ||
testEl.querySelector('section.login').should.be.ok | ||
testEl.querySelector('section.login h1').textContent.should.equal('The login form!') | ||
return pollUntilPassing(function() { | ||
testEl.querySelector('section.thread h4').textContent.should.equal('Viewing thread 4') | ||
}) | ||
}).then(function() { | ||
forums.disposeCount.should.equal(0) | ||
thread.disposeCount.should.equal(1) | ||
location.setURL('/login') | ||
return pollUntilPassing(function() { | ||
testEl.querySelector('section.login h1').textContent.should.equal('please login') | ||
}) | ||
}).then(function() { | ||
forums.disposeCount.should.equal(1) | ||
thread.disposeCount.should.equal(2) | ||
}) | ||
@@ -228,2 +247,6 @@ }) | ||
unreadOnly: 'true' | ||
}, | ||
$root: { | ||
title: 'Viewing thread {0}', | ||
threads: [] | ||
} | ||
@@ -244,8 +267,20 @@ }) | ||
var sub = activeRoutes.subscribe(function(items) { | ||
snapshots.push(items.slice()) | ||
snapshots.push(items.map(function(i) { | ||
i.should.contain.keys(['params', 'query', 'queryParams', 'resolutions']) | ||
return { | ||
name: i.name, | ||
$root: i.$root, | ||
params: i.params, | ||
resolutions: i.resolutions() | ||
} | ||
})) | ||
}) | ||
thread.resolve = { | ||
forum.resolve = { | ||
foo: function() { | ||
return Promise.resolve('bar') | ||
return new Promise(function(r) { | ||
setTimeout(function() { | ||
r('bar') | ||
}, 50) | ||
}) | ||
} | ||
@@ -261,11 +296,9 @@ } | ||
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}') | ||
should.not.exist(snapshots[0][0].$root) | ||
should.not.exist(snapshots[0][1].$root) | ||
snapshots[1][0].$root.forumsViewModel.should.be.true | ||
should.not.exist(snapshots[1][1].$root) | ||
snapshots[2][0].$root.forumsViewModel.should.be.true | ||
snapshots[2][1].$root.title.should.equal('Viewing forum {0}') | ||
snapshots[0][0].should.contain.keys(['params', 'query', 'resolutions']) | ||
snapshots[0][1].params.forumId.should.equal('1') | ||
@@ -275,3 +308,2 @@ snapshots[0].map(function(r) { return r.name }).should.deep.equal(['forums', 'threads']) | ||
snapshots[0][1].params.forumId.should.equal('1') | ||
snapshots[0][1].should.contain.keys(['params', 'query']) | ||
}).then(function() { | ||
@@ -284,6 +316,6 @@ location.setURL('/forums/1/threads/2') | ||
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') | ||
should.not.exist(snapshots[3][2].$root) | ||
snapshots[3][2].resolutions.should.be.ok | ||
snapshots[4][2].$root.title.should.equal('Viewing thread {0}') | ||
snapshots[4][2].resolutions.foo.should.equal('bar') | ||
}) | ||
@@ -322,3 +354,3 @@ }).then(function() { | ||
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.forums h1')).$route.name.should.equal('forums') | ||
}).then(function() { | ||
@@ -329,3 +361,3 @@ location.setURL('/forums/1/threads/2') | ||
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.forums h1')).$route.name.should.equal('forums') | ||
ko.contextFor(testEl.querySelector('section.thread')).$route.name.should.equal('thread') | ||
@@ -337,16 +369,13 @@ ko.contextFor(testEl.querySelector('section.thread h4')).$route.name.should.equal('thread') | ||
it('should expose the route component as $routeComponent', function() { | ||
ko.components.register('some-component', { | ||
template: '<div class="route-comp-test" data-bind="text: $routeComponent.foo"></div>' | ||
}) | ||
it('should expose the route viewModel instance as $route.$root', function() { | ||
router.map(function(route) { | ||
route('routecomp', { | ||
route('myroute', { | ||
viewModel: function() { | ||
this.foo = 'bar' | ||
}, | ||
template: '<some-component></some-component>' | ||
template: '<div class="route-comp-test" data-bind="text: $route.$root.foo"></div>' | ||
}) | ||
}) | ||
location.setURL('/routecomp') | ||
location.setURL('/myroute') | ||
return pollUntilPassing(function() { | ||
@@ -473,3 +502,3 @@ testEl.querySelector('.route-comp-test').textContent.should.equal('bar') | ||
return pollUntilPassing(function() { | ||
testEl.querySelector('section.thread h4 a').should.have.attr('href', '/forums/1') | ||
testEl.querySelector('section.thread nav a').should.have.attr('href', '/forums/1') | ||
}) | ||
@@ -529,3 +558,2 @@ }) | ||
delete thread.viewModel | ||
thread.resolve = { | ||
@@ -539,25 +567,2 @@ forum: function(transition, resolutions) { | ||
} | ||
thread.viewModel = { | ||
createViewModel: function(params) { | ||
params.$route.should.contain.keys(['query', 'params', 'transitionTo']) | ||
return { | ||
title: 'Viewing threads for forum ' + params.forum.name, | ||
threads: params.threads | ||
} | ||
} | ||
} | ||
thread.template = '\ | ||
<section class="thread">\ | ||
<h4 data-bind="text: title"></h4>\ | ||
<ul data-bind="foreach: threads">\ | ||
<li data-bind="text: title"></li>\ | ||
</ul>\ | ||
</section>' | ||
if (ko.components.isRegistered('route:forums')) { | ||
ko.components.unregister('route:forums') | ||
} | ||
if (ko.components.isRegistered('route:thread')) { | ||
ko.components.unregister('route:thread') | ||
} | ||
}) | ||
@@ -582,7 +587,4 @@ | ||
it('can have the loading component replaced by a custom component', function() { | ||
ko.components.unregister('route-loading') | ||
ko.components.register('route-loading', { | ||
template: '<blink class="route-loading">loading!!!</blink>' | ||
}) | ||
it('can have custom loading template by provided', function() { | ||
ko.bindingHandlers.routeView.routeLoading = ko.utils.parseHtmlFragment('<blink>loading!!!</blink>')[0] | ||
@@ -609,12 +611,9 @@ router.map(function(route) { | ||
template: '<div class="campaign"></div>', | ||
viewModel: { | ||
createViewModel: function(params) { | ||
try { | ||
params.$route.params.campaign.should.equal('v2launch') | ||
params.$route.query.should.deep.equal({ title: 'Check out our new version!' }) | ||
setTimeout(done, 1) | ||
} catch (e) { | ||
done(e) | ||
} | ||
return {} | ||
viewModel: function(params, route) { | ||
try { | ||
route.params.campaign.should.equal('v2launch') | ||
route.query.should.deep.equal({ title: 'Check out our new version!' }) | ||
setTimeout(done, 1) | ||
} catch (e) { | ||
done(e) | ||
} | ||
@@ -737,5 +736,3 @@ } | ||
path: '/profile', | ||
viewModel: { | ||
createViewModel: profileViewModel | ||
}, | ||
viewModel: profileViewModel, | ||
template: '<div class="profile"></div>' | ||
@@ -747,5 +744,3 @@ }) | ||
}, | ||
viewModel: { | ||
createViewModel: orderHistoryViewModel | ||
}, | ||
viewModel: orderHistoryViewModel, | ||
template: '<div class="order-history"></div>' | ||
@@ -759,3 +754,3 @@ }) | ||
return pollUntilPassing(function() { | ||
profileViewModel.should.have.been.calledOnce | ||
profileViewModel.should.have.been.calledOnce.and.calledWithNew.mmmkay | ||
profileViewModel.firstCall.args[0].account.should.deep.equal({ name: 'Bob' }) | ||
@@ -878,2 +873,25 @@ }) | ||
it('should allow for extention of objects in query paramater values and take dependencies on them', function() { | ||
location.setURL('/inbox?tags[name]=unread') | ||
var tag = {}, name = ko.observable() | ||
Object.defineProperty(tag, 'name', { get: name, enumerable: true }) | ||
return pollUntilPassing(function() { testEl.querySelector('.inbox a.sort').click }).then(function() { | ||
var tags = ko.contextFor(testEl.querySelector('.inbox a.sort')).$route.queryParams.tags | ||
tags.push(tag) | ||
return router.state.activeTransition | ||
}).then(function() { | ||
name('spam') | ||
return pollUntilPassing(function() { | ||
location.getURL().should.equal('/inbox?tags%5B0%5D%5Bname%5D=unread&tags%5B1%5D%5Bname%5D=spam') | ||
}) | ||
}).then(function() { | ||
name('bar') | ||
return pollUntilPassing(function() { | ||
location.getURL().should.equal('/inbox?tags%5B0%5D%5Bname%5D=unread&tags%5B1%5D%5Bname%5D=bar') | ||
}) | ||
}) | ||
}) | ||
it('should inherit query string parameters from parent routes', function() { | ||
@@ -885,2 +903,9 @@ location.setURL('/inbox/unread?tags=promotion&tags=lastweek') | ||
ko.contextFor(p).$route.queryParams.sort().should.equal('desc') | ||
}).then(function() { | ||
location.setURL('/inbox/compose?search=itinerary&title=Hi') | ||
return pollUntilPassing(function() { | ||
var ctx = ko.contextFor(testEl.querySelector('input[name="title"]')) | ||
ctx.$route.queryParams.search().should.equal('itinerary') | ||
ctx.$route.queryParams.title().should.equal('Hi') | ||
}) | ||
}) | ||
@@ -887,0 +912,0 @@ }) |
Sorry, the diff of this file is not supported yet
57260
1180
92
Updatedknockout@^3.0.0