Tutorial. Todo App con Ember y Mongo

10/06/2016

Hoy he decidido revivir una serie de posts publicados a mediados de abril del años pasado (1,2,3,4). Fueron unos 4 posts escritos por Ryan Christiani y Erik Hanchett muy interesanter sobre como utiliar Mongo con el framework Ember 1.X. Lamentablemente dejaron de funcionar tras el lanzamiento de la version 2.0 del framework Ember dejando inservibles los post mencionados. Hoy reviviremos estos post y aprenderemos a realizar una especie de Todo-App que nos permita listar, añadir, modicar y borrar notas en Mongo usando este simpatico pero en ocasiones cambiante framework.

1º Parte: La Api

Ok. Primero contruiremos una api que haga peticiones de la base de datos. Es importante instalar mongo y nodejs con un simple sudo apt-get install nodejs && mongo, recomiendo instalar Robomongo 0.8.5 para visualizar los registros. Lo siguiente sera crear una carpeta de nombre Tutorial-Ember (por ejemplo) y dentro creamos una de nombre Api que tendra la siguiente estructura de archivos y carpetas. A continuacion, enumeramos la estructura de ficheros que tendra:

carpeta api - note.js carpeta app - routes.js carpeta models - note.js package.json server.json

package.json: Usaremos express y moongose principalmente. Morgan para visualizar en la terminal si funcionan correctamente las peticiones todo correctamente. Cors y body-parser para otras cosas, no quiero profundizar al respecto XD:

{
  "name": "Server",
  "version": "0.0.0",
  "devDependencies": {
    "body-parser": "^1.15.2",
    "cors": "^2.7.1",
    "express": "^4.14.0",
    "mongoose": "^4.5.2",
    "morgan": "^1.7.0"
  }
}

models/note.js: Vamos a crear un registro de notas/cuentos donde almacenaremos 3 campos: el titulo, el contenido y el autor en formato string:

// models/note.js
mongoose = require('mongoose');
/* Usaremos un modelo 'note' con tres campos strings */
var noteSchema = new mongoose.Schema({
	title: 'string',
	content: 'string',
	author: 'string'
});

module.exports = mongoose.model('note',noteSchema);

app/routes.js: Aqui tendremos configuradas las 2 rutas bien diferenciadas. En api/notes tenemos un GET que recupere todas las notas y POST para agregar una nota. En /api/notes/:note_id tendremos las opciones de modificar y borrar mediante DELETE y PATCH:

// app/router.js

var notes = require('../api/note');
module.exports = function (router) {

    /* Ruta de listado de nota (get) y salvado de nuevas notas (post) */
    router.route('/api/notes').get(function (req, res) {
            notes.getAllNotes(req, res)
        })
        .post(function (req, res) {
            notes.addNote(req, res)
        });

    /* Ruta de borrado (delete) y modificado (patch) */
    router.route('/api/notes/:note_id').get(function (req, res) {
            notes.getIdNote(req, res)
        })
        .delete(function (req, res) {
            notes.deleteNote(req, res)
        })
        .patch(function (req, res) {
            notes.saveNote(req, res)
        });

};

api/note.js: Aqui estan las 4 funcionalidades que enlazan a la ruta correspondiente. getAllNotes devuetve un listado de todas las notas en la variable note. getIdNote busca la nota por el identificador que recibe como parametro de la ruta. deleteNote borra en funcion del identificador recibido y addNote añade una nota:

var Note = require('../models/note');

/_ Enviara la lista de notas en una variable 'note' _/
module.exports.getAllNotes = function (req, res) {
Note.find({}, function (err, docs) {
if (err) res.send(err)
console.log(docs);
res.send({
note: docs
});
});
};

/_ Buscara la nota gracias a req.params.note_id y nos devolvera
la nota en una variable 'note' _/
module.exports.getIdNote = function (req, res) {
Note.findById(req.params.note_id, function (err, docs) {
if (err) res.send(err);
console.log(docs);
res.send({
note: docs
});
});
};

/_ Eliminado de nota _/
module.exports.deleteNote = function (req, res) {
Note.findById(req.params.note_id, function (err, elem) {
if (err) res.send(err);
elem.remove(function (err, docs) {
if (err) res.send(err);
console.log(docs);
res.send({
note: docs
});
});
});
};

/_ Salvar nota _/
module.exports.addNote = function (req, res) {
var note = new Note(req.body.note);
note.save(function (err, elem) {
if (err) res.send(err);
console.log(elem);
res.send({
note: elem
});
});
};

/_ Modificar nota ($set: req.body.note) _/
module.exports.saveNote = function (req, res) {
Note.findByIdAndUpdate(req.params.note_id, {
$set: req.body.note
}, function (err, elem) {
if (err) res.send(err);
console.log(elem);
res.send({
note: elem
});
});
};

server.js: Finalmente terminamos con el servidor. El servidor se ejecutara por el puerto 4500. La cosa quedaria como muestro a continuacion:

/* Variables */ var express = require("express") , app = express(); var morgan = require('morgan'); var bodyParser =
    require('body-parser'); var cors = require('cors'); var router = express.Router(); /* Conectamos a la BBDD de Mongo
    */ mongoose = require('mongoose'); mongoose.connect('mongodb://localhost/emberData'); app.use(morgan('dev'));
    app.use(bodyParser.json()); app.use(cors()); /* USAR RUTAS */ app.use('/', router); require('./app/routes')(router);
    /* Puerto */ app.listen(4500); console.log("Node server running on http://localhost:4500"); exports = module.exports
    = app;

Bueno, ahora toca probar nuestra API. Abrimos una terminal en la carpeta Api y ejecutamos npm install. Despues lanzamos node server y dejamos que funcione en el puerto 4500. Solo nos queda probar si funciona ejecutando los siguientes comandos en otra terminal estando siempre pendiente de los mensajes que puedan salir en la terminal con el mensaje Node server running on http://localhost:4500:

// ADD note
curl -d '{"note":{"title":"Oliver Twist","content":"Oliver es un niño huérfano.","author": "Charles Dickens"}}' -H "Content-Type: application/json" http://localhost:4500/api/notes

// GET ALL
curl http://localhost:4500/api/notes

// DELETE note
curl -X DELETE -H "Content-Type: application/json" http://localhost:4500/api/notes/(_id reemplazar por el identificador correcto)

/* Si aparecen 200/204 de color verde es que la cosa va bien
Morgan y los mensajes de la terminal te guiaran. */
2º Parte: El framework

¿Ha funcionado correctamente? si la respuesta es afirmativa entonces pasaremos a la segunda parte. EmberJs es un framework bastante interesante que mediante comandos por terminal nos permitira generar plantillas, rutas y controladores de manera sencilla y rapida. Pero lo primero es instalarlo:

// Instalamos Ember (este tutorial utiliza la version 2.6.2) sudo npm install -g ember-cli@2.6.2 // En la carpeta
    Tutorial-Ember abrimos una terminal y lanzamos un nuevo proyecto: ember new Example

Estupendo, metamonos dentro de la carpeta Example y abramos una terminal. En el siguiente paso se describen los comandos que nos ahorraran escribir:

// 1º Creamos la ruta MADRE // g (generate - generar) & d (deploy - remover) ember g route application // 2º Creamos
    la ruta index y about (Aqui agregaremos y listaremos notas) ember g route index ember g route about // 3º Creamos la
    ruta note/show (Aqui modificaremos y borraremos notas) ember g route note/show // 4º Generamos el modelo note
    (title,content.author) ember g model note title:string content:string author:string // 5º Generamos el serializer
    para que conecte con el id de Mongo. ember g serializer application // 6º Generamos el adaptador para que nuestro
    proyecto Ember apunte a la Api. ember g adapter application // 7º Generamos los controladores donde incluiremos las
    acciones ember g controller index ember g controller note/show // 8º Instalar Bootstrap 4 (addon) bower install
    tether ember install ember-cli-sass ember install ember-bootstrap-4

Ha sido relativamente sencillo pero ahora queda lo realmente dificil, rellenar los ficheros generados, nos centraremos en la carpeta app que se aloja en Example. app/styles/app.scss: Necesitamos agregar bootstrap en la hoja de estilos del proyecto y renombar a *.scss el fichero de estilos:

@import 'bootstrap';

/* ejemplo de estilo propio */

.padding-top {
  padding-top: 2rem;
}

Example/app/adapters/application.js: Fichero para conectar la api con nuestro framework. Le asignamos un nombre, un host para que apunte al puerto de nuestra api y incluimos unos headers para la correcta lectura del json tras las peticiones que hagamos:

import JSONAPIAdapter from 'ember-data/adapters/json-api';

export default JSONAPIAdapter.extend({
    namespace: 'api',
    host: 'http://localhost:4500',
    headers: {"Content-Type":"application/json"}
});

app/router.js: Aqui se configuran las rutas. Index apuntara a la url "/" y about apuntara a "/about":

import Ember from 'ember'; import config from './config/environment';

const Router = Ember.Router.extend({
location: config.locationType
});

Router.map(function() {
this.route('about', {path: "/about"});
this.route('index', {path: "/"});
/_ En la carpeta note se encuentra el template show que recibira un parametro id _/
this.resource('note', function() {
this.route('show', {
path: ":note_id"
});
});
});

export default Router;

app/serializers/application.js: Este fichero sera modificado debido a que la libreria RESTSerializer funciona correctamente para asignar una clave primaria a los _id de los registros que se extraen de Mongo:

import DS from 'ember-data';

export default DS.RESTSerializer.extend({
	primaryKey: '_id'
});

app/templates/routes/index.js: Devuelve una variable model con todas las notas registradas en Mongo.

import Ember from 'ember';

export default Ember.Route.extend({
	model: function() {
		return this.store.findAll('note');
	}
});

app/templates/application.hbs: Esta es la ruta Madre de donde tiraran el resto de vistas. Aqui se ha incluido exclusivamente la cabecera y en outlet se dibujara la vista que se considere oportuno en funcion de la url en la que estemos:

<nav class="navbar navbar-dark navbar-full bg-primary">
  <button class="navbar-toggler hidden-sm-up" type="button" data-toggle="collapse" data-target="#exCollapsingNavbar2">
    &#9776;
  </button>
  <div class="collapse navbar-toggleable-xs" id="exCollapsingNavbar2">
    <a class="navbar-brand" href="#">Todo App - Ember</a>
    <ul class="nav navbar-nav">
      <li class="nav-item active">
        {% raw />{{#link-to 'about' class="nav-link"}}ABOUT{{/link-to}}{% endraw />
      </li>      
    </ul>
  </div>
</nav>
{% raw />{{outlet}}{% endraw />

app/templates/index.hbs: Primer template de nuestra App. Constara de un formulario que tras un submit lanzara la accion save del controlador index. Lo siguiente que vereis es el listado mediante un each que devuelve un item para visualizar las notas:

<div class="container">
    <div class="row">    
        <div class="col-xs-12 col-md-3 padding-top">
            <form {% raw />{{action 'save' on="submit" }}{% endraw />>
                <div class="form-group">
                    <label>Titulo:</label>{% raw />{{textarea value=titulo class="form-control"}}{% endraw /> </div>
                <div class="form-group">
                    <label>Contenido:</label>{% raw />{{textarea rows=10 value=contenido class="form-control"}}{% endraw /> </div>
                <div class="form-group">
                    <label>Autor:</label>{% raw />{{textarea value=autor class="form-control"}}{% endraw /> </div>
                <button class="btn btn-primary">añadir</button>
            </form>
        </div>        
        <div class="col-xs-12 col-md-9 padding-top">
            <div class="row">               
               {% raw />{{#each model as |item|}}{% endraw />
               
                <div class="col-xs-12 col-sm-6 col-md-4">
                    <div class="card text-xs-right">                           
                        <img class="img-fluid card-img-top" src="assets/images/ember.jpg">
                            <div class="card-block">
                                <h4 class="card-title">{% raw />{{item.title}}{% endraw /></h4> 
                                <p>{% raw />{{item.content}}{% endraw /></p>
                                <p>{% raw />{{item.author}}{% endraw /></p>
                                {% raw />{{#link-to 'note.show' item.id class="btn btn-primary" tagName="button" }}
                                ver
                                {{/link-to}}{% endraw />
                            </div>                            
                    </div>
                </div>                 
                {% raw />{{/each}}{% endraw /> 
            </div>          
        </div>        
    </div>
</div>

app/controllers/index.js:El controlador de index dispone de la accion save que hemos visto anteriormente en el formulario. Gracias a createRecord podemos salvar una nota extrayendo el valor de los textareas. Reseteamos el formulario y guardamos:

import Ember from 'ember';

export default Ember.Controller.extend({

actions: {
save: function () {
var note = this.store.createRecord('note', {
title: this.get('titulo'),
content: this.get('contenido'),
author: this.get('autor')
});  
 this.set('titulo', "");
this.set('contenido', "");
this.set('autor', "");
note.save();
}
}
});

app/templates/note/show.hbs: Esta vista es un formulario que dibuja los campos de la nota escogida con tres posibles acciones mediante botones (borrar, modificar y Index):

<div class="container">
    <div class="row">
        <div class="col-xs-12 col-sm-6 padding-top">
            <form>
                <div class="form-group">
                    <label>Titulo:</label>
                    {% raw />{{textarea value=model.title cols="40" rows="1" class="form-control"}}{% endraw />
                </div>
                <div class="form-group">
                    <label>Contenido:</label>
                    {% raw />{{textarea value=model.content cols="40" rows="10" class="form-control"}}{% endraw />
                </div>
                <div class="form-group">
                    <label>Autor:</label>
                    {% raw />{{textarea value=model.author cols="40" rows="1" class="form-control"}}{% endraw />
                </div>
                <div class="btn-group">
                    <button type="delete" class="btn btn-danger" {% raw />{{action 'delete' on='click' }}{% endraw />>borrar</button>
                    <button type="submit" class="btn btn-success" {% raw />{{action 'update' on='click' }}{% endraw />>modificar</button>
                    {% raw />{{#link-to 'index' tagName="button" class="btn btn-primary"}}Index{{/link-to}}{% endraw />
                </div>
            </form>
        </div>
    </div>
</div>

Example/app/controllers/note/show.js: Este controlador tiene dos acciones (borrado y modificado). Tras realizar su cometido, nos reenviaran a la home mediante transitionToRoute('index').

import Ember from 'ember';
export default Ember.Controller.extend({
    actions: {
        delete: function () {
            /* Borrado */
            this.get('model').deleteRecord();
            this.get('model').save();
            this.transitionToRoute('index');
        },
        update: function () {
            /* Seleccionamos la nota */
            var note_selected = this.get('model');
            /* Reescribimos */
            note_selected.save();
            this.transitionToRoute('index');
        }
    }
});

Video del resultado final

Conclusiones del paso de Ember 1.x a 2.x: En Ember 2.x usan PATCH y no PUT para modificaciones, muy raro. Muchas librerias han sido reemplazadas sin repercutir en el funcionamiento exceptuando serializer. Incluir los headers en el adaptador es mas sencillo que modificar el environment. Ha sido un poco desesperante pero finalmente ha funcionado mejor de lo que esperaba. Dejo el codigo por si le interesa a alguien: