cherrytree-for-knockout
Advanced tools
Comparing version 0.0.2 to 0.1.0
@@ -31,3 +31,6 @@ { | ||
"tests" | ||
] | ||
], | ||
"devDependencies": { | ||
"chai-dom": "~1.0.0" | ||
} | ||
} |
@@ -10,3 +10,3 @@ (function(factory) { | ||
var middleware = factory(ko) | ||
ko.bindingHandlers.routeComponent.middleware = middleware | ||
ko.bindingHandlers.routeView.middleware = middleware | ||
} | ||
@@ -17,17 +17,54 @@ })(function(ko) { | ||
ko.components.register('route-blank', { | ||
viewModel: { instance: {} }, | ||
template: '<div></div>' | ||
template: '<div></div>', | ||
synchronous: true | ||
}) | ||
ko.components.register('route-loading', { | ||
viewModel: { instance: {} }, | ||
template: '<div class="route-loading"></div>' | ||
template: '<div class="route-loading"></div>', | ||
synchronous: true | ||
}) | ||
ko.bindingHandlers.routeComponent = { | ||
init: function() { | ||
function clone(obj) { | ||
return Object.keys(obj).reduce(function(clone, key) { | ||
clone[key] = obj[key] | ||
return clone | ||
}, {}) | ||
} | ||
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 | ||
delete ctx._routeCtx | ||
ctx.$routeComponent = dataItemOrAccessor | ||
} | ||
return retval | ||
}) | ||
} | ||
ko.bindingHandlers.routeView = { | ||
init: function(_, valueAccessor, __, ___, bindingContext) { | ||
var router = valueAccessor() | ||
if (router && typeof router.map === 'function' && typeof router.use === 'function') { | ||
if (!bindingContext.$root.router) { | ||
bindingContext.$root.router = router | ||
} | ||
if (!bindingContext.$root.activeRoutes) { | ||
bindingContext.$root.activeRoutes = activeRoutes | ||
} | ||
} | ||
return { controlsDescendantBindings: true } | ||
}, | ||
update: function(element, valueAccessor, allBindings, viewModel, bindingContext) { | ||
var depth = 0, contextIter = bindingContext.$parentContext | ||
update: function(element, valueAccessor, ab, vm, bindingContext) { | ||
var depth = 0, contextIter = bindingContext.$parentContext, | ||
routeComponent = ko.observable({ name: 'route-blank' }), | ||
prevRoute, routeClass | ||
while (contextIter) { | ||
@@ -40,36 +77,105 @@ if ('$route' in contextIter) { | ||
var route = activeRoutes()[depth] || { name: 'route-blank' } | ||
ko.computed(function() { | ||
var route = activeRoutes()[depth] | ||
if (route == prevRoute) return | ||
if (!route) { | ||
routeComponent({ name: 'route-blank' }) | ||
return | ||
} | ||
bindingContext.$route = route | ||
bindingContext.$route = route | ||
bindingContext._routeCtx = true | ||
return ko.bindingHandlers.component.init(element, function() { | ||
if (route.resolutions) { | ||
return route.resolutions() ? | ||
{ name: route.name, params: route.resolutions() } : | ||
{ name: 'route-loading' } | ||
var res = route.resolutions() | ||
if (res) { | ||
var params = clone(res) | ||
params.$route = clone(route) | ||
delete params.$route.resolutions | ||
prevRoute = route | ||
routeComponent({ name: ko.bindingHandlers.routeView.prefix + route.name, params: params }) | ||
} else { | ||
return { name: route.name } | ||
routeComponent({ name: 'route-loading' }) | ||
} | ||
}, 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) | ||
} | ||
routeClass = newClass | ||
element.classList.add(routeClass) | ||
}, null, { disposeWhenNodeIsRemoved: element }) | ||
return ko.bindingHandlers.component.init(element, routeComponent, ab, vm, bindingContext) | ||
} | ||
} | ||
ko.bindingHandlers.routeView.prefix = 'route:' | ||
ko.bindingHandlers.routeHref = { | ||
update: function(element, valueAccessor, allBindings, viewModel, bindingContext) { | ||
var router = bindingContext.$root.router | ||
if (!router) { | ||
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 | ||
if (typeof opts === 'string') { | ||
name = opts | ||
} else if (opts) { | ||
name = ko.unwrap(opts.name) | ||
params = ko.unwrap(opts.params) | ||
} | ||
return { | ||
href: opts && router.generate( | ||
name || bindingContext.$route.name, | ||
params || bindingContext.$route.params) | ||
} | ||
}, allBindings, viewModel, bindingContext) | ||
} | ||
} | ||
ko.bindingHandlers.routeComponent.prefix = 'route:' | ||
function routeEqual(comp, route) { | ||
if (!comp || !route || comp.name !== route.name) return false | ||
return Object.keys(route.params).every(function(param) { | ||
return comp.params[param] === route.params[param] | ||
}) | ||
} | ||
return function knockoutCherrytreeMiddleware(transition) { | ||
var resolutions = {}, routeResolvers = [] | ||
activeRoutes(transition.routes.filter(function(route) { | ||
return route.options && route.options.template | ||
}).map(function(route) { | ||
var routeData = { | ||
name: ko.bindingHandlers.routeComponent.prefix + route.ancestors.concat([route.name]).join('.'), | ||
params: transition.params, | ||
query: transition.query, | ||
resolutions: route.options.resolve && ko.observable() | ||
var resolutions = {}, routeResolvers = [], startIdx = 0, | ||
filteredRoutes = transition.routes.filter(function(route) { | ||
return route.options && !!(route.options.template || route.options.resolve) | ||
}) | ||
while (routeEqual(activeRoutes()[startIdx], filteredRoutes[startIdx])) { | ||
Object.assign(resolutions, activeRoutes()[startIdx].resolutions()) | ||
startIdx++ | ||
} | ||
var newRoutes = filteredRoutes.slice(startIdx).map(function(route) { | ||
var routeData | ||
if (route.options.template) { | ||
routeData = { | ||
name: route.name, | ||
params: transition.params, | ||
query: transition.query, | ||
resolutions: ko.observable(), | ||
transitionTo: transition.redirectTo | ||
} | ||
var compName = ko.bindingHandlers.routeView.prefix + routeData.name | ||
if (!ko.components.isRegistered(compName)) { | ||
ko.components.register(compName, route.options) | ||
} | ||
} | ||
if (!ko.components.isRegistered(routeData.name)) { | ||
ko.components.register(routeData.name, route.options) | ||
} | ||
if (route.options.resolve) { | ||
var resolvers = Object.keys(route.options.resolve) | ||
var resolve = route.options.resolve | ||
if (resolve || routeResolvers.length) { | ||
var resolvers = Object.keys(resolve || {}) | ||
@@ -80,13 +186,18 @@ routeResolvers.push(function() { | ||
})).then(function(moreResolutions) { | ||
routeData.resolutions(moreResolutions.reduce(function(all, r, idx) { | ||
moreResolutions.reduce(function(all, r, idx) { | ||
all[resolvers[idx]] = r | ||
return all | ||
}, resolutions)) | ||
}, resolutions) | ||
routeData && routeData.resolutions(resolutions) | ||
return routeData | ||
}) | ||
}) | ||
} else if (routeData) { | ||
routeData.resolutions(resolutions) | ||
} | ||
return routeData | ||
})) | ||
}).filter(function(i) { return !!i }) | ||
activeRoutes.splice.apply(activeRoutes, [startIdx, activeRoutes().length - startIdx].concat(newRoutes)) | ||
return routeResolvers.reduce(function(promise, then) { | ||
@@ -93,0 +204,0 @@ return promise ? promise.then(then) : then() |
{ | ||
"name": "cherrytree-for-knockout", | ||
"version": "0.0.2", | ||
"version": "0.1.0", | ||
"description": "Use knockout components with CherryTree hiearchial routing", | ||
"main": "cherrytree-for-knockout.js", | ||
"scripts": { | ||
"test": "mocha-phantomjs test/tests.html" | ||
"test": "mocha-phantomjs tests/tests.html && (node -e \"require('grunt').tasks(['test']);\" || true)" | ||
}, | ||
@@ -28,17 +28,20 @@ "repository": { | ||
"homepage": "https://github.com/nathanboktae/cherrytree-for-knockout#readme", | ||
"peerDependencies": { | ||
"dependencies": { | ||
"knockout": "^3.3.0" | ||
}, | ||
"devDependencies": { | ||
"babel-core": "^5.8.23", | ||
"chai": "^3.2.0", | ||
"chai-jquery": "^2.0.0", | ||
"cherrytree": "github:QubitProducts/cherrytree", | ||
"jquery": "^2.1.4", | ||
"knockout": "^3.3.0", | ||
"chai-dom": "^1.2.1", | ||
"cherrytree": "^2.0.0-rc3", | ||
"grunt": "^0.4.5", | ||
"grunt-connect": "^0.2.0", | ||
"grunt-contrib-connect": "^0.11.2", | ||
"grunt-saucelabs": "^8.6.1", | ||
"mocha": "^2.2.5", | ||
"mocha-phantomjs": "^3.6.0", | ||
"phantomjs": "^1.9.7-15", | ||
"sinon": "^1.15.4", | ||
"phantomjs": "1.9.7-15", | ||
"sinon-browser-only": "^1.12.1", | ||
"sinon-chai": "^2.8.0" | ||
} | ||
} |
@@ -5,2 +5,68 @@ ## CherryTree for Knockout | ||
Inspired heavily by angular-ui-router. In early alpha development - see the tests for functionality. | ||
[![Build Status](https://secure.travis-ci.org/nathanboktae/cherrytree-for-knockout.png?branch=master)](https://travis-ci.org/nathanboktae/cherrytree-for-knockout) | ||
[![SauceLabs Test Status](https://saucelabs.com/browser-matrix/Cherrytree-ko.svg)](https://saucelabs.com/u/Cherrytree-ko) | ||
### Overview | ||
As you design your webapp, you will begin to identify workflows and pages in a heirachial fashion. Given the familiar forum domain, you will have a list of forums, then a list of thread in a specific form, then posts in that forum. You may also have an account page which has a private messages section. Each section will have it's own view and logic, and may need data loaded before it can be reached. | ||
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 is extremely lightweight in the microlib spirit at < 200 lines of code. It has one job and does it well. | ||
### Example | ||
Specify your components when you map your routes like so: | ||
```javascript | ||
var login = { | ||
viewModel: function() { | ||
this.username = ko.observable() | ||
// .... | ||
}, | ||
template: '<form class="login"><input name="username" data-bind="value: username></input> .... </form>' | ||
} | ||
var forums = { /* ... */ } | ||
router.map(function(route) { | ||
route('login', login) | ||
route('forums.index', forums) | ||
route('forums', forums, function() { | ||
route('forums.view', forum) | ||
route('threads', forum, function() { | ||
route('thread', thread) | ||
}) | ||
}) | ||
}) | ||
``` | ||
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: | ||
``` | ||
<body> | ||
<header> | ||
<ul data-bind="foreach: route.state && route.state.routes"> | ||
<li data-bind="routeHref: name, text: name"></li> | ||
</ul> | ||
<a class="signout" data-bind="click: signout"></a> | ||
</header> | ||
<main data-bind="routeView: router"></main> | ||
<script> | ||
ko.applyBindings({ | ||
router: router, | ||
signout: function() { /* ... */ } | ||
}) | ||
</script> | ||
</body> | ||
``` | ||
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. | ||
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` | ||
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. | ||
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
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
Uses eval
Supply chain riskPackage uses dynamic code execution (e.g., eval()), which is a dangerous practice. This can prevent the code from running in certain environments and increases the risk that the code may contain exploits or malicious behavior.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
12
72
0
1
37479
13
776
+ Addedknockout@^3.3.0