cherrytree-for-knockout
Advanced tools
Comparing version 0.1.0 to 0.2.2
@@ -5,6 +5,4 @@ (function(factory) { | ||
} else if (typeof exports === 'object' && typeof module === 'object') { | ||
/*global module*/ | ||
module.exports = factory | ||
} else { | ||
/*global ko*/ | ||
var middleware = factory(ko) | ||
@@ -14,3 +12,3 @@ ko.bindingHandlers.routeView.middleware = middleware | ||
})(function(ko) { | ||
var activeRoutes = ko.observableArray() | ||
var activeRoutes = ko.observableArray(), transitioning, router | ||
@@ -27,7 +25,7 @@ ko.components.register('route-blank', { | ||
function clone(obj) { | ||
return Object.keys(obj).reduce(function(clone, key) { | ||
clone[key] = obj[key] | ||
return clone | ||
}, {}) | ||
function extend(target, obj) { | ||
return Object.keys(obj).reduce(function(t, key) { | ||
t[key] = obj[key] | ||
return t | ||
}, target) | ||
} | ||
@@ -54,9 +52,13 @@ | ||
init: function(_, valueAccessor, __, ___, bindingContext) { | ||
var router = valueAccessor() | ||
if (router && typeof router.map === 'function' && typeof router.use === 'function') { | ||
var r = valueAccessor() | ||
if (r && typeof r.map === 'function' && typeof r.use === 'function') { | ||
router = r | ||
if (!bindingContext.$root.router) { | ||
bindingContext.$root.router = router | ||
bindingContext.$root.router = r | ||
} | ||
if (!bindingContext.$root.activeRoutes) { | ||
bindingContext.$root.activeRoutes = activeRoutes | ||
bindingContext.$leafRoute = function() { | ||
return activeRoutes()[activeRoutes().length - 1] | ||
} | ||
} | ||
@@ -92,5 +94,10 @@ } | ||
if (res) { | ||
var params = clone(res) | ||
params.$route = clone(route) | ||
var params = extend({}, res) | ||
params.$route = extend({}, route) | ||
delete params.$route.resolutions | ||
if (route.queryParams) { | ||
extend(params, route.queryParams) | ||
} | ||
prevRoute = route | ||
@@ -121,3 +128,2 @@ routeComponent({ name: ko.bindingHandlers.routeView.prefix + route.name, params: params }) | ||
update: function(element, valueAccessor, allBindings, viewModel, bindingContext) { | ||
var router = bindingContext.$root.router | ||
if (!router) { | ||
@@ -128,3 +134,3 @@ throw new Error('No router found on the root binding context. Make sure to initialize the toplevel routeView with your router as the option.') | ||
return ko.bindingHandlers.attr.update(element, function() { | ||
var opts = ko.unwrap(valueAccessor()), name, params | ||
var opts = ko.unwrap(valueAccessor()), name, params, query | ||
if (typeof opts === 'string') { | ||
@@ -135,2 +141,9 @@ name = opts | ||
params = ko.unwrap(opts.params) | ||
query = ko.unwrap(opts.query) | ||
if (query === true) { | ||
query = Object.keys(bindingContext.$route.queryParams).reduce(function(q, k) { | ||
q[k] = ko.unwrap(bindingContext.$route.queryParams[k]) | ||
return q | ||
}, {}) | ||
} | ||
} | ||
@@ -141,3 +154,4 @@ | ||
name || bindingContext.$route.name, | ||
params || bindingContext.$route.params) | ||
params || bindingContext.$route.params, | ||
query) | ||
} | ||
@@ -148,2 +162,42 @@ }, allBindings, viewModel, bindingContext) | ||
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 : {})) | ||
if (transitioning) return | ||
if (transitioning !== false) { | ||
transitioning = false | ||
return | ||
} | ||
var url = router.location.getURL(), | ||
stringified = router.options.qs.stringify(query) | ||
router.location.setURL(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) | ||
} | ||
}) | ||
} | ||
} | ||
function routeEqual(comp, route) { | ||
@@ -162,6 +216,7 @@ if (!comp || !route || comp.name !== route.name) return false | ||
}) | ||
transitioning = true // router.state.activeTransition isn't set to this one yet | ||
while (routeEqual(activeRoutes()[startIdx], filteredRoutes[startIdx])) { | ||
Object.assign(resolutions, activeRoutes()[startIdx].resolutions()) | ||
updateQueryParams(activeRoutes()[startIdx], transition.query) | ||
startIdx++ | ||
@@ -180,2 +235,3 @@ } | ||
} | ||
var compName = ko.bindingHandlers.routeView.prefix + routeData.name | ||
@@ -185,2 +241,21 @@ if (!ko.components.isRegistered(compName)) { | ||
} | ||
var query = route.options.query | ||
if (query) { | ||
routeData.queryParams = Object.keys(query).reduce(function(q, 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 | ||
} else { | ||
if (queryVal) { | ||
q[key] = ko.observableArray(Array.isArray(queryVal) ? queryVal : [queryVal]) | ||
} else { | ||
q[key] = ko.observableArray(defaultVal) | ||
} | ||
q[key].default = defaultVal.splice() | ||
} | ||
return q | ||
}, {}) | ||
} | ||
} | ||
@@ -211,2 +286,3 @@ | ||
activeRoutes.splice.apply(activeRoutes, [startIdx, activeRoutes().length - startIdx].concat(newRoutes)) | ||
transitioning = false | ||
@@ -213,0 +289,0 @@ return routeResolvers.reduce(function(promise, then) { |
{ | ||
"name": "cherrytree-for-knockout", | ||
"version": "0.1.0", | ||
"version": "0.2.2", | ||
"description": "Use knockout components with CherryTree hiearchial routing", | ||
@@ -43,2 +43,3 @@ "main": "cherrytree-for-knockout.js", | ||
"phantomjs": "1.9.7-15", | ||
"qs": "^6.0.0", | ||
"sinon-browser-only": "^1.12.1", | ||
@@ -45,0 +46,0 @@ "sinon-chai": "^2.8.0" |
@@ -27,3 +27,3 @@ ## CherryTree for Knockout | ||
}, | ||
template: '<form class="login"><input name="username" data-bind="value: username></input> .... </form>' | ||
template: '<form class="login"><input name="username" data-bind="value: username"></input> .... </form>' | ||
} | ||
@@ -49,3 +49,3 @@ | ||
``` | ||
```html | ||
<body> | ||
@@ -74,1 +74,22 @@ <header> | ||
### Two-way binding of Query Parameters | ||
Keeping all your view state in the query parameter allows users to always refresh the page and get back right where they are at, and share links to other people to see exactly what they are seeing. cherrytree-for-knockout will let you bind to query string parameters easily to support this by giving you an observable that reflects the query string, including defaults. | ||
```javascript | ||
var inbox = { | ||
path: 'inbox', | ||
query: { | ||
sort: 'desc' | ||
}, | ||
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>\ | ||
</div>' | ||
} | ||
``` | ||
When `a.sort` is clicked, the URL becomes `/inbox?sort=desc`. When clicked again, it becomes `/inbox` as sort gets set back to it's default. |
describe('CherryTree for Knockout', function() { | ||
var router, location, testEl, forums, forum, thread, login, hrefTest, goToRoute | ||
var router, location, testEl, forums, forum, thread, login, inboxParams, goToRoute | ||
beforeEach(function() { | ||
router = cherrytree({ location: 'memory' }) | ||
router = cherrytree({ location: 'memory', qs: Qs }) | ||
router.use(ko.bindingHandlers.routeView.middleware) | ||
@@ -43,13 +43,2 @@ | ||
hrefTest = { | ||
synchronous: true, | ||
path: 'href-test/:someparam', | ||
viewModel: function() { | ||
return { goToRoute: goToRoute } | ||
}, | ||
template: '<nav class="href-test">\ | ||
<a data-bind="routeHref: goToRoute"></a>\ | ||
</nav>' | ||
} | ||
router.map(function(route) { | ||
@@ -62,3 +51,30 @@ route('login', login) | ||
}) | ||
route('router-href-test', hrefTest) | ||
route('router-href-test', { | ||
synchronous: true, | ||
path: 'href-test/:someparam', | ||
query: { | ||
foo: undefined, | ||
}, | ||
viewModel: function() { | ||
return { goToRoute: goToRoute } | ||
}, | ||
template: '<nav class="href-test">\ | ||
<a data-bind="routeHref: goToRoute"></a>\ | ||
</nav>' | ||
}) | ||
route('inbox', { | ||
synchronous: true, | ||
query: { | ||
sort: 'desc', | ||
search: undefined, | ||
tags: [] | ||
}, | ||
viewModel: function(params) { | ||
return inboxParams = params | ||
}, | ||
template: '<div class="inbox">\ | ||
<input data-bind="textInput: search" />\ | ||
<a class="sort" data-bind="click: function() { sort(sort() === \'asc\' ? \'desc\' : \'asc\') }, text: sort"></a>\ | ||
</div>' | ||
}) | ||
}) | ||
@@ -101,3 +117,3 @@ | ||
} | ||
} | ||
} | ||
setTimeout(attempt, 2) | ||
@@ -202,2 +218,3 @@ | ||
should.not.exist(activeRoutes()[0]) | ||
return pollUntilPassing(function() { | ||
@@ -217,2 +234,20 @@ activeRoutes().map(function(r) { return r.name }).should.deep.equal(['forums', 'threads']) | ||
it('should expose $leafRoute() on the bindingContext for easy access to the deepest route', function() { | ||
location.setURL('/forums/1') | ||
ko.contextFor(testEl).$leafRoute.should.be.instanceof(Function) | ||
should.not.exist(ko.contextFor(testEl).$leafRoute()) | ||
return pollUntilPassing(function() { | ||
ko.contextFor(testEl).$leafRoute().name.should.equal('threads') | ||
ko.contextFor(testEl.querySelector('section.forums h1')).$leafRoute().name.should.equal('threads') | ||
}).then(function() { | ||
location.setURL('/forums/1/threads/2') | ||
return pollUntilPassing(function() { | ||
ko.contextFor(testEl).$leafRoute().name.should.equal('thread') | ||
ko.contextFor(testEl.querySelector('section.forums h1')).$leafRoute().name.should.equal('thread') | ||
ko.contextFor(testEl.querySelector('section.thread p')).$leafRoute().name.should.equal('thread') | ||
}) | ||
}) | ||
}) | ||
it('should expose the route component as $routeComponent', function() { | ||
@@ -267,2 +302,17 @@ ko.components.register('some-component', { | ||
}) | ||
it('should not rerender anything when only the query string changes', function() { | ||
location.setURL('/forums/1') | ||
return pollUntilPassing(function() { | ||
testEl.querySelector('section.forums section.forum').should.be.ok | ||
}).then(function() { | ||
testEl.querySelector('section.forums section.forum').foo = 'bar' | ||
location.setURL('/forums/1?hello=world') | ||
return new Promise(function(res) { | ||
setTimeout(function() { res() }, 100) | ||
}) | ||
}).then(function() { | ||
testEl.querySelector('section.forums section.forum').should.have.property('foo') | ||
}) | ||
}) | ||
}) | ||
@@ -274,3 +324,3 @@ | ||
location.setURL('/href-test/foobar') | ||
location.setURL('/href-test/foobar?foo=bar') | ||
return pollUntilPassing(function() { | ||
@@ -317,2 +367,32 @@ testEl.querySelector('.href-test').should.be.ok | ||
}) | ||
it('should include the query bounded observables if true', function() { | ||
goToRoute({ | ||
name: 'thread', | ||
params: { | ||
forumId: 2, | ||
threadId: 3 | ||
}, | ||
query: true | ||
}) | ||
testEl.querySelector('.href-test a').should.have.attr('href', '/forums/2/threads/3?foo=bar') | ||
location.setURL('/href-test/foobar?foo=1') | ||
return pollUntilPassing(function() { | ||
testEl.querySelector('.href-test a').should.have.attr('href', '/forums/2/threads/3?foo=1') | ||
}) | ||
}) | ||
it('should accept an explicit object, overwriting query values', function() { | ||
goToRoute({ | ||
params: { | ||
someparam: 'baz' | ||
}, | ||
query: { | ||
foo: 1, | ||
somequery: 'a' | ||
} | ||
}) | ||
testEl.querySelector('.href-test a').should.have.attr('href', '/href-test/baz?foo=1&somequery=a') | ||
}) | ||
}) | ||
@@ -569,2 +649,103 @@ | ||
}) | ||
describe('query', function() { | ||
it('should provide an object with observables behind properties defaulting to the specified values on params', function() { | ||
location.setURL('/inbox') | ||
return pollUntilPassing(function() { inboxParams.sort }).then(function() { | ||
ko.isWritableObservable(inboxParams.sort).should.be.true | ||
ko.isWritableObservable(inboxParams.search).should.be.true | ||
ko.isWritableObservable(inboxParams.tags).should.be.true | ||
inboxParams.tags.push.should.be.instanceof(Function) | ||
inboxParams.sort().should.equal('desc') | ||
chai.expect(inboxParams.search()).to.equal(undefined) | ||
inboxParams.tags().should.be.empty | ||
}) | ||
}) | ||
it('should set values from the query string as those property\'s initial value', function() { | ||
location.setURL('/inbox?sort=asc&search=%20Hi&tags[]=suggestion') | ||
return pollUntilPassing(function() { testEl.querySelector('.inbox a.sort').click }).then(function() { | ||
testEl.querySelector('.inbox a.sort').should.have.text('asc') | ||
testEl.querySelector('.inbox input').value.should.equal(' Hi') | ||
inboxParams.tags().should.deep.equal(['suggestion']) | ||
}) | ||
}) | ||
it('should replace the current location history when an observable changes, preserving other querystring values', function() { | ||
location.setURL('/inbox?foo=bar&search=bob') | ||
return pollUntilPassing(function() { testEl.querySelector('.inbox a.sort').click }).then(function() { | ||
var sort = testEl.querySelector('.inbox a.sort') | ||
if (typeof sort.click === 'function') { | ||
sort.click() | ||
} else { | ||
inboxParams.sort('asc') | ||
} | ||
return pollUntilPassing(function() { | ||
location.getURL().should.equal('/inbox?foo=bar&search=bob&sort=asc') | ||
testEl.querySelector('.inbox a.sort').should.have.text('asc') | ||
}) | ||
}).then(function() { | ||
inboxParams.search('Jane') | ||
return pollUntilPassing(function() { | ||
location.getURL().should.equal('/inbox?foo=bar&search=Jane&sort=asc') | ||
testEl.querySelector('.inbox input').value.should.equal('Jane') | ||
}) | ||
}) | ||
}) | ||
it('should remove the query string if it becomes the default', function() { | ||
location.setURL('/inbox?sort=asc') | ||
return pollUntilPassing(function() { testEl.querySelector('.inbox a.sort').click }).then(function() { | ||
inboxParams.sort('desc') | ||
return pollUntilPassing(function() { | ||
location.getURL().should.equal('/inbox') | ||
}) | ||
}).then(function() { | ||
inboxParams.tags.push('unread') | ||
return pollUntilPassing(function() { | ||
location.getURL().should.equal('/inbox?tags%5B0%5D=unread') | ||
}) | ||
}) | ||
}) | ||
it('should expand array items out in the query string properly when an observableArray updates', function() { | ||
location.setURL('/inbox?foo=bar&search=bob') | ||
return pollUntilPassing(function() { testEl.querySelector('.inbox a.sort').click }).then(function() { | ||
inboxParams.tags(['unread']) | ||
return pollUntilPassing(function() { | ||
location.getURL().should.equal('/inbox?foo=bar&search=bob&tags%5B0%5D=unread') | ||
}) | ||
}).then(function() { | ||
inboxParams.tags.push('priority') | ||
return pollUntilPassing(function() { | ||
location.getURL().should.equal('/inbox?foo=bar&search=bob&tags%5B0%5D=unread&tags%5B1%5D=priority') | ||
}) | ||
}) | ||
}) | ||
it('should update the observables if the query string changes at the same location', 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') | ||
testEl.querySelector('.inbox input').value.should.equal('bob') | ||
location.setURL('/inbox?foo=baz&search=Jane&tags=unread&tags=priority') | ||
return pollUntilPassing(function() { | ||
testEl.querySelector('.inbox input').value.should.equal('Jane') | ||
testEl.querySelector('.inbox a.sort').should.have.text('desc') | ||
inboxParams.tags().should.deep.equal(['unread', 'priority']) | ||
location.getURL().should.equal('/inbox?foo=baz&search=Jane&tags=unread&tags=priority') | ||
}) | ||
}).then(function() { | ||
location.setURL('/inbox') | ||
return pollUntilPassing(function() { | ||
testEl.querySelector('.inbox input').value.should.equal('') | ||
testEl.querySelector('.inbox a.sort').should.have.text('desc') | ||
inboxParams.tags().should.be.empty | ||
location.getURL().should.equal('/inbox') | ||
}) | ||
}) | ||
}) | ||
}) | ||
}) |
Sorry, the diff of this file is not supported yet
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
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
48693
1012
93
0
14