@profiscience/knockout-contrib-router-plugins-component
v2.0.17
Published
[![Version][npm-version-shield]][npm] [![Dependency Status][david-dm-shield]][david-dm] [![Peer Dependency Status][david-dm-peer-shield]][david-dm-peer] [![Dev Dependency Status][david-dm-dev-shield]][david-dm-dev] [![Downloads][npm-stats-shield]][npm-sta
Downloads
145
Readme
router.plugins.component
Sets the component for a route. Intended for use with dynamic imports for intuitive code-splitting/lazy-loading of views.
Usage
import { Route, componentRoutePlugin } from '@profiscience/knockout-contrib'
Route.usePlugin(componentRoutePlugin)
// RECOMMENDED USAGE
new Route('/', {
component: () => ({
template: import('./template.html'),
viewModel: import('./viewModel'),
}),
})
Anonymous Components
Annymous components are registered/unregistered by the router as-needed. Simply pass the component configuration you would pass to Knockout.
new Route('/', {
component: {
template: 'Hello, World!',
},
})
By default, the component will be registered using a incrementing GUID (__router_view_${i}__
). You may specify the name to register the component as by providing the optional name
property.
new Route('/', {
component: {
name: 'hello-view',
template: 'Hello, World!',
},
})
NOTE: Custom component loaders will NOT be used.
NOTE: Non-class
viewModels are supported, but not recommended. See the caveats section below.
NOTE: If your viewModel can be instantiated with new
, the instance will be accessible on ctx.component.viewModel
after the beforeRender queue completes. Before this, it will be a promise that resolves its eventual value. i.e.
// after plugin execution, before beforeRender queue completes
ctx.component = Promise<{ viewModel }>
// after beforeRender queue completes, all subsequent middleware lifecycle stages (afterRender, beforeDispose, afterDispose)
ctx.component = { viewModel }
Named Components
Named components are components that are already registered with Knockout.
ko.components.register('hello-component', { template: 'Hello, World!' })
new Route('/', {
component: 'hello-component',
})
Using Accessors
If you need more control, you may use an accessor function. This function recieves the route context as its first and only argument and returns either of the above configurations, optionally promised.
new Route('/', {
component: (ctx) => ({ template: 'Hello, World!' }),
})
API
Several normalization passes are done on the supplied configuration to attempt to handle whatever you throw at it. The full type signature is...
type MaybePromise<T> = T | Promise<T>
type MaybeDefaultExport<T> = T | { default: T }
type MaybeAccessor<A, T> = T | ((A) => T)
type MaybeLazy<T extends {}> = MaybePromise<
{ [P in keyof T]: MaybePromise<MaybeDefaultExport<T[P]>> }
>
interface IRoutedViewModelConstructor {
new (ctx: Context & IContext): any
}
type IAnonymousComponent = {
name?: string
template: string
viewModel?: IRoutedViewModelConstructor
}
interface IRouteConfig {
component?: IRouteComponentConfig
}
type IRouteComponentConfig =
| MaybeAccessor<Context & IContext, MaybePromise<string>>
| MaybeAccessor<
Context & IContext,
MaybePromise<MaybeLazy<IAnonymousComponent>>
>
Caveats / Subtleties
Implicit Default Imports
Take the following...
new Route('/', {
component: async () => ({
viewModel: await import('./viewModel'),
}),
})
In this case, viewModel.(ts|js)
exports the viewModel constructor as default. But, depending on a few factors (your bundling/transpilation setup, and if there are named exports as well as the default), the way to access the viewModel constructor can vary at runtime. In some cases, the import
call will return a promise that resolves the constructor, in other cases it will return a promise that resolves an object with a default
property containing the constructor (Promise<{ default: ViewModel }>
). Rather than requiriring you to figure out when/where to append .then((imports) => imports.default)
or something similar all over the place, if an object with a .default
property is resolved, the contents of that default property will be hoisted.
Implicit Async/Await
If your configuration is an object with promised value, you may forgo wrapping the accessor function in async
/await
. Promised values will be resolved and aggregated into a new object before registering the component.
new Route('/', {
component: () => ({
template: import('./template.html'),
viewModel: import('./viewModel'),
}),
})
"Unable to instantiate viewModel. This may cause unexpected behavior. See caveats/subtleties in documentation."
When using anonymous components, the router prefers to instantiate the viewModel itself and register components using that instance, i.e. ko.components.register('__router_view_1__', { viewModel: { instance } })
. It does this in order to attach the viewModel instance to the context — as ctx.component.viewModel
— to provide opportunities to interop with other middleware/plugins. See the router.plugins.init + model.builders.data packages to better understand why this is desireable, as they make use of this.
Additionally, by providing the router with direct access to the viewModel constructor (and thus instance), it is able to control the timing of the dispose
method on the viewModel (if any), helping to prevent weird timing issues (and adding support for asynchronous disposal via promises!).
You are seeing this warning because you have provided a a) viewModel that is something other than a class which can be called with new
— a createViewModel factory perhaps or b) a named component. In either case, the router is unable to instantiate the viewModel in a predictable manner and falls back to allowing Knockout to instantiate the viewModel at render instead of before render. If you are not using any middleware/plugins that perform introspection on the viewModel instance (i.e. access ctx.component.viewModel
), you can safely ignore this warning. Disable it permanently with the following...
import { disableUninstantiableViewModelWarning } from '@profiscience/knockout-contrib-router-plugins-component'
disableUninstantiableViewModelWarning()