Commit be58d7ce authored by Leonardo Giovanetti's avatar Leonardo Giovanetti Committed by Arthur Verschaeve

Updates to `AngularJS+RequireJS` app

Replaces `angular-loader` with a better RequireJS setup

Also adds tests (#254)

Closes #1277
Fixes #1276
parent fcb062f4
......@@ -3,9 +3,6 @@ node_modules/*
node_modules/angular/*
!node_modules/angular/angular.js
node_modules/angular-loader/*
!node_modules/angular-loader/angular-loader.js
node_modules/requirejs/*
!node_modules/requirejs/require.js
......
......@@ -6,9 +6,6 @@
<link rel="stylesheet" href="node_modules/todomvc-common/base.css">
<link rel="stylesheet" href="node_modules/todomvc-app-css/index.css">
<style>[ng-cloak] { display: none; }</style>
<!-- Include angular-loader to allow modules to be loaded in any order. -->
<script src="node_modules/angular-loader/angular-loader.js"></script>
</head>
<body>
<section id="todoapp" ng-controller="TodoController">
......
/*global define*/
/*global require*/
'use strict';
define(['angular'], function (angular) {
return angular.module('todomvc', []);
require([
'angular'
], function (angular) {
require([
'controllers/todo',
'directives/todoFocus',
'directives/todoEscape',
'services/todoStorage'
], function (todoCtrl, todoFocusDir, todoEscapeDir, todoStorageSrv) {
angular
.module('todomvc', [todoFocusDir, todoEscapeDir, todoStorageSrv])
.controller('TodoController', todoCtrl);
angular.bootstrap(document, ['todomvc']);
});
});
......@@ -7,9 +7,11 @@
* - exposes the model to the template and provides event handlers
*/
define(['app', 'services/todoStorage'], function (app) {
return app.controller('TodoController', ['$scope', '$location', 'todoStorage', 'filterFilter',
function TodoController($scope, $location, todoStorage, filterFilter) {
define([
'angular'
], function (angular) {
return ['$scope', '$location', 'todoStorage', 'filterFilter',
function ($scope, $location, todoStorage, filterFilter) {
var todos = $scope.todos = todoStorage.get();
$scope.newTodo = '';
......@@ -89,5 +91,5 @@ define(['app', 'services/todoStorage'], function (app) {
});
};
}
]);
];
});
/*global define*/
define(['app'], function (app) {
'use strict';
'use strict';
app.directive('todoEscape', function () {
/**
* Directive that catches the "Escape" key on the element applied to and evaluates the expression it binds to.
*/
define([
'angular'
], function (angular) {
var moduleName = 'TodoEscapeDirective';
angular
.module(moduleName, [])
.directive('todoEscape', function () {
var ESCAPE_KEY = 27;
return function (scope, elem, attrs) {
......@@ -17,4 +26,5 @@ define(['app'], function (app) {
});
};
});
return moduleName;
});
/*global define*/
'use strict';
/**
* Directive that places focus on the element it is applied to when the expression it binds to evaluates to true.
*/
define(['app'], function (app) {
app.directive('todoFocus', ['$timeout', function ($timeout) {
define([
'angular'
], function (angular) {
var moduleName = 'TodoFocusDirective';
angular
.module(moduleName, [])
.directive('todoFocus', ['$timeout', function ($timeout) {
return function (scope, elem, attrs) {
scope.$watch(attrs.todoFocus, function (newval) {
if (newval) {
......@@ -15,4 +22,5 @@ define(['app'], function (app) {
});
};
}]);
return moduleName;
});
......@@ -9,9 +9,6 @@ require.config({
angular: {
exports: 'angular'
}
}
});
require(['angular', 'app', 'controllers/todo', 'directives/todoFocus', 'directives/todoEscape'], function (angular) {
angular.bootstrap(document, ['todomvc']);
},
deps: ['app']
});
......@@ -4,8 +4,14 @@
/**
* Services that persists and retrieves TODOs from localStorage.
*/
define(['app'], function (app) {
app.factory('todoStorage', function () {
define([
'angular'
], function (angular) {
var moduleName = 'TodoStorageModule';
angular
.module(moduleName, [])
.factory('todoStorage', function () {
var STORAGE_ID = 'todos-angularjs-requirejs';
return {
......@@ -18,4 +24,5 @@ define(['app'], function (app) {
}
};
});
return moduleName;
});
/**
* @license AngularJS v1.3.13
* (c) 2010-2014 Google, Inc. http://angularjs.org
* License: MIT
*/
(function() {'use strict';
/**
* @description
*
* This object provides a utility for producing rich Error messages within
* Angular. It can be called as follows:
*
* var exampleMinErr = minErr('example');
* throw exampleMinErr('one', 'This {0} is {1}', foo, bar);
*
* The above creates an instance of minErr in the example namespace. The
* resulting error will have a namespaced error code of example.one. The
* resulting error will replace {0} with the value of foo, and {1} with the
* value of bar. The object is not restricted in the number of arguments it can
* take.
*
* If fewer arguments are specified than necessary for interpolation, the extra
* interpolation markers will be preserved in the final string.
*
* Since data will be parsed statically during a build step, some restrictions
* are applied with respect to how minErr instances are created and called.
* Instances should have names of the form namespaceMinErr for a minErr created
* using minErr('namespace') . Error codes, namespaces and template strings
* should all be static strings, not variables or general expressions.
*
* @param {string} module The namespace to use for the new minErr instance.
* @param {function} ErrorConstructor Custom error constructor to be instantiated when returning
* error from returned function, for cases when a particular type of error is useful.
* @returns {function(code:string, template:string, ...templateArgs): Error} minErr instance
*/
function minErr(module, ErrorConstructor) {
ErrorConstructor = ErrorConstructor || Error;
return function() {
var code = arguments[0],
prefix = '[' + (module ? module + ':' : '') + code + '] ',
template = arguments[1],
templateArgs = arguments,
message, i;
message = prefix + template.replace(/\{\d+\}/g, function(match) {
var index = +match.slice(1, -1), arg;
if (index + 2 < templateArgs.length) {
return toDebugString(templateArgs[index + 2]);
}
return match;
});
message = message + '\nhttp://errors.angularjs.org/1.3.13/' +
(module ? module + '/' : '') + code;
for (i = 2; i < arguments.length; i++) {
message = message + (i == 2 ? '?' : '&') + 'p' + (i - 2) + '=' +
encodeURIComponent(toDebugString(arguments[i]));
}
return new ErrorConstructor(message);
};
}
/**
* @ngdoc type
* @name angular.Module
* @module ng
* @description
*
* Interface for configuring angular {@link angular.module modules}.
*/
function setupModuleLoader(window) {
var $injectorMinErr = minErr('$injector');
var ngMinErr = minErr('ng');
function ensure(obj, name, factory) {
return obj[name] || (obj[name] = factory());
}
var angular = ensure(window, 'angular', Object);
// We need to expose `angular.$$minErr` to modules such as `ngResource` that reference it during bootstrap
angular.$$minErr = angular.$$minErr || minErr;
return ensure(angular, 'module', function() {
/** @type {Object.<string, angular.Module>} */
var modules = {};
/**
* @ngdoc function
* @name angular.module
* @module ng
* @description
*
* The `angular.module` is a global place for creating, registering and retrieving Angular
* modules.
* All modules (angular core or 3rd party) that should be available to an application must be
* registered using this mechanism.
*
* When passed two or more arguments, a new module is created. If passed only one argument, an
* existing module (the name passed as the first argument to `module`) is retrieved.
*
*
* # Module
*
* A module is a collection of services, directives, controllers, filters, and configuration information.
* `angular.module` is used to configure the {@link auto.$injector $injector}.
*
* ```js
* // Create a new module
* var myModule = angular.module('myModule', []);
*
* // register a new service
* myModule.value('appName', 'MyCoolApp');
*
* // configure existing services inside initialization blocks.
* myModule.config(['$locationProvider', function($locationProvider) {
* // Configure existing providers
* $locationProvider.hashPrefix('!');
* }]);
* ```
*
* Then you can create an injector and load your modules like this:
*
* ```js
* var injector = angular.injector(['ng', 'myModule'])
* ```
*
* However it's more likely that you'll just use
* {@link ng.directive:ngApp ngApp} or
* {@link angular.bootstrap} to simplify this process for you.
*
* @param {!string} name The name of the module to create or retrieve.
* @param {!Array.<string>=} requires If specified then new module is being created. If
* unspecified then the module is being retrieved for further configuration.
* @param {Function=} configFn Optional configuration function for the module. Same as
* {@link angular.Module#config Module#config()}.
* @returns {module} new module with the {@link angular.Module} api.
*/
return function module(name, requires, configFn) {
var assertNotHasOwnProperty = function(name, context) {
if (name === 'hasOwnProperty') {
throw ngMinErr('badname', 'hasOwnProperty is not a valid {0} name', context);
}
};
assertNotHasOwnProperty(name, 'module');
if (requires && modules.hasOwnProperty(name)) {
modules[name] = null;
}
return ensure(modules, name, function() {
if (!requires) {
throw $injectorMinErr('nomod', "Module '{0}' is not available! You either misspelled " +
"the module name or forgot to load it. If registering a module ensure that you " +
"specify the dependencies as the second argument.", name);
}
/** @type {!Array.<Array.<*>>} */
var invokeQueue = [];
/** @type {!Array.<Function>} */
var configBlocks = [];
/** @type {!Array.<Function>} */
var runBlocks = [];
var config = invokeLater('$injector', 'invoke', 'push', configBlocks);
/** @type {angular.Module} */
var moduleInstance = {
// Private state
_invokeQueue: invokeQueue,
_configBlocks: configBlocks,
_runBlocks: runBlocks,
/**
* @ngdoc property
* @name angular.Module#requires
* @module ng
*
* @description
* Holds the list of modules which the injector will load before the current module is
* loaded.
*/
requires: requires,
/**
* @ngdoc property
* @name angular.Module#name
* @module ng
*
* @description
* Name of the module.
*/
name: name,
/**
* @ngdoc method
* @name angular.Module#provider
* @module ng
* @param {string} name service name
* @param {Function} providerType Construction function for creating new instance of the
* service.
* @description
* See {@link auto.$provide#provider $provide.provider()}.
*/
provider: invokeLater('$provide', 'provider'),
/**
* @ngdoc method
* @name angular.Module#factory
* @module ng
* @param {string} name service name
* @param {Function} providerFunction Function for creating new instance of the service.
* @description
* See {@link auto.$provide#factory $provide.factory()}.
*/
factory: invokeLater('$provide', 'factory'),
/**
* @ngdoc method
* @name angular.Module#service
* @module ng
* @param {string} name service name
* @param {Function} constructor A constructor function that will be instantiated.
* @description
* See {@link auto.$provide#service $provide.service()}.
*/
service: invokeLater('$provide', 'service'),
/**
* @ngdoc method
* @name angular.Module#value
* @module ng
* @param {string} name service name
* @param {*} object Service instance object.
* @description
* See {@link auto.$provide#value $provide.value()}.
*/
value: invokeLater('$provide', 'value'),
/**
* @ngdoc method
* @name angular.Module#constant
* @module ng
* @param {string} name constant name
* @param {*} object Constant value.
* @description
* Because the constant are fixed, they get applied before other provide methods.
* See {@link auto.$provide#constant $provide.constant()}.
*/
constant: invokeLater('$provide', 'constant', 'unshift'),
/**
* @ngdoc method
* @name angular.Module#animation
* @module ng
* @param {string} name animation name
* @param {Function} animationFactory Factory function for creating new instance of an
* animation.
* @description
*
* **NOTE**: animations take effect only if the **ngAnimate** module is loaded.
*
*
* Defines an animation hook that can be later used with
* {@link ngAnimate.$animate $animate} service and directives that use this service.
*
* ```js
* module.animation('.animation-name', function($inject1, $inject2) {
* return {
* eventName : function(element, done) {
* //code to run the animation
* //once complete, then run done()
* return function cancellationFunction(element) {
* //code to cancel the animation
* }
* }
* }
* })
* ```
*
* See {@link ng.$animateProvider#register $animateProvider.register()} and
* {@link ngAnimate ngAnimate module} for more information.
*/
animation: invokeLater('$animateProvider', 'register'),
/**
* @ngdoc method
* @name angular.Module#filter
* @module ng
* @param {string} name Filter name.
* @param {Function} filterFactory Factory function for creating new instance of filter.
* @description
* See {@link ng.$filterProvider#register $filterProvider.register()}.
*/
filter: invokeLater('$filterProvider', 'register'),
/**
* @ngdoc method
* @name angular.Module#controller
* @module ng
* @param {string|Object} name Controller name, or an object map of controllers where the
* keys are the names and the values are the constructors.
* @param {Function} constructor Controller constructor function.
* @description
* See {@link ng.$controllerProvider#register $controllerProvider.register()}.
*/
controller: invokeLater('$controllerProvider', 'register'),
/**
* @ngdoc method
* @name angular.Module#directive
* @module ng
* @param {string|Object} name Directive name, or an object map of directives where the
* keys are the names and the values are the factories.
* @param {Function} directiveFactory Factory function for creating new instance of
* directives.
* @description
* See {@link ng.$compileProvider#directive $compileProvider.directive()}.
*/
directive: invokeLater('$compileProvider', 'directive'),
/**
* @ngdoc method
* @name angular.Module#config
* @module ng
* @param {Function} configFn Execute this function on module load. Useful for service
* configuration.
* @description
* Use this method to register work which needs to be performed on module loading.
* For more about how to configure services, see
* {@link providers#provider-recipe Provider Recipe}.
*/
config: config,
/**
* @ngdoc method
* @name angular.Module#run
* @module ng
* @param {Function} initializationFn Execute this function after injector creation.
* Useful for application initialization.
* @description
* Use this method to register work which should be performed when the injector is done
* loading all modules.
*/
run: function(block) {
runBlocks.push(block);
return this;
}
};
if (configFn) {
config(configFn);
}
return moduleInstance;
/**
* @param {string} provider
* @param {string} method
* @param {String=} insertMethod
* @returns {angular.Module}
*/
function invokeLater(provider, method, insertMethod, queue) {
if (!queue) queue = invokeQueue;
return function() {
queue[insertMethod || 'push']([provider, method, arguments]);
return moduleInstance;
};
}
});
};
});
}
setupModuleLoader(window);
})(window);
/**
* Closure compiler type information
*
* @typedef { {
* requires: !Array.<string>,
* invokeQueue: !Array.<Array.<*>>,
*
* service: function(string, Function):angular.Module,
* factory: function(string, Function):angular.Module,
* value: function(string, *):angular.Module,
*
* filter: function(string, Function):angular.Module,
*
* init: function(Function):angular.Module
* } }
*/
angular.Module;
......@@ -2,7 +2,6 @@
"private": true,
"dependencies": {
"angular": "^1.3.13",
"angular-loader": "^1.3.13",
"requirejs": "^2.1.15",
"todomvc-app-css": "^1.0.0",
"todomvc-common": "^1.0.1"
......
......@@ -34,3 +34,10 @@ Get help from other AngularJS users:
* [AngularjS on Google +](https://plus.google.com/+AngularJS/posts)
_If you have other helpful links to share, or find any of the links above no longer work, please [let us know](https://github.com/tastejs/todomvc/issues)._
## Unit Tests
To run the test suite, run these commands:
npm install
npm test
'use strict';
module.exports = function (grunt) {
grunt.loadNpmTasks('grunt-contrib-jasmine');
grunt.initConfig({
jasmine: {
unit: {
options: {
specs: [
'unit/**/*Spec.js'
],
template: require('grunt-template-jasmine-requirejs'),
templateOptions: {
requireConfigFile: '../js/main.js',
requireConfig: {
baseUrl: '../js',
paths: {
jquery: '../test/node_modules/jquery/dist/jquery',
'angular-mocks': '../test/node_modules/angular-mocks/angular-mocks'
},
shim: {
'angular-mocks': ['angular']
},
deps: ['']
}
},
summary: true
}
}
}
});
};
{
"private": true,
"devDependencies": {
"grunt": "^0.4.5",
"grunt-contrib-jasmine": "^0.8.1",
"grunt-template-jasmine-requirejs": "^0.2.0",
"angular-mocks": "^1.3.13",
"jquery": "^2.1.4"
},
"scripts": {
"test": "grunt jasmine"
}
}
define([
'directives/todoEscape',
'jquery',
'angular-mocks'
], function (todoEscapeDir, jQuery) {
'use strict';
beforeEach(module(todoEscapeDir));
var triggerKeyDown = function (element, keyCode) {
var e = jQuery.Event("keydown");
e.keyCode = keyCode;
element.triggerHandler(e);
};
describe('todoEscape directive', function () {
var scope, compile, browser;
beforeEach(inject(function ($rootScope, $compile, $browser) {
scope = $rootScope.$new();
compile = $compile;
browser = $browser;
}));
it('should evaluate the expression binded to the directive', function () {
var someValue = false,
el = angular.element('<input todo-escape="doSomething()">');
scope.doSomething = function () {
someValue = !someValue;
};
compile(el)(scope);
triggerKeyDown(el, 27);
expect(someValue).toBe(true);
});
});
});
define([
'directives/todoFocus',
'angular-mocks'
], function (todoFocusDir) {
'use strict';
beforeEach(module(todoFocusDir));
describe('todoFocus directive', function () {
var scope, compile, browser;
beforeEach(inject(function ($rootScope, $compile, $browser) {
scope = $rootScope.$new();
compile = $compile;
browser = $browser;
}));
it('should focus on truthy expression', function () {
var el = angular.element('<input todo-focus="focus">');
scope.focus = false;
compile(el)(scope);
expect(browser.deferredFns.length).toBe(0);
scope.$apply(function () {
scope.focus = true;
});
expect(browser.deferredFns.length).toBe(1);
});
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment