refactor(ngOutlet): using some typescript features

Closes #4386
This commit is contained in:
Shahar Talmi
2015-09-28 02:12:23 +03:00
committed by Brian Ford
parent 1272affe5c
commit 31f48ae943
3 changed files with 177 additions and 176 deletions

View File

@ -1,69 +1,51 @@
'use strict'; ///<reference path="../typings/angularjs/angular.d.ts"/>
/*
* A module for adding new a routing system Angular 1.
*/
angular.module('ngComponentRouter', [])
.directive('ngOutlet', ngOutletDirective)
.directive('ngOutlet', ngOutletFillContentDirective)
.directive('ngLink', ngLinkDirective);
/*
* A module for inspecting controller constructors
*/
angular.module('ng')
.provider('$$directiveIntrospector', $$directiveIntrospectorProvider)
.config(compilerProviderDecorator);
/* /*
* decorates $compileProvider so that we have access to routing metadata * decorates $compileProvider so that we have access to routing metadata
*/ */
function compilerProviderDecorator($compileProvider, $$directiveIntrospectorProvider) { function compilerProviderDecorator($compileProvider,
var directive = $compileProvider.directive; $$directiveIntrospectorProvider: DirectiveIntrospectorProvider) {
$compileProvider.directive = function (name, factory) { let directive = $compileProvider.directive;
$compileProvider.directive = function(name: string, factory: Function) {
$$directiveIntrospectorProvider.register(name, factory); $$directiveIntrospectorProvider.register(name, factory);
return directive.apply(this, arguments); return directive.apply(this, arguments);
}; };
} }
/* /*
* private service that holds route mappings for each controller * private service that holds route mappings for each controller
*/ */
function $$directiveIntrospectorProvider() { class DirectiveIntrospectorProvider {
var directiveBuffer = []; private directiveBuffer: any[] = [];
var directiveFactoriesByName = {}; private directiveFactoriesByName: {[name: string]: Function} = {};
var onDirectiveRegistered = null; private onDirectiveRegistered: (name: string, factory: Function) => any = null;
return {
register: function (name, factory) {
if (angular.isArray(factory)) {
factory = factory[factory.length - 1];
}
directiveFactoriesByName[name] = factory;
if (onDirectiveRegistered) {
onDirectiveRegistered(name, factory);
} else {
directiveBuffer.push({name: name, factory: factory});
}
},
$get: function () {
var fn = function (newOnControllerRegistered) {
onDirectiveRegistered = newOnControllerRegistered;
while (directiveBuffer.length > 0) {
var directive = directiveBuffer.pop();
onDirectiveRegistered(directive.name, directive.factory);
}
};
fn.getTypeByName = function (name) { register(name: string, factory: Function) {
return directiveFactoriesByName[name]; if (angular.isArray(factory)) {
}; factory = factory[factory.length - 1];
return fn;
} }
}; this.directiveFactoriesByName[name] = factory;
} if (this.onDirectiveRegistered) {
this.onDirectiveRegistered(name, factory);
} else {
this.directiveBuffer.push({name: name, factory: factory});
}
}
$get() {
let fn: any = newOnControllerRegistered => {
this.onDirectiveRegistered = newOnControllerRegistered;
while (this.directiveBuffer.length > 0) {
let directive = this.directiveBuffer.pop();
this.onDirectiveRegistered(directive.name, directive.factory);
}
};
fn.getTypeByName = name => this.directiveFactoriesByName[name];
return fn;
}
}
/** /**
* @name ngOutlet * @name ngOutlet
@ -79,8 +61,8 @@ function $$directiveIntrospectorProvider() {
* *
* The value for the `ngOutlet` attribute is optional. * The value for the `ngOutlet` attribute is optional.
*/ */
function ngOutletDirective($animate, $q, $router) { function ngOutletDirective($animate, $q: ng.IQService, $router) {
var rootRouter = $router; let rootRouter = $router;
return { return {
restrict: 'AE', restrict: 'AE',
@ -89,117 +71,120 @@ function ngOutletDirective($animate, $q, $router) {
priority: 400, priority: 400,
require: ['?^^ngOutlet', 'ngOutlet'], require: ['?^^ngOutlet', 'ngOutlet'],
link: outletLink, link: outletLink,
controller: function () {}, controller: class {},
controllerAs: '$$ngOutlet' controllerAs: '$$ngOutlet'
}; };
function outletLink(scope, $element, attrs, ctrls, $transclude) { function outletLink(scope, element, attrs, ctrls, $transclude) {
var outletName = attrs.ngOutlet || 'default', class Outlet {
parentCtrl = ctrls[0], constructor(private controller, private router) {}
myCtrl = ctrls[1],
router = (parentCtrl && parentCtrl.$$router) || rootRouter;
myCtrl.$$currentComponent = null; private currentController;
private currentInstruction;
private currentScope;
private currentElement;
private previousLeaveAnimation;
var childRouter, private cleanupLastView() {
currentController, if (this.previousLeaveAnimation) {
currentInstruction, $animate.cancel(this.previousLeaveAnimation);
currentScope, this.previousLeaveAnimation = null;
currentElement, }
previousLeaveAnimation;
function cleanupLastView() { if (this.currentScope) {
if (previousLeaveAnimation) { this.currentScope.$destroy();
$animate.cancel(previousLeaveAnimation); this.currentScope = null;
previousLeaveAnimation = null; }
if (this.currentElement) {
this.previousLeaveAnimation = $animate.leave(this.currentElement);
this.previousLeaveAnimation.then(() => this.previousLeaveAnimation = null);
this.currentElement = null;
}
} }
if (currentScope) { reuse(instruction) {
currentScope.$destroy(); let next = $q.when(true);
currentScope = null; let previousInstruction = this.currentInstruction;
} this.currentInstruction = instruction;
if (currentElement) { if (this.currentController && this.currentController.$onReuse) {
previousLeaveAnimation = $animate.leave(currentElement); next = $q.when(
previousLeaveAnimation.then(function () { this.currentController.$onReuse(this.currentInstruction, previousInstruction));
previousLeaveAnimation = null;
});
currentElement = null;
}
}
router.registerPrimaryOutlet({
reuse: function (instruction) {
var next = $q.when(true);
var previousInstruction = currentInstruction;
currentInstruction = instruction;
if (currentController && currentController.$onReuse) {
next = $q.when(currentController.$onReuse(currentInstruction, previousInstruction));
} }
return next; return next;
}, }
canReuse: function (nextInstruction) {
var result; canReuse(nextInstruction) {
if (!currentInstruction || let result;
currentInstruction.componentType !== nextInstruction.componentType) { if (!this.currentInstruction ||
this.currentInstruction.componentType !== nextInstruction.componentType) {
result = false; result = false;
} else if (currentController && currentController.$canReuse) { } else if (this.currentController && this.currentController.$canReuse) {
result = currentController.$canReuse(nextInstruction, currentInstruction); result = this.currentController.$canReuse(nextInstruction, this.currentInstruction);
} else { } else {
result = nextInstruction === currentInstruction || result = nextInstruction === this.currentInstruction ||
angular.equals(nextInstruction.params, currentInstruction.params); angular.equals(nextInstruction.params, this.currentInstruction.params);
} }
return $q.when(result); return $q.when(result);
}, }
canDeactivate: function (instruction) {
if (currentController && currentController.$canDeactivate) { canDeactivate(instruction) {
return $q.when(currentController.$canDeactivate(instruction, currentInstruction)); if (this.currentController && this.currentController.$canDeactivate) {
return $q.when(
this.currentController.$canDeactivate(instruction, this.currentInstruction));
} }
return $q.when(true); return $q.when(true);
}, }
deactivate: function (instruction) {
if (currentController && currentController.$onDeactivate) {
return $q.when(currentController.$onDeactivate(instruction, currentInstruction));
}
return $q.when();
},
activate: function (instruction) {
var previousInstruction = currentInstruction;
currentInstruction = instruction;
var componentName = myCtrl.$$componentName = instruction.componentType; deactivate(instruction) {
if (this.currentController && this.currentController.$onDeactivate) {
if (typeof componentName != 'string') { return $q.when(
throw new Error('Component is not a string for ' + instruction.urlPath); this.currentController.$onDeactivate(instruction, this.currentInstruction));
}
myCtrl.$$routeParams = instruction.params;
myCtrl.$$template = '<div ' + dashCase(componentName) + '></div>';
myCtrl.$$router = router.childRouter(instruction.componentType);
var newScope = scope.$new();
var clone = $transclude(newScope, function (clone) {
$animate.enter(clone, null, currentElement || $element);
cleanupLastView();
});
currentElement = clone;
currentScope = newScope;
// TODO: prefer the other directive retrieving the controller
// by debug mode
currentController = currentElement.children().eq(0).controller(componentName);
if (currentController && currentController.$onActivate) {
return currentController.$onActivate(instruction, previousInstruction);
} }
return $q.when(); return $q.when();
} }
});
activate(instruction) {
let previousInstruction = this.currentInstruction;
this.currentInstruction = instruction;
let componentName = this.controller.$$componentName = instruction.componentType;
if (typeof componentName !== 'string') {
throw new Error('Component is not a string for ' + instruction.urlPath);
}
this.controller.$$routeParams = instruction.params;
this.controller.$$template = '<div ' + dashCase(componentName) + '></div>';
this.controller.$$router = this.router.childRouter(instruction.componentType);
let newScope = scope.$new();
let clone = $transclude(newScope, clone => {
$animate.enter(clone, null, this.currentElement || element);
this.cleanupLastView();
});
this.currentElement = clone;
this.currentScope = newScope;
// TODO: prefer the other directive retrieving the controller
// by debug mode
this.currentController = this.currentElement.children().eq(0).controller(componentName);
if (this.currentController && this.currentController.$onActivate) {
return this.currentController.$onActivate(instruction, previousInstruction);
}
return $q.when();
}
}
let parentCtrl = ctrls[0], myCtrl = ctrls[1],
router = (parentCtrl && parentCtrl.$$router) || rootRouter;
myCtrl.$$currentComponent = null;
router.registerPrimaryOutlet(new Outlet(myCtrl, router));
} }
} }
/** /**
@ -210,14 +195,14 @@ function ngOutletFillContentDirective($compile) {
restrict: 'EA', restrict: 'EA',
priority: -400, priority: -400,
require: 'ngOutlet', require: 'ngOutlet',
link: function (scope, $element, attrs, ctrl) { link: (scope, element, attrs, ctrl) => {
var template = ctrl.$$template; let template = ctrl.$$template;
$element.html(template); element.html(template);
var link = $compile($element.contents()); let link = $compile(element.contents());
link(scope); link(scope);
// TODO: move to primary directive // TODO: move to primary directive
var componentInstance = scope[ctrl.$$componentName]; let componentInstance = scope[ctrl.$$componentName];
if (componentInstance) { if (componentInstance) {
ctrl.$$currentComponent = componentInstance; ctrl.$$currentComponent = componentInstance;
@ -228,7 +213,6 @@ function ngOutletFillContentDirective($compile) {
}; };
} }
/** /**
* @name ngLink * @name ngLink
* @description * @description
@ -240,9 +224,9 @@ function ngOutletFillContentDirective($compile) {
* ## Example * ## Example
* *
* ```js * ```js
* angular.module('myApp', ['ngFuturisticRouter']) * angular.module('myApp', ['ngComponentRouter'])
* .controller('AppController', ['$router', function($router) { * .controller('AppController', ['$router', function($router) {
* $router.config({ path: '/user/:id' component: 'user' }); * $router.config({ path: '/user/:id', component: 'user' });
* this.user = { name: 'Brian', id: 123 }; * this.user = { name: 'Brian', id: 123 };
* }); * });
* ``` * ```
@ -254,42 +238,35 @@ function ngOutletFillContentDirective($compile) {
* ``` * ```
*/ */
function ngLinkDirective($router, $parse) { function ngLinkDirective($router, $parse) {
var rootRouter = $router; let rootRouter = $router;
return { return {require: '?^^ngOutlet', restrict: 'A', link: ngLinkDirectiveLinkFn};
require: '?^^ngOutlet',
restrict: 'A',
link: ngLinkDirectiveLinkFn
};
function ngLinkDirectiveLinkFn(scope, elt, attrs, ctrl) { function ngLinkDirectiveLinkFn(scope, element, attrs, ctrl) {
var router = (ctrl && ctrl.$$router) || rootRouter; let router = (ctrl && ctrl.$$router) || rootRouter;
if (!router) { if (!router) {
return; return;
} }
var instruction = null; let instruction = null;
var link = attrs.ngLink || ''; let link = attrs.ngLink || '';
function getLink(params) { function getLink(params) {
instruction = router.generate(params); instruction = router.generate(params);
return './' + angular.stringifyInstruction(instruction); return './' + angular.stringifyInstruction(instruction);
} }
var routeParamsGetter = $parse(link); let routeParamsGetter = $parse(link);
// we can avoid adding a watcher if it's a literal // we can avoid adding a watcher if it's a literal
if (routeParamsGetter.constant) { if (routeParamsGetter.constant) {
var params = routeParamsGetter(); let params = routeParamsGetter();
elt.attr('href', getLink(params)); element.attr('href', getLink(params));
} else { } else {
scope.$watch(function () { scope.$watch(() => routeParamsGetter(scope), params => element.attr('href', getLink(params)),
return routeParamsGetter(scope); true);
}, function (params) {
elt.attr('href', getLink(params));
}, true);
} }
elt.on('click', function (event) { element.on('click', event => {
if (event.which !== 1 || !instruction) { if (event.which !== 1 || !instruction) {
return; return;
} }
@ -300,9 +277,21 @@ function ngLinkDirective($router, $parse) {
} }
} }
function dashCase(str: string): string {
function dashCase(str) { return str.replace(/[A-Z]/g, match => '-' + match.toLowerCase());
return str.replace(/([A-Z])/g, function ($1) {
return '-' + $1.toLowerCase();
});
} }
/*
* A module for adding new a routing system Angular 1.
*/
angular.module('ngComponentRouter', [])
.directive('ngOutlet', ngOutletDirective)
.directive('ngOutlet', ngOutletFillContentDirective)
.directive('ngLink', ngLinkDirective);
/*
* A module for inspecting controller constructors
*/
angular.module('ng')
.provider('$$directiveIntrospector', DirectiveIntrospectorProvider)
.config(compilerProviderDecorator);

View File

@ -0,0 +1,12 @@
{
"version": "v4",
"repo": "angular/DefinitelyTyped",
"ref": "master",
"path": "typings",
"bundle": "typings/tsd.d.ts",
"installed": {
"angularjs/angular.d.ts": {
"commit": "746b9a892629060bc853e792afff536e0ec4655e"
}
}
}

View File

@ -24,7 +24,7 @@
}, },
"scripts": { "scripts": {
"preinstall": "node tools/npm/check-node-modules --purge", "preinstall": "node tools/npm/check-node-modules --purge",
"postinstall": "node tools/npm/copy-npm-shrinkwrap && node tools/chromedriverpatch.js && webdriver-manager update && bower install && gulp pubget.dart && tsd reinstall --overwrite --clean --config modules/angular2/tsd.json && tsd reinstall --overwrite --clean --config tools/tsd.json", "postinstall": "node tools/npm/copy-npm-shrinkwrap && node tools/chromedriverpatch.js && webdriver-manager update && bower install && gulp pubget.dart && tsd reinstall --overwrite --clean --config modules/angular2/tsd.json && tsd reinstall --overwrite --clean --config tools/tsd.json && tsd reinstall --overwrite --config modules/angular1_router/tsd.json",
"test": "gulp test.all.js && gulp test.all.dart" "test": "gulp test.all.js && gulp test.all.dart"
}, },
"dependencies": { "dependencies": {