From 1dd3d6a6e8630fbb5c697d9cfa5dd92ebe739c74 Mon Sep 17 00:00:00 2001 From: Joao Figueiredo Date: Mon, 18 Aug 2014 19:23:35 +0200 Subject: [PATCH 01/29] Adds basic controller parsing. --- src/fake-angular.js | 7 +++++++ test/fake-angular.js | 20 +++++++++++++------- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/fake-angular.js b/src/fake-angular.js index 0d5c02a..5d8a3fc 100644 --- a/src/fake-angular.js +++ b/src/fake-angular.js @@ -4,6 +4,7 @@ function Module(name, deps) { this.name = name this.modules = deps this.items = [] + this.controllers = []; } var methods = ['constant', 'controller', 'directive', 'factory', 'filter', 'provider', 'service', 'value'] @@ -39,6 +40,12 @@ methods.forEach(function(method) { } }) +Module.prototype.controller = function (name, controllerDefinition) { + this.controllers.push({'name': name}); + this.items.push(name); + return this; +} + Module.prototype.run = function() { return this }; diff --git a/test/fake-angular.js b/test/fake-angular.js index 336d89d..5d29dea 100644 --- a/test/fake-angular.js +++ b/test/fake-angular.js @@ -134,14 +134,20 @@ describe("angular", function() { }) }) - describe("modules", function() { - it('property should contain all defined modules', function() { - angular.module('testModule1', []) - angular.module('testModule2', []) - - angular.modules.should.have.a.lengthOf(2) + describe("controllers", function() { + it('property should contain all defined controllers', function() { + angular + .module('testModule1', []) + .controller('testController1', ['asdf', function (asd) {}]) + .controller('testController2', ['asdf', function (asd) {}]); + + angular.modules.should.have.a.lengthOf(1) angular.modules[0].should.have.property('name', 'testModule1') - angular.modules[1].should.have.property('name', 'testModule2') + + var testModule1 = angular.modulesMap['testModule1']; + testModule1.controllers.should.have.a.lengthOf(2); + testModule1.controllers[0].should.have.property('name', 'testController1'); + testModule1.controllers[1].should.have.property('name', 'testController2'); }) }) }) From f1838f986e7d1d91e08e978b7f131398a22f366a Mon Sep 17 00:00:00 2001 From: Joao Figueiredo Date: Mon, 18 Aug 2014 19:28:00 +0200 Subject: [PATCH 02/29] Adds dependencies to controllers. --- src/fake-angular.js | 7 +++++-- test/fake-angular.js | 13 +++++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/fake-angular.js b/src/fake-angular.js index 5d8a3fc..20eb33c 100644 --- a/src/fake-angular.js +++ b/src/fake-angular.js @@ -40,8 +40,11 @@ methods.forEach(function(method) { } }) -Module.prototype.controller = function (name, controllerDefinition) { - this.controllers.push({'name': name}); +Module.prototype.controller = function (name, deps) { + if (deps instanceof Array) { + var definition = deps.pop(); + } + this.controllers.push({'name': name, 'deps': deps}); this.items.push(name); return this; } diff --git a/test/fake-angular.js b/test/fake-angular.js index 5d29dea..b6e0bee 100644 --- a/test/fake-angular.js +++ b/test/fake-angular.js @@ -138,8 +138,8 @@ describe("angular", function() { it('property should contain all defined controllers', function() { angular .module('testModule1', []) - .controller('testController1', ['asdf', function (asd) {}]) - .controller('testController2', ['asdf', function (asd) {}]); + .controller('testController1', ['dep1', function (asd) {}]) + .controller('testController2', ['dep2', 'dep3', function (asd) {}]); angular.modules.should.have.a.lengthOf(1) angular.modules[0].should.have.property('name', 'testModule1') @@ -148,6 +148,15 @@ describe("angular", function() { testModule1.controllers.should.have.a.lengthOf(2); testModule1.controllers[0].should.have.property('name', 'testController1'); testModule1.controllers[1].should.have.property('name', 'testController2'); + + testModule1.controllers[0].deps.should.be.a.Array; + testModule1.controllers[0].deps.should.have.a.lengthOf(1); + testModule1.controllers[0].deps[0].should.be.equal('dep1') + + testModule1.controllers[1].deps.should.be.a.Array; + testModule1.controllers[1].deps.should.have.a.lengthOf(2); + testModule1.controllers[1].deps[0].should.be.equal('dep2'); + testModule1.controllers[1].deps[1].should.be.equal('dep3'); }) }) }) From 3dc993da3e047444bf8f874d49bc3fa932c4f0cd Mon Sep 17 00:00:00 2001 From: Joao Figueiredo Date: Mon, 18 Aug 2014 19:35:56 +0200 Subject: [PATCH 03/29] Parses controllers dependencies in the function format. --- src/fake-angular.js | 9 ++++++++- test/fake-angular.js | 26 +++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/fake-angular.js b/src/fake-angular.js index 20eb33c..cfa5841 100644 --- a/src/fake-angular.js +++ b/src/fake-angular.js @@ -41,8 +41,15 @@ methods.forEach(function(method) { }) Module.prototype.controller = function (name, deps) { + var definition; if (deps instanceof Array) { - var definition = deps.pop(); + definition = deps.pop(); + } else if (deps instanceof Function) { + definition = deps; + var deps = /\(([^)]+)/.exec(definition); + if (deps[1]) { + deps = deps[1].split(/\s*,\s*/); + } } this.controllers.push({'name': name, 'deps': deps}); this.items.push(name); diff --git a/test/fake-angular.js b/test/fake-angular.js index b6e0bee..bff6071 100644 --- a/test/fake-angular.js +++ b/test/fake-angular.js @@ -135,7 +135,7 @@ describe("angular", function() { }) describe("controllers", function() { - it('property should contain all defined controllers', function() { + it('property should contain all defined controllers with array definition', function() { angular .module('testModule1', []) .controller('testController1', ['dep1', function (asd) {}]) @@ -153,6 +153,30 @@ describe("angular", function() { testModule1.controllers[0].deps.should.have.a.lengthOf(1); testModule1.controllers[0].deps[0].should.be.equal('dep1') + testModule1.controllers[1].deps.should.be.a.Array; + testModule1.controllers[1].deps.should.have.a.lengthOf(2); + testModule1.controllers[1].deps[0].should.be.equal('dep2'); + testModule1.controllers[1].deps[1].should.be.equal('dep3'); + }) + +it('property should contain all defined controllers with function definition', function() { + angular + .module('testModule1', []) + .controller('testController1', function (dep1) {}) + .controller('testController2', function (dep2, dep3) {}); + + angular.modules.should.have.a.lengthOf(1) + angular.modules[0].should.have.property('name', 'testModule1') + + var testModule1 = angular.modulesMap['testModule1']; + testModule1.controllers.should.have.a.lengthOf(2); + testModule1.controllers[0].should.have.property('name', 'testController1'); + testModule1.controllers[1].should.have.property('name', 'testController2'); + + testModule1.controllers[0].deps.should.be.a.Array; + testModule1.controllers[0].deps.should.have.a.lengthOf(1); + testModule1.controllers[0].deps[0].should.be.equal('dep1') + testModule1.controllers[1].deps.should.be.a.Array; testModule1.controllers[1].deps.should.have.a.lengthOf(2); testModule1.controllers[1].deps[0].should.be.equal('dep2'); From b808bf3eaa399777c284f16198c682128dcb221e Mon Sep 17 00:00:00 2001 From: Joao Figueiredo Date: Mon, 18 Aug 2014 19:37:12 +0200 Subject: [PATCH 04/29] Fixes identation. --- test/fake-angular.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/fake-angular.js b/test/fake-angular.js index bff6071..5a7e4ae 100644 --- a/test/fake-angular.js +++ b/test/fake-angular.js @@ -159,7 +159,7 @@ describe("angular", function() { testModule1.controllers[1].deps[1].should.be.equal('dep3'); }) -it('property should contain all defined controllers with function definition', function() { + it('property should contain all defined controllers with function definition', function() { angular .module('testModule1', []) .controller('testController1', function (dep1) {}) From 2702e7c0efab5e0f67cef9c940cdb7382b5eac1f Mon Sep 17 00:00:00 2001 From: Joao Figueiredo Date: Mon, 18 Aug 2014 19:45:24 +0200 Subject: [PATCH 05/29] Adds more tests- --- test/fake-angular.js | 59 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/test/fake-angular.js b/test/fake-angular.js index 5a7e4ae..e19d729 100644 --- a/test/fake-angular.js +++ b/test/fake-angular.js @@ -138,8 +138,8 @@ describe("angular", function() { it('property should contain all defined controllers with array definition', function() { angular .module('testModule1', []) - .controller('testController1', ['dep1', function (asd) {}]) - .controller('testController2', ['dep2', 'dep3', function (asd) {}]); + .controller('testController1', ['dep1', function (dep1) {}]) + .controller('testController2', ['dep2', 'dep3', function (dep1, dep2) {}]); angular.modules.should.have.a.lengthOf(1) angular.modules[0].should.have.property('name', 'testModule1') @@ -183,4 +183,59 @@ describe("angular", function() { testModule1.controllers[1].deps[1].should.be.equal('dep3'); }) }) + + it('property should contain all defined controllers with variable array definition', function() { + var testController1 = ['dep1', function (dep1) {}], + testController2 = ['dep2', 'dep3', function (dep2, dep3) {}] + ; + + angular + .module('testModule1', []) + .controller('testController1', testController1) + .controller('testController2', testController2); + + angular.modules.should.have.a.lengthOf(1) + angular.modules[0].should.have.property('name', 'testModule1') + + var testModule1 = angular.modulesMap['testModule1']; + testModule1.controllers.should.have.a.lengthOf(2); + testModule1.controllers[0].should.have.property('name', 'testController1'); + testModule1.controllers[1].should.have.property('name', 'testController2'); + + testModule1.controllers[0].deps.should.be.a.Array; + testModule1.controllers[0].deps.should.have.a.lengthOf(1); + testModule1.controllers[0].deps[0].should.be.equal('dep1') + + testModule1.controllers[1].deps.should.be.a.Array; + testModule1.controllers[1].deps.should.have.a.lengthOf(2); + testModule1.controllers[1].deps[0].should.be.equal('dep2'); + testModule1.controllers[1].deps[1].should.be.equal('dep3'); + }) + + it('property should contain all defined controllers with variable function definition', function() { + function testController1 (dep1) {}; + function testController2 (dep2, dep3) {}; + + angular + .module('testModule1', []) + .controller('testController1', testController1) + .controller('testController2', testController2); + + angular.modules.should.have.a.lengthOf(1) + angular.modules[0].should.have.property('name', 'testModule1') + + var testModule1 = angular.modulesMap['testModule1']; + testModule1.controllers.should.have.a.lengthOf(2); + testModule1.controllers[0].should.have.property('name', 'testController1'); + testModule1.controllers[1].should.have.property('name', 'testController2'); + + testModule1.controllers[0].deps.should.be.a.Array; + testModule1.controllers[0].deps.should.have.a.lengthOf(1); + testModule1.controllers[0].deps[0].should.be.equal('dep1') + + testModule1.controllers[1].deps.should.be.a.Array; + testModule1.controllers[1].deps.should.have.a.lengthOf(2); + testModule1.controllers[1].deps[0].should.be.equal('dep2'); + testModule1.controllers[1].deps[1].should.be.equal('dep3'); + }) }) From 2ff2544ccbe91e03b364cf77d192803e90f6c586 Mon Sep 17 00:00:00 2001 From: Joao Figueiredo Date: Tue, 19 Aug 2014 00:07:38 +0200 Subject: [PATCH 06/29] Services first commit. --- src/fake-angular.js | 41 ++++++++++++---- test/controllers.js | 113 +++++++++++++++++++++++++++++++++++++++++++ test/fake-angular.js | 105 ---------------------------------------- test/services.js | 36 ++++++++++++++ 4 files changed, 181 insertions(+), 114 deletions(-) create mode 100644 test/controllers.js create mode 100644 test/services.js diff --git a/src/fake-angular.js b/src/fake-angular.js index cfa5841..8997b38 100644 --- a/src/fake-angular.js +++ b/src/fake-angular.js @@ -5,6 +5,7 @@ function Module(name, deps) { this.modules = deps this.items = [] this.controllers = []; + this.services = []; } var methods = ['constant', 'controller', 'directive', 'factory', 'filter', 'provider', 'service', 'value'] @@ -35,23 +36,45 @@ var globalApis = ['lowercase', methods.forEach(function(method) { Module.prototype[method] = function addItem(name) { - this.items.push(name) - return this + this.items.push(name); + return this; } }) -Module.prototype.controller = function (name, deps) { - var definition; - if (deps instanceof Array) { - definition = deps.pop(); - } else if (deps instanceof Function) { - definition = deps; +function angularDepsToStringDeps (angularDeps) { + var deps, definition; + if (angularDeps instanceof Array) { + definition = angularDeps.pop(); + deps = angularDeps; + } else if (angularDeps instanceof Function) { + definition = angularDeps; var deps = /\(([^)]+)/.exec(definition); if (deps[1]) { deps = deps[1].split(/\s*,\s*/); } } - this.controllers.push({'name': name, 'deps': deps}); + + return { deps: deps, definition: definition }; +}; + +Module.prototype.controller = function (name, deps) { + this.controllers.push({ + 'name': name, + 'deps': angularDepsToStringDeps(deps).deps + }); + this.items.push(name); + return this; +} + +Module.prototype.factory = function (name, deps) { + var angularDeps = angularDepsToStringDeps(deps); + // console.log('yeah', Object.keys(angularDeps.definition())); + this.services.push({ + 'name': name, + 'deps': angularDeps.deps, + 'api': Object.keys( + angularDeps.definition instanceof Function ? angularDeps.definition() : {}) + }); this.items.push(name); return this; } diff --git a/test/controllers.js b/test/controllers.js new file mode 100644 index 0000000..6e1289c --- /dev/null +++ b/test/controllers.js @@ -0,0 +1,113 @@ +var should = require('should'); + +describe("controllers", function() { + var angular + + beforeEach(function() { + angular = require('../src/fake-angular')() + }) + + it('property should contain all defined controllers with array definition', function() { + angular + .module('testModule1', []) + .controller('testController1', ['dep1', function (dep1) {}]) + .controller('testController2', ['dep2', 'dep3', function (dep1, dep2) {}]); + + angular.modules.should.have.a.lengthOf(1) + angular.modules[0].should.have.property('name', 'testModule1') + + var testModule1 = angular.modulesMap['testModule1']; + testModule1.controllers.should.have.a.lengthOf(2); + testModule1.controllers[0].should.have.property('name', 'testController1'); + testModule1.controllers[1].should.have.property('name', 'testController2'); + + testModule1.controllers[0].deps.should.be.a.Array; + testModule1.controllers[0].deps.should.have.a.lengthOf(1); + testModule1.controllers[0].deps[0].should.be.equal('dep1') + + testModule1.controllers[1].deps.should.be.a.Array; + testModule1.controllers[1].deps.should.have.a.lengthOf(2); + testModule1.controllers[1].deps[0].should.be.equal('dep2'); + testModule1.controllers[1].deps[1].should.be.equal('dep3'); + }) + + it('property should contain all defined controllers with function definition', function() { + angular + .module('testModule1', []) + .controller('testController1', function (dep1) {}) + .controller('testController2', function (dep2, dep3) {}); + + angular.modules.should.have.a.lengthOf(1) + angular.modules[0].should.have.property('name', 'testModule1') + + var testModule1 = angular.modulesMap['testModule1']; + testModule1.controllers.should.have.a.lengthOf(2); + testModule1.controllers[0].should.have.property('name', 'testController1'); + testModule1.controllers[1].should.have.property('name', 'testController2'); + + testModule1.controllers[0].deps.should.be.a.Array; + testModule1.controllers[0].deps.should.have.a.lengthOf(1); + testModule1.controllers[0].deps[0].should.be.equal('dep1') + + testModule1.controllers[1].deps.should.be.a.Array; + testModule1.controllers[1].deps.should.have.a.lengthOf(2); + testModule1.controllers[1].deps[0].should.be.equal('dep2'); + testModule1.controllers[1].deps[1].should.be.equal('dep3'); + }) + + + it('property should contain all defined controllers with variable array definition', function() { + var testController1 = ['dep1', function (dep1) {}], + testController2 = ['dep2', 'dep3', function (dep2, dep3) {}] + ; + + angular + .module('testModule1', []) + .controller('testController1', testController1) + .controller('testController2', testController2); + + angular.modules.should.have.a.lengthOf(1) + angular.modules[0].should.have.property('name', 'testModule1') + + var testModule1 = angular.modulesMap['testModule1']; + testModule1.controllers.should.have.a.lengthOf(2); + testModule1.controllers[0].should.have.property('name', 'testController1'); + testModule1.controllers[1].should.have.property('name', 'testController2'); + + testModule1.controllers[0].deps.should.be.a.Array; + testModule1.controllers[0].deps.should.have.a.lengthOf(1); + testModule1.controllers[0].deps[0].should.be.equal('dep1') + + testModule1.controllers[1].deps.should.be.a.Array; + testModule1.controllers[1].deps.should.have.a.lengthOf(2); + testModule1.controllers[1].deps[0].should.be.equal('dep2'); + testModule1.controllers[1].deps[1].should.be.equal('dep3'); + }) + + it('property should contain all defined controllers with variable function definition', function() { + function testController1 (dep1) {}; + function testController2 (dep2, dep3) {}; + + angular + .module('testModule1', []) + .controller('testController1', testController1) + .controller('testController2', testController2); + + angular.modules.should.have.a.lengthOf(1) + angular.modules[0].should.have.property('name', 'testModule1') + + var testModule1 = angular.modulesMap['testModule1']; + testModule1.controllers.should.have.a.lengthOf(2); + testModule1.controllers[0].should.have.property('name', 'testController1'); + testModule1.controllers[1].should.have.property('name', 'testController2'); + + testModule1.controllers[0].deps.should.be.a.Array; + testModule1.controllers[0].deps.should.have.a.lengthOf(1); + testModule1.controllers[0].deps[0].should.be.equal('dep1') + + testModule1.controllers[1].deps.should.be.a.Array; + testModule1.controllers[1].deps.should.have.a.lengthOf(2); + testModule1.controllers[1].deps[0].should.be.equal('dep2'); + testModule1.controllers[1].deps[1].should.be.equal('dep3'); + }) +}) diff --git a/test/fake-angular.js b/test/fake-angular.js index e19d729..a640a24 100644 --- a/test/fake-angular.js +++ b/test/fake-angular.js @@ -133,109 +133,4 @@ describe("angular", function() { }) }) }) - - describe("controllers", function() { - it('property should contain all defined controllers with array definition', function() { - angular - .module('testModule1', []) - .controller('testController1', ['dep1', function (dep1) {}]) - .controller('testController2', ['dep2', 'dep3', function (dep1, dep2) {}]); - - angular.modules.should.have.a.lengthOf(1) - angular.modules[0].should.have.property('name', 'testModule1') - - var testModule1 = angular.modulesMap['testModule1']; - testModule1.controllers.should.have.a.lengthOf(2); - testModule1.controllers[0].should.have.property('name', 'testController1'); - testModule1.controllers[1].should.have.property('name', 'testController2'); - - testModule1.controllers[0].deps.should.be.a.Array; - testModule1.controllers[0].deps.should.have.a.lengthOf(1); - testModule1.controllers[0].deps[0].should.be.equal('dep1') - - testModule1.controllers[1].deps.should.be.a.Array; - testModule1.controllers[1].deps.should.have.a.lengthOf(2); - testModule1.controllers[1].deps[0].should.be.equal('dep2'); - testModule1.controllers[1].deps[1].should.be.equal('dep3'); - }) - - it('property should contain all defined controllers with function definition', function() { - angular - .module('testModule1', []) - .controller('testController1', function (dep1) {}) - .controller('testController2', function (dep2, dep3) {}); - - angular.modules.should.have.a.lengthOf(1) - angular.modules[0].should.have.property('name', 'testModule1') - - var testModule1 = angular.modulesMap['testModule1']; - testModule1.controllers.should.have.a.lengthOf(2); - testModule1.controllers[0].should.have.property('name', 'testController1'); - testModule1.controllers[1].should.have.property('name', 'testController2'); - - testModule1.controllers[0].deps.should.be.a.Array; - testModule1.controllers[0].deps.should.have.a.lengthOf(1); - testModule1.controllers[0].deps[0].should.be.equal('dep1') - - testModule1.controllers[1].deps.should.be.a.Array; - testModule1.controllers[1].deps.should.have.a.lengthOf(2); - testModule1.controllers[1].deps[0].should.be.equal('dep2'); - testModule1.controllers[1].deps[1].should.be.equal('dep3'); - }) - }) - - it('property should contain all defined controllers with variable array definition', function() { - var testController1 = ['dep1', function (dep1) {}], - testController2 = ['dep2', 'dep3', function (dep2, dep3) {}] - ; - - angular - .module('testModule1', []) - .controller('testController1', testController1) - .controller('testController2', testController2); - - angular.modules.should.have.a.lengthOf(1) - angular.modules[0].should.have.property('name', 'testModule1') - - var testModule1 = angular.modulesMap['testModule1']; - testModule1.controllers.should.have.a.lengthOf(2); - testModule1.controllers[0].should.have.property('name', 'testController1'); - testModule1.controllers[1].should.have.property('name', 'testController2'); - - testModule1.controllers[0].deps.should.be.a.Array; - testModule1.controllers[0].deps.should.have.a.lengthOf(1); - testModule1.controllers[0].deps[0].should.be.equal('dep1') - - testModule1.controllers[1].deps.should.be.a.Array; - testModule1.controllers[1].deps.should.have.a.lengthOf(2); - testModule1.controllers[1].deps[0].should.be.equal('dep2'); - testModule1.controllers[1].deps[1].should.be.equal('dep3'); - }) - - it('property should contain all defined controllers with variable function definition', function() { - function testController1 (dep1) {}; - function testController2 (dep2, dep3) {}; - - angular - .module('testModule1', []) - .controller('testController1', testController1) - .controller('testController2', testController2); - - angular.modules.should.have.a.lengthOf(1) - angular.modules[0].should.have.property('name', 'testModule1') - - var testModule1 = angular.modulesMap['testModule1']; - testModule1.controllers.should.have.a.lengthOf(2); - testModule1.controllers[0].should.have.property('name', 'testController1'); - testModule1.controllers[1].should.have.property('name', 'testController2'); - - testModule1.controllers[0].deps.should.be.a.Array; - testModule1.controllers[0].deps.should.have.a.lengthOf(1); - testModule1.controllers[0].deps[0].should.be.equal('dep1') - - testModule1.controllers[1].deps.should.be.a.Array; - testModule1.controllers[1].deps.should.have.a.lengthOf(2); - testModule1.controllers[1].deps[0].should.be.equal('dep2'); - testModule1.controllers[1].deps[1].should.be.equal('dep3'); - }) }) diff --git a/test/services.js b/test/services.js new file mode 100644 index 0000000..8d62cf5 --- /dev/null +++ b/test/services.js @@ -0,0 +1,36 @@ +var should = require('should'); + +describe("factories", function() { + var angular + + beforeEach(function() { + angular = require('../src/fake-angular')() + }) + + it('property should contain all defined services with variable function definition', function() { + function testService (dep1) { + return { + attr: "attribute", + getFoo: function (foo) {}, + getBar: function (bar) {} + } + }; + + angular + .module('testModule1', []) + .factory('testService', testService); + + var testModule1 = angular.modulesMap['testModule1']; + testModule1.services.should.be.a.Array; + testModule1.services.should.have.a.lengthOf(1); + testModule1.services[0].name.should.be.equal('testService'); + testModule1.services[0].deps.should.be.a.Array; + testModule1.services[0].deps.should.have.a.lengthOf(1); + testModule1.services[0].deps[0].should.be.equal('dep1'); + + testModule1.services[0].api.should.be.a.Array; + testModule1.services[0].api.indexOf('attr').should.not.be.equal(-1); + testModule1.services[0].api.indexOf('getFoo').should.not.be.equal(-1); + testModule1.services[0].api.indexOf('getBar').should.not.be.equal(-1); + }) +}) From 8a2bdf6eda98bae5fbc26eac1bd3afe425ae9ae3 Mon Sep 17 00:00:00 2001 From: Joao Figueiredo Date: Tue, 19 Aug 2014 10:31:26 +0200 Subject: [PATCH 07/29] Adds test for controllers with no dependencies. --- src/fake-angular.js | 4 +++- test/controllers.js | 29 +++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/fake-angular.js b/src/fake-angular.js index 8997b38..515756e 100644 --- a/src/fake-angular.js +++ b/src/fake-angular.js @@ -49,8 +49,10 @@ function angularDepsToStringDeps (angularDeps) { } else if (angularDeps instanceof Function) { definition = angularDeps; var deps = /\(([^)]+)/.exec(definition); - if (deps[1]) { + if (deps && deps.length && deps[1]) { deps = deps[1].split(/\s*,\s*/); + } else { + deps = []; } } diff --git a/test/controllers.js b/test/controllers.js index 6e1289c..453fe65 100644 --- a/test/controllers.js +++ b/test/controllers.js @@ -7,6 +7,35 @@ describe("controllers", function() { angular = require('../src/fake-angular')() }) + it('should have an empty property when no controller are present', function () { + angular.module('testModule1'); + angular.module('testModule2'); + + angular.modulesMap['testModule1'].controllers.should.be.a.Array; + angular.modulesMap['testModule1'].controllers.should.have.a.lengthOf(0); + + angular.modulesMap['testModule2'].controllers.should.be.a.Array; + angular.modulesMap['testModule2'].controllers.should.have.a.lengthOf(0); + + }) + + it('should have an empty dependencies array when there are no dependencies', function () { + angular + .module('testModule1') + .controller('myController', function () {}) + .controller('myController2', [function () {}]); + + angular.modulesMap['testModule1'].controllers.should.be.a.Array; + angular.modulesMap['testModule1'].controllers.should.have.a.lengthOf(2); + + angular.modulesMap['testModule1'].controllers[0].deps.should.be.a.Array; + angular.modulesMap['testModule1'].controllers[0].deps.should.have.a.lengthOf(0); + + angular.modulesMap['testModule1'].controllers[1].deps.should.be.a.Array; + angular.modulesMap['testModule1'].controllers[1].deps.should.have.a.lengthOf(0); + + }) + it('property should contain all defined controllers with array definition', function() { angular .module('testModule1', []) From 3eb3626efb1103b2b2f55f30df670bce50cad9f1 Mon Sep 17 00:00:00 2001 From: Joao Figueiredo Date: Tue, 19 Aug 2014 11:08:14 +0200 Subject: [PATCH 08/29] Fixes services deps and adds more tests with empty deps.:wq --- src/fake-angular.js | 11 ++++++----- test/services.js | 30 ++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/src/fake-angular.js b/src/fake-angular.js index 515756e..07339a1 100644 --- a/src/fake-angular.js +++ b/src/fake-angular.js @@ -42,20 +42,22 @@ methods.forEach(function(method) { }) function angularDepsToStringDeps (angularDeps) { - var deps, definition; + var deps, definition, angularDepsStr; if (angularDeps instanceof Array) { definition = angularDeps.pop(); deps = angularDeps; } else if (angularDeps instanceof Function) { definition = angularDeps; - var deps = /\(([^)]+)/.exec(definition); + // We just care about the wrapper function to the dependencies + angularDepsStr = "" + angularDeps; + angularDepsStr = angularDepsStr.slice(0, angularDepsStr.indexOf('{')); + var deps = /\(([^)]+)/.exec(angularDepsStr); if (deps && deps.length && deps[1]) { - deps = deps[1].split(/\s*,\s*/); + deps = deps[1].split(/\s*,\s*/); } else { deps = []; } } - return { deps: deps, definition: definition }; }; @@ -70,7 +72,6 @@ Module.prototype.controller = function (name, deps) { Module.prototype.factory = function (name, deps) { var angularDeps = angularDepsToStringDeps(deps); - // console.log('yeah', Object.keys(angularDeps.definition())); this.services.push({ 'name': name, 'deps': angularDeps.deps, diff --git a/test/services.js b/test/services.js index 8d62cf5..5a6bdd3 100644 --- a/test/services.js +++ b/test/services.js @@ -7,6 +7,36 @@ describe("factories", function() { angular = require('../src/fake-angular')() }) + it('factories with no dependencies should have an empty array', function () { + function testService () { + return { + attr: "attribute", + getFoo: function (foo) {}, + getBar: function (bar) {} + } + }; + + var testService2 = [function testService () { + return { + attr: "attribute", + getFoo: function (foo) {}, + getBar: function (bar) {} + } + }]; + + angular + .module('testModule1', []) + .factory('testService', testService) + .factory('testService2', testService2); + + + var testModule1 = angular.modulesMap['testModule1']; + testModule1.services[1].deps.should.be.a.Array; + testModule1.services[1].deps.should.have.a.lengthOf(0); + testModule1.services[0].deps.should.be.a.Array; + testModule1.services[0].deps.should.have.a.lengthOf(0); + }) + it('property should contain all defined services with variable function definition', function() { function testService (dep1) { return { From b1b715e1134372f3431131e513f8aebb89bbe4cd Mon Sep 17 00:00:00 2001 From: Joao Figueiredo Date: Tue, 19 Aug 2014 22:34:39 +0200 Subject: [PATCH 09/29] Excludes angular-specific dependencies. --- src/fake-angular.js | 11 +++++++++-- test/controllers.js | 19 +++++++++++++++++++ test/services.js | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 2 deletions(-) diff --git a/src/fake-angular.js b/src/fake-angular.js index 07339a1..5829fd1 100644 --- a/src/fake-angular.js +++ b/src/fake-angular.js @@ -42,7 +42,7 @@ methods.forEach(function(method) { }) function angularDepsToStringDeps (angularDeps) { - var deps, definition, angularDepsStr; +var deps, definition, angularDepsStr, depsProcessed = []; if (angularDeps instanceof Array) { definition = angularDeps.pop(); deps = angularDeps; @@ -58,7 +58,14 @@ function angularDepsToStringDeps (angularDeps) { deps = []; } } - return { deps: deps, definition: definition }; + if (deps && deps.length) { + deps.forEach(function (dep) { + if (dep.split('')[0] !== '$') { + depsProcessed.push(dep); + } + }); + } + return { deps: depsProcessed, definition: definition }; }; Module.prototype.controller = function (name, deps) { diff --git a/test/controllers.js b/test/controllers.js index 453fe65..4533c08 100644 --- a/test/controllers.js +++ b/test/controllers.js @@ -139,4 +139,23 @@ describe("controllers", function() { testModule1.controllers[1].deps[0].should.be.equal('dep2'); testModule1.controllers[1].deps[1].should.be.equal('dep3'); }) + + it('excludes angular dependencies', function () { + function testController1 ($scope, dep1) {}; + var testController2 = ['$scope', 'dep3', function ($scope, dep3) {}]; + + angular + .module('testModule1', []) + .controller('testController1', testController1) + .controller('testController2', testController2); + + var testModule1 = angular.modulesMap['testModule1']; + testModule1.controllers[0].deps.should.be.a.Array; + testModule1.controllers[0].deps.should.have.a.lengthOf(1); + testModule1.controllers[0].deps[0].should.be.equal('dep1'); + + testModule1.controllers[1].deps.should.be.a.Array; + testModule1.controllers[1].deps.should.have.a.lengthOf(1); + testModule1.controllers[1].deps[0].should.be.equal('dep3'); + }) }) diff --git a/test/services.js b/test/services.js index 5a6bdd3..1ccf8d3 100644 --- a/test/services.js +++ b/test/services.js @@ -63,4 +63,36 @@ describe("factories", function() { testModule1.services[0].api.indexOf('getFoo').should.not.be.equal(-1); testModule1.services[0].api.indexOf('getBar').should.not.be.equal(-1); }) + + it('should exclude angular dependencies', function () { + var testService2 = ['$resource', 'dep2', function ($resource, dep2) { + return { + attr: "attribute", + getFoo: function (foo) {}, + getBar: function (bar) {} + } + }]; + function testService ($http, dep1) { + return { + attr: "attribute", + getFoo: function (foo) {}, + getBar: function (bar) {} + } + }; + + angular + .module('testModule1', []) + .factory('testService', testService) + .factory('testService2', testService2); + + var testModule1 = angular.modulesMap['testModule1']; + testModule1.services[0].deps.should.be.a.Array; + testModule1.services[0].deps.should.have.a.lengthOf(1); + testModule1.services[0].deps[0].should.be.equal('dep1'); + + testModule1.services[1].deps.should.be.a.Array; + testModule1.services[1].deps.should.have.a.lengthOf(1); + testModule1.services[1].deps[0].should.be.equal('dep2'); + + }) }) From 2bbb9ba4148229646bd00ea4d033e8760d96b609 Mon Sep 17 00:00:00 2001 From: Joao Figueiredo Date: Mon, 25 Aug 2014 19:44:42 +0200 Subject: [PATCH 10/29] Dummy push. --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 53cb5f8..e80eed3 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ angular-modules-graph Create a graph of angular module definitions, extracted from [grunt-angular-modules-graph](https://github.com/carlo-colombo/grunt-angular-modules-graph) + + ```js var scripts = [ { From 5d833304be76ae54d0729d5dd2f082c825414f39 Mon Sep 17 00:00:00 2001 From: Joao Figueiredo Date: Mon, 25 Aug 2014 19:46:32 +0200 Subject: [PATCH 11/29] Adds codeship flag to readme. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e80eed3..a43f4df 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ angular-modules-graph Create a graph of angular module definitions, extracted from [grunt-angular-modules-graph](https://github.com/carlo-colombo/grunt-angular-modules-graph) - +[ ![Codeship Status for lucalanca/angular-modules-graph](https://www.codeship.io/projects/43e2f770-0ead-0132-1978-5ad6f07ad273/status)](https://www.codeship.io/projects/32481) ```js From 7a75cce3e26e09905f559dc16ad13716cc9f0847 Mon Sep 17 00:00:00 2001 From: Joao Figueiredo Date: Mon, 25 Aug 2014 19:49:33 +0200 Subject: [PATCH 12/29] Dummy commit. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index a43f4df..1ced470 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ Create a graph of angular module definitions, extracted from [grunt-angular-modu [ ![Codeship Status for lucalanca/angular-modules-graph](https://www.codeship.io/projects/43e2f770-0ead-0132-1978-5ad6f07ad273/status)](https://www.codeship.io/projects/32481) + ```js var scripts = [ { From 5d3da47ab8d5efc23c7b077ed55fdf247b344037 Mon Sep 17 00:00:00 2001 From: Joao Figueiredo Date: Wed, 27 Aug 2014 14:48:24 +0200 Subject: [PATCH 13/29] Updates README --- README.md | 45 +++++++++++++++++++++------------------------ 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 1ced470..8eaa4aa 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,32 @@ -angular-modules-graph -===================== +# Angular Architecture Graph -Create a graph of angular module definitions, extracted from [grunt-angular-modules-graph](https://github.com/carlo-colombo/grunt-angular-modules-graph) +This project is a node utility that analyses an angular project and exports a graph with the project's architecture: modules, controllers, directives and filters. [ ![Codeship Status for lucalanca/angular-modules-graph](https://www.codeship.io/projects/43e2f770-0ead-0132-1978-5ad6f07ad273/status)](https://www.codeship.io/projects/32481) +## How to use it: +1. Require the module: -```js -var scripts = [ -{ - id: 'file1.js', - text: "angular.module('TestModule',[])" -}, -{ ... } -] + ```js + var angularArchitectureGraph = require('angular-architecture-graph'), + ]) + ``` -var angularModulesGraph = require('angular-modules-graph') +2. Call it with your project code: -var res = angularModulesGraph(scripts) + ```js + var architecture = angularArchitectureGraph([ + { id: 'file1.js', text: '' }, + { id: 'file2.js', text: '' } + ]) + ``` -res.angular // fake angular containig graph -res.angular.modulesNames == ['TestModule' , ...] //list of modules name found -res.angular.modules // modules found -res.angular.modules[0].name == 'TestModule' //module name -res.angular.modules[0].items //untyped array of defined items (controllers, filters, provider, services, ...) -res.angular.modules[0].modules //array of module dependencies +3. Do whatever you want with the resulted architecture object -res.results // array of objects indicating if script evaluation resulted without error -res.results[0].id == 'file1.js' -res.results[0].error == false // true if an error occured during evalution -res.results[0].exception == undefined // exception launched during evaluation +## About -``` +This project was originally forked from , extracted from [@carlo-colombo's](https://github.com/carlo-colombo) [angular-modules-graph](https://github.com/carlo-colombo/angular-modules-graph) + +## License: +MIT \ No newline at end of file From 50707ba5d8677592072c89d067dffee1263d7979 Mon Sep 17 00:00:00 2001 From: Joao Figueiredo Date: Wed, 27 Aug 2014 14:50:26 +0200 Subject: [PATCH 14/29] Modifies package.json to publish under a npm package. --- package.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 973af8f..c84790d 100644 --- a/package.json +++ b/package.json @@ -1,27 +1,27 @@ { - "name": "angular-modules-graph", + "name": "angular-architecture-graph", "version": "0.1.0", - "description": "Create a graph of angular module definitions", + "description": "Create a graph of an angular project's architecture", "main": "index.js", "scripts": { "test": "grunt test" }, "keywords": [ "angular", - "modules", + "architecture", "graph" ], - "author": "Carlo Colombo <@lit_car>", + "author": "João Figueiredo <@lucalanca>", "repository": { "type": "git", - "url": "git://github.com/carlo-colombo/angular-modules-graph.git" + "url": "git://github.com/lucalanca/angular-architecture-graph.git" }, "bugs": { - "url": "https://github.com/carlo-colombo/angular-modules-graph/issues" + "url": "https://github.com/lucalanca/angular-architecture-graph/issues" }, "licenses": [{ "type": "MIT", - "url": "https://github.com/carlo-colombo/angular-modules-graph/blob/master/LICENSE-MIT" + "url": "https://github.com/lucalanca/angular-architecture-graph/blob/master/LICENSE-MIT" }], "devDependencies": { "mocha": "^1.18.2", From 36276012a10827e495dcf52984fea3cebd455212 Mon Sep 17 00:00:00 2001 From: Joao Figueiredo Date: Mon, 6 Oct 2014 14:16:25 +0200 Subject: [PATCH 15/29] A LOT of changes in the infrastructure and tests. --- .editorconfig | 13 + .eslintrc | 140 ++ Gruntfile.js | 20 +- grunt/aliases.yml | 4 + grunt/eslint.js | 6 + grunt/mochaTest.js | 5 + grunt/watch.js | 6 + index.js | 16 +- package.json | 27 +- src/api.js | 37 + src/fake-angular.js | 138 +- src/module.js | 53 + src/utils.js | 35 + test-mocks/example1-chained.js | 60 + test-mocks/example1.js | 58 + test-mocks/ui-router.js | 3660 +++++++++++++++++++++++++++++++ test-mocks/ui-utils-keypress.js | 119 + test/controllers.js | 161 -- test/fake-angular.js | 192 +- test/index.js | 82 +- test/mock-example-1.js | 71 + test/services.js | 98 - test/ui-keypress.js | 32 + test/ui-router.js | 133 ++ 24 files changed, 4625 insertions(+), 541 deletions(-) create mode 100644 .editorconfig create mode 100644 .eslintrc create mode 100644 grunt/aliases.yml create mode 100644 grunt/eslint.js create mode 100644 grunt/mochaTest.js create mode 100644 grunt/watch.js create mode 100644 src/api.js create mode 100644 src/module.js create mode 100644 src/utils.js create mode 100644 test-mocks/example1-chained.js create mode 100644 test-mocks/example1.js create mode 100644 test-mocks/ui-router.js create mode 100644 test-mocks/ui-utils-keypress.js delete mode 100644 test/controllers.js create mode 100644 test/mock-example-1.js delete mode 100644 test/services.js create mode 100644 test/ui-keypress.js create mode 100644 test/ui-router.js diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5d12634 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +# editorconfig.org +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..abe4fcd --- /dev/null +++ b/.eslintrc @@ -0,0 +1,140 @@ +{ + "env": { + "browser": false, + "node": true, + "amd": false, + "mocha": true + }, + + "rules": { + "no-alert": 2, + "no-array-constructor": 2, + "no-bitwise": 0, + "no-caller": 2, + "no-catch-shadow": 2, + "no-comma-dangle": 2, + "no-cond-assign": 2, + "no-console": 2, + "no-constant-condition": 2, + "no-control-regex": 2, + "no-debugger": 2, + "no-delete-var": 2, + "no-div-regex": 0, + "no-dupe-keys": 2, + "no-else-return": 0, + "no-empty": 2, + "no-empty-class": 2, + "no-empty-label": 2, + "no-eq-null": 0, + "no-eval": 2, + "no-ex-assign": 2, + "no-extend-native": 2, + "no-extra-bind": 2, + "no-extra-boolean-cast": 2, + "no-extra-parens": 0, + "no-extra-semi": 2, + "no-extra-strict": 2, + "no-fallthrough": 2, + "no-floating-decimal": 0, + "no-func-assign": 2, + "no-implied-eval": 2, + "no-inner-declarations": [2, "functions"], + "no-invalid-regexp": 2, + "no-iterator": 2, + "no-label-var": 2, + "no-labels": 2, + "no-lone-blocks": 2, + "no-lonely-if": 0, + "no-loop-func": 2, + "no-mixed-requires": [0, false], + "no-mixed-spaces-and-tabs": [2, false], + "no-multi-str": 2, + "no-native-reassign": 2, + "no-negated-in-lhs": 2, + "no-nested-ternary": 0, + "no-new": 2, + "no-new-func": 2, + "no-new-object": 2, + "no-new-require": 0, + "no-new-wrappers": 2, + "no-obj-calls": 2, + "no-octal": 2, + "no-octal-escape": 2, + "no-path-concat": 0, + "no-plusplus": 0, + "no-process-exit": 2, + "no-proto": 2, + "no-redeclare": 2, + "no-regex-spaces": 2, + "no-reserved-keys": 0, + "no-restricted-modules": 0, + "no-return-assign": 2, + "no-script-url": 2, + "no-self-compare": 0, + "no-sequences": 2, + "no-shadow": 2, + "no-shadow-restricted-names": 2, + "no-space-before-semi": 2, + "no-spaced-func": 2, + "no-sparse-arrays": 2, + "no-sync": 0, + "no-ternary": 0, + "no-trailing-spaces": 2, + "no-undef": 2, + "no-undef-init": 2, + "no-undefined": 0, + "no-underscore-dangle": 2, + "no-unreachable": 2, + "no-unused-expressions": 2, + "no-unused-vars": [2, {"vars": "all", "args": "after-used"}], + "no-use-before-define": 2, + "no-void": 0, + "no-warning-comments": [0, { "terms": ["todo", "fixme", "xxx"], "location": "start" }], + "no-with": 2, + "no-wrap-func": 2, + + "block-scoped-var": 0, + "brace-style": [0, "1tbs"], + "camelcase": 2, + "complexity": [0, 11], + "consistent-return": 2, + "consistent-this": [0, "that"], + "curly": [2, "all"], + "default-case": 0, + "dot-notation": 2, + "eol-last": 2, + "eqeqeq": 2, + "func-names": 0, + "func-style": [0, "declaration"], + "global-strict": [2, "never"], + "guard-for-in": 0, + "max-depth": [0, 4], + "max-len": [0, 80, 4], + "max-nested-callbacks": [0, 2], + "max-params": [0, 3], + "max-statements": [0, 10], + "handle-callback-err": 0, + "new-cap": 2, + "new-parens": 2, + "one-var": 0, + "quote-props": 0, + "quotes": [2, "double"], + "radix": 0, + "semi": 2, + "sort-vars": 0, + "space-after-keywords": [0, "always"], + "space-in-brackets": [0, "never"], + "space-in-parens": [0, "never"], + "space-infix-ops": 2, + "space-return-throw-case": 2, + "space-unary-word-ops": 0, + "strict": 2, + "use-isnan": 2, + "valid-jsdoc": 0, + "valid-typeof": 2, + "vars-on-top": 0, + "wrap-iife": 0, + "wrap-regex": 0, + "yoda": [2, "never"] + } +} diff --git a/Gruntfile.js b/Gruntfile.js index 23c5f40..fd1ca93 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -10,23 +10,7 @@ module.exports = function(grunt) { - grunt.initConfig({ - watch: { - test: { - files: ['test/*.js', 'src/*.js'], - tasks: ['test'] - } - }, - mochaTest: { - test: { - src: ['test/*.js'] - } - } - }); - - grunt.loadNpmTasks('grunt-contrib-watch'); - grunt.loadNpmTasks('grunt-mocha-test'); - - grunt.registerTask('test', ['mochaTest:test']); + require('load-grunt-tasks')(grunt); + require('load-grunt-config')(grunt); }; diff --git a/grunt/aliases.yml b/grunt/aliases.yml new file mode 100644 index 0000000..e28b288 --- /dev/null +++ b/grunt/aliases.yml @@ -0,0 +1,4 @@ +test: + - eslint + - mochaTest:test + - watch diff --git a/grunt/eslint.js b/grunt/eslint.js new file mode 100644 index 0000000..317819a --- /dev/null +++ b/grunt/eslint.js @@ -0,0 +1,6 @@ +module.exports = { + files: ["src/*.js", "test/*.js", "!test-mocks/*.js", "index.js", "grunt/*.js"], + options: { + config: ".eslintrc" + } +}; diff --git a/grunt/mochaTest.js b/grunt/mochaTest.js new file mode 100644 index 0000000..e8ac3e9 --- /dev/null +++ b/grunt/mochaTest.js @@ -0,0 +1,5 @@ +module.exports = { + test: { + src: ["test/*.js"] + } +}; diff --git a/grunt/watch.js b/grunt/watch.js new file mode 100644 index 0000000..10c972e --- /dev/null +++ b/grunt/watch.js @@ -0,0 +1,6 @@ +module.exports = { + test: { + files: ["test/*.js", "src/*.js", "test-mocks/*.js", "index.js", "grunt/*.js"], + tasks: ["test"] + } +}; diff --git a/index.js b/index.js index ae09d15..3f2801e 100644 --- a/index.js +++ b/index.js @@ -1,27 +1,29 @@ -var angular = require('./src/fake-angular')(), +/*eslint no-unused-expressions: 0, no-unused-vars: 0, no-eval: 0*/ + +var angular = require("./src/fake-angular")(), document = {}, window = {}, navigator = {}; module.exports = function(scripts) { var results = scripts.map(function(content) { try { - eval(content.text) + eval(content.text); } catch (e) { return { id: content.id, error: true, exception: e - } + }; } return { id: content.id, error: false - } - }) + }; + }); return { angular: angular, results: results - } -} + }; +}; diff --git a/package.json b/package.json index c84790d..1020ef5 100644 --- a/package.json +++ b/package.json @@ -19,15 +19,26 @@ "bugs": { "url": "https://github.com/lucalanca/angular-architecture-graph/issues" }, - "licenses": [{ - "type": "MIT", - "url": "https://github.com/lucalanca/angular-architecture-graph/blob/master/LICENSE-MIT" - }], + "licenses": [ + { + "type": "MIT", + "url": "https://github.com/lucalanca/angular-architecture-graph/blob/master/LICENSE-MIT" + } + ], "devDependencies": { - "mocha": "^1.18.2", - "should": "^3.2.0", - "grunt-mocha-test": "^0.10.0", "grunt": "^0.4.4", - "grunt-contrib-watch": "^0.6.1" + "grunt-contrib-watch": "^0.6.1", + "grunt-eslint": "^1.1.0", + "grunt-mocha-test": "^0.10.0", + "load-grunt-config": "^0.13.1", + "load-grunt-tasks": "^0.6.0", + "mocha": "^1.18.2", + "nodegit": "^0.1.4", + "q-io": "^1.11.5", + "should": "^3.2.0" + }, + "dependencies": { + "lodash": "^2.4.1", + "pluralize": "^1.0.2" } } diff --git a/src/api.js b/src/api.js new file mode 100644 index 0000000..b2861d1 --- /dev/null +++ b/src/api.js @@ -0,0 +1,37 @@ +module.exports = { + methods: [ + "constant", + "controller", + "directive", + "factory", + "filter", + "provider", + "service", + "value" + ], + globalApis: [ + "lowercase", + "uppercase", + "forEach", + "extend", + "identity", + "noop", + "isUndefined", + "isDefined", + "isObject", + "isString", + "isNumber", + "isDate", + "isArray", + "isFunction", + "isElement", + "copy", + "equals", + "bind", + "toJson", + "fromJson", + "bootstrap", + "injector", + "element" + ] +}; diff --git a/src/fake-angular.js b/src/fake-angular.js index 5829fd1..2af7037 100644 --- a/src/fake-angular.js +++ b/src/fake-angular.js @@ -1,126 +1,38 @@ -'use strict'; +"use strict"; -function Module(name, deps) { - this.name = name - this.modules = deps - this.items = [] - this.controllers = []; - this.services = []; -} +var utils = require("./utils"); +var api = require("./api"); +var Module = require("./module"); -var methods = ['constant', 'controller', 'directive', 'factory', 'filter', 'provider', 'service', 'value'] -var globalApis = ['lowercase', - 'uppercase', - 'forEach', - 'extend', - 'identity', - 'noop', - 'isUndefined', - 'isDefined', - 'isObject', - 'isString', - 'isNumber', - 'isDate', - 'isArray', - 'isFunction', - 'isElement', - 'copy', - 'equals', - 'bind', - 'toJson', - 'fromJson', - 'bootstrap', - 'injector', - 'element', -]; - -methods.forEach(function(method) { - Module.prototype[method] = function addItem(name) { - this.items.push(name); - return this; - } -}) - -function angularDepsToStringDeps (angularDeps) { -var deps, definition, angularDepsStr, depsProcessed = []; - if (angularDeps instanceof Array) { - definition = angularDeps.pop(); - deps = angularDeps; - } else if (angularDeps instanceof Function) { - definition = angularDeps; - // We just care about the wrapper function to the dependencies - angularDepsStr = "" + angularDeps; - angularDepsStr = angularDepsStr.slice(0, angularDepsStr.indexOf('{')); - var deps = /\(([^)]+)/.exec(angularDepsStr); - if (deps && deps.length && deps[1]) { - deps = deps[1].split(/\s*,\s*/); - } else { - deps = []; - } - } - if (deps && deps.length) { - deps.forEach(function (dep) { - if (dep.split('')[0] !== '$') { - depsProcessed.push(dep); - } - }); - } - return { deps: depsProcessed, definition: definition }; -}; - -Module.prototype.controller = function (name, deps) { - this.controllers.push({ - 'name': name, - 'deps': angularDepsToStringDeps(deps).deps - }); - this.items.push(name); - return this; -} - -Module.prototype.factory = function (name, deps) { - var angularDeps = angularDepsToStringDeps(deps); - this.services.push({ - 'name': name, - 'deps': angularDeps.deps, - 'api': Object.keys( - angularDeps.definition instanceof Function ? angularDeps.definition() : {}) - }); - this.items.push(name); - return this; -} - -Module.prototype.run = function() { - return this -}; -Module.prototype.config = function() { - return this -}; - -module.exports = function() { +module.exports = function () { var angular = { modules: [], modulesMap: {}, modulesNames: [], module: function(name, deps) { - if (this.modulesNames.indexOf(name)>-1){ + var module; + + // Module was inserted before + if (this.modulesNames.indexOf(name) !== -1) { + module = this.modulesMap[name]; if(deps){ - this.modulesMap[name].modules = deps + this.modulesMap[name].modules = deps; } - return this.modulesMap[name] + // First time we see this module + } else { + module = new Module(name,deps); + this.modulesNames.push(name); + this.modulesMap[name] = module; + this.modules.push(module); } - - var module = new Module(name,deps) - - this.modulesNames.push(name) - this.modulesMap[name] = module - this.modules.push(module) - return module + return module; } - } - var noop = function(){} - globalApis.forEach(function(method) { - angular[method] = noop + }; + + // Adds global apis to the angular object + api.globalApis.forEach(function(method) { + angular[method] = utils.noop; }); - return angular -} + return angular; +}; diff --git a/src/module.js b/src/module.js new file mode 100644 index 0000000..bedc7b5 --- /dev/null +++ b/src/module.js @@ -0,0 +1,53 @@ +"use strict"; + +var pluralize = require("pluralize"); +var utils = require("./utils"); +var api = require("./api"); + +function Module(name, deps) { + this.name = name; + this.modules = deps; + this.items = []; + this.controllers = []; + this.services = []; + this.factories = []; + this.filters = []; + this.providers = []; + this.directives = []; +} + +// Adds module methods +api.methods.forEach(function(method) { + Module.prototype[ method ] = function addItem(name) { + if (!name) { + return this; + } + this.items.push(name); + return this; + }; +}); + +["controller", "factory", "service", "filter", "provider", "directive"].forEach(function (method) { + Module.prototype[ method ] = function (name, deps) { + if (!name) { + return this; + } + var deps2 = utils.parseAngularDeps(deps).deps; + this[pluralize(method)].push({ + "name": name, + "deps": deps2 + }); + this.items.push(name); + return this; + }; +}); + +Module.prototype.run = function() { + return this; +}; + +Module.prototype.config = function() { + return this; +}; + +module.exports = Module; diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000..fdafcc6 --- /dev/null +++ b/src/utils.js @@ -0,0 +1,35 @@ +"use strict"; + +function parseAngularDeps (angularDeps) { + var deps, definition, angularDepsStr, depsProcessed = []; + if (angularDeps instanceof Array) { + definition = angularDeps.pop(); + deps = angularDeps; + } else if (angularDeps instanceof Function) { + definition = angularDeps; + // We just care about the wrapper function to the dependencies + angularDepsStr = "" + angularDeps; + angularDepsStr = angularDepsStr.slice(0, angularDepsStr.indexOf("{")); + deps = /\(([^)]+)/.exec(angularDepsStr); + if (deps && deps.length && deps[ 1 ]) { + deps = deps[1].split(/\s*,\s*/); + } else { + deps = []; + } + } + if (deps && deps.length) { + deps.forEach(function (dep) { + // if (dep.split("")[0] !== "$") { + depsProcessed.push(dep.replace(" ", "")); + // } + }); + } + return { deps: depsProcessed, definition: definition }; +} + +var noop = function() {}; + +module.exports = { + parseAngularDeps: parseAngularDeps, + noop : noop +}; diff --git a/test-mocks/example1-chained.js b/test-mocks/example1-chained.js new file mode 100644 index 0000000..51650d4 --- /dev/null +++ b/test-mocks/example1-chained.js @@ -0,0 +1,60 @@ +module.exports = function(angular) { + var noopService = { + attr: "attribute", + getFoo: function () {}, + getBar: function () {} + }; + + + function NoDependenciesService () { return noopService; } + function OneAngularDependencyService1 ($http) { return noopService; } + function MixedDependenciesService1 ($http, ServiceX, ServiceY) { return noopService; } + + var NoDependenciesService2 = [ + function () { return noopService; } + ]; + var OneAngularDependencyService2 = [ + "$http", + function ($http) { return noopService; } + ]; + var MixedDependenciesService2 = [ + "$http", "ServiceX", "ServiceY", + function ($http, ServiceX, ServiceY) { return noopService; } + ]; + + angular + .module("example1", []) + .factory("NoDependenciesService1", NoDependenciesService) + .factory("NoDependenciesService2", NoDependenciesService2) + .factory("OneAngularDependencyService1", OneAngularDependencyService1) + .factory("OneAngularDependencyService2", OneAngularDependencyService2) + .factory("MixedDependenciesService1", MixedDependenciesService1) + .factory("MixedDependenciesService2", MixedDependenciesService2) + ; + + + + function NoDependenciesCtrl () { return noopService; } + function OneAngularDependencyCtrl1 ($http) { return noopService; } + function MixedDependenciesCtrl1 ($http, ServiceX, ServiceY) { return noopService; } + + var NoDependenciesCtrl2 = [ + function () { return noopService; } + ]; + var OneAngularDependencyCtrl2 = [ + "$http", + function ($http) { return noopService; } + ]; + var MixedDependenciesCtrl2 = [ + "$http", "ServiceX", "ServiceY", + function ($http, ServiceX, ServiceY) { return noopService; } + ]; + angular.module("example1") + .controller("NoDependenciesCtrl1", NoDependenciesCtrl) + .controller("NoDependenciesCtrl2", NoDependenciesCtrl2) + .controller("OneAngularDependencyCtrl1", OneAngularDependencyCtrl1) + .controller("OneAngularDependencyCtrl2", OneAngularDependencyCtrl2) + .controller("MixedDependenciesCtrl1", MixedDependenciesCtrl1) + .controller("MixedDependenciesCtrl2", MixedDependenciesCtrl2) + ; +}; diff --git a/test-mocks/example1.js b/test-mocks/example1.js new file mode 100644 index 0000000..530f275 --- /dev/null +++ b/test-mocks/example1.js @@ -0,0 +1,58 @@ +module.exports = function(angular) { + + var noopService = { + attr: "attribute", + getFoo: function () {}, + getBar: function () {} + }; + + function NoDependenciesService () { return noopService; } + function OneAngularDependencyService1 ($http) { return noopService; } + function MixedDependenciesService1 ($http, ServiceX, ServiceY) { return noopService; } + + var NoDependenciesService2 = [ + function () { return noopService; } + ]; + var OneAngularDependencyService2 = [ + "$http", + function ($http) { return noopService; } + ]; + var MixedDependenciesService2 = [ + "$http", "ServiceX", "ServiceY", + function ($http, ServiceX, ServiceY) { return noopService; } + ]; + + + angular.module("example1") + angular.module("example1").factory("NoDependenciesService1", NoDependenciesService); + angular.module("example1").factory("NoDependenciesService2", NoDependenciesService2); + angular.module("example1").factory("OneAngularDependencyService1", OneAngularDependencyService1); + angular.module("example1").factory("OneAngularDependencyService2", OneAngularDependencyService2); + angular.module("example1").factory("MixedDependenciesService1", MixedDependenciesService1); + angular.module("example1").factory("MixedDependenciesService2", MixedDependenciesService2); + + + + function NoDependenciesCtrl () { return noopService; } + function OneAngularDependencyCtrl1 ($http) { return noopService; } + function MixedDependenciesCtrl1 ($http, ServiceX, ServiceY) { return noopService; } + + var NoDependenciesCtrl2 = [ + function () { return noopService; } + ]; + var OneAngularDependencyCtrl2 = [ + "$http", + function ($http) { return noopService; } + ]; + var MixedDependenciesCtrl2 = [ + "$http", "ServiceX", "ServiceY", + function ($http, ServiceX, ServiceY) { return noopService; } + ]; + + angular.module("example1").controller("NoDependenciesCtrl1", NoDependenciesCtrl); + angular.module("example1").controller("NoDependenciesCtrl2", NoDependenciesCtrl2); + angular.module("example1").controller("OneAngularDependencyCtrl1", OneAngularDependencyCtrl1); + angular.module("example1").controller("OneAngularDependencyCtrl2", OneAngularDependencyCtrl2); + angular.module("example1").controller("MixedDependenciesCtrl1", MixedDependenciesCtrl1); + angular.module("example1").controller("MixedDependenciesCtrl2", MixedDependenciesCtrl2);; +}; diff --git a/test-mocks/ui-router.js b/test-mocks/ui-router.js new file mode 100644 index 0000000..044c35f --- /dev/null +++ b/test-mocks/ui-router.js @@ -0,0 +1,3660 @@ +module.exports = function (angular){ + /** + * State-based routing for AngularJS + * @version v0.2.11 + * @link http://angular-ui.github.com/ + * @license MIT License, http://www.opensource.org/licenses/MIT + */ + + /* commonjs package manager support (eg componentjs) */ + if (typeof module !== "undefined" && typeof exports !== "undefined" && module.exports === exports){ + module.exports = "ui.router"; + } + + (function (window, angular, undefined) { + /*jshint globalstrict:true*/ + /*global angular:false*/ + "use strict"; + + var isDefined = angular.isDefined, + isFunction = angular.isFunction, + isString = angular.isString, + isObject = angular.isObject, + isArray = angular.isArray, + forEach = angular.forEach, + extend = angular.extend, + copy = angular.copy; + + function inherit(parent, extra) { + return extend(new (extend(function() {}, { prototype: parent }))(), extra); + } + + function merge(dst) { + forEach(arguments, function(obj) { + if (obj !== dst) { + forEach(obj, function(value, key) { + if (!dst.hasOwnProperty(key)) dst[key] = value; + }); + } + }); + return dst; + } + + /** + * Finds the common ancestor path between two states. + * + * @param {Object} first The first state. + * @param {Object} second The second state. + * @return {Array} Returns an array of state names in descending order, not including the root. + */ + function ancestors(first, second) { + var path = []; + + for (var n in first.path) { + if (first.path[n] !== second.path[n]) break; + path.push(first.path[n]); + } + return path; + } + + /** + * IE8-safe wrapper for `Object.keys()`. + * + * @param {Object} object A JavaScript object. + * @return {Array} Returns the keys of the object as an array. + */ + function objectKeys(object) { + if (Object.keys) { + return Object.keys(object); + } + var result = []; + + angular.forEach(object, function(val, key) { + result.push(key); + }); + return result; + } + + /** + * IE8-safe wrapper for `Array.prototype.indexOf()`. + * + * @param {Array} array A JavaScript array. + * @param {*} value A value to search the array for. + * @return {Number} Returns the array index value of `value`, or `-1` if not present. + */ + function arraySearch(array, value) { + if (Array.prototype.indexOf) { + return array.indexOf(value, Number(arguments[2]) || 0); + } + var len = array.length >>> 0, from = Number(arguments[2]) || 0; + from = (from < 0) ? Math.ceil(from) : Math.floor(from); + + if (from < 0) from += len; + + for (; from < len; from++) { + if (from in array && array[from] === value) return from; + } + return -1; + } + + /** + * Merges a set of parameters with all parameters inherited between the common parents of the + * current state and a given destination state. + * + * @param {Object} currentParams The value of the current state parameters ($stateParams). + * @param {Object} newParams The set of parameters which will be composited with inherited params. + * @param {Object} $current Internal definition of object representing the current state. + * @param {Object} $to Internal definition of object representing state to transition to. + */ + function inheritParams(currentParams, newParams, $current, $to) { + var parents = ancestors($current, $to), parentParams, inherited = {}, inheritList = []; + + for (var i in parents) { + if (!parents[i].params) continue; + parentParams = objectKeys(parents[i].params); + if (!parentParams.length) continue; + + for (var j in parentParams) { + if (arraySearch(inheritList, parentParams[j]) >= 0) continue; + inheritList.push(parentParams[j]); + inherited[parentParams[j]] = currentParams[parentParams[j]]; + } + } + return extend({}, inherited, newParams); + } + + /** + * Performs a non-strict comparison of the subset of two objects, defined by a list of keys. + * + * @param {Object} a The first object. + * @param {Object} b The second object. + * @param {Array} keys The list of keys within each object to compare. If the list is empty or not specified, + * it defaults to the list of keys in `a`. + * @return {Boolean} Returns `true` if the keys match, otherwise `false`. + */ + function equalForKeys(a, b, keys) { + if (!keys) { + keys = []; + for (var n in a) keys.push(n); // Used instead of Object.keys() for IE8 compatibility + } + + for (var i=0; i + * + * + * + * + * + * + * + * + * + * + * + * + */ + angular.module("ui.router", ["ui.router.state"]); + + angular.module("ui.router.compat", ["ui.router"]); + + /** + * @ngdoc object + * @name ui.router.util.$resolve + * + * @requires $q + * @requires $injector + * + * @description + * Manages resolution of (acyclic) graphs of promises. + */ + $Resolve.$inject = ["$q", "$injector"]; + function $Resolve( $q, $injector) { + + var VISIT_IN_PROGRESS = 1, + VISIT_DONE = 2, + NOTHING = {}, + NO_DEPENDENCIES = [], + NO_LOCALS = NOTHING, + NO_PARENT = extend($q.when(NOTHING), { $$promises: NOTHING, $$values: NOTHING }); + + + /** + * @ngdoc function + * @name ui.router.util.$resolve#study + * @methodOf ui.router.util.$resolve + * + * @description + * Studies a set of invocables that are likely to be used multiple times. + *
+     * $resolve.study(invocables)(locals, parent, self)
+     * 
+ * is equivalent to + *
+     * $resolve.resolve(invocables, locals, parent, self)
+     * 
+ * but the former is more efficient (in fact `resolve` just calls `study` + * internally). + * + * @param {object} invocables Invocable objects + * @return {function} a function to pass in locals, parent and self + */ + this.study = function (invocables) { + if (!isObject(invocables)) throw new Error("'invocables' must be an object"); + + // Perform a topological sort of invocables to build an ordered plan + var plan = [], cycle = [], visited = {}; + function visit(value, key) { + if (visited[key] === VISIT_DONE) return; + + cycle.push(key); + if (visited[key] === VISIT_IN_PROGRESS) { + cycle.splice(0, cycle.indexOf(key)); + throw new Error("Cyclic dependency: " + cycle.join(" -> ")); + } + visited[key] = VISIT_IN_PROGRESS; + + if (isString(value)) { + plan.push(key, [ function() { return $injector.get(value); }], NO_DEPENDENCIES); + } else { + var params = $injector.annotate(value); + forEach(params, function (param) { + if (param !== key && invocables.hasOwnProperty(param)) visit(invocables[param], param); + }); + plan.push(key, value, params); + } + + cycle.pop(); + visited[key] = VISIT_DONE; + } + forEach(invocables, visit); + invocables = cycle = visited = null; // plan is all that"s required + + function isResolve(value) { + return isObject(value) && value.then && value.$$promises; + } + + return function (locals, parent, self) { + if (isResolve(locals) && self === undefined) { + self = parent; parent = locals; locals = null; + } + if (!locals) locals = NO_LOCALS; + else if (!isObject(locals)) { + throw new Error("'locals' must be an object"); + } + if (!parent) parent = NO_PARENT; + else if (!isResolve(parent)) { + throw new Error("'parent' must be a promise returned by $resolve.resolve()"); + } + + // To complete the overall resolution, we have to wait for the parent + // promise and for the promise for each invokable in our plan. + var resolution = $q.defer(), + result = resolution.promise, + promises = result.$$promises = {}, + values = extend({}, locals), + wait = 1 + plan.length/3, + merged = false; + + function done() { + // Merge parent values we haven"t got yet and publish our own $$values + if (!--wait) { + if (!merged) merge(values, parent.$$values); + result.$$values = values; + result.$$promises = true; // keep for isResolve() + delete result.$$inheritedValues; + resolution.resolve(values); + } + } + + function fail(reason) { + result.$$failure = reason; + resolution.reject(reason); + } + + // Short-circuit if parent has already failed + if (isDefined(parent.$$failure)) { + fail(parent.$$failure); + return result; + } + + if (parent.$$inheritedValues) { + merge(values, parent.$$inheritedValues); + } + + // Merge parent values if the parent has already resolved, or merge + // parent promises and wait if the parent resolve is still in progress. + if (parent.$$values) { + merged = merge(values, parent.$$values); + result.$$inheritedValues = parent.$$values; + done(); + } else { + if (parent.$$inheritedValues) { + result.$$inheritedValues = parent.$$inheritedValues; + } + extend(promises, parent.$$promises); + parent.then(done, fail); + } + + // Process each invocable in the plan, but ignore any where a local of the same name exists. + for (var i=0, ii=plan.length; i} The template html as a string, or a promise + * for that string. + */ + this.fromUrl = function (url, params) { + if (isFunction(url)) url = url(params); + if (url == null) return null; + else return $http + .get(url, { cache: $templateCache }) + .then(function(response) { return response.data; }); + }; + + /** + * @ngdoc function + * @name ui.router.util.$templateFactory#fromProvider + * @methodOf ui.router.util.$templateFactory + * + * @description + * Creates a template by invoking an injectable provider function. + * + * @param {Function} provider Function to invoke via `$injector.invoke` + * @param {Object} params Parameters for the template. + * @param {Object} locals Locals to pass to `invoke`. Defaults to + * `{ params: params }`. + * @return {string|Promise.} The template html as a string, or a promise + * for that string. + */ + this.fromProvider = function (provider, params, locals) { + return $injector.invoke(provider, null, locals || { params: params }); + }; + } + + angular.module("ui.router.util").service("$templateFactory", $TemplateFactory); + + /** + * @ngdoc object + * @name ui.router.util.type:UrlMatcher + * + * @description + * Matches URLs against patterns and extracts named parameters from the path or the search + * part of the URL. A URL pattern consists of a path pattern, optionally followed by "?" and a list + * of search parameters. Multiple search parameter names are separated by "&". Search parameters + * do not influence whether or not a URL is matched, but their values are passed through into + * the matched parameters returned by {@link ui.router.util.type:UrlMatcher#methods_exec exec}. + * + * Path parameter placeholders can be specified using simple colon/catch-all syntax or curly brace + * syntax, which optionally allows a regular expression for the parameter to be specified: + * + * * `":"` name - colon placeholder + * * `"*"` name - catch-all placeholder + * * `"{" name "}"` - curly placeholder + * * `"{" name ":" regexp "}"` - curly placeholder with regexp. Should the regexp itself contain + * curly braces, they must be in matched pairs or escaped with a backslash. + * + * Parameter names may contain only word characters (latin letters, digits, and underscore) and + * must be unique within the pattern (across both path and search parameters). For colon + * placeholders or curly placeholders without an explicit regexp, a path parameter matches any + * number of characters other than "/". For catch-all placeholders the path parameter matches + * any number of characters. + * + * Examples: + * + * * `"/hello/"` - Matches only if the path is exactly "/hello/". There is no special treatment for + * trailing slashes, and patterns have to match the entire path, not just a prefix. + * * `"/user/:id"` - Matches "/user/bob" or "/user/1234!!!" or even "/user/" but not "/user" or + * "/user/bob/details". The second path segment will be captured as the parameter "id". + * * `"/user/{id}"` - Same as the previous example, but using curly brace syntax. + * * `"/user/{id:[^/]*}"` - Same as the previous example. + * * `"/user/{id:[0-9a-fA-F]{1,8}}"` - Similar to the previous example, but only matches if the id + * parameter consists of 1 to 8 hex digits. + * * `"/files/{path:.*}"` - Matches any URL starting with "/files/" and captures the rest of the + * path into the parameter "path". + * * `"/files/*path"` - ditto. + * + * @param {string} pattern The pattern to compile into a matcher. + * @param {Object} config A configuration object hash: + * + * * `caseInsensitive` - `true` if URL matching should be case insensitive, otherwise `false`, the default value (for backward compatibility) is `false`. + * * `strict` - `false` if matching against a URL with a trailing slash should be treated as equivalent to a URL without a trailing slash, the default value is `true`. + * + * @property {string} prefix A static prefix of this pattern. The matcher guarantees that any + * URL matching this matcher (i.e. any string for which {@link ui.router.util.type:UrlMatcher#methods_exec exec()} returns + * non-null) will start with this prefix. + * + * @property {string} source The pattern that was passed into the constructor + * + * @property {string} sourcePath The path portion of the source property + * + * @property {string} sourceSearch The search portion of the source property + * + * @property {string} regex The constructed regex that will be used to match against the url when + * it is time to determine which url will match. + * + * @returns {Object} New `UrlMatcher` object + */ + function UrlMatcher(pattern, config) { + config = angular.isObject(config) ? config : {}; + + // Find all placeholders and create a compiled pattern, using either classic or curly syntax: + // "*" name + // ":" name + // "{" name "}" + // "{" name ":" regexp "}" + // The regular expression is somewhat complicated due to the need to allow curly braces + // inside the regular expression. The placeholder regexp breaks down as follows: + // ([:*])(\w+) classic placeholder ($1 / $2) + // \{(\w+)(?:\:( ... ))?\} curly brace placeholder ($3) with optional regexp ... ($4) + // (?: ... | ... | ... )+ the regexp consists of any number of atoms, an atom being either + // [^{}\\]+ - anything other than curly braces or backslash + // \\. - a backslash escape + // \{(?:[^{}\\]+|\\.)*\} - a matched set of curly braces containing other atoms + var placeholder = /([:*])(\w+)|\{(\w+)(?:\:((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g, + compiled = "^", last = 0, m, + segments = this.segments = [], + params = this.params = {}; + + /** + * [Internal] Gets the decoded representation of a value if the value is defined, otherwise, returns the + * default value, which may be the result of an injectable function. + */ + function $value(value) { + /*jshint validthis: true */ + return isDefined(value) ? this.type.decode(value) : $UrlMatcherFactory.$$getDefaultValue(this); + } + + function addParameter(id, type, config) { + if (!/^\w+(-+\w+)*$/.test(id)) throw new Error("Invalid parameter name '" + id + "' in pattern '" + pattern + "'"); + if (params[id]) throw new Error("Duplicate parameter name '" + id + "' in pattern '" + pattern + "'"); + params[id] = extend({ type: type || new Type(), $value: $value }, config); + } + + function quoteRegExp(string, pattern, isOptional) { + var result = string.replace(/[\\\[\]\^$*+?.()|{}]/g, "\\$&"); + if (!pattern) return result; + var flag = isOptional ? "?" : ""; + return result + flag + "(" + pattern + ")" + flag; + } + + function paramConfig(param) { + if (!config.params || !config.params[param]) return {}; + var cfg = config.params[param]; + return isObject(cfg) ? cfg : { value: cfg }; + } + + this.source = pattern; + + // Split into static segments separated by path parameter placeholders. + // The number of segments is always 1 more than the number of parameters. + var id, regexp, segment, type, cfg; + + while ((m = placeholder.exec(pattern))) { + id = m[2] || m[3]; // IE[78] returns "" for unmatched groups instead of null + regexp = m[4] || (m[1] == "*" ? ".*" : "[^/]*"); + segment = pattern.substring(last, m.index); + type = this.$types[regexp] || new Type({ pattern: new RegExp(regexp) }); + cfg = paramConfig(id); + + if (segment.indexOf("?") >= 0) break; // we"re into the search part + + compiled += quoteRegExp(segment, type.$subPattern(), isDefined(cfg.value)); + addParameter(id, type, cfg); + segments.push(segment); + last = placeholder.lastIndex; + } + segment = pattern.substring(last); + + // Find any search parameter names and remove them from the last segment + var i = segment.indexOf("?"); + + if (i >= 0) { + var search = this.sourceSearch = segment.substring(i); + segment = segment.substring(0, i); + this.sourcePath = pattern.substring(0, last + i); + + // Allow parameters to be separated by "?" as well as "&" to make concat() easier + forEach(search.substring(1).split(/[&?]/), function(key) { + addParameter(key, null, paramConfig(key)); + }); + } else { + this.sourcePath = pattern; + this.sourceSearch = ""; + } + + compiled += quoteRegExp(segment) + (config.strict === false ? "\/?" : "") + "$"; + segments.push(segment); + + this.regexp = new RegExp(compiled, config.caseInsensitive ? "i" : undefined); + this.prefix = segments[0]; + } + + /** + * @ngdoc function + * @name ui.router.util.type:UrlMatcher#concat + * @methodOf ui.router.util.type:UrlMatcher + * + * @description + * Returns a new matcher for a pattern constructed by appending the path part and adding the + * search parameters of the specified pattern to this pattern. The current pattern is not + * modified. This can be understood as creating a pattern for URLs that are relative to (or + * suffixes of) the current pattern. + * + * @example + * The following two matchers are equivalent: + *
+   * new UrlMatcher("/user/{id}?q").concat("/details?date");
+   * new UrlMatcher("/user/{id}/details?q&date");
+   * 
+ * + * @param {string} pattern The pattern to append. + * @param {Object} config An object hash of the configuration for the matcher. + * @returns {UrlMatcher} A matcher for the concatenated pattern. + */ + UrlMatcher.prototype.concat = function (pattern, config) { + // Because order of search parameters is irrelevant, we can add our own search + // parameters to the end of the new pattern. Parse the new pattern by itself + // and then join the bits together, but it"s much easier to do this on a string level. + return new UrlMatcher(this.sourcePath + pattern + this.sourceSearch, config); + }; + + UrlMatcher.prototype.toString = function () { + return this.source; + }; + + /** + * @ngdoc function + * @name ui.router.util.type:UrlMatcher#exec + * @methodOf ui.router.util.type:UrlMatcher + * + * @description + * Tests the specified path against this matcher, and returns an object containing the captured + * parameter values, or null if the path does not match. The returned object contains the values + * of any search parameters that are mentioned in the pattern, but their value may be null if + * they are not present in `searchParams`. This means that search parameters are always treated + * as optional. + * + * @example + *
+   * new UrlMatcher("/user/{id}?q&r").exec("/user/bob", {
+   *   x: "1", q: "hello"
+   * });
+   * // returns { id: "bob", q: "hello", r: null }
+   * 
+ * + * @param {string} path The URL path to match, e.g. `$location.path()`. + * @param {Object} searchParams URL search parameters, e.g. `$location.search()`. + * @returns {Object} The captured parameter values. + */ + UrlMatcher.prototype.exec = function (path, searchParams) { + var m = this.regexp.exec(path); + if (!m) return null; + searchParams = searchParams || {}; + + var params = this.parameters(), nTotal = params.length, + nPath = this.segments.length - 1, + values = {}, i, cfg, param; + + if (nPath !== m.length - 1) throw new Error("Unbalanced capture group in route '" + this.source + "'"); + + for (i = 0; i < nPath; i++) { + param = params[i]; + cfg = this.params[param]; + values[param] = cfg.$value(m[i + 1]); + } + for (/**/; i < nTotal; i++) { + param = params[i]; + cfg = this.params[param]; + values[param] = cfg.$value(searchParams[param]); + } + + return values; + }; + + /** + * @ngdoc function + * @name ui.router.util.type:UrlMatcher#parameters + * @methodOf ui.router.util.type:UrlMatcher + * + * @description + * Returns the names of all path and search parameters of this pattern in an unspecified order. + * + * @returns {Array.} An array of parameter names. Must be treated as read-only. If the + * pattern has no parameters, an empty array is returned. + */ + UrlMatcher.prototype.parameters = function (param) { + if (!isDefined(param)) return objectKeys(this.params); + return this.params[param] || null; + }; + + /** + * @ngdoc function + * @name ui.router.util.type:UrlMatcher#validate + * @methodOf ui.router.util.type:UrlMatcher + * + * @description + * Checks an object hash of parameters to validate their correctness according to the parameter + * types of this `UrlMatcher`. + * + * @param {Object} params The object hash of parameters to validate. + * @returns {boolean} Returns `true` if `params` validates, otherwise `false`. + */ + UrlMatcher.prototype.validates = function (params) { + var result = true, isOptional, cfg, self = this; + + forEach(params, function(val, key) { + if (!self.params[key]) return; + cfg = self.params[key]; + isOptional = !val && isDefined(cfg.value); + result = result && (isOptional || cfg.type.is(val)); + }); + return result; + }; + + /** + * @ngdoc function + * @name ui.router.util.type:UrlMatcher#format + * @methodOf ui.router.util.type:UrlMatcher + * + * @description + * Creates a URL that matches this pattern by substituting the specified values + * for the path and search parameters. Null values for path parameters are + * treated as empty strings. + * + * @example + *
+   * new UrlMatcher("/user/{id}?q").format({ id:"bob", q:"yes" });
+   * // returns "/user/bob?q=yes"
+   * 
+ * + * @param {Object} values the values to substitute for the parameters in this pattern. + * @returns {string} the formatted URL (path and optionally search part). + */ + UrlMatcher.prototype.format = function (values) { + var segments = this.segments, params = this.parameters(); + + if (!values) return segments.join("").replace("//", "/"); + + var nPath = segments.length - 1, nTotal = params.length, + result = segments[0], i, search, value, param, cfg, array; + + if (!this.validates(values)) return null; + + for (i = 0; i < nPath; i++) { + param = params[i]; + value = values[param]; + cfg = this.params[param]; + + if (!isDefined(value) && (segments[i] === "/" || segments[i + 1] === "/")) continue; + if (value != null) result += encodeURIComponent(cfg.type.encode(value)); + result += segments[i + 1]; + } + + for (/**/; i < nTotal; i++) { + param = params[i]; + value = values[param]; + if (value == null) continue; + array = isArray(value); + + if (array) { + value = value.map(encodeURIComponent).join("&" + param + "="); + } + result += (search ? "&" : "?") + param + "=" + (array ? value : encodeURIComponent(value)); + search = true; + } + return result; + }; + + UrlMatcher.prototype.$types = {}; + + /** + * @ngdoc object + * @name ui.router.util.type:Type + * + * @description + * Implements an interface to define custom parameter types that can be decoded from and encoded to + * string parameters matched in a URL. Used by {@link ui.router.util.type:UrlMatcher `UrlMatcher`} + * objects when matching or formatting URLs, or comparing or validating parameter values. + * + * See {@link ui.router.util.$urlMatcherFactory#methods_type `$urlMatcherFactory#type()`} for more + * information on registering custom types. + * + * @param {Object} config A configuration object hash that includes any method in `Type`"s public + * interface, and/or `pattern`, which should contain a custom regular expression used to match + * string parameters originating from a URL. + * + * @property {RegExp} pattern The regular expression pattern used to match values of this type when + * coming from a substring of a URL. + * + * @returns {Object} Returns a new `Type` object. + */ + function Type(config) { + extend(this, config); + } + + /** + * @ngdoc function + * @name ui.router.util.type:Type#is + * @methodOf ui.router.util.type:Type + * + * @description + * Detects whether a value is of a particular type. Accepts a native (decoded) value + * and determines whether it matches the current `Type` object. + * + * @param {*} val The value to check. + * @param {string} key Optional. If the type check is happening in the context of a specific + * {@link ui.router.util.type:UrlMatcher `UrlMatcher`} object, this is the name of the + * parameter in which `val` is stored. Can be used for meta-programming of `Type` objects. + * @returns {Boolean} Returns `true` if the value matches the type, otherwise `false`. + */ + Type.prototype.is = function(val, key) { + return true; + }; + + /** + * @ngdoc function + * @name ui.router.util.type:Type#encode + * @methodOf ui.router.util.type:Type + * + * @description + * Encodes a custom/native type value to a string that can be embedded in a URL. Note that the + * return value does *not* need to be URL-safe (i.e. passed through `encodeURIComponent()`), it + * only needs to be a representation of `val` that has been coerced to a string. + * + * @param {*} val The value to encode. + * @param {string} key The name of the parameter in which `val` is stored. Can be used for + * meta-programming of `Type` objects. + * @returns {string} Returns a string representation of `val` that can be encoded in a URL. + */ + Type.prototype.encode = function(val, key) { + return val; + }; + + /** + * @ngdoc function + * @name ui.router.util.type:Type#decode + * @methodOf ui.router.util.type:Type + * + * @description + * Converts a string URL parameter value to a custom/native value. + * + * @param {string} val The URL parameter value to decode. + * @param {string} key The name of the parameter in which `val` is stored. Can be used for + * meta-programming of `Type` objects. + * @returns {*} Returns a custom representation of the URL parameter value. + */ + Type.prototype.decode = function(val, key) { + return val; + }; + + /** + * @ngdoc function + * @name ui.router.util.type:Type#equals + * @methodOf ui.router.util.type:Type + * + * @description + * Determines whether two decoded values are equivalent. + * + * @param {*} a A value to compare against. + * @param {*} b A value to compare against. + * @returns {Boolean} Returns `true` if the values are equivalent/equal, otherwise `false`. + */ + Type.prototype.equals = function(a, b) { + return a == b; + }; + + Type.prototype.$subPattern = function() { + var sub = this.pattern.toString(); + return sub.substr(1, sub.length - 2); + }; + + Type.prototype.pattern = /.*/; + + /** + * @ngdoc object + * @name ui.router.util.$urlMatcherFactory + * + * @description + * Factory for {@link ui.router.util.type:UrlMatcher `UrlMatcher`} instances. The factory + * is also available to providers under the name `$urlMatcherFactoryProvider`. + */ + function $UrlMatcherFactory() { + + var isCaseInsensitive = false, isStrictMode = true; + + var enqueue = true, typeQueue = [], injector, defaultTypes = { + int: { + decode: function(val) { + return parseInt(val, 10); + }, + is: function(val) { + if (!isDefined(val)) return false; + return this.decode(val.toString()) === val; + }, + pattern: /\d+/ + }, + bool: { + encode: function(val) { + return val ? 1 : 0; + }, + decode: function(val) { + return parseInt(val, 10) === 0 ? false : true; + }, + is: function(val) { + return val === true || val === false; + }, + pattern: /0|1/ + }, + string: { + pattern: /[^\/]*/ + }, + date: { + equals: function (a, b) { + return a.toISOString() === b.toISOString(); + }, + decode: function (val) { + return new Date(val); + }, + encode: function (val) { + return [ + val.getFullYear(), + ("0" + (val.getMonth() + 1)).slice(-2), + ("0" + val.getDate()).slice(-2) + ].join("-"); + }, + pattern: /[0-9]{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[1-2][0-9]|3[0-1])/ + } + }; + + function getDefaultConfig() { + return { + strict: isStrictMode, + caseInsensitive: isCaseInsensitive + }; + } + + function isInjectable(value) { + return (isFunction(value) || (isArray(value) && isFunction(value[value.length - 1]))); + } + + /** + * [Internal] Get the default value of a parameter, which may be an injectable function. + */ + $UrlMatcherFactory.$$getDefaultValue = function(config) { + if (!isInjectable(config.value)) return config.value; + if (!injector) throw new Error("Injectable functions cannot be called at configuration time"); + return injector.invoke(config.value); + }; + + /** + * @ngdoc function + * @name ui.router.util.$urlMatcherFactory#caseInsensitive + * @methodOf ui.router.util.$urlMatcherFactory + * + * @description + * Defines whether URL matching should be case sensitive (the default behavior), or not. + * + * @param {boolean} value `false` to match URL in a case sensitive manner; otherwise `true`; + */ + this.caseInsensitive = function(value) { + isCaseInsensitive = value; + }; + + /** + * @ngdoc function + * @name ui.router.util.$urlMatcherFactory#strictMode + * @methodOf ui.router.util.$urlMatcherFactory + * + * @description + * Defines whether URLs should match trailing slashes, or not (the default behavior). + * + * @param {boolean} value `false` to match trailing slashes in URLs, otherwise `true`. + */ + this.strictMode = function(value) { + isStrictMode = value; + }; + + /** + * @ngdoc function + * @name ui.router.util.$urlMatcherFactory#compile + * @methodOf ui.router.util.$urlMatcherFactory + * + * @description + * Creates a {@link ui.router.util.type:UrlMatcher `UrlMatcher`} for the specified pattern. + * + * @param {string} pattern The URL pattern. + * @param {Object} config The config object hash. + * @returns {UrlMatcher} The UrlMatcher. + */ + this.compile = function (pattern, config) { + return new UrlMatcher(pattern, extend(getDefaultConfig(), config)); + }; + + /** + * @ngdoc function + * @name ui.router.util.$urlMatcherFactory#isMatcher + * @methodOf ui.router.util.$urlMatcherFactory + * + * @description + * Returns true if the specified object is a `UrlMatcher`, or false otherwise. + * + * @param {Object} object The object to perform the type check against. + * @returns {Boolean} Returns `true` if the object matches the `UrlMatcher` interface, by + * implementing all the same methods. + */ + this.isMatcher = function (o) { + if (!isObject(o)) return false; + var result = true; + + forEach(UrlMatcher.prototype, function(val, name) { + if (isFunction(val)) { + result = result && (isDefined(o[name]) && isFunction(o[name])); + } + }); + return result; + }; + + /** + * @ngdoc function + * @name ui.router.util.$urlMatcherFactory#type + * @methodOf ui.router.util.$urlMatcherFactory + * + * @description + * Registers a custom {@link ui.router.util.type:Type `Type`} object that can be used to + * generate URLs with typed parameters. + * + * @param {string} name The type name. + * @param {Object|Function} def The type definition. See + * {@link ui.router.util.type:Type `Type`} for information on the values accepted. + * + * @returns {Object} Returns `$urlMatcherFactoryProvider`. + * + * @example + * This is a simple example of a custom type that encodes and decodes items from an + * array, using the array index as the URL-encoded value: + * + *
+     * var list = ["John", "Paul", "George", "Ringo"];
+     *
+     * $urlMatcherFactoryProvider.type("listItem", {
+     *   encode: function(item) {
+     *     // Represent the list item in the URL using its corresponding index
+     *     return list.indexOf(item);
+     *   },
+     *   decode: function(item) {
+     *     // Look up the list item by index
+     *     return list[parseInt(item, 10)];
+     *   },
+     *   is: function(item) {
+     *     // Ensure the item is valid by checking to see that it appears
+     *     // in the list
+     *     return list.indexOf(item) > -1;
+     *   }
+     * });
+     *
+     * $stateProvider.state("list", {
+     *   url: "/list/{item:listItem}",
+     *   controller: function($scope, $stateParams) {
+     *     console.log($stateParams.item);
+     *   }
+     * });
+     *
+     * // ...
+     *
+     * // Changes URL to "/list/3", logs "Ringo" to the console
+     * $state.go("list", { item: "Ringo" });
+     * 
+ * + * This is a more complex example of a type that relies on dependency injection to + * interact with services, and uses the parameter name from the URL to infer how to + * handle encoding and decoding parameter values: + * + *
+     * // Defines a custom type that gets a value from a service,
+     * // where each service gets different types of values from
+     * // a backend API:
+     * $urlMatcherFactoryProvider.type("dbObject", function(Users, Posts) {
+     *
+     *   // Matches up services to URL parameter names
+     *   var services = {
+     *     user: Users,
+     *     post: Posts
+     *   };
+     *
+     *   return {
+     *     encode: function(object) {
+     *       // Represent the object in the URL using its unique ID
+     *       return object.id;
+     *     },
+     *     decode: function(value, key) {
+     *       // Look up the object by ID, using the parameter
+     *       // name (key) to call the correct service
+     *       return services[key].findById(value);
+     *     },
+     *     is: function(object, key) {
+     *       // Check that object is a valid dbObject
+     *       return angular.isObject(object) && object.id && services[key];
+     *     }
+     *     equals: function(a, b) {
+     *       // Check the equality of decoded objects by comparing
+     *       // their unique IDs
+     *       return a.id === b.id;
+     *     }
+     *   };
+     * });
+     *
+     * // In a config() block, you can then attach URLs with
+     * // type-annotated parameters:
+     * $stateProvider.state("users", {
+     *   url: "/users",
+     *   // ...
+     * }).state("users.item", {
+     *   url: "/{user:dbObject}",
+     *   controller: function($scope, $stateParams) {
+     *     // $stateParams.user will now be an object returned from
+     *     // the Users service
+     *   },
+     *   // ...
+     * });
+     * 
+ */ + this.type = function (name, def) { + if (!isDefined(def)) return UrlMatcher.prototype.$types[name]; + typeQueue.push({ name: name, def: def }); + if (!enqueue) flushTypeQueue(); + return this; + }; + + /* No need to document $get, since it returns this */ + this.$get = ["$injector", function ($injector) { + injector = $injector; + enqueue = false; + UrlMatcher.prototype.$types = {}; + flushTypeQueue(); + + forEach(defaultTypes, function(type, name) { + if (!UrlMatcher.prototype.$types[name]) UrlMatcher.prototype.$types[name] = new Type(type); + }); + return this; + }]; + + // To ensure proper order of operations in object configuration, and to allow internal + // types to be overridden, `flushTypeQueue()` waits until `$urlMatcherFactory` is injected + // before actually wiring up and assigning type definitions + function flushTypeQueue() { + forEach(typeQueue, function(type) { + if (UrlMatcher.prototype.$types[type.name]) { + throw new Error("A type named '" + type.name + "' has already been defined."); + } + var def = new Type(isInjectable(type.def) ? injector.invoke(type.def) : type.def); + UrlMatcher.prototype.$types[type.name] = def; + }); + } + } + + // Register as a provider so it"s available to other providers + angular.module("ui.router.util").provider("$urlMatcherFactory", $UrlMatcherFactory); + + /** + * @ngdoc object + * @name ui.router.router.$urlRouterProvider + * + * @requires ui.router.util.$urlMatcherFactoryProvider + * @requires $locationProvider + * + * @description + * `$urlRouterProvider` has the responsibility of watching `$location`. + * When `$location` changes it runs through a list of rules one by one until a + * match is found. `$urlRouterProvider` is used behind the scenes anytime you specify + * a url in a state configuration. All urls are compiled into a UrlMatcher object. + * + * There are several methods on `$urlRouterProvider` that make it useful to use directly + * in your module config. + */ + $UrlRouterProvider.$inject = ["$locationProvider", "$urlMatcherFactoryProvider"]; + function $UrlRouterProvider( $locationProvider, $urlMatcherFactory) { + var rules = [], otherwise = null, interceptDeferred = false, listener; + + // Returns a string that is a prefix of all strings matching the RegExp + function regExpPrefix(re) { + var prefix = /^\^((?:\\[^a-zA-Z0-9]|[^\\\[\]\^$*+?.()|{}]+)*)/.exec(re.source); + return (prefix != null) ? prefix[1].replace(/\\(.)/g, "$1") : ""; + } + + // Interpolates matched values into a String.replace()-style pattern + function interpolate(pattern, match) { + return pattern.replace(/\$(\$|\d{1,2})/, function (m, what) { + return match[what === "$" ? 0 : Number(what)]; + }); + } + + /** + * @ngdoc function + * @name ui.router.router.$urlRouterProvider#rule + * @methodOf ui.router.router.$urlRouterProvider + * + * @description + * Defines rules that are used by `$urlRouterProvider` to find matches for + * specific URLs. + * + * @example + *
+     * var app = angular.module("app", ["ui.router.router"]);
+     *
+     * app.config(function ($urlRouterProvider) {
+     *   // Here"s an example of how you might allow case insensitive urls
+     *   $urlRouterProvider.rule(function ($injector, $location) {
+     *     var path = $location.path(),
+     *         normalized = path.toLowerCase();
+     *
+     *     if (path !== normalized) {
+     *       return normalized;
+     *     }
+     *   });
+     * });
+     * 
+ * + * @param {object} rule Handler function that takes `$injector` and `$location` + * services as arguments. You can use them to return a valid path as a string. + * + * @return {object} `$urlRouterProvider` - `$urlRouterProvider` instance + */ + this.rule = function (rule) { + if (!isFunction(rule)) throw new Error("'rule' must be a function"); + rules.push(rule); + return this; + }; + + /** + * @ngdoc object + * @name ui.router.router.$urlRouterProvider#otherwise + * @methodOf ui.router.router.$urlRouterProvider + * + * @description + * Defines a path that is used when an invalid route is requested. + * + * @example + *
+     * var app = angular.module("app", ["ui.router.router"]);
+     *
+     * app.config(function ($urlRouterProvider) {
+     *   // if the path doesn"t match any of the urls you configured
+     *   // otherwise will take care of routing the user to the
+     *   // specified url
+     *   $urlRouterProvider.otherwise("/index");
+     *
+     *   // Example of using function rule as param
+     *   $urlRouterProvider.otherwise(function ($injector, $location) {
+     *     return "/a/valid/url";
+     *   });
+     * });
+     * 
+ * + * @param {string|object} rule The url path you want to redirect to or a function + * rule that returns the url path. The function version is passed two params: + * `$injector` and `$location` services, and must return a url string. + * + * @return {object} `$urlRouterProvider` - `$urlRouterProvider` instance + */ + this.otherwise = function (rule) { + if (isString(rule)) { + var redirect = rule; + rule = function () { return redirect; }; + } + else if (!isFunction(rule)) throw new Error("'rule' must be a function"); + otherwise = rule; + return this; + }; + + + function handleIfMatch($injector, handler, match) { + if (!match) return false; + var result = $injector.invoke(handler, handler, { $match: match }); + return isDefined(result) ? result : true; + } + + /** + * @ngdoc function + * @name ui.router.router.$urlRouterProvider#when + * @methodOf ui.router.router.$urlRouterProvider + * + * @description + * Registers a handler for a given url matching. if handle is a string, it is + * treated as a redirect, and is interpolated according to the syntax of match + * (i.e. like `String.replace()` for `RegExp`, or like a `UrlMatcher` pattern otherwise). + * + * If the handler is a function, it is injectable. It gets invoked if `$location` + * matches. You have the option of inject the match object as `$match`. + * + * The handler can return + * + * - **falsy** to indicate that the rule didn"t match after all, then `$urlRouter` + * will continue trying to find another one that matches. + * - **string** which is treated as a redirect and passed to `$location.url()` + * - **void** or any **truthy** value tells `$urlRouter` that the url was handled. + * + * @example + *
+     * var app = angular.module("app", ["ui.router.router"]);
+     *
+     * app.config(function ($urlRouterProvider) {
+     *   $urlRouterProvider.when($state.url, function ($match, $stateParams) {
+     *     if ($state.$current.navigable !== state ||
+     *         !equalForKeys($match, $stateParams) {
+     *      $state.transitionTo(state, $match, false);
+     *     }
+     *   });
+     * });
+     * 
+ * + * @param {string|object} what The incoming path that you want to redirect. + * @param {string|object} handler The path you want to redirect your user to. + */ + this.when = function (what, handler) { + var redirect, handlerIsString = isString(handler); + if (isString(what)) what = $urlMatcherFactory.compile(what); + + if (!handlerIsString && !isFunction(handler) && !isArray(handler)) + throw new Error("invalid 'handler' in when()"); + + var strategies = { + matcher: function (what, handler) { + if (handlerIsString) { + redirect = $urlMatcherFactory.compile(handler); + handler = ["$match", function ($match) { return redirect.format($match); }]; + } + return extend(function ($injector, $location) { + return handleIfMatch($injector, handler, what.exec($location.path(), $location.search())); + }, { + prefix: isString(what.prefix) ? what.prefix : "" + }); + }, + regex: function (what, handler) { + if (what.global || what.sticky) throw new Error("when() RegExp must not be global or sticky"); + + if (handlerIsString) { + redirect = handler; + handler = ["$match", function ($match) { return interpolate(redirect, $match); }]; + } + return extend(function ($injector, $location) { + return handleIfMatch($injector, handler, what.exec($location.path())); + }, { + prefix: regExpPrefix(what) + }); + } + }; + + var check = { matcher: $urlMatcherFactory.isMatcher(what), regex: what instanceof RegExp }; + + for (var n in check) { + if (check[n]) return this.rule(strategies[n](what, handler)); + } + + throw new Error("invalid 'what' in when()"); + }; + + /** + * @ngdoc function + * @name ui.router.router.$urlRouterProvider#deferIntercept + * @methodOf ui.router.router.$urlRouterProvider + * + * @description + * Disables (or enables) deferring location change interception. + * + * If you wish to customize the behavior of syncing the URL (for example, if you wish to + * defer a transition but maintain the current URL), call this method at configuration time. + * Then, at run time, call `$urlRouter.listen()` after you have configured your own + * `$locationChangeSuccess` event handler. + * + * @example + *
+     * var app = angular.module("app", ["ui.router.router"]);
+     *
+     * app.config(function ($urlRouterProvider) {
+     *
+     *   // Prevent $urlRouter from automatically intercepting URL changes;
+     *   // this allows you to configure custom behavior in between
+     *   // location changes and route synchronization:
+     *   $urlRouterProvider.deferIntercept();
+     *
+     * }).run(function ($rootScope, $urlRouter, UserService) {
+     *
+     *   $rootScope.$on("$locationChangeSuccess", function(e) {
+     *     // UserService is an example service for managing user state
+     *     if (UserService.isLoggedIn()) return;
+     *
+     *     // Prevent $urlRouter"s default handler from firing
+     *     e.preventDefault();
+     *
+     *     UserService.handleLogin().then(function() {
+     *       // Once the user has logged in, sync the current URL
+     *       // to the router:
+     *       $urlRouter.sync();
+     *     });
+     *   });
+     *
+     *   // Configures $urlRouter"s listener *after* your custom listener
+     *   $urlRouter.listen();
+     * });
+     * 
+ * + * @param {boolean} defer Indicates whether to defer location change interception. Passing + no parameter is equivalent to `true`. + */ + this.deferIntercept = function (defer) { + if (defer === undefined) defer = true; + interceptDeferred = defer; + }; + + /** + * @ngdoc object + * @name ui.router.router.$urlRouter + * + * @requires $location + * @requires $rootScope + * @requires $injector + * @requires $browser + * + * @description + * + */ + this.$get = $get; + $get.$inject = ["$location", "$rootScope", "$injector", "$browser"]; + function $get( $location, $rootScope, $injector, $browser) { + + var baseHref = $browser.baseHref(), location = $location.url(); + + function appendBasePath(url, isHtml5, absolute) { + if (baseHref === "/") return url; + if (isHtml5) return baseHref.slice(0, -1) + url; + if (absolute) return baseHref.slice(1) + url; + return url; + } + + // TODO: Optimize groups of rules with non-empty prefix into some sort of decision tree + function update(evt) { + if (evt && evt.defaultPrevented) return; + + function check(rule) { + var handled = rule($injector, $location); + + if (!handled) return false; + if (isString(handled)) $location.replace().url(handled); + return true; + } + var n = rules.length, i; + + for (i = 0; i < n; i++) { + if (check(rules[i])) return; + } + // always check otherwise last to allow dynamic updates to the set of rules + if (otherwise) check(otherwise); + } + + function listen() { + listener = listener || $rootScope.$on("$locationChangeSuccess", update); + return listener; + } + + if (!interceptDeferred) listen(); + + return { + /** + * @ngdoc function + * @name ui.router.router.$urlRouter#sync + * @methodOf ui.router.router.$urlRouter + * + * @description + * Triggers an update; the same update that happens when the address bar url changes, aka `$locationChangeSuccess`. + * This method is useful when you need to use `preventDefault()` on the `$locationChangeSuccess` event, + * perform some custom logic (route protection, auth, config, redirection, etc) and then finally proceed + * with the transition by calling `$urlRouter.sync()`. + * + * @example + *
+         * angular.module("app", ["ui.router"])
+         *   .run(function($rootScope, $urlRouter) {
+         *     $rootScope.$on("$locationChangeSuccess", function(evt) {
+         *       // Halt state change from even starting
+         *       evt.preventDefault();
+         *       // Perform custom logic
+         *       var meetsRequirement = ...
+         *       // Continue with the update and state transition if logic allows
+         *       if (meetsRequirement) $urlRouter.sync();
+         *     });
+         * });
+         * 
+ */ + sync: function() { + update(); + }, + + listen: function() { + return listen(); + }, + + update: function(read) { + if (read) { + location = $location.url(); + return; + } + if ($location.url() === location) return; + + $location.url(location); + $location.replace(); + }, + + push: function(urlMatcher, params, options) { + $location.url(urlMatcher.format(params || {})); + if (options && options.replace) $location.replace(); + }, + + /** + * @ngdoc function + * @name ui.router.router.$urlRouter#href + * @methodOf ui.router.router.$urlRouter + * + * @description + * A URL generation method that returns the compiled URL for a given + * {@link ui.router.util.type:UrlMatcher `UrlMatcher`}, populated with the provided parameters. + * + * @example + *
+         * $bob = $urlRouter.href(new UrlMatcher("/about/:person"), {
+         *   person: "bob"
+         * });
+         * // $bob == "/about/bob";
+         * 
+ * + * @param {UrlMatcher} urlMatcher The `UrlMatcher` object which is used as the template of the URL to generate. + * @param {object=} params An object of parameter values to fill the matcher"s required parameters. + * @param {object=} options Options object. The options are: + * + * - **`absolute`** - {boolean=false}, If true will generate an absolute url, e.g. "http://www.example.com/fullurl". + * + * @returns {string} Returns the fully compiled URL, or `null` if `params` fail validation against `urlMatcher` + */ + href: function(urlMatcher, params, options) { + if (!urlMatcher.validates(params)) return null; + + var isHtml5 = $locationProvider.html5Mode(); + var url = urlMatcher.format(params); + options = options || {}; + + if (!isHtml5 && url !== null) { + url = "#" + $locationProvider.hashPrefix() + url; + } + url = appendBasePath(url, isHtml5, options.absolute); + + if (!options.absolute || !url) { + return url; + } + + var slash = (!isHtml5 && url ? "/" : ""), port = $location.port(); + port = (port === 80 || port === 443 ? "" : ":" + port); + + return [$location.protocol(), "://", $location.host(), port, slash, url].join(""); + } + }; + } + } + + angular.module("ui.router.router").provider("$urlRouter", $UrlRouterProvider); + + /** + * @ngdoc object + * @name ui.router.state.$stateProvider + * + * @requires ui.router.router.$urlRouterProvider + * @requires ui.router.util.$urlMatcherFactoryProvider + * + * @description + * The new `$stateProvider` works similar to Angular"s v1 router, but it focuses purely + * on state. + * + * A state corresponds to a "place" in the application in terms of the overall UI and + * navigation. A state describes (via the controller / template / view properties) what + * the UI looks like and does at that place. + * + * States often have things in common, and the primary way of factoring out these + * commonalities in this model is via the state hierarchy, i.e. parent/child states aka + * nested states. + * + * The `$stateProvider` provides interfaces to declare these states for your app. + */ + $StateProvider.$inject = ["$urlRouterProvider", "$urlMatcherFactoryProvider"]; + function $StateProvider( $urlRouterProvider, $urlMatcherFactory) { + + var root, states = {}, $state, queue = {}, abstractKey = "abstract"; + + // Builds state properties from definition passed to registerState() + var stateBuilder = { + + // Derive parent state from a hierarchical name only if "parent" is not explicitly defined. + // state.children = []; + // if (parent) parent.children.push(state); + parent: function(state) { + if (isDefined(state.parent) && state.parent) return findState(state.parent); + // regex matches any valid composite state name + // would match "contact.list" but not "contacts" + var compositeName = /^(.+)\.[^.]+$/.exec(state.name); + return compositeName ? findState(compositeName[1]) : root; + }, + + // inherit "data" from parent and override by own values (if any) + data: function(state) { + if (state.parent && state.parent.data) { + state.data = state.self.data = extend({}, state.parent.data, state.data); + } + return state.data; + }, + + // Build a URLMatcher if necessary, either via a relative or absolute URL + url: function(state) { + var url = state.url, config = { params: state.params || {} }; + + if (isString(url)) { + if (url.charAt(0) == "^") return $urlMatcherFactory.compile(url.substring(1), config); + return (state.parent.navigable || root).url.concat(url, config); + } + + if (!url || $urlMatcherFactory.isMatcher(url)) return url; + throw new Error("Invalid url '" + url + "' in state '" + state + "'"); + }, + + // Keep track of the closest ancestor state that has a URL (i.e. is navigable) + navigable: function(state) { + return state.url ? state : (state.parent ? state.parent.navigable : null); + }, + + // Derive parameters for this state and ensure they"re a super-set of parent"s parameters + params: function(state) { + if (!state.params) { + return state.url ? state.url.params : state.parent.params; + } + return state.params; + }, + + // If there is no explicit multi-view configuration, make one up so we don"t have + // to handle both cases in the view directive later. Note that having an explicit + // "views" property will mean the default unnamed view properties are ignored. This + // is also a good time to resolve view names to absolute names, so everything is a + // straight lookup at link time. + views: function(state) { + var views = {}; + + forEach(isDefined(state.views) ? state.views : { "": state }, function (view, name) { + if (name.indexOf("@") < 0) name += "@" + state.parent.name; + views[name] = view; + }); + return views; + }, + + ownParams: function(state) { + state.params = state.params || {}; + + if (!state.parent) { + return objectKeys(state.params); + } + var paramNames = {}; forEach(state.params, function (v, k) { paramNames[k] = true; }); + + forEach(state.parent.params, function (v, k) { + if (!paramNames[k]) { + throw new Error("Missing required parameter '" + k + "' in state '" + state.name + "'"); + } + paramNames[k] = false; + }); + var ownParams = []; + + forEach(paramNames, function (own, p) { + if (own) ownParams.push(p); + }); + return ownParams; + }, + + // Keep a full path from the root down to this state as this is needed for state activation. + path: function(state) { + return state.parent ? state.parent.path.concat(state) : []; // exclude root from path + }, + + // Speed up $state.contains() as it"s used a lot + includes: function(state) { + var includes = state.parent ? extend({}, state.parent.includes) : {}; + includes[state.name] = true; + return includes; + }, + + $delegates: {} + }; + + function isRelative(stateName) { + return stateName.indexOf(".") === 0 || stateName.indexOf("^") === 0; + } + + function findState(stateOrName, base) { + if (!stateOrName) return undefined; + + var isStr = isString(stateOrName), + name = isStr ? stateOrName : stateOrName.name, + path = isRelative(name); + + if (path) { + if (!base) throw new Error("No reference point given for path '" + name + "'"); + var rel = name.split("."), i = 0, pathLength = rel.length, current = base; + + for (; i < pathLength; i++) { + if (rel[i] === "" && i === 0) { + current = base; + continue; + } + if (rel[i] === "^") { + if (!current.parent) throw new Error("Path '" + name + "' not valid for state '" + base.name + "'"); + current = current.parent; + continue; + } + break; + } + rel = rel.slice(i).join("."); + name = current.name + (current.name && rel ? "." : "") + rel; + } + var state = states[name]; + + if (state && (isStr || (!isStr && (state === stateOrName || state.self === stateOrName)))) { + return state; + } + return undefined; + } + + function queueState(parentName, state) { + if (!queue[parentName]) { + queue[parentName] = []; + } + queue[parentName].push(state); + } + + function registerState(state) { + // Wrap a new object around the state so we can store our private details easily. + state = inherit(state, { + self: state, + resolve: state.resolve || {}, + toString: function() { return this.name; } + }); + + var name = state.name; + if (!isString(name) || name.indexOf("@") >= 0) throw new Error("State must have a valid name"); + if (states.hasOwnProperty(name)) throw new Error("State '" + name + "' is already defined"); + + // Get parent name + var parentName = (name.indexOf(".") !== -1) ? name.substring(0, name.lastIndexOf(".")) + : (isString(state.parent)) ? state.parent + : ""; + + // If parent is not registered yet, add state to queue and register later + if (parentName && !states[parentName]) { + return queueState(parentName, state.self); + } + + for (var key in stateBuilder) { + if (isFunction(stateBuilder[key])) state[key] = stateBuilder[key](state, stateBuilder.$delegates[key]); + } + states[name] = state; + + // Register the state in the global state list and with $urlRouter if necessary. + if (!state[abstractKey] && state.url) { + $urlRouterProvider.when(state.url, ["$match", "$stateParams", function ($match, $stateParams) { + if ($state.$current.navigable != state || !equalForKeys($match, $stateParams)) { + $state.transitionTo(state, $match, { location: false }); + } + }]); + } + + // Register any queued children + if (queue[name]) { + for (var i = 0; i < queue[name].length; i++) { + registerState(queue[name][i]); + } + } + + return state; + } + + // Checks text to see if it looks like a glob. + function isGlob (text) { + return text.indexOf("*") > -1; + } + + // Returns true if glob matches current $state name. + function doesStateMatchGlob (glob) { + var globSegments = glob.split("."), + segments = $state.$current.name.split("."); + + //match greedy starts + if (globSegments[0] === "**") { + segments = segments.slice(segments.indexOf(globSegments[1])); + segments.unshift("**"); + } + //match greedy ends + if (globSegments[globSegments.length - 1] === "**") { + segments.splice(segments.indexOf(globSegments[globSegments.length - 2]) + 1, Number.MAX_VALUE); + segments.push("**"); + } + + if (globSegments.length != segments.length) { + return false; + } + + //match single stars + for (var i = 0, l = globSegments.length; i < l; i++) { + if (globSegments[i] === "*") { + segments[i] = "*"; + } + } + + return segments.join("") === globSegments.join(""); + } + + + // Implicit root state that is always active + root = registerState({ + name: "", + url: "^", + views: null, + "abstract": true + }); + root.navigable = null; + + + /** + * @ngdoc function + * @name ui.router.state.$stateProvider#decorator + * @methodOf ui.router.state.$stateProvider + * + * @description + * Allows you to extend (carefully) or override (at your own peril) the + * `stateBuilder` object used internally by `$stateProvider`. This can be used + * to add custom functionality to ui-router, for example inferring templateUrl + * based on the state name. + * + * When passing only a name, it returns the current (original or decorated) builder + * function that matches `name`. + * + * The builder functions that can be decorated are listed below. Though not all + * necessarily have a good use case for decoration, that is up to you to decide. + * + * In addition, users can attach custom decorators, which will generate new + * properties within the state"s internal definition. There is currently no clear + * use-case for this beyond accessing internal states (i.e. $state.$current), + * however, expect this to become increasingly relevant as we introduce additional + * meta-programming features. + * + * **Warning**: Decorators should not be interdependent because the order of + * execution of the builder functions in non-deterministic. Builder functions + * should only be dependent on the state definition object and super function. + * + * + * Existing builder functions and current return values: + * + * - **parent** `{object}` - returns the parent state object. + * - **data** `{object}` - returns state data, including any inherited data that is not + * overridden by own values (if any). + * - **url** `{object}` - returns a {@link ui.router.util.type:UrlMatcher UrlMatcher} + * or `null`. + * - **navigable** `{object}` - returns closest ancestor state that has a URL (aka is + * navigable). + * - **params** `{object}` - returns an array of state params that are ensured to + * be a super-set of parent"s params. + * - **views** `{object}` - returns a views object where each key is an absolute view + * name (i.e. "viewName@stateName") and each value is the config object + * (template, controller) for the view. Even when you don"t use the views object + * explicitly on a state config, one is still created for you internally. + * So by decorating this builder function you have access to decorating template + * and controller properties. + * - **ownParams** `{object}` - returns an array of params that belong to the state, + * not including any params defined by ancestor states. + * - **path** `{string}` - returns the full path from the root down to this state. + * Needed for state activation. + * - **includes** `{object}` - returns an object that includes every state that + * would pass a `$state.includes()` test. + * + * @example + *
+     * // Override the internal "views" builder with a function that takes the state
+     * // definition, and a reference to the internal function being overridden:
+     * $stateProvider.decorator("views", function (state, parent) {
+     *   var result = {},
+     *       views = parent(state);
+     *
+     *   angular.forEach(views, function (config, name) {
+     *     var autoName = (state.name + "." + name).replace(".", "/");
+     *     config.templateUrl = config.templateUrl || "/partials/" + autoName + ".html";
+     *     result[name] = config;
+     *   });
+     *   return result;
+     * });
+     *
+     * $stateProvider.state("home", {
+     *   views: {
+     *     "contact.list": { controller: "ListController" },
+     *     "contact.item": { controller: "ItemController" }
+     *   }
+     * });
+     *
+     * // ...
+     *
+     * $state.go("home");
+     * // Auto-populates list and item views with /partials/home/contact/list.html,
+     * // and /partials/home/contact/item.html, respectively.
+     * 
+ * + * @param {string} name The name of the builder function to decorate. + * @param {object} func A function that is responsible for decorating the original + * builder function. The function receives two parameters: + * + * - `{object}` - state - The state config object. + * - `{object}` - super - The original builder function. + * + * @return {object} $stateProvider - $stateProvider instance + */ + this.decorator = decorator; + function decorator(name, func) { + /*jshint validthis: true */ + if (isString(name) && !isDefined(func)) { + return stateBuilder[name]; + } + if (!isFunction(func) || !isString(name)) { + return this; + } + if (stateBuilder[name] && !stateBuilder.$delegates[name]) { + stateBuilder.$delegates[name] = stateBuilder[name]; + } + stateBuilder[name] = func; + return this; + } + + /** + * @ngdoc function + * @name ui.router.state.$stateProvider#state + * @methodOf ui.router.state.$stateProvider + * + * @description + * Registers a state configuration under a given state name. The stateConfig object + * has the following acceptable properties. + * + * + * + * - **`template`** - {string|function=} - html template as a string or a function that returns + * an html template as a string which should be used by the uiView directives. This property + * takes precedence over templateUrl. + * + * If `template` is a function, it will be called with the following parameters: + * + * - {array.<object>} - state parameters extracted from the current $location.path() by + * applying the current state + * + * + * + * - **`templateUrl`** - {string|function=} - path or function that returns a path to an html + * template that should be used by uiView. + * + * If `templateUrl` is a function, it will be called with the following parameters: + * + * - {array.<object>} - state parameters extracted from the current $location.path() by + * applying the current state + * + * + * + * - **`templateProvider`** - {function=} - Provider function that returns HTML content + * string. + * + * + * + * - **`controller`** - {string|function=} - Controller fn that should be associated with newly + * related scope or the name of a registered controller if passed as a string. + * + * + * + * - **`controllerProvider`** - {function=} - Injectable provider function that returns + * the actual controller or string. + * + * + * + * - **`controllerAs`** – {string=} – A controller alias name. If present the controller will be + * published to scope under the controllerAs name. + * + * + * + * - **`resolve`** - {object.<string, function>=} - An optional map of dependencies which + * should be injected into the controller. If any of these dependencies are promises, + * the router will wait for them all to be resolved or one to be rejected before the + * controller is instantiated. If all the promises are resolved successfully, the values + * of the resolved promises are injected and $stateChangeSuccess event is fired. If any + * of the promises are rejected the $stateChangeError event is fired. The map object is: + * + * - key - {string}: name of dependency to be injected into controller + * - factory - {string|function}: If string then it is alias for service. Otherwise if function, + * it is injected and return value it treated as dependency. If result is a promise, it is + * resolved before its value is injected into controller. + * + * + * + * - **`url`** - {string=} - A url with optional parameters. When a state is navigated or + * transitioned to, the `$stateParams` service will be populated with any + * parameters that were passed. + * + * + * + * - **`params`** - {object=} - An array of parameter names or regular expressions. Only + * use this within a state if you are not using url. Otherwise you can specify your + * parameters within the url. When a state is navigated or transitioned to, the + * $stateParams service will be populated with any parameters that were passed. + * + * + * + * - **`views`** - {object=} - Use the views property to set up multiple views or to target views + * manually/explicitly. + * + * + * + * - **`abstract`** - {boolean=} - An abstract state will never be directly activated, + * but can provide inherited properties to its common children states. + * + * + * + * - **`onEnter`** - {object=} - Callback function for when a state is entered. Good way + * to trigger an action or dispatch an event, such as opening a dialog. + * If minifying your scripts, make sure to use the `["injection1", "injection2", function(injection1, injection2){}]` syntax. + * + * + * + * - **`onExit`** - {object=} - Callback function for when a state is exited. Good way to + * trigger an action or dispatch an event, such as opening a dialog. + * If minifying your scripts, make sure to use the `["injection1", "injection2", function(injection1, injection2){}]` syntax. + * + * + * + * - **`reloadOnSearch = true`** - {boolean=} - If `false`, will not retrigger the same state + * just because a search/query parameter has changed (via $location.search() or $location.hash()). + * Useful for when you"d like to modify $location.search() without triggering a reload. + * + * + * + * - **`data`** - {object=} - Arbitrary data object, useful for custom configuration. + * + * @example + *
+     * // Some state name examples
+     *
+     * // stateName can be a single top-level name (must be unique).
+     * $stateProvider.state("home", {});
+     *
+     * // Or it can be a nested state name. This state is a child of the
+     * // above "home" state.
+     * $stateProvider.state("home.newest", {});
+     *
+     * // Nest states as deeply as needed.
+     * $stateProvider.state("home.newest.abc.xyz.inception", {});
+     *
+     * // state() returns $stateProvider, so you can chain state declarations.
+     * $stateProvider
+     *   .state("home", {})
+     *   .state("about", {})
+     *   .state("contacts", {});
+     * 
+ * + * @param {string} name A unique state name, e.g. "home", "about", "contacts". + * To create a parent/child state use a dot, e.g. "about.sales", "home.newest". + * @param {object} definition State configuration object. + */ + this.state = state; + function state(name, definition) { + /*jshint validthis: true */ + if (isObject(name)) definition = name; + else definition.name = name; + registerState(definition); + return this; + } + + /** + * @ngdoc object + * @name ui.router.state.$state + * + * @requires $rootScope + * @requires $q + * @requires ui.router.state.$view + * @requires $injector + * @requires ui.router.util.$resolve + * @requires ui.router.state.$stateParams + * @requires ui.router.router.$urlRouter + * + * @property {object} params A param object, e.g. {sectionId: section.id)}, that + * you"d like to test against the current active state. + * @property {object} current A reference to the state"s config object. However + * you passed it in. Useful for accessing custom data. + * @property {object} transition Currently pending transition. A promise that"ll + * resolve or reject. + * + * @description + * `$state` service is responsible for representing states as well as transitioning + * between them. It also provides interfaces to ask for current state or even states + * you"re coming from. + */ + this.$get = $get; + $get.$inject = ["$rootScope", "$q", "$view", "$injector", "$resolve", "$stateParams", "$urlRouter"]; + function $get( $rootScope, $q, $view, $injector, $resolve, $stateParams, $urlRouter) { + + var TransitionSuperseded = $q.reject(new Error("transition superseded")); + var TransitionPrevented = $q.reject(new Error("transition prevented")); + var TransitionAborted = $q.reject(new Error("transition aborted")); + var TransitionFailed = $q.reject(new Error("transition failed")); + + // Handles the case where a state which is the target of a transition is not found, and the user + // can optionally retry or defer the transition + function handleRedirect(redirect, state, params, options) { + /** + * @ngdoc event + * @name ui.router.state.$state#$stateNotFound + * @eventOf ui.router.state.$state + * @eventType broadcast on root scope + * @description + * Fired when a requested state **cannot be found** using the provided state name during transition. + * The event is broadcast allowing any handlers a single chance to deal with the error (usually by + * lazy-loading the unfound state). A special `unfoundState` object is passed to the listener handler, + * you can see its three properties in the example. You can use `event.preventDefault()` to abort the + * transition and the promise returned from `go` will be rejected with a `"transition aborted"` value. + * + * @param {Object} event Event object. + * @param {Object} unfoundState Unfound State information. Contains: `to, toParams, options` properties. + * @param {State} fromState Current state object. + * @param {Object} fromParams Current state params. + * + * @example + * + *
+         * // somewhere, assume lazy.state has not been defined
+         * $state.go("lazy.state", {a:1, b:2}, {inherit:false});
+         *
+         * // somewhere else
+         * $scope.$on("$stateNotFound",
+         * function(event, unfoundState, fromState, fromParams){
+         *     console.log(unfoundState.to); // "lazy.state"
+         *     console.log(unfoundState.toParams); // {a:1, b:2}
+         *     console.log(unfoundState.options); // {inherit:false} + default options
+         * })
+         * 
+ */ + var evt = $rootScope.$broadcast("$stateNotFound", redirect, state, params); + + if (evt.defaultPrevented) { + $urlRouter.update(); + return TransitionAborted; + } + + if (!evt.retry) { + return null; + } + + // Allow the handler to return a promise to defer state lookup retry + if (options.$retry) { + $urlRouter.update(); + return TransitionFailed; + } + var retryTransition = $state.transition = $q.when(evt.retry); + + retryTransition.then(function() { + if (retryTransition !== $state.transition) return TransitionSuperseded; + redirect.options.$retry = true; + return $state.transitionTo(redirect.to, redirect.toParams, redirect.options); + }, function() { + return TransitionAborted; + }); + $urlRouter.update(); + + return retryTransition; + } + + root.locals = { resolve: null, globals: { $stateParams: {} } }; + + $state = { + params: {}, + current: root.self, + $current: root, + transition: null + }; + + /** + * @ngdoc function + * @name ui.router.state.$state#reload + * @methodOf ui.router.state.$state + * + * @description + * A method that force reloads the current state. All resolves are re-resolved, events are not re-fired, + * and controllers reinstantiated (bug with controllers reinstantiating right now, fixing soon). + * + * @example + *
+       * var app angular.module("app", ["ui.router"]);
+       *
+       * app.controller("ctrl", function ($scope, $state) {
+       *   $scope.reload = function(){
+       *     $state.reload();
+       *   }
+       * });
+       * 
+ * + * `reload()` is just an alias for: + *
+       * $state.transitionTo($state.current, $stateParams, {
+       *   reload: true, inherit: false, notify: false
+       * });
+       * 
+ */ + $state.reload = function reload() { + $state.transitionTo($state.current, $stateParams, { reload: true, inherit: false, notify: false }); + }; + + /** + * @ngdoc function + * @name ui.router.state.$state#go + * @methodOf ui.router.state.$state + * + * @description + * Convenience method for transitioning to a new state. `$state.go` calls + * `$state.transitionTo` internally but automatically sets options to + * `{ location: true, inherit: true, relative: $state.$current, notify: true }`. + * This allows you to easily use an absolute or relative to path and specify + * only the parameters you"d like to update (while letting unspecified parameters + * inherit from the currently active ancestor states). + * + * @example + *
+       * var app = angular.module("app", ["ui.router"]);
+       *
+       * app.controller("ctrl", function ($scope, $state) {
+       *   $scope.changeState = function () {
+       *     $state.go("contact.detail");
+       *   };
+       * });
+       * 
+ * + * + * @param {string} to Absolute state name or relative state path. Some examples: + * + * - `$state.go("contact.detail")` - will go to the `contact.detail` state + * - `$state.go("^")` - will go to a parent state + * - `$state.go("^.sibling")` - will go to a sibling state + * - `$state.go(".child.grandchild")` - will go to grandchild state + * + * @param {object=} params A map of the parameters that will be sent to the state, + * will populate $stateParams. Any parameters that are not specified will be inherited from currently + * defined parameters. This allows, for example, going to a sibling state that shares parameters + * specified in a parent state. Parameter inheritance only works between common ancestor states, I.e. + * transitioning to a sibling will get you the parameters for all parents, transitioning to a child + * will get you all current parameters, etc. + * @param {object=} options Options object. The options are: + * + * - **`location`** - {boolean=true|string=} - If `true` will update the url in the location bar, if `false` + * will not. If string, must be `"replace"`, which will update url and also replace last history record. + * - **`inherit`** - {boolean=true}, If `true` will inherit url parameters from current url. + * - **`relative`** - {object=$state.$current}, When transitioning with relative path (e.g "^"), + * defines which state to be relative from. + * - **`notify`** - {boolean=true}, If `true` will broadcast $stateChangeStart and $stateChangeSuccess events. + * - **`reload`** (v0.2.5) - {boolean=false}, If `true` will force transition even if the state or params + * have not changed, aka a reload of the same state. It differs from reloadOnSearch because you"d + * use this when you want to force a reload when *everything* is the same, including search params. + * + * @returns {promise} A promise representing the state of the new transition. + * + * Possible success values: + * + * - $state.current + * + *
Possible rejection values: + * + * - "transition superseded" - when a newer transition has been started after this one + * - "transition prevented" - when `event.preventDefault()` has been called in a `$stateChangeStart` listener + * - "transition aborted" - when `event.preventDefault()` has been called in a `$stateNotFound` listener or + * when a `$stateNotFound` `event.retry` promise errors. + * - "transition failed" - when a state has been unsuccessfully found after 2 tries. + * - *resolve error* - when an error has occurred with a `resolve` + * + */ + $state.go = function go(to, params, options) { + return $state.transitionTo(to, params, extend({ inherit: true, relative: $state.$current }, options)); + }; + + /** + * @ngdoc function + * @name ui.router.state.$state#transitionTo + * @methodOf ui.router.state.$state + * + * @description + * Low-level method for transitioning to a new state. {@link ui.router.state.$state#methods_go $state.go} + * uses `transitionTo` internally. `$state.go` is recommended in most situations. + * + * @example + *
+       * var app = angular.module("app", ["ui.router"]);
+       *
+       * app.controller("ctrl", function ($scope, $state) {
+       *   $scope.changeState = function () {
+       *     $state.transitionTo("contact.detail");
+       *   };
+       * });
+       * 
+ * + * @param {string} to State name. + * @param {object=} toParams A map of the parameters that will be sent to the state, + * will populate $stateParams. + * @param {object=} options Options object. The options are: + * + * - **`location`** - {boolean=true|string=} - If `true` will update the url in the location bar, if `false` + * will not. If string, must be `"replace"`, which will update url and also replace last history record. + * - **`inherit`** - {boolean=false}, If `true` will inherit url parameters from current url. + * - **`relative`** - {object=}, When transitioning with relative path (e.g "^"), + * defines which state to be relative from. + * - **`notify`** - {boolean=true}, If `true` will broadcast $stateChangeStart and $stateChangeSuccess events. + * - **`reload`** (v0.2.5) - {boolean=false}, If `true` will force transition even if the state or params + * have not changed, aka a reload of the same state. It differs from reloadOnSearch because you"d + * use this when you want to force a reload when *everything* is the same, including search params. + * + * @returns {promise} A promise representing the state of the new transition. See + * {@link ui.router.state.$state#methods_go $state.go}. + */ + $state.transitionTo = function transitionTo(to, toParams, options) { + toParams = toParams || {}; + options = extend({ + location: true, inherit: false, relative: null, notify: true, reload: false, $retry: false + }, options || {}); + + var from = $state.$current, fromParams = $state.params, fromPath = from.path; + var evt, toState = findState(to, options.relative); + + if (!isDefined(toState)) { + var redirect = { to: to, toParams: toParams, options: options }; + var redirectResult = handleRedirect(redirect, from.self, fromParams, options); + + if (redirectResult) { + return redirectResult; + } + + // Always retry once if the $stateNotFound was not prevented + // (handles either redirect changed or state lazy-definition) + to = redirect.to; + toParams = redirect.toParams; + options = redirect.options; + toState = findState(to, options.relative); + + if (!isDefined(toState)) { + if (!options.relative) throw new Error("No such state '" + to + "'"); + throw new Error("Could not resolve '" + to + "' from state '" + options.relative + "'"); + } + } + if (toState[abstractKey]) throw new Error("Cannot transition to abstract state '" + to + "'"); + if (options.inherit) toParams = inheritParams($stateParams, toParams || {}, $state.$current, toState); + to = toState; + + var toPath = to.path; + + // Starting from the root of the path, keep all levels that haven"t changed + var keep = 0, state = toPath[keep], locals = root.locals, toLocals = []; + + if (!options.reload) { + while (state && state === fromPath[keep] && equalForKeys(toParams, fromParams, state.ownParams)) { + locals = toLocals[keep] = state.locals; + keep++; + state = toPath[keep]; + } + } + + // If we"re going to the same state and all locals are kept, we"ve got nothing to do. + // But clear "transition", as we still want to cancel any other pending transitions. + // TODO: We may not want to bump "transition" if we"re called from a location change + // that we"ve initiated ourselves, because we might accidentally abort a legitimate + // transition initiated from code? + if (shouldTriggerReload(to, from, locals, options)) { + if (to.self.reloadOnSearch !== false) $urlRouter.update(); + $state.transition = null; + return $q.when($state.current); + } + + // Filter parameters before we pass them to event handlers etc. + toParams = filterByKeys(objectKeys(to.params), toParams || {}); + + // Broadcast start event and cancel the transition if requested + if (options.notify) { + /** + * @ngdoc event + * @name ui.router.state.$state#$stateChangeStart + * @eventOf ui.router.state.$state + * @eventType broadcast on root scope + * @description + * Fired when the state transition **begins**. You can use `event.preventDefault()` + * to prevent the transition from happening and then the transition promise will be + * rejected with a `"transition prevented"` value. + * + * @param {Object} event Event object. + * @param {State} toState The state being transitioned to. + * @param {Object} toParams The params supplied to the `toState`. + * @param {State} fromState The current state, pre-transition. + * @param {Object} fromParams The params supplied to the `fromState`. + * + * @example + * + *
+           * $rootScope.$on("$stateChangeStart",
+           * function(event, toState, toParams, fromState, fromParams){
+           *     event.preventDefault();
+           *     // transitionTo() promise will be rejected with
+           *     // a "transition prevented" error
+           * })
+           * 
+ */ + if ($rootScope.$broadcast("$stateChangeStart", to.self, toParams, from.self, fromParams).defaultPrevented) { + $urlRouter.update(); + return TransitionPrevented; + } + } + + // Resolve locals for the remaining states, but don"t update any global state just + // yet -- if anything fails to resolve the current state needs to remain untouched. + // We also set up an inheritance chain for the locals here. This allows the view directive + // to quickly look up the correct definition for each view in the current state. Even + // though we create the locals object itself outside resolveState(), it is initially + // empty and gets filled asynchronously. We need to keep track of the promise for the + // (fully resolved) current locals, and pass this down the chain. + var resolved = $q.when(locals); + + for (var l = keep; l < toPath.length; l++, state = toPath[l]) { + locals = toLocals[l] = inherit(locals); + resolved = resolveState(state, toParams, state === to, resolved, locals); + } + + // Once everything is resolved, we are ready to perform the actual transition + // and return a promise for the new state. We also keep track of what the + // current promise is, so that we can detect overlapping transitions and + // keep only the outcome of the last transition. + var transition = $state.transition = resolved.then(function () { + var l, entering, exiting; + + if ($state.transition !== transition) return TransitionSuperseded; + + // Exit "from" states not kept + for (l = fromPath.length - 1; l >= keep; l--) { + exiting = fromPath[l]; + if (exiting.self.onExit) { + $injector.invoke(exiting.self.onExit, exiting.self, exiting.locals.globals); + } + exiting.locals = null; + } + + // Enter "to" states not kept + for (l = keep; l < toPath.length; l++) { + entering = toPath[l]; + entering.locals = toLocals[l]; + if (entering.self.onEnter) { + $injector.invoke(entering.self.onEnter, entering.self, entering.locals.globals); + } + } + + // Run it again, to catch any transitions in callbacks + if ($state.transition !== transition) return TransitionSuperseded; + + // Update globals in $state + $state.$current = to; + $state.current = to.self; + $state.params = toParams; + copy($state.params, $stateParams); + $state.transition = null; + + if (options.location && to.navigable) { + $urlRouter.push(to.navigable.url, to.navigable.locals.globals.$stateParams, { + replace: options.location === "replace" + }); + } + + if (options.notify) { + /** + * @ngdoc event + * @name ui.router.state.$state#$stateChangeSuccess + * @eventOf ui.router.state.$state + * @eventType broadcast on root scope + * @description + * Fired once the state transition is **complete**. + * + * @param {Object} event Event object. + * @param {State} toState The state being transitioned to. + * @param {Object} toParams The params supplied to the `toState`. + * @param {State} fromState The current state, pre-transition. + * @param {Object} fromParams The params supplied to the `fromState`. + */ + $rootScope.$broadcast("$stateChangeSuccess", to.self, toParams, from.self, fromParams); + } + $urlRouter.update(true); + + return $state.current; + }, function (error) { + if ($state.transition !== transition) return TransitionSuperseded; + + $state.transition = null; + /** + * @ngdoc event + * @name ui.router.state.$state#$stateChangeError + * @eventOf ui.router.state.$state + * @eventType broadcast on root scope + * @description + * Fired when an **error occurs** during transition. It"s important to note that if you + * have any errors in your resolve functions (javascript errors, non-existent services, etc) + * they will not throw traditionally. You must listen for this $stateChangeError event to + * catch **ALL** errors. + * + * @param {Object} event Event object. + * @param {State} toState The state being transitioned to. + * @param {Object} toParams The params supplied to the `toState`. + * @param {State} fromState The current state, pre-transition. + * @param {Object} fromParams The params supplied to the `fromState`. + * @param {Error} error The resolve error object. + */ + evt = $rootScope.$broadcast("$stateChangeError", to.self, toParams, from.self, fromParams, error); + + if (!evt.defaultPrevented) { + $urlRouter.update(); + } + + return $q.reject(error); + }); + + return transition; + }; + + /** + * @ngdoc function + * @name ui.router.state.$state#is + * @methodOf ui.router.state.$state + * + * @description + * Similar to {@link ui.router.state.$state#methods_includes $state.includes}, + * but only checks for the full state name. If params is supplied then it will be + * tested for strict equality against the current active params object, so all params + * must match with none missing and no extras. + * + * @example + *
+       * $state.$current.name = "contacts.details.item";
+       *
+       * // absolute name
+       * $state.is("contact.details.item"); // returns true
+       * $state.is(contactDetailItemStateObject); // returns true
+       *
+       * // relative name (. and ^), typically from a template
+       * // E.g. from the "contacts.details" template
+       * 
Item
+ *
+ * + * @param {string|object} stateName The state name (absolute or relative) or state object you"d like to check. + * @param {object=} params A param object, e.g. `{sectionId: section.id}`, that you"d like + * to test against the current active state. + * @returns {boolean} Returns true if it is the state. + */ + $state.is = function is(stateOrName, params) { + var state = findState(stateOrName); + + if (!isDefined(state)) { + return undefined; + } + + if ($state.$current !== state) { + return false; + } + + return isDefined(params) && params !== null ? angular.equals($stateParams, params) : true; + }; + + /** + * @ngdoc function + * @name ui.router.state.$state#includes + * @methodOf ui.router.state.$state + * + * @description + * A method to determine if the current active state is equal to or is the child of the + * state stateName. If any params are passed then they will be tested for a match as well. + * Not all the parameters need to be passed, just the ones you"d like to test for equality. + * + * @example + * Partial and relative names + *
+       * $state.$current.name = "contacts.details.item";
+       *
+       * // Using partial names
+       * $state.includes("contacts"); // returns true
+       * $state.includes("contacts.details"); // returns true
+       * $state.includes("contacts.details.item"); // returns true
+       * $state.includes("contacts.list"); // returns false
+       * $state.includes("about"); // returns false
+       *
+       * // Using relative names (. and ^), typically from a template
+       * // E.g. from the "contacts.details" template
+       * 
Item
+ *
+ * + * Basic globbing patterns + *
+       * $state.$current.name = "contacts.details.item.url";
+       *
+       * $state.includes("*.details.*.*"); // returns true
+       * $state.includes("*.details.**"); // returns true
+       * $state.includes("**.item.**"); // returns true
+       * $state.includes("*.details.item.url"); // returns true
+       * $state.includes("*.details.*.url"); // returns true
+       * $state.includes("*.details.*"); // returns false
+       * $state.includes("item.**"); // returns false
+       * 
+ * + * @param {string} stateOrName A partial name, relative name, or glob pattern + * to be searched for within the current state name. + * @param {object} params A param object, e.g. `{sectionId: section.id}`, + * that you"d like to test against the current active state. + * @returns {boolean} Returns true if it does include the state + */ + $state.includes = function includes(stateOrName, params) { + if (isString(stateOrName) && isGlob(stateOrName)) { + if (!doesStateMatchGlob(stateOrName)) { + return false; + } + stateOrName = $state.$current.name; + } + var state = findState(stateOrName); + + if (!isDefined(state)) { + return undefined; + } + if (!isDefined($state.$current.includes[state.name])) { + return false; + } + return equalForKeys(params, $stateParams); + }; + + + /** + * @ngdoc function + * @name ui.router.state.$state#href + * @methodOf ui.router.state.$state + * + * @description + * A url generation method that returns the compiled url for the given state populated with the given params. + * + * @example + *
+       * expect($state.href("about.person", { person: "bob" })).toEqual("/about/bob");
+       * 
+ * + * @param {string|object} stateOrName The state name or state object you"d like to generate a url from. + * @param {object=} params An object of parameter values to fill the state"s required parameters. + * @param {object=} options Options object. The options are: + * + * - **`lossy`** - {boolean=true} - If true, and if there is no url associated with the state provided in the + * first parameter, then the constructed href url will be built from the first navigable ancestor (aka + * ancestor with a valid url). + * - **`inherit`** - {boolean=true}, If `true` will inherit url parameters from current url. + * - **`relative`** - {object=$state.$current}, When transitioning with relative path (e.g "^"), + * defines which state to be relative from. + * - **`absolute`** - {boolean=false}, If true will generate an absolute url, e.g. "http://www.example.com/fullurl". + * + * @returns {string} compiled state url + */ + $state.href = function href(stateOrName, params, options) { + options = extend({ + lossy: true, + inherit: true, + absolute: false, + relative: $state.$current + }, options || {}); + + var state = findState(stateOrName, options.relative); + + if (!isDefined(state)) return null; + if (options.inherit) params = inheritParams($stateParams, params || {}, $state.$current, state); + + var nav = (state && options.lossy) ? state.navigable : state; + + if (!nav || !nav.url) { + return null; + } + return $urlRouter.href(nav.url, filterByKeys(objectKeys(state.params), params || {}), { + absolute: options.absolute + }); + }; + + /** + * @ngdoc function + * @name ui.router.state.$state#get + * @methodOf ui.router.state.$state + * + * @description + * Returns the state configuration object for any specific state or all states. + * + * @param {string|Sbject=} stateOrName (absolute or relative) If provided, will only get the config for + * the requested state. If not provided, returns an array of ALL state configs. + * @returns {Object|Array} State configuration object or array of all objects. + */ + $state.get = function (stateOrName, context) { + if (arguments.length === 0) return objectKeys(states).map(function(name) { return states[name].self; }); + var state = findState(stateOrName, context); + return (state && state.self) ? state.self : null; + }; + + function resolveState(state, params, paramsAreFiltered, inherited, dst) { + // Make a restricted $stateParams with only the parameters that apply to this state if + // necessary. In addition to being available to the controller and onEnter/onExit callbacks, + // we also need $stateParams to be available for any $injector calls we make during the + // dependency resolution process. + var $stateParams = (paramsAreFiltered) ? params : filterByKeys(objectKeys(state.params), params); + var locals = { $stateParams: $stateParams }; + + // Resolve "global" dependencies for the state, i.e. those not specific to a view. + // We"re also including $stateParams in this; that way the parameters are restricted + // to the set that should be visible to the state, and are independent of when we update + // the global $state and $stateParams values. + dst.resolve = $resolve.resolve(state.resolve, locals, dst.resolve, state); + var promises = [dst.resolve.then(function (globals) { + dst.globals = globals; + })]; + if (inherited) promises.push(inherited); + + // Resolve template and dependencies for all views. + forEach(state.views, function (view, name) { + var injectables = (view.resolve && view.resolve !== state.resolve ? view.resolve : {}); + injectables.$template = [ function () { + return $view.load(name, { view: view, locals: locals, params: $stateParams }) || ""; + }]; + + promises.push($resolve.resolve(injectables, locals, dst.resolve, state).then(function (result) { + // References to the controller (only instantiated at link time) + if (isFunction(view.controllerProvider) || isArray(view.controllerProvider)) { + var injectLocals = angular.extend({}, injectables, locals); + result.$$controller = $injector.invoke(view.controllerProvider, null, injectLocals); + } else { + result.$$controller = view.controller; + } + // Provide access to the state itself for internal use + result.$$state = state; + result.$$controllerAs = view.controllerAs; + dst[name] = result; + })); + }); + + // Wait for all the promises and then return the activation object + return $q.all(promises).then(function (values) { + return dst; + }); + } + + return $state; + } + + function shouldTriggerReload(to, from, locals, options) { + if (to === from && ((locals === from.locals && !options.reload) || (to.self.reloadOnSearch === false))) { + return true; + } + } + } + + angular.module("ui.router.state") + .value("$stateParams", {}) + .provider("$state", $StateProvider); + + + $ViewProvider.$inject = []; + function $ViewProvider() { + + this.$get = $get; + /** + * @ngdoc object + * @name ui.router.state.$view + * + * @requires ui.router.util.$templateFactory + * @requires $rootScope + * + * @description + * + */ + $get.$inject = ["$rootScope", "$templateFactory"]; + function $get( $rootScope, $templateFactory) { + return { + // $view.load("full.viewName", { template: ..., controller: ..., resolve: ..., async: false, params: ... }) + /** + * @ngdoc function + * @name ui.router.state.$view#load + * @methodOf ui.router.state.$view + * + * @description + * + * @param {string} name name + * @param {object} options option object. + */ + load: function load(name, options) { + var result, defaults = { + template: null, controller: null, view: null, locals: null, notify: true, async: true, params: {} + }; + options = extend(defaults, options); + + if (options.view) { + result = $templateFactory.fromConfig(options.view, options.params, options.locals); + } + if (result && options.notify) { + /** + * @ngdoc event + * @name ui.router.state.$state#$viewContentLoading + * @eventOf ui.router.state.$view + * @eventType broadcast on root scope + * @description + * + * Fired once the view **begins loading**, *before* the DOM is rendered. + * + * @param {Object} event Event object. + * @param {Object} viewConfig The view config properties (template, controller, etc). + * + * @example + * + *
+           * $scope.$on("$viewContentLoading",
+           * function(event, viewConfig){
+           *     // Access to all the view config properties.
+           *     // and one special property "targetView"
+           *     // viewConfig.targetView
+           * });
+           * 
+ */ + $rootScope.$broadcast("$viewContentLoading", options); + } + return result; + } + }; + } + } + + angular.module("ui.router.state").provider("$view", $ViewProvider); + + /** + * @ngdoc object + * @name ui.router.state.$uiViewScrollProvider + * + * @description + * Provider that returns the {@link ui.router.state.$uiViewScroll} service function. + */ + function $ViewScrollProvider() { + + var useAnchorScroll = false; + + /** + * @ngdoc function + * @name ui.router.state.$uiViewScrollProvider#useAnchorScroll + * @methodOf ui.router.state.$uiViewScrollProvider + * + * @description + * Reverts back to using the core [`$anchorScroll`](http://docs.angularjs.org/api/ng.$anchorScroll) service for + * scrolling based on the url anchor. + */ + this.useAnchorScroll = function () { + useAnchorScroll = true; + }; + + /** + * @ngdoc object + * @name ui.router.state.$uiViewScroll + * + * @requires $anchorScroll + * @requires $timeout + * + * @description + * When called with a jqLite element, it scrolls the element into view (after a + * `$timeout` so the DOM has time to refresh). + * + * If you prefer to rely on `$anchorScroll` to scroll the view to the anchor, + * this can be enabled by calling {@link ui.router.state.$uiViewScrollProvider#methods_useAnchorScroll `$uiViewScrollProvider.useAnchorScroll()`}. + */ + this.$get = ["$anchorScroll", "$timeout", function ($anchorScroll, $timeout) { + if (useAnchorScroll) { + return $anchorScroll; + } + + return function ($element) { + $timeout(function () { + $element[0].scrollIntoView(); + }, 0, false); + }; + }]; + } + + angular.module("ui.router.state").provider("$uiViewScroll", $ViewScrollProvider); + + /** + * @ngdoc directive + * @name ui.router.state.directive:ui-view + * + * @requires ui.router.state.$state + * @requires $compile + * @requires $controller + * @requires $injector + * @requires ui.router.state.$uiViewScroll + * @requires $document + * + * @restrict ECA + * + * @description + * The ui-view directive tells $state where to place your templates. + * + * @param {string=} ui-view A view name. The name should be unique amongst the other views in the + * same state. You can have views of the same name that live in different states. + * + * @param {string=} autoscroll It allows you to set the scroll behavior of the browser window + * when a view is populated. By default, $anchorScroll is overridden by ui-router"s custom scroll + * service, {@link ui.router.state.$uiViewScroll}. This custom service let"s you + * scroll ui-view elements into view when they are populated during a state activation. + * + * *Note: To revert back to old [`$anchorScroll`](http://docs.angularjs.org/api/ng.$anchorScroll) + * functionality, call `$uiViewScrollProvider.useAnchorScroll()`.* + * + * @param {string=} onload Expression to evaluate whenever the view updates. + * + * @example + * A view can be unnamed or named. + *
+   * 
+   * 
+ * + * + *
+ *
+ * + * You can only have one unnamed view within any template (or root html). If you are only using a + * single view and it is unnamed then you can populate it like so: + *
+   * 
+ * $stateProvider.state("home", { + * template: "

HELLO!

" + * }) + *
+ * + * The above is a convenient shortcut equivalent to specifying your view explicitly with the {@link ui.router.state.$stateProvider#views `views`} + * config property, by name, in this case an empty name: + *
+   * $stateProvider.state("home", {
+   *   views: {
+   *     "": {
+   *       template: "

HELLO!

" + * } + * } + * }) + *
+ * + * But typically you"ll only use the views property if you name your view or have more than one view + * in the same template. There"s not really a compelling reason to name a view if its the only one, + * but you could if you wanted, like so: + *
+   * 
+ *
+ *
+   * $stateProvider.state("home", {
+   *   views: {
+   *     "main": {
+   *       template: "

HELLO!

" + * } + * } + * }) + *
+ * + * Really though, you"ll use views to set up multiple views: + *
+   * 
+ *
+ *
+ *
+ * + *
+   * $stateProvider.state("home", {
+   *   views: {
+   *     "": {
+   *       template: "

HELLO!

" + * }, + * "chart": { + * template: "" + * }, + * "data": { + * template: "" + * } + * } + * }) + *
+ * + * Examples for `autoscroll`: + * + *
+   * 
+   * 
+   *
+   * 
+   * 
+   * 
+   * 
+   * 
+ */ + $ViewDirective.$inject = ["$state", "$injector", "$uiViewScroll"]; + function $ViewDirective( $state, $injector, $uiViewScroll) { + + function getService() { + return ($injector.has) ? function(service) { + return $injector.has(service) ? $injector.get(service) : null; + } : function(service) { + try { + return $injector.get(service); + } catch (e) { + return null; + } + }; + } + + var service = getService(), + $animator = service("$animator"), + $animate = service("$animate"); + + // Returns a set of DOM manipulation functions based on which Angular version + // it should use + function getRenderer(attrs, scope) { + var statics = function() { + return { + enter: function (element, target, cb) { target.after(element); cb(); }, + leave: function (element, cb) { element.remove(); cb(); } + }; + }; + + if ($animate) { + return { + enter: function(element, target, cb) { $animate.enter(element, null, target, cb); }, + leave: function(element, cb) { $animate.leave(element, cb); } + }; + } + + if ($animator) { + var animate = $animator && $animator(scope, attrs); + + return { + enter: function(element, target, cb) {animate.enter(element, null, target); cb(); }, + leave: function(element, cb) { animate.leave(element); cb(); } + }; + } + + return statics(); + } + + var directive = { + restrict: "ECA", + terminal: true, + priority: 400, + transclude: "element", + compile: function (tElement, tAttrs, $transclude) { + return function (scope, $element, attrs) { + var previousEl, currentEl, currentScope, latestLocals, + onloadExp = attrs.onload || "", + autoScrollExp = attrs.autoscroll, + renderer = getRenderer(attrs, scope); + + scope.$on("$stateChangeSuccess", function() { + updateView(false); + }); + scope.$on("$viewContentLoading", function() { + updateView(false); + }); + + updateView(true); + + function cleanupLastView() { + if (previousEl) { + previousEl.remove(); + previousEl = null; + } + + if (currentScope) { + currentScope.$destroy(); + currentScope = null; + } + + if (currentEl) { + renderer.leave(currentEl, function() { + previousEl = null; + }); + + previousEl = currentEl; + currentEl = null; + } + } + + function updateView(firstTime) { + var newScope, + name = getUiViewName(attrs, $element.inheritedData("$uiView")), + previousLocals = name && $state.$current && $state.$current.locals[name]; + + if (!firstTime && previousLocals === latestLocals) return; // nothing to do + newScope = scope.$new(); + latestLocals = $state.$current.locals[name]; + + var clone = $transclude(newScope, function(clone) { + renderer.enter(clone, $element, function onUiViewEnter() { + if (angular.isDefined(autoScrollExp) && !autoScrollExp || scope.$eval(autoScrollExp)) { + $uiViewScroll(clone); + } + }); + cleanupLastView(); + }); + + currentEl = clone; + currentScope = newScope; + /** + * @ngdoc event + * @name ui.router.state.directive:ui-view#$viewContentLoaded + * @eventOf ui.router.state.directive:ui-view + * @eventType emits on ui-view directive scope + * @description * + * Fired once the view is **loaded**, *after* the DOM is rendered. + * + * @param {Object} event Event object. + */ + currentScope.$emit("$viewContentLoaded"); + currentScope.$eval(onloadExp); + } + }; + } + }; + + return directive; + } + + $ViewDirectiveFill.$inject = ["$compile", "$controller", "$state"]; + function $ViewDirectiveFill ($compile, $controller, $state) { + return { + restrict: "ECA", + priority: -400, + compile: function (tElement) { + var initial = tElement.html(); + return function (scope, $element, attrs) { + var current = $state.$current, + name = getUiViewName(attrs, $element.inheritedData("$uiView")), + locals = current && current.locals[name]; + + if (! locals) { + return; + } + + $element.data("$uiView", { name: name, state: locals.$$state }); + $element.html(locals.$template ? locals.$template : initial); + + var link = $compile($element.contents()); + + if (locals.$$controller) { + locals.$scope = scope; + var controller = $controller(locals.$$controller, locals); + if (locals.$$controllerAs) { + scope[locals.$$controllerAs] = controller; + } + $element.data("$ngControllerController", controller); + $element.children().data("$ngControllerController", controller); + } + + link(scope); + }; + } + }; + } + + /** + * Shared ui-view code for both directives: + * Given attributes and inherited $uiView data, return the view"s name + */ + function getUiViewName(attrs, inherited) { + var name = attrs.uiView || attrs.name || ""; + return name.indexOf("@") >= 0 ? name : (name + "@" + (inherited ? inherited.state.name : "")); + } + + angular.module("ui.router.state").directive("uiView", $ViewDirective); + angular.module("ui.router.state").directive("uiView", $ViewDirectiveFill); + + function parseStateRef(ref, current) { + var preparsed = ref.match(/^\s*({[^}]*})\s*$/), parsed; + if (preparsed) ref = current + "(" + preparsed[1] + ")"; + parsed = ref.replace(/\n/g, " ").match(/^([^(]+?)\s*(\((.*)\))?$/); + if (!parsed || parsed.length !== 4) throw new Error("Invalid state ref '" + ref + "'"); + return { state: parsed[1], paramExpr: parsed[3] || null }; + } + + function stateContext(el) { + var stateData = el.parent().inheritedData("$uiView"); + + if (stateData && stateData.state && stateData.state.name) { + return stateData.state; + } + } + + /** + * @ngdoc directive + * @name ui.router.state.directive:ui-sref + * + * @requires ui.router.state.$state + * @requires $timeout + * + * @restrict A + * + * @description + * A directive that binds a link (`` tag) to a state. If the state has an associated + * URL, the directive will automatically generate & update the `href` attribute via + * the {@link ui.router.state.$state#methods_href $state.href()} method. Clicking + * the link will trigger a state transition with optional parameters. + * + * Also middle-clicking, right-clicking, and ctrl-clicking on the link will be + * handled natively by the browser. + * + * You can also use relative state paths within ui-sref, just like the relative + * paths passed to `$state.go()`. You just need to be aware that the path is relative + * to the state that the link lives in, in other words the state that loaded the + * template containing the link. + * + * You can specify options to pass to {@link ui.router.state.$state#go $state.go()} + * using the `ui-sref-opts` attribute. Options are restricted to `location`, `inherit`, + * and `reload`. + * + * @example + * Here"s an example of how you"d use ui-sref and how it would compile. If you have the + * following template: + *
+   * Home | About | Next page
+   *
+   * 
+   * 
+ * + * Then the compiled html would be (assuming Html5Mode is off and current state is contacts): + *
+   * Home | About | Next page
+   *
+   * 
    + *
  • + * Joe + *
  • + *
  • + * Alice + *
  • + *
  • + * Bob + *
  • + *
+ * + * Home + *
+ * + * @param {string} ui-sref "stateName" can be any valid absolute or relative state + * @param {Object} ui-sref-opts options to pass to {@link ui.router.state.$state#go $state.go()} + */ + $StateRefDirective.$inject = ["$state", "$timeout"]; + function $StateRefDirective($state, $timeout) { + var allowedOptions = ["location", "inherit", "reload"]; + + return { + restrict: "A", + require: ["?^uiSrefActive", "?^uiSrefActiveEq"], + link: function(scope, element, attrs, uiSrefActive) { + var ref = parseStateRef(attrs.uiSref, $state.current.name); + var params = null, url = null, base = stateContext(element) || $state.$current; + var isForm = element[0].nodeName === "FORM"; + var attr = isForm ? "action" : "href", nav = true; + + var options = { relative: base, inherit: true }; + var optionsOverride = scope.$eval(attrs.uiSrefOpts) || {}; + + angular.forEach(allowedOptions, function(option) { + if (option in optionsOverride) { + options[option] = optionsOverride[option]; + } + }); + + var update = function(newVal) { + if (newVal) params = newVal; + if (!nav) return; + + var newHref = $state.href(ref.state, params, options); + + var activeDirective = uiSrefActive[1] || uiSrefActive[0]; + if (activeDirective) { + activeDirective.$$setStateInfo(ref.state, params); + } + if (newHref === null) { + nav = false; + return false; + } + element[0][attr] = newHref; + }; + + if (ref.paramExpr) { + scope.$watch(ref.paramExpr, function(newVal, oldVal) { + if (newVal !== params) update(newVal); + }, true); + params = scope.$eval(ref.paramExpr); + } + update(); + + if (isForm) return; + + element.bind("click", function(e) { + var button = e.which || e.button; + if ( !(button > 1 || e.ctrlKey || e.metaKey || e.shiftKey || element.attr("target")) ) { + // HACK: This is to allow ng-clicks to be processed before the transition is initiated: + var transition = $timeout(function() { + $state.go(ref.state, params, options); + }); + e.preventDefault(); + + e.preventDefault = function() { + $timeout.cancel(transition); + }; + } + }); + } + }; + } + + /** + * @ngdoc directive + * @name ui.router.state.directive:ui-sref-active + * + * @requires ui.router.state.$state + * @requires ui.router.state.$stateParams + * @requires $interpolate + * + * @restrict A + * + * @description + * A directive working alongside ui-sref to add classes to an element when the + * related ui-sref directive"s state is active, and removing them when it is inactive. + * The primary use-case is to simplify the special appearance of navigation menus + * relying on `ui-sref`, by having the "active" state"s menu button appear different, + * distinguishing it from the inactive menu items. + * + * ui-sref-active can live on the same element as ui-sref or on a parent element. The first + * ui-sref-active found at the same level or above the ui-sref will be used. + * + * Will activate when the ui-sref"s target state or any child state is active. If you + * need to activate only when the ui-sref target state is active and *not* any of + * it"s children, then you will use + * {@link ui.router.state.directive:ui-sref-active-eq ui-sref-active-eq} + * + * @example + * Given the following template: + *
+   * 
+   * 
+ * + * + * When the app state is "app.user" (or any children states), and contains the state parameter "user" with value "bilbobaggins", + * the resulting HTML will appear as (note the "active" class): + *
+   * 
+   * 
+ * + * The class name is interpolated **once** during the directives link time (any further changes to the + * interpolated value are ignored). + * + * Multiple classes may be specified in a space-separated format: + *
+   * 
    + *
  • + * link + *
  • + *
+ *
+ */ + + /** + * @ngdoc directive + * @name ui.router.state.directive:ui-sref-active-eq + * + * @requires ui.router.state.$state + * @requires ui.router.state.$stateParams + * @requires $interpolate + * + * @restrict A + * + * @description + * The same as {@link ui.router.state.directive:ui-sref-active ui-sref-active} but will will only activate + * when the exact target state used in the `ui-sref` is active; no child states. + * + */ + $StateRefActiveDirective.$inject = ["$state", "$stateParams", "$interpolate"]; + function $StateRefActiveDirective($state, $stateParams, $interpolate) { + return { + restrict: "A", + controller: ["$scope", "$element", "$attrs", function ($scope, $element, $attrs) { + var state, params, activeClass; + + // There probably isn"t much point in $observing this + // uiSrefActive and uiSrefActiveEq share the same directive object with some + // slight difference in logic routing + activeClass = $interpolate($attrs.uiSrefActiveEq || $attrs.uiSrefActive || "", false)($scope); + + // Allow uiSref to communicate with uiSrefActive[Equals] + this.$$setStateInfo = function (newState, newParams) { + state = $state.get(newState, stateContext($element)); + params = newParams; + update(); + }; + + $scope.$on("$stateChangeSuccess", update); + + // Update route state + function update() { + if (isMatch()) { + $element.addClass(activeClass); + } else { + $element.removeClass(activeClass); + } + } + + function isMatch() { + if (typeof $attrs.uiSrefActiveEq !== "undefined") { + return $state.$current.self === state && matchesParams(); + } else { + return $state.includes(state.name) && matchesParams(); + } + } + + function matchesParams() { + return !params || equalForKeys(params, $stateParams); + } + }] + }; + } + + angular.module("ui.router.state") + .directive("uiSref", $StateRefDirective) + .directive("uiSrefActive", $StateRefActiveDirective) + .directive("uiSrefActiveEq", $StateRefActiveDirective); + + /** + * @ngdoc filter + * @name ui.router.state.filter:isState + * + * @requires ui.router.state.$state + * + * @description + * Translates to {@link ui.router.state.$state#methods_is $state.is("stateName")}. + */ + $IsStateFilter.$inject = ["$state"]; + function $IsStateFilter($state) { + return function(state) { + return $state.is(state); + }; + } + + /** + * @ngdoc filter + * @name ui.router.state.filter:includedByState + * + * @requires ui.router.state.$state + * + * @description + * Translates to {@link ui.router.state.$state#methods_includes $state.includes("fullOrPartialStateName")}. + */ + $IncludedByStateFilter.$inject = ["$state"]; + function $IncludedByStateFilter($state) { + return function(state) { + return $state.includes(state); + }; + } + + angular.module("ui.router.state") + .filter("isState", $IsStateFilter) + .filter("includedByState", $IncludedByStateFilter); + })({}, angular); +}; diff --git a/test-mocks/ui-utils-keypress.js b/test-mocks/ui-utils-keypress.js new file mode 100644 index 0000000..32e9aac --- /dev/null +++ b/test-mocks/ui-utils-keypress.js @@ -0,0 +1,119 @@ +module.exports = function (angular) { + "use strict"; + + angular.module("ui.keypress",[]). + factory("keypressHelper", ["$parse", function keypress($parse){ + var keysByCode = { + 8: "backspace", + 9: "tab", + 13: "enter", + 27: "esc", + 32: "space", + 33: "pageup", + 34: "pagedown", + 35: "end", + 36: "home", + 37: "left", + 38: "up", + 39: "right", + 40: "down", + 45: "insert", + 46: "delete" + }; + + var capitaliseFirstLetter = function (string) { + return string.charAt(0).toUpperCase() + string.slice(1); + }; + + return function(mode, scope, elm, attrs) { + var params, combinations = []; + params = scope.$eval(attrs["ui"+capitaliseFirstLetter(mode)]); + + // Prepare combinations for simple checking + angular.forEach(params, function (v, k) { + var combination, expression; + expression = $parse(v); + + angular.forEach(k.split(" "), function(variation) { + combination = { + expression: expression, + keys: {} + }; + angular.forEach(variation.split("-"), function (value) { + combination.keys[value] = true; + }); + combinations.push(combination); + }); + }); + + // Check only matching of pressed keys one of the conditions + elm.bind(mode, function (event) { + // No need to do that inside the cycle + var metaPressed = !!(event.metaKey && !event.ctrlKey); + var altPressed = !!event.altKey; + var ctrlPressed = !!event.ctrlKey; + var shiftPressed = !!event.shiftKey; + var keyCode = event.keyCode; + + // normalize keycodes + if (mode === "keypress" && !shiftPressed && keyCode >= 97 && keyCode <= 122) { + keyCode = keyCode - 32; + } + + // Iterate over prepared combinations + angular.forEach(combinations, function (combination) { + + var mainKeyPressed = combination.keys[keysByCode[keyCode]] || combination.keys[keyCode.toString()]; + + var metaRequired = !!combination.keys.meta; + var altRequired = !!combination.keys.alt; + var ctrlRequired = !!combination.keys.ctrl; + var shiftRequired = !!combination.keys.shift; + + if ( + mainKeyPressed && + ( metaRequired === metaPressed ) && + ( altRequired === altPressed ) && + ( ctrlRequired === ctrlPressed ) && + ( shiftRequired === shiftPressed ) + ) { + // Run the function + scope.$apply(function () { + combination.expression(scope, { "$event": event }); + }); + } + }); + }); + }; + }]); + + /** + * Bind one or more handlers to particular keys or their combination + * @param hash {mixed} keyBindings Can be an object or string where keybinding expression of keys or keys combinations and AngularJS Exspressions are set. Object syntax: "{ keys1: expression1 [, keys2: expression2 [ , ... ]]}". String syntax: ""expression1 on keys1 [ and expression2 on keys2 [ and ... ]]"". Expression is an AngularJS Expression, and key(s) are dash-separated combinations of keys and modifiers (one or many, if any. Order does not matter). Supported modifiers are "ctrl", "shift", "alt" and key can be used either via its keyCode (13 for Return) or name. Named keys are "backspace", "tab", "enter", "esc", "space", "pageup", "pagedown", "end", "home", "left", "up", "right", "down", "insert", "delete". + * @example + **/ + angular.module("ui.keypress").directive("uiKeydown", ["keypressHelper", function(keypressHelper){ + return { + link: function (scope, elm, attrs) { + keypressHelper("keydown", scope, elm, attrs); + } + }; + }]); + + angular.module("ui.keypress").directive("uiKeypress", ["keypressHelper", function(keypressHelper){ + return { + link: function (scope, elm, attrs) { + keypressHelper("keypress", scope, elm, attrs); + } + }; + }]); + + angular.module("ui.keypress").directive("uiKeyup", ["keypressHelper", function(keypressHelper){ + return { + link: function (scope, elm, attrs) { + keypressHelper("keyup", scope, elm, attrs); + } + }; + }]); + +}; diff --git a/test/controllers.js b/test/controllers.js deleted file mode 100644 index 4533c08..0000000 --- a/test/controllers.js +++ /dev/null @@ -1,161 +0,0 @@ -var should = require('should'); - -describe("controllers", function() { - var angular - - beforeEach(function() { - angular = require('../src/fake-angular')() - }) - - it('should have an empty property when no controller are present', function () { - angular.module('testModule1'); - angular.module('testModule2'); - - angular.modulesMap['testModule1'].controllers.should.be.a.Array; - angular.modulesMap['testModule1'].controllers.should.have.a.lengthOf(0); - - angular.modulesMap['testModule2'].controllers.should.be.a.Array; - angular.modulesMap['testModule2'].controllers.should.have.a.lengthOf(0); - - }) - - it('should have an empty dependencies array when there are no dependencies', function () { - angular - .module('testModule1') - .controller('myController', function () {}) - .controller('myController2', [function () {}]); - - angular.modulesMap['testModule1'].controllers.should.be.a.Array; - angular.modulesMap['testModule1'].controllers.should.have.a.lengthOf(2); - - angular.modulesMap['testModule1'].controllers[0].deps.should.be.a.Array; - angular.modulesMap['testModule1'].controllers[0].deps.should.have.a.lengthOf(0); - - angular.modulesMap['testModule1'].controllers[1].deps.should.be.a.Array; - angular.modulesMap['testModule1'].controllers[1].deps.should.have.a.lengthOf(0); - - }) - - it('property should contain all defined controllers with array definition', function() { - angular - .module('testModule1', []) - .controller('testController1', ['dep1', function (dep1) {}]) - .controller('testController2', ['dep2', 'dep3', function (dep1, dep2) {}]); - - angular.modules.should.have.a.lengthOf(1) - angular.modules[0].should.have.property('name', 'testModule1') - - var testModule1 = angular.modulesMap['testModule1']; - testModule1.controllers.should.have.a.lengthOf(2); - testModule1.controllers[0].should.have.property('name', 'testController1'); - testModule1.controllers[1].should.have.property('name', 'testController2'); - - testModule1.controllers[0].deps.should.be.a.Array; - testModule1.controllers[0].deps.should.have.a.lengthOf(1); - testModule1.controllers[0].deps[0].should.be.equal('dep1') - - testModule1.controllers[1].deps.should.be.a.Array; - testModule1.controllers[1].deps.should.have.a.lengthOf(2); - testModule1.controllers[1].deps[0].should.be.equal('dep2'); - testModule1.controllers[1].deps[1].should.be.equal('dep3'); - }) - - it('property should contain all defined controllers with function definition', function() { - angular - .module('testModule1', []) - .controller('testController1', function (dep1) {}) - .controller('testController2', function (dep2, dep3) {}); - - angular.modules.should.have.a.lengthOf(1) - angular.modules[0].should.have.property('name', 'testModule1') - - var testModule1 = angular.modulesMap['testModule1']; - testModule1.controllers.should.have.a.lengthOf(2); - testModule1.controllers[0].should.have.property('name', 'testController1'); - testModule1.controllers[1].should.have.property('name', 'testController2'); - - testModule1.controllers[0].deps.should.be.a.Array; - testModule1.controllers[0].deps.should.have.a.lengthOf(1); - testModule1.controllers[0].deps[0].should.be.equal('dep1') - - testModule1.controllers[1].deps.should.be.a.Array; - testModule1.controllers[1].deps.should.have.a.lengthOf(2); - testModule1.controllers[1].deps[0].should.be.equal('dep2'); - testModule1.controllers[1].deps[1].should.be.equal('dep3'); - }) - - - it('property should contain all defined controllers with variable array definition', function() { - var testController1 = ['dep1', function (dep1) {}], - testController2 = ['dep2', 'dep3', function (dep2, dep3) {}] - ; - - angular - .module('testModule1', []) - .controller('testController1', testController1) - .controller('testController2', testController2); - - angular.modules.should.have.a.lengthOf(1) - angular.modules[0].should.have.property('name', 'testModule1') - - var testModule1 = angular.modulesMap['testModule1']; - testModule1.controllers.should.have.a.lengthOf(2); - testModule1.controllers[0].should.have.property('name', 'testController1'); - testModule1.controllers[1].should.have.property('name', 'testController2'); - - testModule1.controllers[0].deps.should.be.a.Array; - testModule1.controllers[0].deps.should.have.a.lengthOf(1); - testModule1.controllers[0].deps[0].should.be.equal('dep1') - - testModule1.controllers[1].deps.should.be.a.Array; - testModule1.controllers[1].deps.should.have.a.lengthOf(2); - testModule1.controllers[1].deps[0].should.be.equal('dep2'); - testModule1.controllers[1].deps[1].should.be.equal('dep3'); - }) - - it('property should contain all defined controllers with variable function definition', function() { - function testController1 (dep1) {}; - function testController2 (dep2, dep3) {}; - - angular - .module('testModule1', []) - .controller('testController1', testController1) - .controller('testController2', testController2); - - angular.modules.should.have.a.lengthOf(1) - angular.modules[0].should.have.property('name', 'testModule1') - - var testModule1 = angular.modulesMap['testModule1']; - testModule1.controllers.should.have.a.lengthOf(2); - testModule1.controllers[0].should.have.property('name', 'testController1'); - testModule1.controllers[1].should.have.property('name', 'testController2'); - - testModule1.controllers[0].deps.should.be.a.Array; - testModule1.controllers[0].deps.should.have.a.lengthOf(1); - testModule1.controllers[0].deps[0].should.be.equal('dep1') - - testModule1.controllers[1].deps.should.be.a.Array; - testModule1.controllers[1].deps.should.have.a.lengthOf(2); - testModule1.controllers[1].deps[0].should.be.equal('dep2'); - testModule1.controllers[1].deps[1].should.be.equal('dep3'); - }) - - it('excludes angular dependencies', function () { - function testController1 ($scope, dep1) {}; - var testController2 = ['$scope', 'dep3', function ($scope, dep3) {}]; - - angular - .module('testModule1', []) - .controller('testController1', testController1) - .controller('testController2', testController2); - - var testModule1 = angular.modulesMap['testModule1']; - testModule1.controllers[0].deps.should.be.a.Array; - testModule1.controllers[0].deps.should.have.a.lengthOf(1); - testModule1.controllers[0].deps[0].should.be.equal('dep1'); - - testModule1.controllers[1].deps.should.be.a.Array; - testModule1.controllers[1].deps.should.have.a.lengthOf(1); - testModule1.controllers[1].deps[0].should.be.equal('dep3'); - }) -}) diff --git a/test/fake-angular.js b/test/fake-angular.js index a640a24..83ff661 100644 --- a/test/fake-angular.js +++ b/test/fake-angular.js @@ -1,136 +1,136 @@ -var should = require('should'); +var should = require("should"); describe("angular", function() { - var angular + var angular; beforeEach(function() { - angular = require('../src/fake-angular')() - }) + angular = require("../src/fake-angular")(); + }); it("should be defined", function() { - should.exist(angular) - }) + should.exist(angular); + }); it("should be a different instance every time is required", function() { - var angular1 = require('../src/fake-angular')() - var angular2 = require('../src/fake-angular')() + var angular1 = require("../src/fake-angular")(); + var angular2 = require("../src/fake-angular")(); - angular1.should.not.be.equal(angular2) - - }) + angular1.should.not.be.equal(angular2); + }); describe("module", function() { it("should define a module if called with an array as 2nd argument", function() { - var module = angular.module("testModule", []) - should.exist(module) - }) + var module = angular.module("testModule", []); + should.exist(module); + }); it("should return a defined method if called with a single argument", function() { - angular.module('testModule', []) - should.exist(angular.module('testModule')) - }) - - var methods = ['constant', - 'controller', - 'directive', - 'factory', - 'filter', - 'provider', - 'service', - 'value', - 'run', - 'config' + angular.module("testModule", []); + should.exist(angular.module("testModule")); + }); + + var methods = ["constant", + "controller", + "directive", + "factory", + "filter", + "provider", + "service", + "value", + "run", + "config" ]; - var globalApis = ['lowercase', - 'uppercase', - 'forEach', - 'extend', - 'noop', - 'identity', - 'isUndefined', - 'isDefined', - 'isObject', - 'isString', - 'isNumber', - 'isDate', - 'isArray', - 'isFunction', - 'isElement', - 'copy', - 'equals', - 'bind', - 'toJson', - 'fromJson', - 'bootstrap', - 'injector', - 'element', - 'module' + var globalApis = ["lowercase", + "uppercase", + "forEach", + "extend", + "noop", + "identity", + "isUndefined", + "isDefined", + "isObject", + "isString", + "isNumber", + "isDate", + "isArray", + "isFunction", + "isElement", + "copy", + "equals", + "bind", + "toJson", + "fromJson", + "bootstrap", + "injector", + "element", + "module" ]; describe("should have defined method", function() { methods.forEach(function(method) { it(method, function() { - var module = angular.module('testModule', []); - should.exist(module[method]) - }) - }) - }) + var module = angular.module("testModule", []); + should.exist(module[method]); + }); + }); + }); describe("should have defined methods of global api", function(){ globalApis.forEach(function(method){ it(method, function(){ - should.exist(angular[method]) - angular[method].should.be.a.Function - }) - }) - }) + should.exist(angular[method]); + return angular[method].should.be.a.Function; + }); + }); + }); describe("every method should return module to allow chainability", function() { methods.forEach(function(method) { - it('#' + method, function() { - var module = angular.module('testModule', []); - module.should.be.equal(module[method]()) - }) - }) - }) + it("#" + method, function() { + var module = angular.module("testModule", []); + module.should.be.equal(module[method]()); + }); + }); + }); it("should contains reference to modules on which it depenends", function() { - var module1 = angular.module('testModule1', []) - var modules = angular.module('testModule2', ['testModule1']).modules + var module1 = angular.module("testModule1", []); + var modules = angular.module("testModule2", ["testModule1"]).modules; + - modules.should.have.a.lengthOf(1) - modules[0].should.be.equal('testModule1') - }) + modules.should.have.a.lengthOf(1); + modules[0].should.be.equal(module1.name); + }); - describe('items', function() { - it('should contains defined items', function() { - var module = angular.module('testModule') - .filter('testFilter') - .controller('testController') + describe("items", function() { + it("should contains defined items", function() { + var module = angular.module("testModule") + .filter("testFilter", function () {}) + .controller("testController", function () {}); - module.items.should.have.a.lengthOf(2) - module.items.should.containEql('testFilter') - module.items.should.containEql('testController') - }) - }) + module.items.should.have.a.lengthOf(2); + module.items.should.containEql("testFilter"); + module.items.should.containEql("testController"); + }); + }); describe("requiring module before declaring", function() { it("should return module definition even if module is not already defined", function() { - var module = angular.module('testModule') - should.exist(module) - }) + var module = angular.module("testModule"); + should.exist(module); + }); it("should replace afterwise", function () { - var module1 = angular.module('testModule1') - var module = angular.module('testModule') - var sameModule = angular.module('testModule', ['testModule1']) - - module.should.be.equal(sameModule) - module.modules.should.have.a.lengthOf(1) - }) - }) - }) -}) + angular.module("testModule1"); + var module = angular.module("testModule"); + var sameModule = angular.module("testModule", ["testModule1"]); + + module.should.be.equal(sameModule); + module.modules.should.have.a.lengthOf(1); + }); + }); + }); +}); diff --git a/test/index.js b/test/index.js index 9309791..8425e65 100644 --- a/test/index.js +++ b/test/index.js @@ -1,58 +1,60 @@ -var should = require('should'); +var should = require("should"); + +/*eslint no-unused-expressions: 0, no-unused-vars: 0*/ + describe("angular-modules-graph", function() { - var angularModulesGraph = require('../index.js') + var angularModulesGraph = require("../index.js"); it("should accept an array", function() { - should.exist(angularModulesGraph([])) - }) + should.exist(angularModulesGraph([])); + }); it("should return on object", function() { - var res = angularModulesGraph([]) - res.should.be.an.Object - res.should.have.properties('angular', 'results') - - }) + var res = angularModulesGraph([]); + res.should.be.an.Object; + res.should.have.properties("angular", "results"); + }); describe("return object", function() { describe("angular property", function() { it("should return an object with an angular properties describing the graph ", function() { var script = { - id: 'example', - text: 'angular.module("TestModule",[])' - } - var res = angularModulesGraph([script]) + id: "example", + text: "angular.module('TestModule',[])" + }; + var res = angularModulesGraph([script]); - res.angular.modulesNames.should.containEql('TestModule') - }) - }) + res.angular.modulesNames.should.containEql("TestModule"); + }); + }); - describe('results property', function() { + describe("results property", function() { it("should be an array", function() { - angularModulesGraph([]).results.should.be.an.Array - }) + angularModulesGraph([]).results.should.be.an.Array; + }); it("should contain an item for any input script", function() { var script = { - id: 'example', - text: 'angular.module("TestModule",[])' - } - var res = angularModulesGraph([script]).results - - res.should.have.a.lengthOf(1) - res[0].should.have.properties('id', 'error') - res[0].id.should.be.eql('example') - }) + id: "example", + text: "angular.module('TestModule',[])" + }; + var res = angularModulesGraph([script]).results; + + res.should.have.a.lengthOf(1); + res[0].should.have.properties("id", "error"); + res[0].id.should.be.eql("example"); + }); it("should report if an error occured while evaluating", function() { var script = { - id: 'example', - text: 'undefined_global.module("TestModule",[])' - } - var res = angularModulesGraph([script]).results - - res[0].error.should.be.true - res[0].should.have.properties('exception') - res[0].exception.should.be.an.Exception - }) - }) - }) -}) + id: "example", + text: "undefined_global.module('TestModule',[])" + }; + var res = angularModulesGraph([script]).results; + + res[0].should.have.properties("exception"); + res[0].error.should.be.true; + res[0].exception.should.be.an.Exception; + }); + }); + }); +}); diff --git a/test/mock-example-1.js b/test/mock-example-1.js new file mode 100644 index 0000000..1055fdf --- /dev/null +++ b/test/mock-example-1.js @@ -0,0 +1,71 @@ +/*eslint no-unused-vars: 0, no-unused-expr: 0*/ +var should = require("should"); + +describe("process factories", function() { + var angular; + + function assertions(example1) { + // Factories with no dependencies + example1.factories[0].name.should.be.equal("NoDependenciesService1"); + example1.factories[1].name.should.be.equal("NoDependenciesService2"); + example1.factories[0].deps.should.have.a.lengthOf(0); + example1.factories[1].deps.should.have.a.lengthOf(0); + + // Factories with angular dependencies + example1.factories[2].name.should.be.equal("OneAngularDependencyService1"); + example1.factories[3].name.should.be.equal("OneAngularDependencyService2"); + example1.factories[2].deps.should.have.a.lengthOf(1); + example1.factories[3].deps.should.have.a.lengthOf(1); + example1.factories[2].deps[0].should.be.equal("$http"); + example1.factories[3].deps[0].should.be.equal("$http"); + + // Factories with angular and other dependencies + example1.factories[4].name.should.be.equal("MixedDependenciesService1"); + example1.factories[5].name.should.be.equal("MixedDependenciesService2"); + example1.factories[4].deps.should.have.a.lengthOf(3); + example1.factories[5].deps.should.have.a.lengthOf(3); + + // Controllers with no dependencies + example1.controllers[0].name.should.be.equal("NoDependenciesCtrl1"); + example1.controllers[1].name.should.be.equal("NoDependenciesCtrl2"); + example1.controllers[0].deps.should.have.a.lengthOf(0); + example1.controllers[1].deps.should.have.a.lengthOf(0); + + + // Controllers with angular dependencies + example1.controllers[2].name.should.be.equal("OneAngularDependencyCtrl1"); + example1.controllers[3].name.should.be.equal("OneAngularDependencyCtrl2"); + example1.controllers[2].deps.should.have.a.lengthOf(1); + example1.controllers[3].deps.should.have.a.lengthOf(1); + example1.controllers[2].deps[0].should.be.equal("$http"); + example1.controllers[3].deps[0].should.be.equal("$http"); + + // Controllers with angular and other dependencies + example1.controllers[4].name.should.be.equal("MixedDependenciesCtrl1"); + example1.controllers[5].name.should.be.equal("MixedDependenciesCtrl2"); + example1.controllers[4].deps.should.have.a.lengthOf(3); + example1.controllers[5].deps.should.have.a.lengthOf(3); + } + + describe("in chained definition", function () { + beforeEach(function() { + angular = require("../src/fake-angular")(); + require("../test-mocks/example1-chained")(angular); + }); + + it("process factories names and dependencies", function () { + assertions(angular.modulesMap.example1); + }); + }); + + describe("in not-chained definition", function () { + beforeEach(function() { + angular = require("../src/fake-angular")(); + require("../test-mocks/example1")(angular); + }); + + it("process factories names and dependencies", function () { + assertions(angular.modulesMap.example1); + }); + }); +}); diff --git a/test/services.js b/test/services.js deleted file mode 100644 index 1ccf8d3..0000000 --- a/test/services.js +++ /dev/null @@ -1,98 +0,0 @@ -var should = require('should'); - -describe("factories", function() { - var angular - - beforeEach(function() { - angular = require('../src/fake-angular')() - }) - - it('factories with no dependencies should have an empty array', function () { - function testService () { - return { - attr: "attribute", - getFoo: function (foo) {}, - getBar: function (bar) {} - } - }; - - var testService2 = [function testService () { - return { - attr: "attribute", - getFoo: function (foo) {}, - getBar: function (bar) {} - } - }]; - - angular - .module('testModule1', []) - .factory('testService', testService) - .factory('testService2', testService2); - - - var testModule1 = angular.modulesMap['testModule1']; - testModule1.services[1].deps.should.be.a.Array; - testModule1.services[1].deps.should.have.a.lengthOf(0); - testModule1.services[0].deps.should.be.a.Array; - testModule1.services[0].deps.should.have.a.lengthOf(0); - }) - - it('property should contain all defined services with variable function definition', function() { - function testService (dep1) { - return { - attr: "attribute", - getFoo: function (foo) {}, - getBar: function (bar) {} - } - }; - - angular - .module('testModule1', []) - .factory('testService', testService); - - var testModule1 = angular.modulesMap['testModule1']; - testModule1.services.should.be.a.Array; - testModule1.services.should.have.a.lengthOf(1); - testModule1.services[0].name.should.be.equal('testService'); - testModule1.services[0].deps.should.be.a.Array; - testModule1.services[0].deps.should.have.a.lengthOf(1); - testModule1.services[0].deps[0].should.be.equal('dep1'); - - testModule1.services[0].api.should.be.a.Array; - testModule1.services[0].api.indexOf('attr').should.not.be.equal(-1); - testModule1.services[0].api.indexOf('getFoo').should.not.be.equal(-1); - testModule1.services[0].api.indexOf('getBar').should.not.be.equal(-1); - }) - - it('should exclude angular dependencies', function () { - var testService2 = ['$resource', 'dep2', function ($resource, dep2) { - return { - attr: "attribute", - getFoo: function (foo) {}, - getBar: function (bar) {} - } - }]; - function testService ($http, dep1) { - return { - attr: "attribute", - getFoo: function (foo) {}, - getBar: function (bar) {} - } - }; - - angular - .module('testModule1', []) - .factory('testService', testService) - .factory('testService2', testService2); - - var testModule1 = angular.modulesMap['testModule1']; - testModule1.services[0].deps.should.be.a.Array; - testModule1.services[0].deps.should.have.a.lengthOf(1); - testModule1.services[0].deps[0].should.be.equal('dep1'); - - testModule1.services[1].deps.should.be.a.Array; - testModule1.services[1].deps.should.have.a.lengthOf(1); - testModule1.services[1].deps[0].should.be.equal('dep2'); - - }) -}) diff --git a/test/ui-keypress.js b/test/ui-keypress.js new file mode 100644 index 0000000..38d06a9 --- /dev/null +++ b/test/ui-keypress.js @@ -0,0 +1,32 @@ +/*eslint no-unused-vars: 0, no-unused-expr: 0, no-unused-expressions: 0*/ +var should = require("should"); + +describe("angular ui", function () { + var angular, keypress; + + beforeEach(function() { + angular = require("../src/fake-angular")(); + require("../test-mocks/ui-utils-keypress")(angular); + }); + + it("ui-utils keypress", function () { + var directives = angular.modules[0].directives; + var factories = angular.modules[0].factories; + + // Directives + directives[0].name.should.be.equal("uiKeydown"); + directives[0].deps.should.containEql("keypressHelper"); + directives[1].name.should.be.equal("uiKeypress"); + directives[1].deps.should.containEql("keypressHelper"); + directives[2].name.should.be.equal("uiKeyup"); + directives[2].deps.should.containEql("keypressHelper"); + + // Factories + factories.should.be.an.Array; + factories.should.have.lengthOf(1); + factories[0].name.should.be.equal("keypressHelper"); + factories[0].deps.should.be.an.Array; + factories[0].deps.should.have.lengthOf(1); + factories[0].deps.should.containEql("$parse"); + }); +}); diff --git a/test/ui-router.js b/test/ui-router.js new file mode 100644 index 0000000..82484f2 --- /dev/null +++ b/test/ui-router.js @@ -0,0 +1,133 @@ +/*eslint no-unused-vars: 0, no-unused-expr: 0, no-unused-expressions: 0*/ +var should = require("should"); + +describe("angular ui", function () { + var angular, modules; + + angular = require("../src/fake-angular")(); + require("../test-mocks/ui-router")(angular); + modules = angular.modulesMap; + + it("ui.router.util", function () { + var util = modules["ui.router.util"]; + + // Module dependencies + util.modules.should.be.an.Array; + util.modules.should.have.lengthOf(1); + util.modules.should.containDeep(["ng"]); + + // Services + util.services.should.be.an.Array; + util.services.should.have.lengthOf(2); + util.services[0].name.should.be.equal("$resolve"); + util.services[0].deps.should.containDeep(["$q", "$injector"]); + util.services[1].name.should.be.equal("$templateFactory"); + util.services[1].deps.should.containDeep(["$http", "$templateCache", "$injector"]); + + // Providers + util.providers.should.be.an.Array; + util.providers.should.have.lengthOf(1); + util.providers[0].name.should.be.equal("$urlMatcherFactory"); + util.providers[0].deps.should.be.an.Array; + util.providers[0].deps.should.have.lengthOf(0); + + }); + + it("ui.router.router", function () { + var router = modules["ui.router.router"]; + + // Module dependencies + router.modules.should.be.an.Array; + router.modules.should.have.lengthOf(1); + router.modules.should.containDeep(["ui.router.util"]); + + // Providers + router.providers.should.be.an.Array; + router.providers.should.have.lengthOf(1); + router.providers[0].name.should.be.equal("$urlRouter"); + router.providers[0].deps.should.be.an.Array; + router.providers[0].deps.should.have.lengthOf(2); + router.providers[0].deps.should.containDeep(["$locationProvider", "$urlMatcherFactory"]); + }); + + it("ui.router.state", function () { + var state = modules["ui.router.state"]; + + // Module dependencies + state.modules.should.be.an.Array; + state.modules.should.have.lengthOf(2); + state.modules.should.containDeep(["ui.router.router", "ui.router.util"]); + + // Module dependencies + state.modules.should.be.an.Array; + state.modules.should.have.lengthOf(2); + state.modules.should.containDeep(["ui.router.router", "ui.router.util"]); + + // Filters + state.filters.should.be.an.Array; + state.filters.should.have.lengthOf(2); + state.filters[0].name.should.be.equal("isState"); + state.filters[0].deps.should.be.an.Array; + state.filters[0].deps.should.have.lengthOf(1); + state.filters[0].deps.should.containDeep(["$state"]); + state.filters[1].name.should.be.equal("includedByState"); + state.filters[1].deps.should.be.an.Array; + state.filters[1].deps.should.have.lengthOf(1); + state.filters[1].deps.should.containDeep(["$state"]); + + // Providers + state.providers.should.be.an.Array; + state.providers.should.have.lengthOf(3); + state.providers[0].name.should.be.equal("$state"); + state.providers[0].deps.should.be.an.Array; + state.providers[0].deps.should.have.lengthOf(2); + state.providers[0].deps.should.containDeep(["$urlRouterProvider", "$urlMatcherFactory"]); + state.providers[1].name.should.be.equal("$view"); + state.providers[1].deps.should.be.an.Array; + state.providers[1].deps.should.have.lengthOf(0); + state.providers[1].deps.should.containDeep([]); + state.providers[2].name.should.be.equal("$uiViewScroll"); + state.providers[2].deps.should.be.an.Array; + state.providers[2].deps.should.have.lengthOf(0); + state.providers[2].deps.should.containDeep([]); + + // Directives + state.directives.should.be.an.Array; + state.directives.should.have.lengthOf(5); + state.directives[0].name.should.be.equal("uiView"); + state.directives[0].deps.should.be.an.Array; + state.directives[0].deps.should.have.lengthOf(3); + state.directives[0].deps.should.containDeep(["$state", "$injector", "$uiViewScroll"]); + state.directives[1].name.should.be.equal("uiView"); + state.directives[1].deps.should.be.an.Array; + state.directives[1].deps.should.have.lengthOf(3); + state.directives[1].deps.should.containDeep(["$compile", "$controller", "$state"]); + state.directives[2].name.should.be.equal("uiSref"); + state.directives[2].deps.should.be.an.Array; + state.directives[2].deps.should.have.lengthOf(2); + state.directives[2].deps.should.containDeep(["$state", "$timeout"]); + state.directives[3].name.should.be.equal("uiSrefActive"); + state.directives[3].deps.should.be.an.Array; + state.directives[3].deps.should.have.lengthOf(3); + state.directives[3].deps.should.containDeep(["$state", "$stateParams", "$interpolate"]); + state.directives[4].name.should.be.equal("uiSrefActiveEq"); + state.directives[4].deps.should.be.an.Array; + state.directives[4].deps.should.have.lengthOf(3); + state.directives[4].deps.should.containDeep(["$state", "$stateParams", "$interpolate"]); + }); + + it("ui.router.compat and ui.router", function () { + var compat = modules["ui.router.compat"]; + var main = modules["ui.router"]; + + // Module dependencies + compat.modules.should.be.an.Array; + compat.modules.should.have.lengthOf(1); + compat.modules.should.containDeep(["ui.router"]); + + // Module dependencies + main.modules.should.be.an.Array; + main.modules.should.have.lengthOf(1); + main.modules.should.containDeep(["ui.router"]); + }); +}); From 77509e058aad7af836d54507f3493b8b1d996c4d Mon Sep 17 00:00:00 2001 From: Joao Figueiredo Date: Mon, 6 Oct 2014 14:21:32 +0200 Subject: [PATCH 16/29] Adds use strict to missing files. --- index.js | 2 ++ test/fake-angular.js | 2 ++ test/index.js | 4 ++-- test/mock-example-1.js | 2 ++ test/ui-keypress.js | 2 ++ test/ui-router.js | 2 ++ 6 files changed, 12 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 3f2801e..1a84e6a 100644 --- a/index.js +++ b/index.js @@ -1,4 +1,6 @@ /*eslint no-unused-expressions: 0, no-unused-vars: 0, no-eval: 0*/ +"use strict"; + var angular = require("./src/fake-angular")(), document = {}, window = {}, navigator = {}; diff --git a/test/fake-angular.js b/test/fake-angular.js index 83ff661..58f5c1a 100644 --- a/test/fake-angular.js +++ b/test/fake-angular.js @@ -1,3 +1,5 @@ +"use strict"; + var should = require("should"); describe("angular", function() { diff --git a/test/index.js b/test/index.js index 8425e65..2748cef 100644 --- a/test/index.js +++ b/test/index.js @@ -1,7 +1,7 @@ -var should = require("should"); - /*eslint no-unused-expressions: 0, no-unused-vars: 0*/ +"use strict"; +var should = require("should"); describe("angular-modules-graph", function() { var angularModulesGraph = require("../index.js"); diff --git a/test/mock-example-1.js b/test/mock-example-1.js index 1055fdf..d2baf6f 100644 --- a/test/mock-example-1.js +++ b/test/mock-example-1.js @@ -1,4 +1,6 @@ /*eslint no-unused-vars: 0, no-unused-expr: 0*/ +"use strict"; + var should = require("should"); describe("process factories", function() { diff --git a/test/ui-keypress.js b/test/ui-keypress.js index 38d06a9..fd5cff9 100644 --- a/test/ui-keypress.js +++ b/test/ui-keypress.js @@ -1,4 +1,6 @@ /*eslint no-unused-vars: 0, no-unused-expr: 0, no-unused-expressions: 0*/ +"use strict"; + var should = require("should"); describe("angular ui", function () { diff --git a/test/ui-router.js b/test/ui-router.js index 82484f2..817c7d1 100644 --- a/test/ui-router.js +++ b/test/ui-router.js @@ -1,4 +1,6 @@ /*eslint no-unused-vars: 0, no-unused-expr: 0, no-unused-expressions: 0*/ +"use strict"; + var should = require("should"); describe("angular ui", function () { From 154900303fc6865660e27f1af7268db7e67e1099 Mon Sep 17 00:00:00 2001 From: Joao Figueiredo Date: Mon, 6 Oct 2014 14:23:46 +0200 Subject: [PATCH 17/29] Fixes tasks so that grunt test doesn't do the watching. --- grunt/aliases.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/grunt/aliases.yml b/grunt/aliases.yml index e28b288..a087f92 100644 --- a/grunt/aliases.yml +++ b/grunt/aliases.yml @@ -1,4 +1,7 @@ +default: + - eslint + - mochaTest:test + test: - eslint - mochaTest:test - - watch From a13cea762a27d3530948ca6731c95392c81afaf6 Mon Sep 17 00:00:00 2001 From: Joao Figueiredo Date: Mon, 6 Oct 2014 19:41:41 +0200 Subject: [PATCH 18/29] Released 0.2.0 --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 1020ef5..bdb6617 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "angular-architecture-graph", - "version": "0.1.0", + "version": "0.2.0", "description": "Create a graph of an angular project's architecture", "main": "index.js", "scripts": { @@ -30,6 +30,7 @@ "grunt-contrib-watch": "^0.6.1", "grunt-eslint": "^1.1.0", "grunt-mocha-test": "^0.10.0", + "grunt-publish": "0.0.5", "load-grunt-config": "^0.13.1", "load-grunt-tasks": "^0.6.0", "mocha": "^1.18.2", From 78cd8f3a137aa76c9e67a7d97ff8344db7176182 Mon Sep 17 00:00:00 2001 From: Joao Figueiredo Date: Fri, 10 Oct 2014 14:55:12 +0200 Subject: [PATCH 19/29] Adds reference to grunt tasks on README.md. --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8eaa4aa..08fdb3a 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,8 @@ This project is a node utility that analyses an angular project and exports a gr ]) ``` +It was designed with grunt in mind. Use it with [grunt-angular-modules-graph](https://github.com/lucalanca/grunt-angular-modules-graph). + 3. Do whatever you want with the resulted architecture object ## About @@ -29,4 +31,4 @@ This project is a node utility that analyses an angular project and exports a gr This project was originally forked from , extracted from [@carlo-colombo's](https://github.com/carlo-colombo) [angular-modules-graph](https://github.com/carlo-colombo/angular-modules-graph) ## License: -MIT \ No newline at end of file +MIT From 0671ca978a1956903d241375a237bd4989d27594 Mon Sep 17 00:00:00 2001 From: Joao Figueiredo Date: Fri, 10 Oct 2014 15:29:26 +0200 Subject: [PATCH 20/29] Adds option to hide angular dependencies. --- index.js | 5 +- src/api.js | 27 ++++++ src/fake-angular.js | 12 ++- src/module.js | 17 +++- test-mocks/example1.js | 12 +-- test/fake-angular.js | 206 +++++++++++++++++++++-------------------- test/mock-example-1.js | 24 +++++ 7 files changed, 188 insertions(+), 115 deletions(-) diff --git a/index.js b/index.js index 1a84e6a..a627edd 100644 --- a/index.js +++ b/index.js @@ -1,11 +1,12 @@ /*eslint no-unused-expressions: 0, no-unused-vars: 0, no-eval: 0*/ "use strict"; +module.exports = function(scripts, options) { -var angular = require("./src/fake-angular")(), + + var angular = require("./src/fake-angular")(options), document = {}, window = {}, navigator = {}; -module.exports = function(scripts) { var results = scripts.map(function(content) { try { diff --git a/src/api.js b/src/api.js index b2861d1..fd5f64c 100644 --- a/src/api.js +++ b/src/api.js @@ -33,5 +33,32 @@ module.exports = { "bootstrap", "injector", "element" + ], + angularServices: [ + "$anchorScroll", + "$animate", + "$cacheFactory", + "$templateCache", + "$compile", + "$controller", + "$document", + "$exceptionHandler", + "$filter", + "$http", + "$httpBackend", + "$interpolate", + "$interval", + "$locate", + "$location", + "$log", + "$parse", + "$q", + "$rootElement", + "$rootScope", + "$sceDelegate", + "$sce", + "$templateRequest", + "$timout", + "$window" ] }; diff --git a/src/fake-angular.js b/src/fake-angular.js index 2af7037..0401afd 100644 --- a/src/fake-angular.js +++ b/src/fake-angular.js @@ -1,14 +1,22 @@ "use strict"; +var _ = require("lodash"); + var utils = require("./utils"); var api = require("./api"); var Module = require("./module"); -module.exports = function () { +module.exports = function (options) { + + options = _.merge({ + hideAngularServices: false + }, options || {}); + var angular = { modules: [], modulesMap: {}, modulesNames: [], + options: options, module: function(name, deps) { var module; @@ -20,7 +28,7 @@ module.exports = function () { } // First time we see this module } else { - module = new Module(name,deps); + module = new Module(name,deps, options); this.modulesNames.push(name); this.modulesMap[name] = module; this.modules.push(module); diff --git a/src/module.js b/src/module.js index bedc7b5..974fb59 100644 --- a/src/module.js +++ b/src/module.js @@ -1,10 +1,12 @@ "use strict"; +var _ = require("lodash"); + var pluralize = require("pluralize"); var utils = require("./utils"); var api = require("./api"); -function Module(name, deps) { +function Module(name, deps, options) { this.name = name; this.modules = deps; this.items = []; @@ -14,6 +16,8 @@ function Module(name, deps) { this.filters = []; this.providers = []; this.directives = []; + + this.options = options; } // Adds module methods @@ -32,10 +36,17 @@ api.methods.forEach(function(method) { if (!name) { return this; } - var deps2 = utils.parseAngularDeps(deps).deps; + deps = utils.parseAngularDeps(deps).deps; + + // Exclude angular services from dependencies + if (this.options.hideAngularServices) { + deps = _.filter(deps, function (dep) { + return !_.contains(api.angularServices, dep); + }); + } this[pluralize(method)].push({ "name": name, - "deps": deps2 + "deps": deps }); this.items.push(name); return this; diff --git a/test-mocks/example1.js b/test-mocks/example1.js index 530f275..1a5f4b3 100644 --- a/test-mocks/example1.js +++ b/test-mocks/example1.js @@ -24,12 +24,12 @@ module.exports = function(angular) { angular.module("example1") - angular.module("example1").factory("NoDependenciesService1", NoDependenciesService); - angular.module("example1").factory("NoDependenciesService2", NoDependenciesService2); - angular.module("example1").factory("OneAngularDependencyService1", OneAngularDependencyService1); - angular.module("example1").factory("OneAngularDependencyService2", OneAngularDependencyService2); - angular.module("example1").factory("MixedDependenciesService1", MixedDependenciesService1); - angular.module("example1").factory("MixedDependenciesService2", MixedDependenciesService2); + angular.module("example1").factory("NoDependenciesService1", NoDependenciesService); + angular.module("example1").factory("NoDependenciesService2", NoDependenciesService2); + angular.module("example1").factory("OneAngularDependencyService1", OneAngularDependencyService1); + angular.module("example1").factory("OneAngularDependencyService2", OneAngularDependencyService2); + angular.module("example1").factory("MixedDependenciesService1", MixedDependenciesService1); + angular.module("example1").factory("MixedDependenciesService2", MixedDependenciesService2); diff --git a/test/fake-angular.js b/test/fake-angular.js index 58f5c1a..92c95ec 100644 --- a/test/fake-angular.js +++ b/test/fake-angular.js @@ -6,132 +6,134 @@ describe("angular", function() { var angular; - beforeEach(function() { - angular = require("../src/fake-angular")(); - }); + describe("default options", function () { + beforeEach(function() { + angular = require("../src/fake-angular")(); + }); - it("should be defined", function() { - should.exist(angular); - }); + it("should be defined", function() { + should.exist(angular); + }); - it("should be a different instance every time is required", function() { - var angular1 = require("../src/fake-angular")(); - var angular2 = require("../src/fake-angular")(); + it("should be a different instance every time is required", function() { + var angular1 = require("../src/fake-angular")(); + var angular2 = require("../src/fake-angular")(); - angular1.should.not.be.equal(angular2); - }); + angular1.should.not.be.equal(angular2); + }); - describe("module", function() { + describe("module", function() { - it("should define a module if called with an array as 2nd argument", function() { - var module = angular.module("testModule", []); - should.exist(module); - }); + it("should define a module if called with an array as 2nd argument", function() { + var module = angular.module("testModule", []); + should.exist(module); + }); - it("should return a defined method if called with a single argument", function() { - angular.module("testModule", []); - should.exist(angular.module("testModule")); - }); + it("should return a defined method if called with a single argument", function() { + angular.module("testModule", []); + should.exist(angular.module("testModule")); + }); - var methods = ["constant", - "controller", - "directive", - "factory", - "filter", - "provider", - "service", - "value", - "run", - "config" - ]; - - var globalApis = ["lowercase", - "uppercase", - "forEach", - "extend", - "noop", - "identity", - "isUndefined", - "isDefined", - "isObject", - "isString", - "isNumber", - "isDate", - "isArray", - "isFunction", - "isElement", - "copy", - "equals", - "bind", - "toJson", - "fromJson", - "bootstrap", - "injector", - "element", - "module" - ]; - - describe("should have defined method", function() { - methods.forEach(function(method) { - it(method, function() { - var module = angular.module("testModule", []); - should.exist(module[method]); + var methods = ["constant", + "controller", + "directive", + "factory", + "filter", + "provider", + "service", + "value", + "run", + "config" + ]; + + var globalApis = ["lowercase", + "uppercase", + "forEach", + "extend", + "noop", + "identity", + "isUndefined", + "isDefined", + "isObject", + "isString", + "isNumber", + "isDate", + "isArray", + "isFunction", + "isElement", + "copy", + "equals", + "bind", + "toJson", + "fromJson", + "bootstrap", + "injector", + "element", + "module" + ]; + + describe("should have defined method", function() { + methods.forEach(function(method) { + it(method, function() { + var module = angular.module("testModule", []); + should.exist(module[method]); + }); }); }); - }); - describe("should have defined methods of global api", function(){ - globalApis.forEach(function(method){ - it(method, function(){ - should.exist(angular[method]); - return angular[method].should.be.a.Function; + describe("should have defined methods of global api", function(){ + globalApis.forEach(function(method){ + it(method, function(){ + should.exist(angular[method]); + return angular[method].should.be.a.Function; + }); }); }); - }); - describe("every method should return module to allow chainability", function() { - methods.forEach(function(method) { - it("#" + method, function() { - var module = angular.module("testModule", []); - module.should.be.equal(module[method]()); + describe("every method should return module to allow chainability", function() { + methods.forEach(function(method) { + it("#" + method, function() { + var module = angular.module("testModule", []); + module.should.be.equal(module[method]()); + }); }); }); - }); - it("should contains reference to modules on which it depenends", function() { - var module1 = angular.module("testModule1", []); - var modules = angular.module("testModule2", ["testModule1"]).modules; + it("should contains reference to modules on which it depenends", function() { + var module1 = angular.module("testModule1", []); + var modules = angular.module("testModule2", ["testModule1"]).modules; - modules.should.have.a.lengthOf(1); - modules[0].should.be.equal(module1.name); - }); + modules.should.have.a.lengthOf(1); + modules[0].should.be.equal(module1.name); + }); - describe("items", function() { - it("should contains defined items", function() { - var module = angular.module("testModule") - .filter("testFilter", function () {}) - .controller("testController", function () {}); + describe("items", function() { + it("should contains defined items", function() { + var module = angular.module("testModule") + .filter("testFilter", function () {}) + .controller("testController", function () {}); - module.items.should.have.a.lengthOf(2); - module.items.should.containEql("testFilter"); - module.items.should.containEql("testController"); + module.items.should.have.a.lengthOf(2); + module.items.should.containEql("testFilter"); + module.items.should.containEql("testController"); + }); }); - }); - describe("requiring module before declaring", function() { - it("should return module definition even if module is not already defined", function() { - var module = angular.module("testModule"); - should.exist(module); - }); + describe("requiring module before declaring", function() { + it("should return module definition even if module is not already defined", function() { + var module = angular.module("testModule"); + should.exist(module); + }); - it("should replace afterwise", function () { - angular.module("testModule1"); - var module = angular.module("testModule"); - var sameModule = angular.module("testModule", ["testModule1"]); + it("should replace afterwise", function () { + angular.module("testModule1"); + var module = angular.module("testModule"); + var sameModule = angular.module("testModule", ["testModule1"]); - module.should.be.equal(sameModule); - module.modules.should.have.a.lengthOf(1); + module.should.be.equal(sameModule); + module.modules.should.have.a.lengthOf(1); + }); }); }); }); diff --git a/test/mock-example-1.js b/test/mock-example-1.js index d2baf6f..eac4ad1 100644 --- a/test/mock-example-1.js +++ b/test/mock-example-1.js @@ -70,4 +70,28 @@ describe("process factories", function() { assertions(angular.modulesMap.example1); }); }); + + describe("parses options", function () { + + var options = { + hideAngularServices: true + }; + + beforeEach(function() { + angular = require("../src/fake-angular")(options); + require("../test-mocks/example1")(angular); + }); + + it("should hide angular services", function () { + angular.options.hideAngularServices.should.be.equal(true); + + var MixedDependenciesCtrl1 = angular.modulesMap.example1.controllers[4]; + + // Note, the $http dependency was excluded + MixedDependenciesCtrl1.deps.should.have.a.lengthOf(2); + MixedDependenciesCtrl1.deps[0].should.be.equal("ServiceX"); + MixedDependenciesCtrl1.deps[1].should.be.equal("ServiceY"); + }); + + }); }); From f4a23a76677cef60f043c8868fcb60ffedf6f422 Mon Sep 17 00:00:00 2001 From: Joao Figueiredo Date: Fri, 10 Oct 2014 15:32:27 +0200 Subject: [PATCH 21/29] Bumps version on package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bdb6617..8716eba 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "angular-architecture-graph", - "version": "0.2.0", + "version": "0.2.1", "description": "Create a graph of an angular project's architecture", "main": "index.js", "scripts": { From a4ef0c8a33913bae38aa60fb38106fa38000d3c9 Mon Sep 17 00:00:00 2001 From: Joao Figueiredo Date: Fri, 10 Oct 2014 15:43:05 +0200 Subject: [PATCH 22/29] Fixes typo on $timeout service. --- src/api.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api.js b/src/api.js index fd5f64c..1b9a5b2 100644 --- a/src/api.js +++ b/src/api.js @@ -58,7 +58,7 @@ module.exports = { "$sceDelegate", "$sce", "$templateRequest", - "$timout", + "$timeout", "$window" ] }; From a098fe36e1c9f3249ad6dd36aa97604f503ec5b7 Mon Sep 17 00:00:00 2001 From: Joao Figueiredo Date: Fri, 10 Oct 2014 15:44:08 +0200 Subject: [PATCH 23/29] Bump package.json version. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8716eba..0191b25 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "angular-architecture-graph", - "version": "0.2.1", + "version": "0.2.2", "description": "Create a graph of an angular project's architecture", "main": "index.js", "scripts": { From 6ff036cdca74ae148527ee383dcc9fd369578e9e Mon Sep 17 00:00:00 2001 From: Joao Figueiredo Date: Fri, 10 Oct 2014 16:15:42 +0200 Subject: [PATCH 24/29] Adds ng as angular service. Trims services for defensive programming. --- src/api.js | 3 ++- src/module.js | 8 +++++++- src/utils.js | 5 ++--- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/api.js b/src/api.js index 1b9a5b2..feb8421 100644 --- a/src/api.js +++ b/src/api.js @@ -59,6 +59,7 @@ module.exports = { "$sce", "$templateRequest", "$timeout", - "$window" + "$window", + "ng" ] }; diff --git a/src/module.js b/src/module.js index 974fb59..35e095c 100644 --- a/src/module.js +++ b/src/module.js @@ -8,7 +8,6 @@ var api = require("./api"); function Module(name, deps, options) { this.name = name; - this.modules = deps; this.items = []; this.controllers = []; this.services = []; @@ -18,6 +17,13 @@ function Module(name, deps, options) { this.directives = []; this.options = options; + + if (this.options.hideAngularServices) { + deps = _.filter(deps, function (dep) { + return !_.contains(api.angularServices, dep); + }); + } + this.modules = deps; } // Adds module methods diff --git a/src/utils.js b/src/utils.js index fdafcc6..6969cff 100644 --- a/src/utils.js +++ b/src/utils.js @@ -19,9 +19,8 @@ function parseAngularDeps (angularDeps) { } if (deps && deps.length) { deps.forEach(function (dep) { - // if (dep.split("")[0] !== "$") { - depsProcessed.push(dep.replace(" ", "")); - // } + dep = dep.trim(); + depsProcessed.push(dep); }); } return { deps: depsProcessed, definition: definition }; From d4532edb3a0b7a6c0e41dfbbcfae8e1d50ea708a Mon Sep 17 00:00:00 2001 From: Joao Figueiredo Date: Fri, 10 Oct 2014 16:16:35 +0200 Subject: [PATCH 25/29] Bumps package version. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0191b25..9fb4393 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "angular-architecture-graph", - "version": "0.2.2", + "version": "0.2.3", "description": "Create a graph of an angular project's architecture", "main": "index.js", "scripts": { From 47e62140111c97d75130ae0f4a5cae8c0f2910a8 Mon Sep 17 00:00:00 2001 From: Vladimir Feskov Date: Fri, 14 Aug 2015 16:44:19 +0200 Subject: [PATCH 26/29] Wrap eval call in window context --- index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index a627edd..bedbca1 100644 --- a/index.js +++ b/index.js @@ -10,7 +10,9 @@ module.exports = function(scripts, options) { var results = scripts.map(function(content) { try { - eval(content.text); + (function() { + eval(content.text); + }.call(window)); } catch (e) { return { id: content.id, From 1f09a1cc3d896602da04eb2e86e3084025e233aa Mon Sep 17 00:00:00 2001 From: Vladimir Feskov Date: Wed, 25 Nov 2015 11:50:15 +0100 Subject: [PATCH 27/29] Ignore ngMaterial module dependency It creates too much noise in the graph and gives no benefit --- src/fake-angular.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/fake-angular.js b/src/fake-angular.js index 0401afd..e042dce 100644 --- a/src/fake-angular.js +++ b/src/fake-angular.js @@ -20,6 +20,12 @@ module.exports = function (options) { module: function(name, deps) { var module; + if(deps) { + var ngMaterialDepIndex = deps.indexOf('ngMaterial'); + if(ngMaterialDepIndex !== -1) { + deps.splice(ngMaterialDepIndex, 1); + } + } // Module was inserted before if (this.modulesNames.indexOf(name) !== -1) { module = this.modulesMap[name]; From 970a3a3a8126609b3cc75b09b47157848b98bfe4 Mon Sep 17 00:00:00 2001 From: Vincent Ogloblinsky Date: Fri, 28 Oct 2016 03:32:30 +0200 Subject: [PATCH 28/29] feat(application) : add .component API support --- src/api.js | 21 ++++++++++++++++++++- src/module.js | 3 ++- test/fake-angular.js | 1 + 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/api.js b/src/api.js index feb8421..9a474dc 100644 --- a/src/api.js +++ b/src/api.js @@ -3,6 +3,7 @@ module.exports = { "constant", "controller", "directive", + "component", "factory", "filter", "provider", @@ -36,30 +37,48 @@ module.exports = { ], angularServices: [ "$anchorScroll", + "$aria", "$animate", + "$animateCss", + "$attrs", "$cacheFactory", - "$templateCache", "$compile", + "$cookieStore", + "$cookies", + "$componentController", "$controller", "$document", + "$element", "$exceptionHandler", "$filter", "$http", "$httpBackend", + "$httpParamSerializer", + "$httpParamSerializerJQLike", "$interpolate", "$interval", + "$injector", + "$jsonpCallbacks", "$locate", "$location", "$log", + "$modelOptions", "$parse", + "$provide", "$q", + "$resource", "$rootElement", "$rootScope", "$sceDelegate", "$sce", + "$scope", + "$swipe", + "$templateCache", "$templateRequest", "$timeout", + "$touch", "$window", + "$xhrFactory", "ng" ] }; diff --git a/src/module.js b/src/module.js index 35e095c..95f43fd 100644 --- a/src/module.js +++ b/src/module.js @@ -15,6 +15,7 @@ function Module(name, deps, options) { this.filters = []; this.providers = []; this.directives = []; + this.components = []; this.options = options; @@ -37,7 +38,7 @@ api.methods.forEach(function(method) { }; }); -["controller", "factory", "service", "filter", "provider", "directive"].forEach(function (method) { +["controller", "factory", "service", "filter", "provider", "directive", "component"].forEach(function (method) { Module.prototype[ method ] = function (name, deps) { if (!name) { return this; diff --git a/test/fake-angular.js b/test/fake-angular.js index 92c95ec..79d2746 100644 --- a/test/fake-angular.js +++ b/test/fake-angular.js @@ -37,6 +37,7 @@ describe("angular", function() { var methods = ["constant", "controller", "directive", + "component", "factory", "filter", "provider", From 704a4be651aa197bba48dc164bc7a67a8f6970b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joa=CC=83o=20Figueiredo?= Date: Fri, 28 Oct 2016 13:27:34 +0200 Subject: [PATCH 29/29] Bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9fb4393..f962f28 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "angular-architecture-graph", - "version": "0.2.3", + "version": "0.2.4", "description": "Create a graph of an angular project's architecture", "main": "index.js", "scripts": {