Skip to content

Commit

Permalink
feat(ui-view): Route-to-component: Wire component "&" bindings
Browse files Browse the repository at this point in the history
When using route-to-component, this feature allows "&" component bindings to be wired to either 1) a function returned by a resolve or 2) a function in the parent component.

Closes #3239
Closes #3111
  • Loading branch information
christopherthielen committed Jan 3, 2017
1 parent 3a8fb11 commit af95206
Show file tree
Hide file tree
Showing 5 changed files with 232 additions and 55 deletions.
2 changes: 1 addition & 1 deletion karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ webpackConfig.plugins = [];
webpackConfig.devtool = 'inline-source-map';

module.exports = function(config) {
var ngVersion = config.ngversion || "1.2.28";
var ngVersion = config.ngversion || "1.6.0";

config.set({
singleRun: true,
Expand Down
8 changes: 4 additions & 4 deletions src/directives/viewDirective.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import { ng as angular } from "../angular";
import {
IInterpolateService, IScope, ITranscludeFunction, IAugmentedJQuery,
ICompileService, IControllerService, ITimeoutService
ICompileService, IControllerService, ITimeoutService, noop
} from "angular";

import {
Expand Down Expand Up @@ -357,15 +357,15 @@ function $ViewDirectiveFill ($compile: ICompileService, $controller: IController
return;
}

let cfg: Ng1ViewConfig = data.$cfg || <any> { viewDecl: {} };
$element.html(cfg.template || initial);
let cfg: Ng1ViewConfig = data.$cfg || <any> { viewDecl: {}, getTemplate: noop };
let resolveCtx: ResolveContext = cfg.path && new ResolveContext(cfg.path);
$element.html(cfg.getTemplate($element, resolveCtx) || initial);
trace.traceUIViewFill(data.$uiView, $element.html());

let link = $compile($element.contents());
let controller = cfg.controller;
let controllerAs: string = getControllerAs(cfg);
let resolveAs: string = getResolveAs(cfg);
let resolveCtx: ResolveContext = cfg.path && new ResolveContext(cfg.path);
let locals = resolveCtx && getLocals(resolveCtx);

scope[resolveAs] = locals;
Expand Down
21 changes: 6 additions & 15 deletions src/statebuilders/views.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@ export class Ng1ViewConfig implements ViewConfig {
loaded: boolean = false;
controller: Function; // actually IInjectable|string
template: string;
component: string;
locals: any; // TODO: delete me
factory = new TemplateFactory();

constructor(public path: PathNode[], public viewDecl: Ng1ViewDeclaration) {
}
Expand All @@ -78,31 +80,20 @@ export class Ng1ViewConfig implements ViewConfig {
let params = this.path.reduce((acc, node) => extend(acc, node.paramValues), {});

let promises: any = {
template: $q.when(this.getTemplate(params, new TemplateFactory(), context)),
template: $q.when(this.factory.fromConfig(this.viewDecl, params, context)),
controller: $q.when(this.getController(context))
};

return $q.all(promises).then((results) => {
trace.traceViewServiceEvent("Loaded", this);
this.controller = results.controller;
this.template = results.template;
extend(this, results.template); // Either { template: "tpl" } or { component: "cmpName" }
return this;
});
}

/**
* Checks a view configuration to ensure that it specifies a template.
*
* @return {boolean} Returns `true` if the configuration contains a valid template, otherwise `false`.
*/
hasTemplate() {
var templateKeys = ['template', 'templateUrl', 'templateProvider', 'component', 'componentProvider'];
return hasAnyKey(templateKeys, this.viewDecl);
}

getTemplate(params: RawParams, $factory: TemplateFactory, context: ResolveContext) {
return $factory.fromConfig(this.viewDecl, params, context);
}
getTemplate = (uiView, context: ResolveContext) =>
this.component ? this.factory.makeComponentTemplate(uiView, context, this.component, this.viewDecl.bindings) : this.template;

/**
* Gets the controller for a view configuration.
Expand Down
97 changes: 63 additions & 34 deletions src/templateFactory.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
/** @module view */ /** for typedoc */
/** @module view */
/** for typedoc */
import { ng as angular } from "./angular";
import { IAugmentedJQuery } from "angular";
import {
isArray, isDefined, isFunction, isObject, services, Obj, IInjectable, tail, kebobString, unnestR, ResolveContext, Resolvable, RawParams
isArray, isDefined, isFunction, isObject, services, Obj, IInjectable, tail, kebobString, unnestR, ResolveContext,
Resolvable, RawParams, identity
} from "ui-router-core";
import { Ng1ViewDeclaration } from "./interface";

Expand All @@ -25,13 +28,16 @@ export class TemplateFactory {
fromConfig(config: Ng1ViewDeclaration, params: any, context: ResolveContext) {
const defaultTemplate = "<ui-view></ui-view>";

const asTemplate = (result) => services.$q.when(result).then(str => ({ template: str }));
const asComponent = (result) => services.$q.when(result).then(str => ({ component: str }));

return (
isDefined(config.template) ? this.fromString(config.template, params) :
isDefined(config.templateUrl) ? this.fromUrl(config.templateUrl, params) :
isDefined(config.templateProvider) ? this.fromProvider(config.templateProvider, params, context) :
isDefined(config.component) ? this.fromComponent(config.component, config.bindings) :
isDefined(config.componentProvider) ? this.fromComponentProvider(config.componentProvider, params, context) :
defaultTemplate
isDefined(config.template) ? asTemplate(this.fromString(config.template, params)) :
isDefined(config.templateUrl) ? asTemplate(this.fromUrl(config.templateUrl, params)) :
isDefined(config.templateProvider) ? asTemplate(this.fromProvider(config.templateProvider, params, context)) :
isDefined(config.component) ? asComponent(config.component) :
isDefined(config.componentProvider) ? asComponent(this.fromComponentProvider(config.componentProvider, params, context)) :
asTemplate(defaultTemplate)
);
};

Expand Down Expand Up @@ -78,50 +84,73 @@ export class TemplateFactory {
return resolvable.get(context);
};

/**
* Creates a component's template by invoking an injectable provider function.
*
* @param provider Function to invoke via `locals`
* @param {Function} injectFn a function used to invoke the template provider
* @return {string} The template html as a string: "<component-name input1='::$resolve.foo'></component-name>".
*/
fromComponentProvider(provider: IInjectable, params: any, context: ResolveContext) {
let deps = services.$injector.annotate(provider);
let providerFn = isArray(provider) ? tail(<any[]> provider) : provider;
let resolvable = new Resolvable("", <Function> providerFn, deps);
return resolvable.get(context);
};

/**
* Creates a template from a component's name
*
* @param uiView {object} The parent ui-view (for binding outputs to callbacks)
* @param context The ResolveContext (for binding outputs to callbacks returned from resolves)
* @param component {string} Component's name in camel case.
* @param bindings An object defining the component's bindings: {foo: '<'}
* @return {string} The template as a string: "<component-name input1='::$resolve.foo'></component-name>".
*/
fromComponent(component: string, bindings?: any) {
const resolveFor = (key: string) =>
bindings && bindings[key] || key;
makeComponentTemplate(uiView: IAugmentedJQuery, context: ResolveContext, component: string, bindings?: any) {
bindings = bindings || {};

// Bind once prefix
const prefix = angular.version.minor >= 3 ? "::" : "";

const attributeTpl = (input: BindingTuple) => {
var attrName = kebobString(input.name);
var resolveName = resolveFor(input.name);
if (input.type === '@')
let {name, type } = input;
let attrName = kebobString(name);
// If the ui-view has an attribute which matches a binding on the routed component
// then pass that attribute through to the routed component template.
// Prefer ui-view wired mappings to resolve data, unless the resolve was explicitly bound using `bindings:`
if (uiView.attr(attrName) && !bindings[name])
return `${attrName}='${uiView.attr(attrName)}'`;

let resolveName = bindings[name] || name;
// Pre-evaluate the expression for "@" bindings by enclosing in {{ }}
// some-attr="{{ ::$resolve.someResolveName }}"
if (type === '@')
return `${attrName}='{{${prefix}$resolve.${resolveName}}}'`;

// Wire "&" callbacks to resolves that return a callback function
// Get the result of the resolve (should be a function) and annotate it to get its arguments.
// some-attr="$resolve.someResolveResultName(foo, bar)"
if (type === '&') {
let res = context.getResolvable(resolveName);
let fn = res && res.data;
let args = fn && services.$injector.annotate(fn) || [];
let arrayIdxStr = isArray(fn) ? `[${fn.length - 1}]` : '';
return `${attrName}='$resolve.${resolveName}${arrayIdxStr}(${args.join(",")})'`;
}

// some-attr="::$resolve.someResolveName"
return `${attrName}='${prefix}$resolve.${resolveName}'`;
};

let attrs = getComponentInputs(component).map(attributeTpl).join(" ");
let attrs = getComponentBindings(component).map(attributeTpl).join(" ");
let kebobName = kebobString(component);
return `<${kebobName} ${attrs}></${kebobName}>`;
};

/**
* Creates a component's template by invoking an injectable provider function.
*
* @param provider Function to invoke via `locals`
* @param {Function} injectFn a function used to invoke the template provider
* @return {string} The template html as a string: "<component-name input1='::$resolve.foo'></component-name>".
*/
fromComponentProvider(provider: IInjectable, params: any, context: ResolveContext) {
let deps = services.$injector.annotate(provider);
let providerFn = isArray(provider) ? tail(<any[]> provider) : provider;
let resolvable = new Resolvable("", <Function> providerFn, deps);
return resolvable.get(context).then((componentName) => {
return this.fromComponent(componentName);
});
};
}

// Gets all the directive(s)' inputs ('@', '=', and '<')
function getComponentInputs(name: string) {
// Gets all the directive(s)' inputs ('@', '=', and '<') and outputs ('&')
function getComponentBindings(name: string) {
let cmpDefs = <any[]> services.$injector.get(name + "Directive"); // could be multiple
if (!cmpDefs || !cmpDefs.length) throw new Error(`Unable to find component named '${name}'`);
return cmpDefs.map(getBindings).reduce(unnestR, []);
Expand All @@ -143,7 +172,7 @@ interface BindingTuple {
// for ng 1.3 through ng 1.5, process the component's bindToController: { input: "=foo" } object
const scopeBindings = (bindingsObj: Obj) => Object.keys(bindingsObj || {})
// [ 'input', [ '=foo', '=', 'foo' ] ]
.map(key => [key, /^([=<@])[?]?(.*)/.exec(bindingsObj[key])])
.map(key => [key, /^([=<@&])[?]?(.*)/.exec(bindingsObj[key])])
// skip malformed values
.filter(tuple => isDefined(tuple) && isArray(tuple[1]))
// { name: ('foo' || 'input'), type: '=' }
Expand Down
Loading

0 comments on commit af95206

Please sign in to comment.