aurelia-view-manager
Version:
A view manager for aurelia plugins. Add support for overriding views, and multiple frameworks
216 lines (178 loc) • 5.93 kB
JavaScript
import extend from 'extend';
import {inject} from 'aurelia-dependency-injection';
import {viewStrategy,ViewEngine,ViewCompileInstruction,ResourceLoadContext,ViewFactory,useViewStrategy} from 'aurelia-templating';
import {relativeToFile} from 'aurelia-path';
export class Config {
/* these can be overwritten with the configureDefaults function */
defaults = {
location : '{{framework}}/{{view}}.html',
framework: 'bootstrap',
map : {}
};
/* stores the namespaced configs */
namespaces = {};
constructor() {
/***
* have the defaults object showup like a namespaces in the namespaces
* object
*/
this.namespaces.defaults = this.defaults;
}
/**
* extends the defaults object
* @param {object} configs
* @return {Config} self
*/
configureDefaults(configs) {
extend(true, this.defaults, configs);
return this;
}
/**
* Register configuration for the view manager to use later.
*
* @param {string} name of the namespace
* @param {object} [configs={}] configs used to resolve template paths
*
* @returns {Config}
*/
configureNamespace(name, configs = {map: {}}) {
let namespace = this.fetch(name);
extend(true, namespace, configs);
this.configure({[name]: namespace});
return this;
}
/**
* extends the configuration object. When writing to the namespaces
* object it is best to use this function.
*
* @param {object} config
* @returns {Config} self
*/
configure(config) {
extend(true, this.namespaces, config);
return this;
}
/**
* convenient for getting a (nested) property in the namespaces
* object.
*
* @param {...string} properties when prop is falsy it returns the whole
* namespaces object
*
* @returns {*} the value of that property or undefined
*/
fetch(properties) {
if (!this.namespaces[properties]) {
/* if namespace is not defined it creates a new object with proto defaults */
return this.defaults;
}
let result = this.namespaces;
let args = Array.from(arguments); // eslint-disable-line prefer-rest-params
for (let index in args) {
if (!args.hasOwnProperty(index)) {
continue;
}
let key = args[index];
let value = result[key];
if (!value) {
return value;
}
result = result[key];
}
return result;
}
}
export function configure(aurelia, configOrConfigure) {
let config = aurelia.container.get(Config);
if (typeof configOrConfigure === 'function') {
return configOrConfigure(config);
}
config.configure(configOrConfigure);
}
export class ViewManager {
constructor(config) {
this.config = config;
}
/**
* returns the location of a template as a string.
*
* @param {string} [namespace] a string that represents a namespace(plugin)
* when not defined it defaults to the "global" configurations.
*
* @param {string} view a string that equals the template filename.
* @returns {string} represents the path of the view
*/
resolve(namespace, view) {
if (!namespace || !view) { /* both arguments are required */
throw new Error(
`Cannot resolve without namespace and view. Got namespace "${namespace}" and view "${view}" in stead`
);
}
let namespaceOrDefault = Object.create(this.config.fetch(namespace));
namespaceOrDefault.view = view;
let location = (namespaceOrDefault.map || {})[view] || namespaceOrDefault.location;
return render(location, namespaceOrDefault);
}
}
/**
* returns the rendered string based on a template with mustaches and an object
*
* @param {string} template
* @param {object} data
*
* @returns {string}
*/
function render(template, data) {
let result = template;
for (let key in data) { //eslint-disable-line guard-for-in
let regexString = ['{{', key, '}}'].join('');
let regex = new RegExp(regexString, 'g');
let value = data[key];
result = result.replace(regex, value);
}
/* performs the rendering of nested templates */
if (template !== result) {
result = render(result, data);
}
return result;
}
/**
* A view strategy that loads a view based on namespace and view name registered with the ViewManager
*/
export class ResolvedViewStrategy {
/**
* Creates an instance of ResolvedViewStrategy.
* @param namespace The namespace of the view.
* @param view The name of the view.
*/
constructor(namespace: string, view: string) {
this.namespace = namespace;
this.view = view;
}
/**
* Loads a view factory.
* @param viewEngine The view engine to use during the load process.
* @param compileInstruction Additional instructions to use during compilation of the view.
* @param loadContext The loading context used for loading all resources and dependencies.
* @return A promise for the view factory that is produced by this strategy.
*/
loadViewFactory(viewEngine: ViewEngine, compileInstruction: ViewCompileInstruction, loadContext?: ResourceLoadContext): Promise<ViewFactory> {
let viewManager = viewEngine.container.get(ViewManager);
let path = viewManager.resolve(this.namespace, this.view);
compileInstruction.associatedModuleId = this.moduleId;
return viewEngine.loadViewFactory(this.moduleId ? relativeToFile(path, this.moduleId) : path, compileInstruction, loadContext);
}
}
/**
* Decorates a custom element class in a way that it loads it's view from
* elsewhere
*
* @param {string} namespace used to separate different view configurations
* @param {string} view used to find the value that belongs to the view
* @returns {function} that takes the target and sets the view strategy on the element
*/
export function resolvedView(namespace, view) {
return useViewStrategy(new ResolvedViewStrategy(namespace, view));
}