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 $rootScope. Como la creación de scopes, está dirigida por el árbol del DOM, no debería resultar extraño que el árbol de scopes imite de alguna manera el árbol del DOM.

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:

scopes

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.

eventos

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

name

string

Nombre del evento que se propaga.

args

*

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

name

string

Nombre del evento al que nos suscribimos

listener

function(event, …​args)

Función a invocar cuando se recibe el evento.

El objeto event que se le pasa al listener tiene los siguientes atributos:

  • targetScope: el scope en el que el evento fue emitido o difundido

  • currentScope: el scope 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 flag defaultPrevented a true.

  • defaultPrevented: valdrá true si se ha llamado a la función preventDefault.

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}} &euro;)</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}} &euro; </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.