Superheroic JavaScript MVW Framework
Todo es un objeto
var string = "texto",
array = [1,2,3],
objeto = {"a":1};
funcion = function () {}
var objeto = {
propiedad: 1,
metodo: function () {
return this.propiedad;
}
}
objeto.metodo() -> 1
var funcion = objeto.metodo;
funcion(); -> ?!
// ES5 bindings
funcion.bind(objeto);
funcion.call(objeto);
class Coche {
int puertas;
static String[] tipos = ["grande", pequeño];
Coche (puertas) {
this.puertas = puertas;
}
int numPuertas () {
return this.puertas;
}
}
Coche coche = new Coche(3);
function Coche(puertas) {
this.puertas = puertas;
}
Coche.tipos = ["grande", "pequeño"];
Coche.prototype = {
numPuertas: function () {
return this.puertas;
}
}
var coche = new Coche(3);
Functions which take a small amount of metadata that describes the environment in which they were defined.
var counter = (function () {
var count = 0;
return function () {
console.log(count++);
}
})();
counter(); // 0
counter(); // 1
Java 7 doesn't support closures
var object;
object = {
"a": 1,
"b": true,
"c": [1,2,3]
};
object.__proto__ = {
"d": 1,
"e": true,
"f": [1,2,3]
};
object.__proto__.__proto__ === {}.__proto__;
object.__proto__.__proto__.__proto__ === null;
var b = {
"d": 1,
"e": true,
"f": [1,2,3]
};
var object = Object.create(b);
object["a"] = 1;
object["b"] = true;
object["c"] = [1, 2, 3];
function A() {
this["a"] = 1;
this["b"] = true;
this["c"] = [1,2,3];
}
function B() {
this["d"] = 1;
this["e"] = true;
this["f"] = [1,2,3];
}
A.prototype = new B();
var object = new A();
Framework javascript para desarrollar páginas web dinámicas.
HTML como lenguaje de templating.
Sistema de componentes extendiendo la sintaxis HTML.
El data binding y la inyección de dependencias simplifica la programación.
Versión minificada y gzipeada ocupa 30KBs.
Gmail
Google Maps
Asynchronous JavaScript and XML
Añadir un
$(".selectorcss").click(function (ev) {
alert("");
});
$( "body" ).on( "click", "script", function() {
alert( $(this).text() );
});
Leer el contenido de un input
var contenido = $("input").val();
Asignar un valor a un input
$("input").val("valor");
Añadir una clase css
$("selector").addClass("clickado");
Crear un nuevo elemento y añadirlo a otro
$(this).append(
$("<h4>has hecho Click!</h4")
);
$.ajax( "ruta.php" )
.done(function() {
alert( "success" );
})
.fail(function() {
alert( "error" );
})
.always(function() {
alert( "complete" );
});
$(".selectorcss").click(function (ev) {
$( "#block" ).animate({
opacity: 0.6
}, 1500 );
});
$('#example').popover({
title: "titulo",
placement: 'left',
html: "<h4>Contenido</h4>"
});
Crearemos una lista de tareas con jQuery.
El proceso nos ayudará a comprender los porqué del diseño de un framework MVC como AngularJS.
Creamos una lista para guardar las tareas
<ol id="lista">
<li>Elemento 1</li>
<li>Elemento 2</li>
</ol>
<input placeholder="Escribe un elemento" />
<button>Añadir</button>
Añadimos un gestor de eventos que inserta un nuevo elemento.
$("button").click(function () {
// Obtenemos el valor del cuadro de texto
var contenido = $("input").value();
// Creamos un nuevo elemento
var nuevoElemento = $("<li></li>");
// Añadimos el texto a dicho elemento
nuevoElemento.text("contenido");
// Añadimos el elemento a la lista
$("ol").append(nuevoElemento);
});
Añadimos la posibilidad de borrar elementos.
<ol id="lista">
<li>Elemento 1 <button>borrar</button></li>
<li>Elemento 2 <button>borrar</button></li>
</ol>
$("ol").on("click", "button", function (ev) {
$(this).parent().remove();
});
Inicializamos la lista desde el servidor
// Pedimos el contenido al servidor
$.get( "lista.php" ).done(function(obj) {
// Por cada elemento
obj.forEach(function(value, index, collection) {
$("ol").append($("<li></li>").text(value));
$("ol [tooltip]").tooltip();
});
});
Añadamos un efecto, un tooltip por ejemplo
Necesitaremos inicializar el plugin cada vez que añadamos un nuevo elemento
$("button").click(function () {
...
$("ol [tooltip]").tooltip();
});
Supongamos que queremos que nuestro diseñador gráfico cambie el aspecto.
No es trivial, el html está empotrado en el propio código.
¿Quién se atreverá a modificar el html de este plugin?
https://github.com/arshaw/fullcalendar/blob/master/src/basic/BasicView.js
Añadamos soporte para salvar la lista
¿Dónde almacenamos la lista? Por ahora sólo tenemos HTML.
¿Sacamos del HTML, o matenemos paralelamente una lista?
¿Y si deseamos usar la lista en varios lugares?
El paso de referencias es complejo.
var lista = [];
$.get("lista").done(function(obj) {
lista = obj;
});
$("ol").on("click", "button", function (ev) {
lista.splice($index, 1);
});
Más preguntas
¿Es eficiente? ¿Cúal es coste de los selectores de jQuery?
¿Y si recibimos eventos a traves de un bus de mensajes con un websocket?
¿Y si queremos que la lista se visualize en varios lugares?
¿Se os ocurre como definir unos tests para comprobar el funcionamiento de esta página?
http://en.wikipedia.org/wiki/Separation_of_concerns
Principio de diseño de software
Sólo con jQuery carecemos de mucha infraestructura.
Un framework debe enfocarnos hacia buenas prácticas.
jQuery sigue siendo fantástica
AngularJS añade funcionalidad extra a jQuery
angular.min.js (con jqLite incluido) 104K
jquery.min.js 84K
- Gestión de eventos
- Manipulación DOM
- AJAX
- introducción al patrón MVW
- data binding empleando interpolación y con ng-model
- los ámbitos creados por las directivas ng-controller y ng-repeater
Sincronización automática entre el modelo y la vista.
<body ng-app ng-init="name = 'World'">
Say hello to: <input type="text" ng-model="name">
<h1>Hello, {{name}}!</h1>
</body>
Interpolaciones {{variable}}
ng-model
modelos
ámbito
controller
Vista
var scope = angular.element($("body")).scope();
El scope permite enviar mensajes hacía arriba o hacia abajo
- eventos
- validación de formularios
- inyección de dependencias
- filtros
Patrón de diseño orientado a objetos
Delegamos la construcción/obtención de objetos a un componente el inyector.
function Cafetera() {
var molinillo = new Molinillo();
var bomba = Bomba.getInstance();
var calentador = app.get('Calentador');
this.infusion = function() {
molinillo.moler();
calentador.on();
bomba.bombear();
};
}
El paso de referencias de objetos resulta compleja
function CoffeeMaker(molinillo, calentador, bomba) {
this.infusion = function() {
molinillo.moler();
calentador.on();
bomba.bombear();
};
}
El inyector observará el nombre de los parámetros
Un valor
app.value('existencia', 42);
Una servicio
app.service('lista', function () {
var lista = [];
return {
get: function (id) {
return lista[id];
}
}
});
Un servicio
app.factory('services', function () {
return $resource('/services/:serviceId', {serviceId:'@id'});
});
inferidas
$injector.invoke(function($compile, $rootScope) {
//
});
inline
injector.invoke(['$compile', '$rootScope', function(obfCompile, obfRootScope) {
//
}]);
Anotadas
var tmpFn = function(obfuscatedCompile, obfuscatedRootScope) {
//
};
tmpFn.$inject = ['$compile', '$rootScope'];
$injector.invoke(tmpFn);
app.config(function ($routeProvider) {
$routeProvider
.when('/services/:serviceId/service', {
controller: 'ServiceCtrl',
resolve: {
service: function ($route, services) {
return services.get($route.current.params.serviceId);
}
}
});
});
app.controller('ServiceCtrl', function ($scope, service) {
$scope.service = service;
});
var injector = angular.element($("body")).injector();
var $templateCache = injector.get("$templateCache");
$templateCache.get("popover/docs/popover.tpl.demo.html");
- vistas con el router de angular
- comunicación rest con angular resources
- paginación con angular bootstrap ui
- filtros
- animaciones
Extra:
- bus de comunicación
- asincronía javascript mediante promesas
app.config(function ($routeProvider) {
$routeProvider
.when('/', {
templateUrl: 'lista.html',
controller: 'ListaCtrl'
})
.otherwise({
redirectTo: '/'
});
});
.when('/:id', {
templateUrl: 'editar.html',
controller: 'EditarCtrl',
resolve: {
ingrediente: function ($route, Ingrediente) {
return Ingrediente
.get({id: $route.current.params.id})
.$promise;
}
}
})
app.factory('services', function () {
return $resource('/services/:serviceId', {serviceId:'@id'});
});
Index
var services = Services.query(function () {
console.log("hemos recibido los servicios!", services);
});
Create
Service.$save(function(service) {
$location.url('/');
});
Update
Service.$update(function(service) {
$location.url('/');
});
Service.get({serviceId:123}, function(service) {
$scope.service = service;
});
- templates con ng-include
- ventanas modales y popovers con AngularStrap
- Angular Translate
- Galeria con Masonry
Lleva un momento inevitable en el que:
link: function(scope, element, attrs, ngModelController) { ... }
<my-directive></my-directive>
<input my-directive>
<!-- directive: my-directive-->
<input class="my-directive">
Angular normalizará los nombres:
reutilizar el scope
scope: false
nuevo scope
scope: true
Aislar
{
interpolado: "@",
databind: "=",
expression: "&"
}
Convención
- Elemento si tomará el control del template
- Atributo si "decora" el elemento.
La restricción se define con:
- 'A' attribute name
- 'E' element name
- 'C' class name
require: '?ngModel' or '^ngModel'
myModule.directive('button', function() {
return {
restrict: 'E',
compile: function(element, attributes) {
element.addClass('btn');
if ( attributes.type === 'submit' ) {
element.addClass('btn-primary');
}
if ( attributes.size ) {
element.addClass('btn-' + attributes.size);
}
}
};
});
describe('button directive', function () {
var $compile, $rootScope;
beforeEach(module('directives.button'));
beforeEach(inject(function(_$compile_, _$rootScope_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
}));
it('adds a "btn" class to the button element', function() {
var element = $compile('<button></button>')($rootScope);
expect(element.hasClass('btn')).toBe(true);
});
});
myModule.directive('validateEquals', function() {
return {
require: 'ngModel',
link: function(scope, elm, attrs, ngModelCtrl) {
function validateEqual(myValue) {
var valid = (myValue === scope.$eval(attrs.validateEquals));
ngModelCtrl.$setValidity('equal', valid);
return valid ? myValue : undefined;
}
ngModelCtrl.$parsers.push(validateEqual);
ngModelCtrl.$formatters.push(validateEqual);
scope.$watch(attrs.validateEquals, function() {
ngModelCtrl.$setViewValue(ngModelCtrl.$viewValue);
});
}
};
});
describe('validateEquals directive', function() {
var $scope, modelCtrl, modelValue;
beforeEach(inject(function($compile, $rootScope) {
...
modelValue = $scope.model = {};
modelCtrl = $scope.testForm.testInput;
...
}));
...
describe('model value changes', function() {
it('should be invalid if the model changes', function() {
modelValue.testValue = 'different';
$scope.$digest();
expect(modelCtrl.$valid).toBeFalsy();
expect(modelCtrl.$viewValue).toBe(undefined);
});
it('should be invalid if the reference model changes', function() {
modelValue.compareTo = 'different';
$scope.$digest();
expect(modelCtrl.$valid).toBeFalsy();
expect(modelCtrl.$viewValue).toBe(undefined);
});
it('should be valid if the modelValue changes to be the same as the reference', function() {
modelValue.compareTo = 'different';
$scope.$digest();
expect(modelCtrl.$valid).toBeFalsy();
modelValue.testValue = 'different';
$scope.$digest();
expect(modelCtrl.$valid).toBeTruthy();
expect(modelCtrl.$viewValue).toBe('different');
});
});