Skip to content

Commit

Permalink
fix(ng1.component): Allow route-to-component "@" and optional bindings
Browse files Browse the repository at this point in the history
Closes #2708
  • Loading branch information
christopherthielen committed May 12, 2016
1 parent 9842fb7 commit 71b3393
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 16 deletions.
6 changes: 5 additions & 1 deletion src/common/strings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@ export function padString(length: number, str: string) {
return str;
}

export const kebobString = (camelCase: string) => camelCase.replace(/([A-Z])/g, $1 => "-"+$1.toLowerCase());
export function kebobString(camelCase: string) {
return camelCase
.replace(/^([A-Z])/, $1 => $1.toLowerCase()) // replace first char
.replace(/([A-Z])/g, $1 => "-" + $1.toLowerCase()); // replace rest
}

function _toJson(obj) {
return JSON.stringify(obj);
Expand Down
31 changes: 16 additions & 15 deletions src/ng1/viewsBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,19 @@ export function ng1ViewsBuilder(state: State) {
throw new Error(`Cannot combine: ${compKeys.join("|")} with: ${nonCompKeys.join("|")} in stateview: 'name@${state.name}'`);
}

// Dynamically build a template like "<component-name input1='$resolve.foo'></component-name>"
// Dynamically build a template like "<component-name input1='::$resolve.foo'></component-name>"
config.templateProvider = ['$injector', function($injector) {
const resolveFor = key => config.bindings && config.bindings[key] || key;
const prefix = angular.version.minor >= 3 ? "::" : "";
let attrs = getComponentInputs($injector, config.component)
.map(key => `${kebobString(key)}='${prefix}$resolve.${resolveFor(key)}'`).join(" ");
const attributeTpl = input => {
var attrName = kebobString(input.name);
var resolveName = resolveFor(input.name);
if (input.type === '@')
return `${attrName}='{{${prefix}$resolve.${resolveName}}}'`;
return `${attrName}='${prefix}$resolve.${resolveName}'`;
};

let attrs = getComponentInputs($injector, config.component).map(attributeTpl).join(" ");
let kebobName = kebobString(config.component);
return `<${kebobName} ${attrs}></${kebobName}>`;
}];
Expand All @@ -70,27 +77,21 @@ export function ng1ViewsBuilder(state: State) {
return views;
}

// for ng 1.2 style, process the scope: { input: "=foo" } object
// for ng 1.2 style, process the scope: { input: "=foo" }
// for ng 1.3 through ng 1.5, process the component's bindToController: { input: "=foo" } object
const scopeBindings = bindingsObj => Object.keys(bindingsObj || {})
.map(key => [key, /^[=<](.*)/.exec(bindingsObj[key])])
.filter(tuple => isDefined(tuple[1]))
.map(tuple => tuple[1][1] || tuple[0]);

// for ng 1.3+ bindToController or 1.5 component style, process a $$bindings object
const bindToCtrlBindings = bindingsObj => Object.keys(bindingsObj || {})
.filter(key => !!/[=<]/.exec(bindingsObj[key].mode))
.map(key => bindingsObj[key].attrName);
.map(key => [key, /^([=<@])[?]?(.*)/.exec(bindingsObj[key])]) // [ 'input', [ '=foo', '=', 'foo' ] ]
.filter(tuple => isDefined(tuple) && isDefined(tuple[1])) // skip malformed values
.map(tuple => ({ name: tuple[1][2] || tuple[0], type: tuple[1][1] }));// { name: ('foo' || 'input'), type: '=' }

// Given a directive definition, find its object input attributes
// Use different properties, depending on the type of directive (component, bindToController, normal)
const getBindings = def => {
if (isObject(def.bindToController)) return scopeBindings(def.bindToController);
if (def.$$bindings && def.$$bindings.bindToController) return bindToCtrlBindings(def.$$bindings.bindToController);
if (def.$$isolateBindings) return bindToCtrlBindings(def.$$isolateBindings);
return <any> scopeBindings(def.scope);
};

// Gets all the directive(s)' inputs ('=' and '<')
// Gets all the directive(s)' inputs ('@', '=', and '<')
function getComponentInputs($injector, name) {
let cmpDefs = $injector.get(name + "Directive"); // could be multiple
if (!cmpDefs || !cmpDefs.length) throw new Error(`Unable to find component named '${name}'`);
Expand Down
46 changes: 46 additions & 0 deletions test/ng1/viewDirectiveSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -804,6 +804,16 @@ describe('angular 1.5+ style .component()', function() {
bindings: { status: '<' },
template: '#{{ $ctrl.status }}#'
});

app.component('bindingTypes', {
bindings: { oneway: '<oneway', twoway: '=', attribute: '@attr' },
template: '-{{ $ctrl.oneway }},{{ $ctrl.twoway }},{{ $ctrl.attribute }}-'
});

app.component('optionalBindingTypes', {
bindings: { oneway: '<?oneway', twoway: '=?', attribute: '@?attr' },
template: '-{{ $ctrl.oneway }},{{ $ctrl.twoway }},{{ $ctrl.attribute }}-'
});
}
}));

Expand Down Expand Up @@ -993,6 +1003,42 @@ describe('angular 1.5+ style .component()', function() {

expect(log).toBe('onInit;');
});

it('should supply resolve data to "<", "=", "@" bindings', function () {
$stateProvider.state('bindingtypes', {
component: 'bindingTypes',
resolve: {
oneway: function () { return "ONEWAY"; },
twoway: function () { return "TWOWAY"; },
attribute: function () { return "ATTRIBUTE"; }
},
bindings: { attr: 'attribute' }
});

var $state = svcs.$state, $httpBackend = svcs.$httpBackend, $q = svcs.$q;

$state.transitionTo('bindingtypes'); $q.flush();

expect(el.text()).toBe('-ONEWAY,TWOWAY,ATTRIBUTE-');
});

it('should supply resolve data to optional "<?", "=?", "@?" bindings', function () {
$stateProvider.state('optionalbindingtypes', {
component: 'optionalBindingTypes',
resolve: {
oneway: function () { return "ONEWAY"; },
twoway: function () { return "TWOWAY"; },
attribute: function () { return "ATTRIBUTE"; }
},
bindings: { attr: 'attribute' }
});

var $state = svcs.$state, $httpBackend = svcs.$httpBackend, $q = svcs.$q;

$state.transitionTo('optionalbindingtypes'); $q.flush();

expect(el.text()).toBe('-ONEWAY,TWOWAY,ATTRIBUTE-');
});
}
});

Expand Down

0 comments on commit 71b3393

Please sign in to comment.