3. Scopes
La mayoría de las aplicaciones web están basadas en el patrón MVC (Model-View-Controller). Sin embargo, MVC no es un patrón muy preciso, sino un patrón arquitectural de alto nivel. Además, existen muchas variaciones del patrón original, siendo los más conocidos MVP y MVVM. Para añadir un poco más de confusión, muchos frameworks y desarrolladores interpretan estos patrones de manera diferente. Esto da como resultado que el nombre MVC se use para describir diferentes arquitecturas y aproximaciones.
El equipo de AngularJS ha sido más pragmático con su aproximación, definiendo el framework como basado en el patrón MVW (Model-View-Whatever).
3.1. Hola mundo (otra vez)
Veamos nuevamente un típico ejemplo de Hola mundo para desgranar todos los elementos que intervienen:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 <!DOCTYPE html>
<html>
<head>
<title>Hola mundo</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body ng-app="holaMundo">
<div ng-controller="SaludaCtrl">
Saluda a: <input type="text" ng-model="nombre" /><br/><br/>
<h1>¡Hola, {{ nombre }}!</h1>
</div>
<script src="/angular.js/angular.js" type="text/javascript"></script>
<script>
angular
.module('holaMundo', [])
.controller('SaludaCtrl', function($scope){
$scope.nombre = 'mundo';
});
</script>
</body>
</html>
3.2. El objeto Scope
Siempre que queramos exponer un modelo a la vista (plantilla), haremos uso del objeto $scope
.
Para ello, simplemente deberemos asignar nuevas propiedades a una instancia de este objeto.
Al hacerlo, ya estarán los valores disponibles en la plantilla.
Además, podemos también exponer funcionalidades a la vista, asociando funciones como propiedades de un $scope
.
Por ejemplo, podríamos crear un getter para la variable nombre
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27 <!DOCTYPE html>
<html>
<head>
<title>Hola mundo</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body ng-app="holaMundo">
<div ng-controller="SaludaCtrl">
Saluda a: <input type="text" ng-model="nombre" /><br/><br/>
<h1>¡Hola, {{ getNombre() }}!</h1>
</div>
<script src="/angular.js/angular.js" type="text/javascript"></script>
<script>
angular
.module('holaMundo', [])
.controller('SaludaCtrl', function($scope){
$scope.nombre = 'mundo';
$scope.getNombre = function() {
return $scope.nombre.toUpperCase();
};
});
</script>
</body>
</html>
De esta forma, podemos controlar precisamente qué parte del modelo y qué operaciones queremos exponer a la capa de presentación. Conceptualmente, un $scope tiene un comportamiento muy similar al de un ViewModel en el patrón MVVM.
3.2.1. Jerarquía y herencia de scopes
Cuando iniciamos una aplicación con AngularJS, se genera un scope a nivel de aplicación, llamado $rootScope. Desde ese momento, todo nuevo scope será hijo del $rootScope.
Podemos instanciar un nuevo $scope en cualquier momento a través del método $new(). Cuando lo instanciamos, un $scope hereda las propiedades de su padre, como vemos en este ejemplo:
1
2
3
4
5
6
7
8
9
10
11
12 var padre = $rootScope;
var hijo = padre.$new();
padre.saludo = "Hola";
hijo.nombre = "Mundo";
console.log(hijo.saludo); // --> 'Hola'
hijo.saludo = "Bienvenido";
console.log(hijo.saludo); // --> 'Bienvenido'
console.log(padre.saludo); // --> 'Hola'
Como hemos visto, podemos instanciar un nuevo scope en cualquier momento, lo normal es que AngularJS lo haga por nosotros cuando lo necesitemos. Por ejemplo, la directiva ngController instancia un nuevo scope, que será el que inyecte en el controlador. En este caso, el scope será hijo del $rootScope.
A las directivas que crean nuevos scopes se las conoce como scope creating directives, y como hemos dicho será el propio AngularJS el encargado de crear los scopes por nosotros cuando se encuentre con una de estas directivas en el árbol del DOM.
Los scopes forman una estructura de árbol, cuya raíz siempre será el |
Ahora que sabemos que algunas directivas crean nuevos scopes hijos, quizá nos preguntemos por qué tanta complejidad.
Para entenderlo, echemos un ojo a este ejemplo que utiliza la directiva ng-repeat
:
El controlador sería:
1
2
3
4
5
6
7 var peopleCtrl = function($scope){
$scope.people = [
{ name:'Alex', subject: 'Angular', hours: 20},
{ name:'Otto', subject: 'Backbone', hours: 20},
{ name:'Domingo', subject: 'JPA', hours: 15}
];
};
Nuestra plantilla tendría la siguiente forma:
1
2
3 <ul ng-controller="peopleCtrl">
<li ng-repeat="person in people">{{person.name}} da la asignatura: {{person.subject}} ({{ person.hours}} horas)</li>
</ul>
La directiva ng-repeat
nos permite iterar sobre una colección de, en este caso, personas.
Mientras itera, irá creando nuevos elementos en el DOM.
El problema es que si utilizáramos el mismo $scope
, estaríamos sobreescribiendo el valor de la variable persona
.
AngularJS soluciona este problema creando un nuevo $scope
para cada elemento de la colección.
Como se ha comentado, los scopes generan una jerarquía en forma de árbol, similar a la de los elementos del DOM.
La extensión de AngularJS para Chrome nos permite verla:

Podemos ver en el pantallazo que cada scope (delimitado por un recuadro rojo) tiene su propio conjunto de valores del modelo.
Así, cada item tiene su propio namespace, donde cada <li>
posee un scope
propio donde se puede definir la variable person
.
Otra característica interesante de los objetos scope
es que toda propiedad que
definamos en un scope
será visible para sus descendientes. Es muy interesante
porque hace que no sea necesario redefinir elementos a medida que creamos scopes
hijos.
Siguiendo con el ejemplo anterior, podemos calcular, en el scope
del controlador padre,
el número total de horas en base al conjunto de gente:
1
2
3 $scope.hours = $scope.people.reduce(function(value, person){
return value ` person.hours;
}, 0);
En este caso, se ha hecho uso del método reduce de un Array. Este método fue introducido en la versión 5 de ECMAScript, con lo que puede haber problemas de retrocompatibilidad con algunos navegadores. Aquí podemos ver una tabla de compatibilidad de todas las funciones de ECMAScript 5 con los navegadores más usados del mercado. La mayoría de estas funciones pueden implementarse en los navegadores que no las soportan de primeras, maximizando así la compatibilidad. Existen multiud de librerías que ya las implementan. Una de ellas es es5-shim. Siempre que se necesite hacer uso de estas funcionalidades y sea necesaria retrocompatibilidad, es muy recomendable usar librerías de este tipo. |
Como habíamos comentado, este total de horas se propagará a los ámbitos hijos, que podrán hacer uso de él para, por ejemplo, determinar el porcentaje del total que supondrá cada asignatura:
1
2
3 <ul ng-controller="PeopleCtrl">
<li ng-repeat="person in people">{{person.name}} da la asignatura: {{person.subject}} ({{ person.hours}} horas - {{person.hours / hours * 100 | number:2}}%)</li>
</ul>
La herencia de scopes sigue el mismo patrón que la herencia prototípica de JavaScript: si no encontramos una propiedad en el objeto, subimos por el árbol de la jerarquía hasta dar con ella.
La herencia resulta muy sencilla de usar cuando estamos leyendo, pero sin embargo cuando estamos escribiendo puede darnos algún problema. Supongamos el siguiente bloque de código:
1
2
3
4
5
6
7
8
9
10
11
12
13
14 <div ng-app="myApp">
<form name="myForm" ng-controller="Ctrl">
Input dentro de un switch:
<span ng-switch="show">
<input ng-switch-when="true" type="text" ng-model="myModel" />
</span>
<br/>
Input fuera del switch:
<input type="text" ng-model="myModel" />
<br/>
Valor:
{{myModel}}
</form>
</div>
1
2
3
4
5
6 angular
.module('myApp')
.controllert('Ctrl', function Ctrl($scope) {
$scope.show = true;
$scope.myModel = 'hello';
});
Si manipulamos el segundo input
, todos los elementos se modificarán a la vez.
Pero, ¿qué sucede si modificamos el primero, y luego el segundo nuevamente? Parece
como que el primero queda desconectado del resto. De hecho, se crea una nueva
variable en el scope
hijo que hace que esto funcione de esta manera. Podéis hacer la
prueba usando el inspector de AngularJS para Chrome.
Esto se debe a la herencia prototípica (prototipal inheritance a partir de ahora)
de JavaScript, y todas las reglas que se aplican a ésta, se aplican a los scopes,
que al fin y al cabo son objetos JavaScript. El scope
no es el modelo, sino que
referencia al modelo. Por tanto, en el momento que modificamos el primer input,
que tiene un scope
propio, estamos referenciando a un nuevo modelo.
Esto es fácil de solucionar, haciendo uso de objetos. Podemos redeclarar el
objeto myModel
de la siguiente manera:
1
2
3
4
5
6 angular
.module('myApp')
.controllert('Ctrl', function Ctrl($scope) {
$scope.show = true;
$scope.myModel = { value: 'hello'};
});
Y usarlo así en nuestra vista:
1
2
3
4
5
6
7
8
9
10
11
12
13
14 <div ng-app="myApp">
<form name="myForm" ng-controller="Ctrl">
Input dentro de un switch:
<span ng-switch="show">
<input ng-switch-when="true" type="text" ng-model="myModel.value" />
</span>
<br/>
Input fuera del switch:
<input type="text" ng-model="myModel.value" />
<br/>
Valor:
{{ myModel.value }}
</form>
</div>
De esta manera, estaremos referenciando siempre al mismo objeto y no tendremos elementos desconectados sin querer.
Existe otra manera, que es hacer uso del elemento parent
. Éste hace referencia
al ámbito padre, y lo podríamos llamar de la siguiente manera en el switch:
1
2
3 <span ng-switch="show">
<input ng-switch-when="true" type="text" ng-model="$parent.myModel" />
</span>
Sin embargo, esto resolvería nuestros problemas sólo si el dato estuviera en el
ámbito padre, no si el padre también lo hubiera heredado. Además, no podemos estar
seguros al 100% que parent
va a ser el ámbito superior, ya que puede que estemos
empleando alguna directiva que haya creado un scope
adicional.
Podemos encontrar más información acerca de esto en este enlace También, en este vídeo, Miško Hevery hace una serie de reflexiones sobre cómo trabajar con los scopes. |
3.2.2. Propagación de eventos
Como hemos comentado, en nuestra aplicación se generará un árbol de objetos scope
similar a la estructura del DOM. En la raíz de este árbol se encuentra el objeto
$rootScope
Podemos usar esta jerarquía para transmitir eventos dentro del árbol, tanto en
dirección ascendente con el método scope.$emit
como descendente con scope.$broadcast
.
La captura de estos eventos se realiza con el método scope.$on
.

La función scope.$emit(name, args);
envía un evento hacia arriba en la jearquía,
notificando a todos los listeners. El ciclo de vida del evento comienza en aquel
scope
que ha llamado a $emit
. Este evento irá hacia arriba hasta llegar al
$rootScope
, y todos los que hayan dado de alta un listener y estén en el camino
del evento serán notificados. Los suscritos a un evento pueden cancelarlo.
Por su parte, la función scope.$broadcast(name, args);
envía un evento hacia abajo
en la jearquía, notificando todos los listeners herederos.
Ambos métodos tienen los mismos argumentos:
Param | Tipo | Detalles |
---|---|---|
|
|
Nombre del evento que se propaga. |
|
|
Uno o más argumentos, que se propagarán con el evento. |
Para suscribirnos a un evento, lo hacemos con scope.$on(eventName, listener)
.
Los parámetros son:
Param | Tipo | Detalles |
---|---|---|
|
|
Nombre del evento al que nos suscribimos |
|
|
Función a invocar cuando se recibe el evento. |
El objeto event
que se le pasa al listener tiene los siguientes atributos:
-
targetScope
: elscope
en el que el evento fue emitido o difundido -
currentScope
: elscope
que maneja el evento. -
name
: nombre del evento -
stopPropagation
: esta función cancela el evneto y hace que no se siga propagando. -
preventDefault
: establece el flagdefaultPrevented
atrue
. -
defaultPrevented
: valdrátrue
si se ha llamado a la funciónpreventDefault
.
Eventos de AngularJS
Dentro del framework, existen tres eventos que se emiten
-
$includeContentRequested
-
$includeContentLoaded
-
$viewContentLoaded
y siente eventos que se difunden
-
$locationChangeStart
-
$locationChangeSuccess
-
$routeUpdate
-
$routeChangeStart
-
$routeChangeSuccess
-
$routeChangeError
-
$destroy
Podemos ver que se usan escasamemnte en el core de AngularJS. Pese a ser una manera sencilla de intercambiar datos entre controladores, debemos evaluar si es la mejor opción. Por ejemplo, en muchos casos puede ser útil el uso del two-way data binding para obtener una solución más sencilla.
Esto nos lleva a otro método interesante del objeto scope
. Es el método watch(watchExpression, [listener], [objectEquality])
,
que registra un listener que se ejecuta cada vez que el resultado de la expresión
watchExpression cambia.
La función watchExpression
se llama en cada iteración del ciclo de vida de Angular,
y debe devolver el valor que queramos observar.
El listener
se ejecuta sólo cuando el valor de la watchExpression
ha variado
desde su última ejecución. Esta inecualidad se determina según la función angular.equals
También, se hace uso de la función angular.copy
para guardar el objeto, para utilizarlo en la siguiente iteración de la comparación.
La watchExpression
debe ser lo más sencilla posible, ya que de otra
manera podríamos tener problemas de rendimiento de rendimiento y memoria.
El listener
puede modificar el modelo si así lo desea, lo que podría implicar
la activación de otros listeners, re-lanzando los watchers hasta que no se detecta
ningún cambio. Para prevenir entrar en bucles infinitos, existe un límite de re-lanzado
de iteraciones, que es 10.
El siguiente ejemplo hace uso de una expresión y de una función de evaluación para observar una serie de cambios, y realizar una acción al respecto, que será contabilizar el número de cambios realizados sobre la variable.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45 <!DOCTYPE html>
<html ng-app="ses03.watch">
<head lang="en">
<meta charset="UTF-8">
<title></title>
</head>
<body ng-controller="WatchCtrl">
<p> <label>Nombre <input type="text" ng-model="name"/></label> </p>
<p> Contador de cambios en el nombre: {{ counter }} </p>
<p> <label>Alimento <input type="text" ng-model="food"/></label> </p>
<p> Contador de cambios en el alimento: {{ foodCounter }} </p>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.19/angular.min.js"></script>
<script>
angular
.module('ses03.watch', [])
.controller('WatchCtrl', function ($scope) {
$scope.name = 'alex';
$scope.counter = 0;
//Usaremos una expresión para evaluar
$scope.$watch('name', function (newValue, oldValue) {
$scope.counter++;
});
// Usaremos una función para evaluar
$scope.food = 'paella';
$scope.foodCounter = 0;
$scope.$watch(
function () { return $scope.food.length; }, //Función de evaluación
function (newValue, oldValue) { //Listener
if (newValue !== oldValue) {
$scope.foodCounter++;
}
}
);
});
</script>
</body>
</html>
3.2.3. El ciclo de vida del scope
El flujo normal de un browser cuando se recibe un evento es que éste termine llamando a una función JavaScript de callback. Una vez ésta ha finalizado, el browser renderiza el DOM de nuevo y espera a recibir más eventos.
Cuando se hace esta llamada JavaScript al navegador, el código se ejecuta fuera
del contexto de ejecución de AngularJS, lo que significa que AngularJS no tiene ni
idea de que se haya modificado el modelo. Para poder procesar estas modificaciones
en el modelo, hay que hacer que todo lo que se ha hecho fuera del contexto de ejecución
de AngularJS entre dentro de él a través del método $apply
. Sólo las modificaciones del
modelo que hagamos dentro de un método $apply
serán tenidas en cuenta por AngularJS.
Por ejemplo, una directiva como ng-click
que escucha eventos del DOM, debe evaluar la expresión
dentro de un método $apply
. Así lo podemos ver En su código fuente.
Tras evaluar la expresión el método $apply
realiza un $digest
. En esta fase, el
scope
examina todas las expresiones $watch
y compara sus resultados con los
valores previos de manera asíncrona. Esto significa que una asignación como $scope.username = 'admin'
no lanzará inmediatamente el listener de $watch('username')
. En lugar de eso, se
retrasará hasta la fase de $digest
, de manera que se unifican las actualizaciones
del modelo, y se garantiza que se ejecute una función $watch
a la vez. Si un
$watch
cambia el modelo, forzará un ciclo $digest
adicional.
Esto podemos resumirlo en las cinco fases por las que pasa una aplicación en AngularJS:
Creación
El $injector
crea el objeto rootScope
durante el application bootstrap.
Cuando se produce el linkado de plantillas, algunas directivas crearán nuevos scopes.
Registro de watchers
Durante el linkado de plantillas, las directivas suelen registrar watchers. Éstos se usarán para propagar los valores del modelo al DOM.
Mutación del modelo
Para observar correctamente las mutaciones, habría que hacerlo dentro de scope.$apply()
.
Afortunadamente para nosotros, la API de AngularJS lo hace implícitamente, de manera
que no es necesario hacerlo dentro de nuestros controladores si estamos realizando
alguna tarea síncrona, o si estamos realizando tareas asíncronas con los servicios $http
,
$timeout
o $interval
.
Observación de la mutación
Al final de $apply
, AngularJS realiza un ciclo $digest
en el rootScope
que
se propagará posteriormente a todos los hijos. Durante este ciclo, todas las expresiones
en un $watch
se evaluarán para observar cambios en el modelo y, si esta se detecta,
se invocará al listener.
Destrucción
Cuando no se necesita más un scope
hijo, su creador tiene la responsabilidad de
destruirlo mediante una llamada a scope.destroy()
. Esto detendrá la propagación
llamadas $digest
al hijo, y permitirá la llamada al recolector de basura para eliminar
la memoria usada.
3.3. La notación controller-as
Hasta ahora, los controladores que hemos visto son una especie de clases que gestionan los cambios entre el modelo y la vista, utilizando el objeto scope para esta comunicación.
Desde la versión 1.2 de AngularJS, podemos desvincular el Controlador aún más del scope. Veamos el siguiente ejemplo:
1
2
3
4
5
6 ---
// <div ng-controller="MainCtrl"></div>
app.controller('MainCtrl', function ($scope) {
$scope.title = 'Título';
});
---
Y comparémoslo con el siguiente, donde el Controlador está totalmente desvinculado del $scope:
1
2
3
4
5 ---
app.controller('MainCtrl', function () {
this.title = 'Some title';
});
---
3.3.1. Controladores como clases
Podemos instanciar una clase en JavaScript de la siguiente manera:
1
2
3
4 ---
var myClass = function () {
this.title = 'Class title';
}
var myInstance = new myClass(); ---
De esta manera podemos utilizar la instancia myInstance para acceder a métodos y propiedades de la clase. Ésto mismo es lo que nos permite la sintaxis Controller As:
1
2
3
4
5
6 ---
// we declare as usual, just using the `this` Object instead of `$scope`
app.controller('MainCtrl', function () {
this.title = 'Some title';
});
---
Cuando instanciamos un controlador en el DOM, tenemos que hacerlo contra una variable:
1
2
3
4
5
6 ---
<div ng-controller="MainCtrl as main">
// MainCtrl no esiste, sólo la instancia 'main'
{{ main.title }}
</div>
---
3.3.2. Controladores anidados
Cuando tenemos controladores anidados es cuando vemos las ventajas de la sintaxis controller as. Muchas veces tenemos que acceder a la propiedad $parent del scope en que nos encontramos para obtener lo que queremos:
1
2
3
4
5
6
7
8
9
10
11 ---
<div ng-controller="MainCtrl">
{{ title }}
<div ng-controller="AnotherCtrl">
{{ title }}
<div ng-controller="YetAnotherCtrl">
{{ title }}
</div>
</div>
</div>
---
Aquí podemos hacernos un lío y no saber muy bien a qué variable title estamos accediendo. Sin embargo, con la sintaxis controller as estamos definiendo un namespace que nos clarifica perfectamente a qué estamos accediendo:
1
2
3
4
5
6
7
8
9
10
11 ---
<div ng-controller="MainCtrl as main">
{{ main.title }}
<div ng-controller="AnotherCtrl as another">
{{ another.title }}
<div ng-controller="YetAnotherCtrl as yet">
{{ yet.title }}
</div>
</div>
</div>
---
Con esta mecánica también podemos acceder a los scopes padres de una manera sencilla:
1
2
3
4
5
6
7
8
9
10
11
12
13
14 ---
<div ng-controller="MainCtrl">
{{ title }}
<div ng-controller="AnotherCtrl">
Scope title: {{ title }}
Parent title: {{ $parent.title }}
<div ng-controller="YetAnotherCtrl">
{{ title }}
Parent title: {{ $parent.title }}
Parent parent title: {{ $parent.$parent.title }}
</div>
</div>
</div>
---
E incluso hacer cosas más mágicas:
1
2
3
4
5
6
7
8
9
10
11
12
13
14 ---
<div ng-controller="MainCtrl as main">
{{ main.title }}
<div ng-controller="AnotherCtrl as another">
Scope title: {{ another.title }}
Parent title: {{ main.title }}
<div ng-controller="YetAnotherCtrl as yet">
Scope title: {{ yet.title }}
Parent title: {{ another.title }}
Parent parent title: {{ main.title }}
</div>
</div>
</div>
---
No hacky $parent calls. If a Controller’s position in the DOM/stack were to change, the position in sequential $parent.$parent.$parent.$parent may change! Accessing the scope lexically makes perfect sense.
$watchers/$scope methods The first time I used the Controller as syntax I was like “yeah, awesome!”, but then to use scope watchers or methods (such as $watch, $broadcast, $on etc.) we need to dependency inject $scope. Gargh, this is what we tried so hard to get away from. But then I realised this was awesome.
The way the Controller as syntax works, is by binding the Controller to the current $scope rather than it being all one $scope-like class-like Object. For me, the key is the separation between the class and special Angular features.
This means I can have my pretty class-like Controller:
app.controller('MainCtrl', function () { this.title = 'Some title'; }); When I need something above and beyond generic bindings, I introduce the magnificent $scope dependency to do something special, rather than ordinary.
Those special things include all the $scope methods, let’s look at an example:
app.controller('MainCtrl', function ($scope) { this.title = 'Some title'; $scope.$on('someEventFiredFromElsewhere', function (event, data) { // do something! }); }); Ironing a quirk Interestingly enough, whilst writing this I wanted to provide a $scope.$watch() example. Doing this usually is very simple, but using the Controller as syntax doesn’t work quite as expected:
app.controller('MainCtrl', function ($scope) { this.title = 'Some title'; // doesn’t work! $scope.$watch('title', function (newVal, oldVal) {}); // doesn’t work! $scope.$watch('this.title', function (newVal, oldVal) {}); }); Uh oh! So what do we do? Interestingly enough I was reading the other day, and you can actually pass in a function as the first argument of a $watch():
app.controller('MainCtrl', function ($scope) { this.title = 'Some title'; // hmmm, a function $scope.$watch(function () {}, function (newVal, oldVal) {}); }); Which means we can return our this.title reference:
app.controller('MainCtrl', function ($scope) {
this.title = 'Some title';
// nearly there…
$scope.$watch(function () {
return this.title; // this
isn’t the this
above!!
}, function (newVal, oldVal) {});
});
Let’s change some execution context using angular.bind():
app.controller('MainCtrl', function ($scope) {
this.title = 'Some title';
// boom
$scope.$watch(angular.bind(this, function () {
return this.title; // this
IS the this
above!!
}), function (newVal, oldVal) {
// now we will pickup changes to newVal and oldVal
});
});
3.4. Ejercicios
Aplica el tag scopes
a la versión que quieres que se corrija.
3.4.1. Calculadora (0,66 puntos)
Este ejercicio lo realizaremos en una carpeta llamada calculator
.
Completa el siguiente código para implementar una calculadora que haga sumas, restas, multiplicaciones y divisiones.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 <div ng-app="calculatorApp">
<h1>Calculadora</h1>
<div ng-controller="CalcController">
<div>
<label>Primer operando <input type="number" /></label>
</div>
<div>
<label>Segundo operando operando <input type="number" /></label>
</div>
<div>
<button ng-click="">Suma</button>
<button ng-click="">Resta</button>
<button ng-click="">Multiplicación</button>
<button ng-click="">División</button>
</div>
<h2>Resultado: XXX</h2>
</div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 angular
.module('calculatorApp', [])
.controller('CalcController', function CalcController($scope) {
$scope.add = function() {
};
$scope.substract = function(a,b) {
};
$scope.divide = function() {
};
$scope.multiply = function(a,b) {
};
})
3.4.2. Carrito de la compra (0,67 puntos)
Este ejercicio lo realizaremos en una carpeta llamada shoppingcart
.
Vamos a hacer uso de la función $watch
que nos ofrece el scope
para implementar un sencillo carro de la compra.
Disponemos de una plantilla ya hecha, que muestra un listado de productos (generados aleatoriamente con JSON Generator). También, disponemos de un controlador donde tenemos el listado de productos, una serie de variables y una función addToCart
vacía.
Tendremos que implementar la función addToCart
, para que añada ítems al carro. Además, implementaremos un watcher que observará cambios en el tamaño de dicho array. Cuando éstos se produzcan, actualizaremos la variable $scope.totalItems
al número de ítems del carro. También, actualizaremos el valor de la variable $scope.total
, con el importe total de los productos. Se recomienda hacer uso de la función Array.prototype.reduce
para calcular este total.
La plantilla de nuestro índice será:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 <div ng-app="org.expertojava.carrito">
<div ng-controller="CartCtrl">
<h2>{{totalItems}} ítems en la cesta ({{total}} €)</h2>
<div ng-repeat="product in products" style="float:left">
<div>
<img ng-src="{{product.picture}}" alt="" />
<p>
{{product.brand}} {{product.name}}
<br/>
{{product.price}} €
</p>
<button ng-click="addToCart(product)">Añadir a la cesta</button>
</div>
</div>
</div>
</div>
Por su parte, la plantilla de nuestro controlador será:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176 angular
.module('org.expertojava.carrito', [])
.controller('CartCtrl', function CartCtrl($scope) {
$scope.total = 0;
$scope.totalItems = 0;
$scope.cart = [];
$scope.addToCart = function(product){
//TODO: ADD TO CART
};
//TODO: WATCH
$scope.products = [
{
"_id": "54c688af49814edb036a2c33",
"price": 145.4,
"picture": "http://lorempixel.com/200/200/food/?id=846.9758",
"brand": "adidas",
"name": "Zone Job",
"rating": 4
},
{
"_id": "54c688afeffb1bad23bea9f7",
"price": 137.24,
"picture": "http://lorempixel.com/200/200/technics/?id=155.1389",
"brand": "nike",
"name": "Zoomair",
"rating": 0
},
{
"_id": "54c688af5f342fa2c06d2f3b",
"price": 80.47,
"picture": "http://lorempixel.com/200/200/food/?id=927.9926",
"brand": "reebok",
"name": "Volit",
"rating": 2
},
{
"_id": "54c688afbe7e2107363d0945",
"price": 93.53,
"picture": "http://lorempixel.com/200/200/animals/?id=387.7504",
"brand": "nike",
"name": "Konkis",
"rating": 4
},
{
"_id": "54c688af92223b7f877f096f",
"price": 94.82,
"picture": "http://lorempixel.com/200/200/nightlife/?id=296.9771",
"brand": "adidas",
"name": "Stockstring",
"rating": 3
},
{
"_id": "54c688af24de4b9fc39e0d48",
"price": 109.24,
"picture": "http://lorempixel.com/200/200/city/?id=427.4133",
"brand": "adidas",
"name": "Dong-Phase",
"rating": 1
},
{
"_id": "54c688afee99272b911e93fd",
"price": 92.19,
"picture": "http://lorempixel.com/200/200/nature/?id=580.5475",
"brand": "adidas",
"name": "Duozoozap",
"rating": 0
},
{
"_id": "54c688af3593d3f6a34bc2a4",
"price": 82.37,
"picture": "http://lorempixel.com/200/200/nightlife/?id=366.9091",
"brand": "reebok",
"name": "X-dom",
"rating": 3
},
{
"_id": "54c688af804d847b847935ac",
"price": 76.53,
"picture": "http://lorempixel.com/200/200/nature/?id=971.7978",
"brand": "nike",
"name": "Konkis",
"rating": 3
},
{
"_id": "54c688af96ba1759662c4274",
"price": 90.01,
"picture": "http://lorempixel.com/200/200/city/?id=37.581",
"brand": "reebok",
"name": "Ecooveit",
"rating": 4
},
{
"_id": "54c688afa4ee53c977c9ca3a",
"price": 81.28,
"picture": "http://lorempixel.com/200/200/transport/?id=752.8523",
"brand": "adidas",
"name": "Superstrong",
"rating": 1
},
{
"_id": "54c688af85e103fa79c83752",
"price": 134.79,
"picture": "http://lorempixel.com/200/200/fashion/?id=358.5133",
"brand": "reebok",
"name": "Superstrong",
"rating": 5
},
{
"_id": "54c688af04a986612841b7dc",
"price": 76.37,
"picture": "http://lorempixel.com/200/200/cats/?id=912.0469",
"brand": "reebok",
"name": "Fresh-Home",
"rating": 0
},
{
"_id": "54c688af5605253556078cff",
"price": 147.47,
"picture": "http://lorempixel.com/200/200/transport/?id=884.8266",
"brand": "adidas",
"name": "Touch-Hold",
"rating": 2
},
{
"_id": "54c688afa71c0978b878efd6",
"price": 106.83,
"picture": "http://lorempixel.com/200/200/fashion/?id=598.1251",
"brand": "nike",
"name": "Saorunlab",
"rating": 2
},
{
"_id": "54c688af0450426ca2d7680a",
"price": 72.76,
"picture": "http://lorempixel.com/200/200/food/?id=831.3375",
"brand": "adidas",
"name": "Konkis",
"rating": 1
},
{
"_id": "54c688afe30ff32f443e7c20",
"price": 83.46,
"picture": "http://lorempixel.com/200/200/sports/?id=604.4555",
"brand": "adidas",
"name": "Rank Sololax",
"rating": 5
},
{
"_id": "54c688afd24500b5cbc85148",
"price": 77.19,
"picture": "http://lorempixel.com/200/200/abstract/?id=333.8619",
"brand": "reebok",
"name": "Ecooveit",
"rating": 0
},
{
"_id": "54c688afb08a3950c86aa47f",
"price": 82.05,
"picture": "http://lorempixel.com/200/200/food/?id=118.5947",
"brand": "nike",
"name": "Zonedex",
"rating": 5
},
{
"_id": "54c688af234056a9e3f5c902",
"price": 100.76,
"picture": "http://lorempixel.com/200/200/technics/?id=20.7657",
"brand": "reebok",
"name": "Saorunlab",
"rating": 0
}
];
});
3.4.3. Ping-pong (0,67 puntos)
Este ejercicio lo realizaremos en una carpeta llamada ping-pong
.
En este ejercicio vamos a probar la propagación de eventos. Dispondremos de la siguiente plantilla HTML:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37 <div ng-app="pingPong">
<style>
.ng-scope {
border: 1px dotted red;
margin: 5px;
}
</style>
<div ng-controller="Controller1">
<div ng-init="ping = 0"></div>
<div ng-init="pong = 0"></div>
<button ng-click="$emit('ping')">Emit event</button>
<button ng-click="$broadcast('pong')">Broadcast event</button>
<div>ping = {{ ping }}</div>
<div>pong = {{ pong }}</div>
<div ng-controller="Controller2">
<button ng-click="">Emit event</button>
<button ng-click="">Broadcast event</button>
<div>ping = {{ ping }}</div>
<div>pong = {{ pong }}</div>
<div ng-controller="Controller2">
<button ng-click="">Emit event</button>
<button ng-click="">Broadcast event</button>
<div>ping = {{ ping }}</div>
<div>pong = {{ pong }}</div></div>
</div>
</div>
</div>
Y la siguiente plantilla JavaScript:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 function Controller1($scope) {
$scope.$on('ping', function(){
$scope.ping``;
});
$scope.$on('pong', function(){
$scope.pong``;
});
}
function Controller2($scope) {
}
function Controller3($scope) {
}
angular
.module('pingPong')
.controller('Controller1', Controller1)
.controller('Controller2', Controller2)
.controller('Controller3', Controller3);
Si ahora pulsamos en cualquiera de los dos primeros botones, veremos que los contadores ping y pong adquieren los mismos valores.
Deberemos:
-
Emitir eventos ping y pong en el resto de botones.
-
Introducir la lógica necesaria para que el evento que desencadene cada botón afecte al
scope
propio y a los ascendentes (en caso de$emit
) o descendientes (en caso de$broadcast
).
No se puede renombrar ninguno de los nombres de variable de la vista (todos deben llamarse ping
y pong
).
Tener en cuenta la herencia prototípica que hemos estado viendo.
Se ha introducido un poco de CSS para que se delimiten bien los tres scope
que hay en la aplicación.
Si los bloques ng-init
te molestan, puedes quitarlos
3.4.4. Carrito de la compra II (0,66 puntos)
Realiza el ejercicio anterior del carrito de la compra aplicando la sintaxix de controller as. Seguiremos necesitando el objeto scope para nuestros watchers. Recordemos que podíamos pasar una expresión o una función a nuestro watcher
No machaques nada, guárdalo en la carpeta shoppingcart-controlleras |
Como dentro de una función el ámbito del this varía, una manera sencilla de solventar esto es de la forma:
1
2
3
4 ---
angular
.module('org.expertojava.carrito', [])
.controller('CartCtrl', function CartCtrl($scope) {
//... //Watch changes on cart length var _this = this; var watchFn = function() { return _this.cart.length; };
$scope.$watch(watchFn, function(){ _this.totalItems = _this.cart.length; //REST OF YOUR CODE }); //...
}); ---
Hay maneras más elegantes de hacerlo, como mediante el uso de bind u otras técnicas de binding, pero la descrita es una forma comúnmente usada en infinidad de proyetos.