Antes de entrar en materia, decir que este artículo hace referencia a MEAN.JS, no a MEAN.io que se parecen pero no son lo mismo. Para más info sobre MEAN.JS os remito a genbetadev.com

meanjs-logo

Como sabréis la M es de MongoDB, la E de Express, la A de Angular y la N de Node. Sin embargo MEAN es algo más que juntarlo todo, si os bajáis el proyecto de meanjs.org veréis que viene de paso con un pequeño ejemplo. Es una buena forma de iniciarse, ayudándose de la documentación oficial que se apoya precisamente en ese ejemplo. Sin embargo en seguida se os quedará corto, es demasiado simple como para servir de ejemplo de lo que sería un proyecto normalito.

Así que voy a intentar explicar las dudas que me han ido surgiendo, especialmente de Angular ya que Express y MongoDB dan poca guerra.
El proyecto consiste en un panel de control de espacios publicitarios web. Es decir, «huecos» de publicidad, anuncios que poner en esos huecos y «campañas» publicitarias. Ya tenemos por tanto los modelos: Places, Ads y Campaigns. Y el típico Users que no puede faltar.

Modelos

El primer modelo es sencillo: Places. Constan de un ancho, un alto y un nombre descriptivo. En mongoose:

var mongoose = require('mongoose'),
	Schema = mongoose.Schema;

/**
 * Place Schema
 */
var PlaceSchema = new Schema({
	created: {
		type: Date,
		default: Date.now
	},
	name: {
		type: String,
		default: '',
		trim: true,
		required: 'Name cannot be blank'
	},
	width: {
		type: Number,
		default: 728,
		required: 'Width cannot be blank'
	},
	height: {
		type: Number,
		default: 90,
		required: 'Height cannot be blank'
	}
});

mongoose.model('Place', PlaceSchema);

Nada nuevo hasta aquí. Sin embargo las campañas quiero que tengan un dueño, y que abarquen unos determinados lugares publicitarios, y que contengan lógicamente unos cuantos anuncios para llenar esos huecos. Y aquí ya la cosa se pone un poco más interesante, hay que referenciar otros modelos y agruparlos en forma de array, la forma correcta en mongoose sería:

var mongoose = require('mongoose'),
	Schema = mongoose.Schema;

var CampaignSchema = new Schema({
	//etc etc
	ads: [{ type: Schema.ObjectId, ref: 'Ad' }],
	owner: {
		type: Schema.ObjectId,
		ref: 'User',
		required: 'Owner cannot be blank'
	}
});

mongoose.model('Campaign', CampaignSchema);

Vale, teniendo el modelo no es complicado completar el resto del código backend. Siguiendo el ejemplo Articles haremos las rutas, los controllers y los tests si vuestra religión os obliga 😛

Es hora de abordar el lado del cliente, donde la cosa se pone más interesante.

Frontend – Angular

Angular.js permite crear páginas con navegación a base de AJAX, es decir, nada de recargar la página. Lo malo es que esto implica meter código javascript para gestionar esas llamadas, más código para manejar las vistas… en definitiva, nos obliga a trasladar parte de la lógica que generalmente estaría en el backend. Es el precio a pagar por una interfaz más rápida y más interactiva.

En mi caso la interfaz permitirá realizar operaciones CRUD sobre los modelos que he mencionado. Nada especialmente complicado, salvo cuando es la primera vez!

Módulos

Para cada modelo de datos (Place, Ad, Campaign) voy a crear un módulo en Angular. Aquí todo funciona a base de módulos. Y en el caso de Angular con MEAN la estructura de estos módulos es vertical. Esto significa que cada módulo tiene su carpeta, y dentro de cada carpeta hay carpetas para las vistas, los servicios, los controllers… Vertical, pues eso. Lo contrario sería una carpeta de controllers para todos los módulos, otra para servicios, etc.

Siguiendo el ejemplo Articles que viene con MEAN es sencillo crear el primer módulo para Places. Carpetita, con sus subcarpetas, donde pone Article pones Place y sale solo prácticamente. Lo único con más miga son las vistas donde ya entramos a hacer uso de los controllers de Angular. Veamos cómo queda la vista para crear un Place:

<section data-ng-controller="PlacesController">
	<div class="page-header">
		<h1>New Place</h1>
	</div>
	<div class="col-md-12">
		<form class="form-horizontal" data-ng-submit="create()" novalidate>
			<fieldset>
				<div class="form-group">
					<label class="control-label" for="name">Name</label>
					<div class="controls">
						<input type="text" data-ng-model="name" id="name" class="form-control" placeholder="Name" required>
					</div>
				</div>
				<div class="form-group">
					<label class="control-label" for="width">Width</label>
					<div class="controls">
                        <input type="text" data-ng-model="width" id="width" class="form-control" placeholder="Width" required>
					</div>
                    <label class="control-label" for="height">Height</label>
                    <div class="controls">
                        <input type="text" data-ng-model="height" id="height" class="form-control" placeholder="Height" required>
                    </div>
				</div>
				<div class="form-group">
					<input type="submit" class="btn btn-default">
				</div>
				<div data-ng-show="error" class="text-danger">
					<strong data-ng-bind="error"></strong>
				</div>
			</fieldset>
		</form>
	</div>
</section>

Puntos interesantes aquí, lo primero el data-ng-controller=»PlacesController»  que nos permite hacer uso de lo que haya en el controlador. Por ejemplo más adelante vemos un data-ng-submit=»create()» que lo que hace es llamar al método create que tenemos en ese controller. Igual que en Articles, no estoy descubriendo nada nuevo. El último detalle son los data-ng-model=»name» que indican en qué variable de javascript se guardarán los inputs del usuario. De esta forma cuando enviemos el formulario, Angular ejecutará el método create() que le hemos indicado, y en ese método create recogeremos las variables «name», «width» y «height» para enviárselas al backend y que nos cree un Place:

$scope.create = function() {
	var place = new Places({
		name: this.name,
		width: this.width,
		height: this.height
	});
	place.$save(function(response) {
		$location.path('places/' + response._id);
	}, function(errorResponse) {
		$scope.error = errorResponse.data.message;
	});

	this.name = '';
	this.width = '';
	this.height = '';
};

Es casi mágico, lo sé, pero funciona así sin apenas hacer nada.

Vale, pero qué pasará cuando queramos por ejemplo crear una campaña, que debe ir asociada a un usuario, y a unos places? Ahí ya vamos a necesitar loops, y encima el módulo Campaigns va a tener que saber algo de Users y de Places… se complica, y eso no viene en el ejemplito Articles.

Controllers anidados

Sí, esa es la respuesta a «cómo muestro un select con todos los usuarios?». O a «cómo pongo un select múltiple para elegir los places?». Controllers dentro de controllers dentro de una vista. Ahí va la vista para crear una campaña:

<section data-ng-controller="CampaignsController">
	<div class="page-header">
		<h1>New Campaign</h1>
	</div>
	<div class="col-md-12">
		<form class="form-horizontal" data-ng-submit="create()" novalidate>
			<fieldset>
				<div class="form-group">
					<label class="control-label" for="name">Name</label>
					<div class="controls">
						<input type="text" data-ng-model="name" id="name" class="form-control" placeholder="Name" required>
					</div>
				</div>

                <div class="form-group">
                    <label class="control-label">Places</label>
                    <div class="controls">
                        <section data-ng-controller="PlacesController" data-ng-init="find()">
                            <select data-ng-model="$parent.includedPlaces" ng-options="p.name for p in places" multiple ng-multiple="true">
                            </select>
                            {{includedPlaces}}
                        </section>
                    </div>
                </div>

                <div class="form-group">
                    <label class="control-label">Owner</label>
                    <div class="controls">
                        <section data-ng-controller="UsersController" data-ng-init="find()">
                            <select data-ng-model="$parent.owner" ng-options="user.displayName for user in users">
                            </select>
                        </section>
                    </div>
                </div>

				<div class="form-group">
					<input type="submit" class="btn btn-default">
				</div>
				<div data-ng-show="error" class="text-danger">
					<strong data-ng-bind="error"></strong>
				</div>
			</fieldset>
		</form>
	</div>
</section>

Primera cosa distinta, que hay tres controllers, uno global y dos como hijos. Bueno, no deja de ser copia/pega solo que añadiendo un poquito de tabulador rico. Bueno y para el caso de listar usuarios ese UsersControllers tuve que crearlo pero bueno. La duda es, ¿desde el módulo Campaigns de Angular podremos usar el PlacesController que está en otro módulo distinto? Hmm, habrá inyección de dependencias? Pues si lo buscáis sí, en Angular cuando creas un módulo puedes hacer algo así:

angular.module(‘MainModule’, [‘FirstDependency’, ‘SecondDependency’]);

Pero en MEAN.JS esto no lo veréis. Sorpresa! Los módulos se crean así:

ApplicationConfiguration.registerModule('moduleName');

Y no, no admite más parámetros. Vaya, problemilla a la vista. Pues no, resulta que ese registerModule se encarga de las dependencias, lo mete todo como dependencia del módulo principal y al final to dios se entera de todo. Cómodo, si, pero si os da por buscar ayuda de Angular al respecto no os molestéis, esto es cosa solo de MEAN.JS.

Bien, salvado el obstáculo de la dependencia, volvemos a los controllers anidados, hay un detalle que quizá se os haya pasado. Fijaros como dentro de la sección UsersControllers se hace referencia al «owner» de una campaña:

data-ng-model="$parent.owner"

Ese $parent es clave, porque si no lo pones, esa variable «owner» se queda solo dentro de UsersController, y cuando CampaignsController llame al create() no se enviará, porque está fuera de visibilidad, en otro $scope. Detallín sin importancia 😛

Back to backend

Ok, el frontend más o menos toma forma, el create() nos debería enviar datos al servidor pero oh, peta. Un error que me indica que estoy intentando convertir un objeto en un id de MongoDB. Esto es por el owner, si intento hacer esto:

var campaign = new Campaign(req.body);

Internamente mongoose pasa lo que venga en la variable req.body.owner (que es un objeto User, enterito) a un ObjectID, es decir, el id de un usuario. Y claro, la conversión no la hace solo. Así que en vez de un User hay que pasarle el _id de ese user. Es tan sencillo como hacer:

req.body.owner = req.body.owner._id;
var campaign = new Campaign(req.body);

Y lo mismo si se tratase por ejemplo de un array de Places o de Ads, mongoose quiere arrays de ids.

Bonus Tip – grunt

Añado un consejo, mientras estéis desarrollando la app seguramente usaréis grunt que viene por defecto y es muy cómodo, porque según cambias algún archivo él se entera y relanza la app. Pero a mi me ha tenido varios días comiéndome el coco con las dependencias porque resulta que no recarga la app todo lo bien que debería. Si tienes la app arrancada y mueves un controller de un módulo a otro, se queda idiota y lanza errores diciendo que el controller no existe:

Error: [ng:areq] Argument 'UsersController' is not a function, got undefined

Pero si matas grunt y lo ejecutas de nuevo, magia, el controller reaparece. Moraleja, si os salen letras rojas en consola matad grunt y arrancadlo a mano, el reload automático no va todo lo fino que debería.

Bueno, pues con eso por hoy suficiente, entre la docu de meanjs.org, el ejemplo Articles que viene incluido y estos pequeños detalles ya se puede decir que tenemos la introducción suficiente para crear algo interesante, y no el típico ejemplo chorra de todos los tutoriales que no va a ninguna parte.

If you think my content is worth it you can Buy me a Coffee at ko-fi.com Buy me a Ko-fi