Advertisement
  1. Code
  2. Coding Fundamentals
  3. AJAX

Creación de aplicaciones web de una sola página con Sinatra: Parte 1

Scroll to top
Read Time: 12 min

() translation by (you can also view the original English article)

¿Alguna vez has querido aprender a crear una aplicación de una sola página con Sinatra y Knockout.js? Bueno, ¡hoy es el día en que aprenderás! En esta primera sección de una serie de dos partes, revisaremos el proceso para crear una aplicación de tareas pendientes de una sola página donde los usuarios pueden ver sus tareas, ordenarlas, marcarlas como completas, eliminarlas, buscarlas y agregar nuevas tareas.


¿Qué es Sinatra?

Según su sitio web:

Sinatra es un DSL para crear rápidamente aplicaciones web en Ruby con un mínimo esfuerzo.

Sinatra te permite hacer cosas como:

1
get "/task/new" do
2
    erb :form
3
end

Esta es una ruta que maneja solicitudes GET para "/task/new" y presenta un formulario erb llamado form.erb. No usaremos Sinatra para renderizar plantillas Ruby; en su lugar, lo usaremos solo para enviar respuestas JSON a nuestro front-end administrado por Knockout.js (y algunas funciones de utilidad de jQuery como $.ajax). Usaremos erb solo para representar el archivo HTML principal.


¿Qué es Knockout?

Knockout es un framework de JavaScript de tipo Model-View-ViewModel (Modelo - Vista - Vista - Modelo) (MVVM) que te permite mantener tus modelos en objetos especiales "observables". También mantiene tu interfaz de usuario actualizada, según los objetos observados.

1
-ToDo/
2
 -app.rb
3
 -models.rb
4
--views/
5
  -index.erb
6
-- public /
7
--- scripts/
8
   - knockout.js
9
   - jquery.js
10
   - app.js
11
--- styles/
12
   - styles.css

Esto es lo que estarás construyendo:

Comenzaremos definiendo nuestro modelo y luego nuestras acciones CRUD en Sinatra. Confiaremos en DataMapper y SQLite para el almacenamiento persistente, pero puedes usar cualquier ORM que prefieras.

Agreguemos el modelo Task al archivo models.rb:

1
    DataMapper.setup(:default, 'sqlite:///path/to/project.db')
2
    class Task
3
      include DataMapper::Resource
4
5
      property :id,             Serial
6
      property :complete,        Boolean
7
      property :description,    Text
8
      property :created_at,        DateTime
9
      property :updated_at,        DateTime
10
11
    end
12
    DataMapper.auto_upgrade!

Este modelo "Task" consiste esencialmente en algunas propiedades diferentes que queremos manipular en nuestra aplicación de tareas pendientes.

A continuación, escribamos nuestro servidor JSON Sinatra. En el archivo app.rb, comenzaremos requiriendo algunos módulos diferentes:

1
    require 'rubygems'
2
    require 'sinatra'
3
    require 'data_mapper'
4
    require File.dirname(__FILE__) + '/models.rb'
5
    require 'json'
6
    require 'Date'

El siguiente paso es definir algunos valores predeterminados globales; en particular, necesitamos un tipo MIME enviado con cada uno de nuestros encabezados de respuesta para especificar que cada respuesta es JSON.

1
before do
2
    content_type 'application/json'
3
end

La función de ayuda before se ejecuta antes de cada coincidencia de ruta. También puedes especificar rutas coincidentes después de before; si, por ejemplo, solo quisieras ejecutar respuestas JSON si la URL terminaba en ".json", usarías esto:

1
before %r{.+\.json$} do
2
    content_type 'application/json'
3
end

A continuación, definimos nuestras rutas CRUD, así como una ruta para servir nuestro archivo index.erb:

1
get "/" do
2
	content_type 'html'
3
	erb :index
4
end
5
get "/tasks" do
6
    @tasks = Task.all
7
    @tasks.to_json
8
end
9
post "/tasks/new" do
10
    @task = Task.new
11
    @task.complete = false
12
    @task.description = params[:description]
13
    @task.created_at = DateTime.now
14
    @task.updated_at = null
15
end
16
put "/tasks/:id" do
17
    @task = Task.find(params[:id])
18
    @task.complete = params[:complete]
19
    @task.description = params[:description]
20
    @task.updated_at = DateTime.now
21
    if @task.save
22
        {:task => @task, :status => "success"}.to_json
23
    else
24
        {:task => @task, :status => "failure"}.to_json
25
    end
26
end
27
delete "/tasks/:id" do
28
    @task = Task.find(params[:id])
29
    if @task.destroy
30
        {:task => @task, :status => "success"}.to_json
31
    else
32
        {:task => @task, :status => "failure"}.to_json
33
    end
34
end

Entonces, el archivo app.rb ahora se ve así:

1
require 'rubygems'
2
require 'sinatra'
3
require 'data_mapper'
4
require File.dirname(__FILE__) + '/models.rb'
5
require 'json'
6
require 'Date'
7
before do
8
    content_type 'application/json'
9
end
10
get "/" do
11
	content_type 'html'
12
	erb :index
13
end
14
get "/tasks" do
15
    @tasks = Task.all
16
    @tasks.to_json
17
end
18
post "/tasks/new" do
19
    @task = Task.new
20
    @task.complete = false
21
    @task.description = params[:description]
22
    @task.created_at = DateTime.now
23
    @task.updated_at = null
24
    if @task.save
25
        {:task => @task, :status => "success"}.to_json
26
    else
27
        {:task => @task, :status => "failure"}.to_json
28
    end
29
end
30
put "/tasks/:id" do
31
    @task = Task.find(params[:id])
32
    @task.complete = params[:complete]
33
    @task.description = params[:description]
34
    @task.updated_at = DateTime.now
35
    if @task.save
36
        {:task => @task, :status => "success"}.to_json
37
    else
38
        {:task => @task, :status => "failure"}.to_json
39
    end
40
end
41
delete "/tasks/:id" do
42
    @task = Task.find(params[:id])
43
    if @task.destroy
44
        {:task => @task, :status => "success"}.to_json
45
    else
46
        {:task => @task, :status => "failure"}.to_json
47
    end
48
end

Cada una de estas rutas se asigna a una acción. Solo hay una vista (la vista "todas las tareas") que contiene cada acción. Recuerda: en Ruby, el valor final regresa implícitamente. Puedes regresar explícitamente antes, pero cualquier contenido que devuelvan estas rutas será la respuesta enviada desde el servidor.


Knockout: Modelos

A continuación, comenzamos definiendo nuestros modelos en Knockout. En app.js, coloca el siguiente código:

1
function Task(data) {
2
    this.description = ko.observable(data.description);
3
    this.complete = ko.observable(data.complete);
4
    this.created_at = ko.observable(data.created_at);
5
    this.updated_at = ko.observable(data.updated_at);
6
    this.id = ko.observable(data.id);
7
}

Como puedes ver, estas propiedades se asignan directamente a nuestro modelo en models.rb. Un ko.observable mantiene el valor actualizado en la interfaz de usuario cuando esta cambia sin tener que depender del servidor o del DOM para realizar un seguimiento de su estado.

A continuación, agregaremos un TaskViewModel.

1
function TaskViewModel() {
2
    var t = this;
3
    t.tasks = ko.observableArray([]);
4
    $.getJSON("/tasks", function(raw) {
5
        var tasks = $.map(raw, function(item) { return new Task(item) });
6
        self.tasks(tasks);
7
    });
8
}
9
ko.applyBindings(new TaskListViewModel());

Este es el comienzo de lo que será el meollo de nuestra aplicación. Comenzamos creando una función constructora TaskViewModel; se pasa una nueva instancia de esta función a la función de Knockout llamada applyBindings() al final de nuestro archivo.

Dentro de nuestro TaskViewModel hay una llamada inicial para recuperar tareas de la base de datos, a través de la url "/tasks". Después, se asignan a ko.observableArray, que se establece en t.tasks. Esta matriz es el corazón de la funcionalidad de nuestra aplicación.

Entonces, ahora, tenemos una función de recuperación que muestra las tareas. Hagamos una función de creación y luego creemos nuestra vista de plantilla real. Agrega el siguiente código al TaskViewModel:

1
	t.newTaskDesc = ko.observable();
2
    t.addTask = function() {
3
        var newtask = new Task({ description: this.newTaskDesc() });
4
        $.getJSON("/getdate", function(data){
5
            newtask.created_at(data.date);
6
            newtask.updated_at(data.date);
7
            t.tasks.push(newtask);
8
            t.saveTask(newtask);
9
            t.newTaskDesc("");
10
        })
11
    };
12
    t.saveTask = function(task) {
13
        var t = ko.toJS(task);
14
        $.ajax({
15
             url: "http://localhost:9393/tasks",
16
             type: "POST",
17
             data: t
18
        }).done(function(data){
19
            task.id(data.task.id);
20
        });
21
    }

Knockout proporciona una capacidad de iteración conveniente...

Primero, configuramos newTaskDesc como un observable. Esto nos permite usar un campo de entrada fácilmente para escribir una descripción de la tarea. A continuación, definimos nuestra función addTask(), que agrega una tarea al observableArray; llama a la función saveTask(), pasando el nuevo objeto de tarea.

La función saveTask() es independiente del tipo de guardado que realiza. (Más tarde, usamos la función saveTask() para eliminar tareas o marcarlas como completas.) Una nota importante aquí: confiamos en una función conveniente para tomar la marca de tiempo actual. Esta no será la marca de tiempo exacta guardada en la base de datos, pero proporciona algunos datos para colocar en la vista.

La ruta es muy simple:

1
get "/getdate" do
2
    {:date => DateTime.now}.to_json
3
end

También debe tenerse en cuenta que la identificación de la tarea no se establece hasta que se completa la solicitud Ajax, ya que debemos asignarla en función de la respuesta del servidor.

Creemos el HTML que controla nuestro JavaScript recién creado. Una gran parte de este archivo proviene del archivo de índice repetitivo HTML5. Esto va al archivo index.erb:

1
<!DOCTYPE html >
2
<html>
3
<!--[if lt IE 7]>      <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
4
<!--[if IE 7]>         <html class="no-js lt-ie9 lt-ie8"> <![endif]-->
5
<!--[if IE 8]>         <html class="no-js lt-ie9"> <![endif]-->
6
<!--[if gt IE 8]><!-->  <!--<![endif]-->
7
    <body>
8
        <meta charset="utf-8">
9
        <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
10
        <title>ToDo</title>
11
        <meta name="description" content="">
12
        <meta name="viewport" content="width=device-width">
13
14
        <!-- Place favicon.ico and apple-touch-icon.png in the root directory -->
15
        <link rel="stylesheet" href="styles/styles.css">
16
        <script src="scripts/modernizr-2.6.2.min.js"></script>
17
    
18
    
19
        <!--[if lt IE 7]>

20
            <p class="chromeframe">You are using an outdated browser. <a href="http://browsehappy.com/">Upgrade your browser today</a> or <a href="http://www.google.com/chromeframe/?redirect=true">install Google Chrome Frame</a> to better experience this site.</p>

21
        <![endif]-->
22
        <!-- Add your site or application content here -->
23
        <div id="container">
24
            <section id="taskforms" class="clearfix">
25
                <div id="newtaskform" class="floatleft fifty">
26
                    <h2>Create a New Task</h2>
27
                    <form id="addtask">
28
                        <input>
29
                        <input type="submit">
30
                    </form>
31
                </div>
32
                <div id="tasksearchform" class="floatright fifty">
33
                    <h2>Search Tasks</h2>
34
                    <form id="searchtask">
35
                        <input>
36
                    </form>
37
                </div>
38
            </section>
39
            <section id="tasktable">
40
                <h2>Incomplete Tasks remaining: <span></span></h2>
41
                <a>Delete All Complete Tasks</a>
42
                <table>
43
                    <tbody><tr>
44
                        <th>DB ID</th>
45
                        <th>Description</th>
46
                        <th>Date Added</th>
47
                        <th>Date Modified</th>
48
                        <th>Complete?</th>
49
                        <th>Delete</th>
50
                    </tr>
51
                    <tr>
52
                        <td></td>
53
                        <td></td>
54
                        <td></td>
55
                        <td></td>
56
                        <td><input type="checkbox"> </td>
57
                        <td class="destroytask"><a>X</a></td>
58
                    </tr>
59
                </tbody></table>
60
            </section>
61
        </div>
62
63
        <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.8.1/jquery.min.js"></script>
64
        <script>window.jQuery || document.write('<script src="scripts/jquery.js"><\/script>')</script>
65
        <script src="scripts/knockout.js"></script>
66
        <script src="scripts/app.js"></script>
67
68
        <!-- Google Analytics: change UA-XXXXX-X to be your site's ID. -->
69
        <script>
70
            var _gaq=[['_setAccount','UA-XXXXX-X'],['_trackPageview']];
71
            (function(d,t){var g=d.createElement(t),s=d.getElementsByTagName(t)[0];
72
            g.src=('https:'==location.protocol?'//ssl':'//www')+'.google-analytics.com/ga.js';
73
            s.parentNode.insertBefore(g,s)}(document,'script'));
74
        </script>
75
    </body>
76
</html>

Tomemos esta plantilla y completemos los enlaces que utiliza Knockout para mantener la interfaz de usuario sincronizada. Para esta parte, cubrimos la creación de elementos de tareas pendientes. En la segunda parte, cubriremos funciones más avanzadas (incluida la búsqueda, clasificación, eliminación y marcado como terminada).

Antes de continuar, démosle un poco de estilo a nuestra página. Dado que este tutorial no se trata de CSS, lo incluiremos y seguiremos adelante. El siguiente código está dentro del archivo CSS HTML5 Boilerplate, que incluye un restablecimiento y algunas otras cosas.

1
section {
2
    width: 800px;
3
    margin: 20px auto;
4
}
5
table {
6
    width: 100%;
7
}
8
th {
9
    cursor: pointer;
10
}
11
tr {
12
    border-bottom: 1px solid #ddd;
13
}
14
tr.complete, tr.complete:nth-child(odd) {
15
    background: #efffd7;
16
    color: #ddd;
17
}
18
tr:nth-child(odd) {
19
    background-color: #dedede;
20
}
21
td {
22
    padding: 10px 20px;
23
}
24
td.destroytask {
25
    background: #ffeaea;
26
    color: #943c3c;
27
    font-weight: bold;
28
    opacity: 0.4;
29
}
30
td.destroytask:hover {
31
    cursor: pointer;
32
    background: #ffacac;
33
    color: #792727;
34
    opacity: 1;
35
}
36
.fifty { width: 50%; }
37
input {
38
    background: #fefefe;
39
    box-shadow: inset 0 0 6px #aaa;
40
    padding: 6px;
41
    border: none;
42
    width: 90%;
43
    margin: 4px;
44
}
45
input:focus {
46
    outline: none;
47
    box-shadow: inset 0 0 6px rgb(17, 148, 211);
48
    -webkit-transition: 0.2s all;
49
    background: rgba(17, 148, 211, 0.05);
50
}
51
input[type=submit] {
52
    background-color: #1194d3;
53
    background-image: -webkit-gradient(linear, left top, left bottom, from(rgb(17, 148, 211)), to(rgb(59, 95, 142)));
54
    background-image: -webkit-linear-gradient(top, rgb(17, 148, 211), rgb(59, 95, 142));
55
    background-image: -moz-linear-gradient(top, rgb(17, 148, 211), rgb(59, 95, 142));
56
    background-image: -o-linear-gradient(top, rgb(17, 148, 211), rgb(59, 95, 142));
57
    background-image: -ms-linear-gradient(top, rgb(17, 148, 211), rgb(59, 95, 142));
58
    background-image: linear-gradient(top, rgb(17, 148, 211), rgb(59, 95, 142));
59
    filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr='#1194d3', EndColorStr='#3b5f8e');
60
    padding: 6px 9px;
61
    border-radius: 3px;
62
    color: #fff;
63
    text-shadow: 1px 1px 1px #0a3d52;
64
    border: none;
65
    width: 30%;
66
}
67
input[type=submit]:hover {
68
    background: #0a3d52;
69
}
70
.floatleft { float: left; }
71
.floatright { float: right; }

Agrega este código a tu archivo styles.css.

Ahora, cubramos el formulario "new task". Agregaremos atributos de tipo data-bind al formulario para que los enlaces Knockout funcionen. El atributo de enlace de datos o conocido como data-bind es la forma en que Knockout mantiene la interfaz de usuario sincronizada y permite el enlace de eventos y otras funciones importantes. Reemplaza el formulario "new task" con el siguiente código.

1
<div id="newtaskform" class="floatleft fifty">
2
    <h2>Create a New Task</h2>
3
    <form id="addtask" data-bind="submit: addTask">
4
        <input data-bind="value: newTaskDesc">
5
        <input type="submit">
6
    </form>
7
</div>

Veremos estos uno por uno. Primero, el elemento de formulario tiene un enlace para el evento submit. Cuando se envía el formulario, se ejecuta la función addTask() definida en TaskViewModel. El primer elemento de entrada (que es implícitamente type="text") contiene el valor ko.observable newTaskDesc que definimos anteriormente. Todo lo que esté en este campo al enviar el formulario se convierte en la propiedad de descripción llamado description de la tarea.

Tenemos una manera de agregar tareas, pero necesitamos mostrarlas. También necesitamos agregar cada una de las propiedades de la tarea. Repitamos las tareas y agrégalas a la tabla. Knockout proporciona una capacidad de iteración conveniente para facilitar esto; define un bloque de comentarios con la siguiente sintaxis:

1
<!-- ko foreach: tasks -->
2
	<td data-bind="text: id"></td>
3
	<td data-bind="text: description"></td>
4
	<td data-bind="text: created_at"></td>
5
	<td data-bind="text: updated_at"></td>
6
	<td> <input type="checkbox"></td>
7
	<td> <a>X</a></td>
8
<!-- /ko -->

En Ruby, el valor final se devuelve implícitamente.

Esto usa la capacidad de iteración de Knockout. Cada tarea se define específicamente en TaskViewModel (t.tasks) y permanece sincronizada en toda la interfaz de usuario. La identificación de cada tarea se agrega solo después de que hayamos terminado la llamada a la base de datos (ya que no hay forma de asegurarnos de que tengamos la identificación correcta de la base de datos hasta que esté escrita), pero la interfaz no necesita reflejar inconsistencias como estas.

Ahora deberías poder usar shotgun app.rb (gem install shotgun) desde tu directorio de trabajo y probar tu aplicación en el navegador en http://localhost:9393. (Nota: asegúrate de haber ejecutado gem install para tener todas tus dependencias/bibliotecas requeridas antes de intentar ejecutar tu aplicación). Deberías poder agregar tareas y verlas aparecer inmediatamente.


Hasta la segunda parte

En este tutorial, aprendiste cómo crear una interfaz JSON con Sinatra y, posteriormente, cómo reflejar esos modelos en Knockout.js. También aprendiste a crear enlaces para mantener nuestra interfaz de usuario sincronizada con nuestros datos. En la siguiente parte de este tutorial, hablaremos únicamente sobre Knockout y explicaremos cómo crear funciones de clasificación, búsqueda y actualización.

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.