Skip to content

Commit

Permalink
feat(StateConfig): New StateConfig decorator for configuring ui-route…
Browse files Browse the repository at this point in the history
…r states with components
  • Loading branch information
timkindberg committed Nov 26, 2015
1 parent 596b70e commit 6daf87b
Show file tree
Hide file tree
Showing 8 changed files with 256 additions and 28 deletions.
2 changes: 1 addition & 1 deletion lib/classes/module.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ describe('Integration: Module', () => {
angular = ng.useReal();
});

it('should let you create an Angular module', function(){
it('should let you create an Angular module', () => {
let module = Module('test', []);
angular.module('test').should.be.equal(module.publish());
});
Expand Down
25 changes: 24 additions & 1 deletion lib/decorators/component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import events from '../events/events';
import { quickFixture } from '../tests/utils';
import EventEmitter from '../events/event-emitter';
import CustomEvent from '../util/custom-event';
import InvalidDueToNoAnnotations from "../classes/provider.spec";

describe('@Component', function(){

Expand Down Expand Up @@ -377,6 +376,30 @@ describe('@Component', function(){
componentEl.html().should.be.eql('template content');
});

it('creates a directive with transclusion', () => {
@Component({ selector: 'foo', template: "** <ng-transclude></ng-transclude> **" })
class MyClass{ }

let fixture = quickFixture({
directives: [MyClass],
template: `<foo>hello</foo>`
});

fixture.debugElement.text().should.be.equal('** hello **');
});

it('creates a directive with transclusion using ng-content alias', () => {
@Component({ selector: 'foo', template: "** <ng-content></ng-content> **" })
class MyClass{ }

let fixture = quickFixture({
directives: [MyClass],
template: `<foo>hello</foo>`
});

fixture.debugElement.text().should.be.equal('** hello **');
});

describe('inputs', () => {

beforeEach(() => {
Expand Down
87 changes: 64 additions & 23 deletions lib/decorators/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,16 @@ import {Providers} from './providers';
// Provider parser will need to be registered with Module
import Module from '../classes/module';
import directiveControllerFactory from '../util/directive-controller';
import {getInjectableName} from '../util/get-injectable-name';
import {writeMapMulti} from './input-output';
import {inputsMap} from '../properties/inputs-builder';
import events from '../events/events';
import {flatten, createConfigErrorMessage} from '../util/helpers';
import {uiRouterChildConfigsStoreKey, uiRouterConfigsStoreKey, uiRouterResolvedMapStoreKey, IComponentState} from './state-config';

import {IComponentState} from "./state-config";
import IStateProvider = ng.ui.IStateProvider;
import IInjectorService = angular.auto.IInjectorService;

const TYPE = 'component';

Expand Down Expand Up @@ -192,7 +199,6 @@ export function View(
}
}

// ## Component Provider Parser
Module.addProvider(TYPE, (target: any, name: string, injects: string[], ngModule: ng.IModule) => {
// First create an empty object to contain the directive definition object
let ddo: any = {};
Expand All @@ -206,36 +212,71 @@ Module.addProvider(TYPE, (target: any, name: string, injects: string[], ngModule
let bindProp = angular.version.minor >= 4 ? 'bindToController' : 'scope';
ddo[bindProp] = inputsMap(ddo.inputMap);

checkComponentConfig();
// If the selector type was not an element, throw an error. Components can only
// be elements in Angular 2, so we want to enforce that strictly here.
if(ddo.restrict !== 'E') {
throw new Error(createConfigErrorMessage(target, ngModule,
`@Component selectors can only be elements. ` +
`Perhaps you meant to use @Directive?`));
}

// Component controllers must be created from a factory. Checkout out
// util/directive-controller.js for more information about what's going on here
ddo.controller = [
'$scope', '$element', '$attrs', '$transclude', '$injector',
function($scope: any, $element: any, $attrs: any, $transclude: any, $injector: any): any{
return directiveControllerFactory(this, injects, target, ddo, $injector, {
$scope,
$element,
$attrs,
$transclude
});
}
];
controller.$inject = ['$scope', '$element', '$attrs', '$transclude', '$injector'];
function controller($scope: any, $element: any, $attrs: any, $transclude: any, $injector: any): any{
let resolvesMap = componentStore.get(uiRouterResolvedMapStoreKey, target);
//console.log('component.ts, controller::235', `resolvesMap:`, resolvesMap);
let locals = Object.assign({ $scope, $element, $attrs, $transclude }, resolvesMap);
return directiveControllerFactory(this, injects, target, ddo, $injector, locals);
}

ddo.controller = controller;

if (ddo.template && ddo.template.replace) {
// Template Aliases
ddo.template = ddo.template
.replace(/ng-content/g, 'ng-transclude')
.replace(/ng-outlet/g, 'ui-view');
}

// Finally add the component to the raw module
ngModule.directive(name, () => ddo);

function createConfigErrorMessage(message: string): string {
return `Processing "${target.name}" in "${ngModule.name}": ${message}`;
}

function checkComponentConfig() {
// If the selector type was not an element, throw an error. Components can only
// be elements in Angular 2, so we want to enforce that strictly here.
if(ddo.restrict !== 'E') {
throw new Error(createConfigErrorMessage(
`@Component selectors can only be elements. ` +
`Perhaps you meant to use @Directive?`));
/////////////////
/* StateConfig */
/////////////////

let childStateConfigs: IComponentState[] = componentStore.get(uiRouterChildConfigsStoreKey, target);

if (childStateConfigs) {
if (!Array.isArray(childStateConfigs)) {
throw new TypeError(createConfigErrorMessage(target, ngModule, '@StateConfig param must be an array of state objects.'));
}

ngModule.config(['$stateProvider', function($stateProvider: IStateProvider) {
if (!$stateProvider) return;

childStateConfigs.forEach((config: IComponentState) => {
let tagName = providerStore.get('name', config.component);
let childInjects = bundleStore.get('$inject', config.component);
let injectedResolves = childInjects ? childInjects.map(getInjectableName) : [];

//console.log('component.ts, parser::274', `injectedResolves:`, injectedResolves);

function stateController(...resolves): any {
let resolvedMap = resolves.reduce((obj, val, i) => {
obj[injectedResolves[i]] = val;
return obj;
}, {});
//console.log('component.ts, stateController::282', `resolvedMap:`, resolvedMap);
componentStore.set(uiRouterResolvedMapStoreKey, resolvedMap, config.component);
}

config.controller = [...injectedResolves, stateController];
config.template = config.template || `<${tagName}></${tagName}>`;
$stateProvider.state(config.name, config);
});
}]);
}
});
125 changes: 125 additions & 0 deletions lib/decorators/state-config.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { sinon } from '../tests/frameworks';
import { ng } from '../tests/angular';
import { componentStore, bundleStore } from '../writers';
import Module from '../classes/module';
import { quickFixture } from '../tests/utils';
import { Component } from './component';
import { Injectable } from './injectable';
import { Inject } from './inject';
import { StateConfig } from './state-config';
//noinspection TypeScriptCheckImport
import uiRouter from 'angular-ui-router';

describe('@StateConfig Decorator', function(){
let fixture, Parent, ChildA, ChildB, ChildBA, states;

beforeEach(() => {
ng.useStub();

@Injectable()
class Thing {}

@Component({ selector: 'childa', template: 'childA contents' })
@Inject('resolveA')
class _ChildA {
constructor(public resolveA, public thing) {}
}

@Component({ selector: 'childba', template: 'childBA contents' })
@Inject('resolveB', 'resolveBA', Thing)
class _ChildBA {
constructor(public resolveB, public resolveBA) {}
}

@Component({ selector: 'childb', template: 'childB contents { <ng-outlet></ng-outlet> }' })
@StateConfig([
{ name: 'childB.childBA', url: '/childBA', component: _ChildBA,
resolve: {
resolveBA: () => 'BA resolved!'
}
}
])
class _ChildB {}

states = [
{ name: 'childA', url: '/childA', component: _ChildA,
resolve: {
resolveA: () => 'A resolved!'
}
},
{ name: 'childB', url: '/childB', component: _ChildB,
resolve: {
resolveB: () => 'B resolved!'
}
}
];

@Component({
selector: 'parent',
providers: [uiRouter],
template: `<ng-outlet></ng-outlet><ng-outlet name="aux"></ng-outlet>`
})
@StateConfig(states)
class _Parent {}

Parent = _Parent;
ChildA = _ChildA;
ChildB = _ChildB;
ChildBA = _ChildBA;
});

it('adds state metadata to the class', () => {
componentStore.get('ui-router.stateChildConfigs', Parent).should.eql(states);
});

it('adds state metadata to each routed component', () => {
componentStore.get('ui-router.stateConfigs', ChildA)[0].should.eql(states[0]);
componentStore.get('ui-router.stateConfigs', ChildB)[0].should.eql(states[1]);
});

it('adds state components to class providers', () => {
bundleStore.get('providers', Parent).should.eql([ChildA, ChildB]);
});

it('relies on ui-router', () => {
fixture = quickFixture({ providers: [uiRouter] });
fixture.debugElement.getLocal('$state').should.respondTo('go');
});

it('replaces instances of ng-outlet with ui-view', () => {
fixture = quickFixture({ directives: [Parent], template: '<parent></parent>' });
fixture.debugElement.getLocal('parentDirective')[0].template.should.eql('<ui-view></ui-view><ui-view name="aux"></ui-view>')
});

it('renders child component into outlet when state is activated', () => {
fixture = quickFixture({ directives: [Parent], template: '<parent></parent>' });
let $state = fixture.debugElement.getLocal('$state');

$state.go('childA');
fixture.detectChanges();

fixture.debugElement.text().should.match(/childA contents/);
});

it('injects resolved deps into child component controller', () => {
fixture = quickFixture({ directives: [Parent], template: '<parent></parent>' });
let $state = fixture.debugElement.getLocal('$state');

$state.go('childA');
fixture.detectChanges();

fixture.debugElement.find('childa').componentInstance.should.have.property('resolveA', 'A resolved!')
});

it('injects inherited resolved deps into child component controller', () => {
fixture = quickFixture({ directives: [Parent], template: '<parent></parent>' });
let $state = fixture.debugElement.getLocal('$state');

$state.go('childB.childBA');
fixture.detectChanges();

let childBA = fixture.debugElement.find('childba').componentInstance;
childBA.should.have.property('resolveB', 'B resolved!');
childBA.should.have.property('resolveBA', 'BA resolved!');
});
});
28 changes: 28 additions & 0 deletions lib/decorators/state-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import {componentStore, bundleStore, providerStore} from '../writers';
import Module from '../classes/module';
import {Providers} from './providers';
import IState = ng.ui.IState;

export let uiRouterChildConfigsStoreKey = 'ui-router.stateChildConfigs';
export let uiRouterConfigsStoreKey = 'ui-router.stateConfigs';
export let uiRouterResolvedMapStoreKey = 'ui-router.resolvedMap';

export interface IComponentState extends IState {
component: any;
path: string;
as: string;
redirect: string;
}

export function StateConfig(stateConfigs: IComponentState[]){
return function(t: any){
Providers(...stateConfigs.map(sc => sc.component))(t, `while analyzing StateConfig '${t.name}' state components`);
componentStore.set(uiRouterChildConfigsStoreKey, stateConfigs, t);
stateConfigs.forEach(config => {
if (!config.component) return;
let existingConfigs = componentStore.get(uiRouterConfigsStoreKey, config.component) || [];
componentStore.set(uiRouterConfigsStoreKey, [...existingConfigs, config], config.component);
});
}
}

9 changes: 8 additions & 1 deletion lib/util/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ export function dashToCamel(dash: string): string{
}

export function dasherize(name: string, separator: string = '-'): string {

return name.replace(SNAKE_CASE_REGEXP, (letter: string, pos: number) => {
return `${(pos ? separator : '')}${letter.toLowerCase()}`;
});
Expand All @@ -34,4 +33,12 @@ export function flatten(items: any[]): any[]{
}

return resolved;
}

export interface INamed {
name: string;
}

export function createConfigErrorMessage(target: INamed, ngModule: ng.IModule, message: string): string {
return `Processing "${target.name}" in "${ngModule.name}": ${message}`;
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"aliasify": "^1.8.0",
"angular": "^1.4.7",
"angular-mocks": "^1.4.7",
"angular-ui-router": "^0.2.15",
"babel": "^5.8.29",
"babel-core": "^5.3.0",
"babelify": "^6.4.0",
Expand Down
7 changes: 5 additions & 2 deletions tsd.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
"bundle": "typings/tsd.d.ts",
"installed": {
"angularjs/angular.d.ts": {
"commit": "9fb1a5074160e679289b56b99a763104926a4a24"
"commit": "c2c22c3b953fe9730d4802022d5e0d18d083909e"
},
"jquery/jquery.d.ts": {
"commit": "9fb1a5074160e679289b56b99a763104926a4a24"
"commit": "c2c22c3b953fe9730d4802022d5e0d18d083909e"
},
"mocha/mocha.d.ts": {
"commit": "3191f6e0088eee07c4d8fd24e4d27a40a60d9eb9"
Expand All @@ -31,6 +31,9 @@
},
"es6-shim/es6-shim.d.ts": {
"commit": "62eedc3121a5e28c50473d2e4a9cefbcb9c3957f"
},
"angular-ui-router/angular-ui-router.d.ts": {
"commit": "c2c22c3b953fe9730d4802022d5e0d18d083909e"
}
}
}

0 comments on commit 6daf87b

Please sign in to comment.