Aplicación sencilla
Desarrollemos una aplicación sencilla que muestre cómo realizar la mayoría de las tareas importantes que necesitarás al usar Mithril.
Puedes ver un ejemplo interactivo del resultado final aquí
Primero, creemos un punto de entrada para la aplicación. Crea un archivo index.html
:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Mi Aplicación</title>
</head>
<body>
<script src="bin/app.js"></script>
</body>
</html>
La línea <!doctype html>
indica que se trata de un documento HTML5. La primera metaetiqueta charset
especifica la codificación de caracteres del documento, y la metaetiqueta viewport
define cómo los navegadores móviles deben escalar la página. La etiqueta title
contiene el texto que se mostrará en la pestaña del navegador para esta aplicación, y la etiqueta script
indica la ruta al archivo JavaScript que controla la aplicación.
Podríamos crear toda la aplicación en un solo archivo JavaScript, pero eso dificultaría la navegación por el código más adelante. En su lugar, dividamos el código en módulos y generemos un paquete bin/app.js
.
Hay muchas maneras de configurar una herramienta de empaquetado, pero la mayoría se distribuye a través de npm. De hecho, la mayoría de las bibliotecas y herramientas modernas de JavaScript se distribuyen de esa manera, incluido Mithril. Para descargar npm, instala Node.js; npm se instala automáticamente con él. Una vez que tengas Node.js y npm instalados, abre la línea de comandos y ejecuta este comando:
npm init -y
Si npm se ha instalado correctamente, se creará un archivo package.json
. Este archivo contendrá una estructura básica de metadescripción del proyecto. Puedes editar la información del proyecto y del autor en este archivo.
Para instalar Mithril.js, sigue las instrucciones en la página de instalación. Una vez que tengas un esqueleto de proyecto con Mithril.js instalado, estamos listos para crear la aplicación.
Comencemos creando un módulo para almacenar nuestro estado. Creemos un archivo llamado src/models/User.js
// src/models/User.js
var User = {
list: [],
};
module.exports = User;
Ahora agreguemos código para cargar datos desde un servidor. Para comunicarnos con un servidor, podemos usar la utilidad XHR de Mithril.js, m.request
. Primero, importamos Mithril.js en el módulo:
// src/models/User.js
var m = require('mithril');
var User = {
list: [],
};
module.exports = User;
A continuación, creamos una función que realizará una llamada XHR. Llamémosla loadList
// src/models/User.js
var m = require('mithril');
var User = {
list: [],
loadList: function () {
// TODO: make XHR call
},
};
module.exports = User;
Luego podemos agregar una llamada m.request
para hacer una solicitud XHR. Para este tutorial, haremos llamadas XHR a la API REM (ENLACE MUERTO, FIXME: https //rem-rest-api.herokuapp.com/), una API REST simulada diseñada para la creación rápida de prototipos. Esta API devuelve una lista de usuarios desde el punto final GET https://mithril-rem.fly.dev/api/users
. Usemos m.request
para hacer una solicitud XHR y llenar nuestros datos con la respuesta de ese punto final.
Nota: es posible que deban habilitarse las cookies de terceros para que el punto final REM funcione.
// src/models/User.js
var m = require('mithril');
var User = {
list: [],
loadList: function () {
return m
.request({
method: 'GET',
url: 'https://mithril-rem.fly.dev/api/users',
withCredentials: true,
})
.then(function (result) {
User.list = result.data;
});
},
};
module.exports = User;
La opción method
define el método HTTP. Para recuperar datos del servidor sin causar efectos secundarios, necesitamos usar el método GET
. La url
es la dirección del punto final de la API. La línea withCredentials: true
indica que estamos utilizando cookies (lo cual es un requisito para la API REM).
La llamada a m.request
devuelve una Promesa que se resuelve con los datos del punto final. De forma predeterminada, Mithril.js asume que el cuerpo de la respuesta HTTP está en formato JSON y lo analiza automáticamente en un objeto o matriz de JavaScript. La función de callback .then
se ejecuta cuando se completa la solicitud XHR. En este caso, la función de callback asigna la matriz result.data
a User.list
.
Observa que también hay una declaración return
en loadList
. Esta es una buena práctica general cuando se trabaja con Promesas, lo que nos permite encadenar más funciones de callback para que se ejecuten después de la finalización de la solicitud XHR.
Este modelo simple expone dos miembros: User.list
(una matriz de objetos de usuario) y User.loadList
(un método que llena User.list
con datos del servidor).
Ahora, creemos un módulo de vista para mostrar datos de nuestro módulo de modelo de Usuario.
Crea un archivo llamado src/views/UserList.js
. Primero, importamos Mithril.js y nuestro modelo, ya que necesitaremos usar ambos:
// src/views/UserList.js
var m = require('mithril');
var User = require('../models/User');
A continuación, creemos un componente de Mithril.js. Un componente es simplemente un objeto que tiene un método view
:
// src/views/UserList.js
var m = require('mithril');
var User = require('../models/User');
module.exports = {
view: function () {
// TODO add code here
},
};
Por defecto, las vistas de Mithril.js se describen utilizando hyperscript. Hyperscript ofrece una sintaxis concisa que se puede indentar de forma más natural que HTML para etiquetas complejas, y dado que su sintaxis es solo JavaScript, es posible aprovechar una gran cantidad del ecosistema de herramientas de JavaScript. Por ejemplo:
- Puedes usar Babel para transpilación de ES6+ a ES5 para IE y para transpilación de JSX (una extensión de sintaxis en línea similar a HTML) a llamadas de hyperscript apropiadas.
- Puedes usar ESLint para facilitar el linting sin complementos especiales.
- Puedes usar Terser o UglifyJS (solo ES5) para minimizar tu código fácilmente.
- Puedes usar Istanbul para la cobertura de código.
- Puedes usar TypeScript para facilitar el análisis de código. (Hay definiciones de tipo compatibles con la comunidad disponibles, por lo que no necesitas crear las tuyas propias).
Comencemos con hyperscript y creemos una lista de elementos. Hyperscript es la forma idiomática de usar Mithril.js, pero JSX funciona de manera muy similar.
// src/views/UserList.js
var m = require('mithril');
var User = require('../models/User');
module.exports = {
view: function () {
return m('.user-list');
},
};
La cadena .user-list
es un selector CSS y, como era de esperar, .user-list
representa una clase. Cuando no se especifica una etiqueta, div
es el valor predeterminado. Entonces, esta vista es equivalente a <div class="user-list"></div>
.
Ahora, hagamos referencia a la lista de usuarios del modelo que creamos anteriormente (User.list
) para iterar dinámicamente sobre los datos:
// src/views/UserList.js
var m = require('mithril');
var User = require('../models/User');
module.exports = {
view: function () {
return m(
'.user-list',
User.list.map(function (user) {
return m('.user-list-item', user.firstName + ' ' + user.lastName);
})
);
},
};
Dado que User.list
es una matriz de JavaScript, y dado que las vistas de hyperscript son simplemente JavaScript, podemos iterar sobre la matriz usando el método .map
. Esto crea una matriz de vnodes que representa una lista de div
s, cada uno conteniendo el nombre de un usuario.
El problema, por supuesto, es que nunca hemos llamado a la función User.loadList
. Por lo tanto, User.list
sigue siendo una matriz vacía y, por lo tanto, esta vista renderizaría una página en blanco. Dado que queremos que se llame a User.loadList
al renderizar este componente, podemos aprovechar los métodos del ciclo de vida del componente.
// src/views/UserList.js
var m = require('mithril');
var User = require('../models/User');
module.exports = {
oninit: User.loadList,
view: function () {
return m(
'.user-list',
User.list.map(function (user) {
return m('.user-list-item', user.firstName + ' ' + user.lastName);
})
);
},
};
Observa que agregamos un método oninit
al componente, que hace referencia a User.loadList
. Esto significa que cuando se inicializa el componente, se llamará a User.loadList
, lo que activará una solicitud XHR. Cuando el servidor devuelve una respuesta, User.list
se llenará.
También observa que no hicimos oninit: User.loadList()
(con paréntesis al final). La diferencia es que oninit: User.loadList()
llama a la función una vez e inmediatamente, pero oninit: User.loadList
solo llama a esa función cuando se renderiza el componente. Esta es una diferencia importante y una trampa común para los desarrolladores nuevos en JavaScript: llamar a la función inmediatamente significa que la solicitud XHR se activará tan pronto como se evalúe el código fuente, incluso si el componente nunca se renderiza. Además, si el componente se vuelve a crear (al navegar hacia atrás y hacia adelante a través de la aplicación), la función no se volverá a llamar como se esperaba.
Rendericemos la vista desde el archivo de punto de entrada src/index.js
que creamos anteriormente:
// src/index.js
var m = require('mithril');
var UserList = require('./views/UserList');
m.mount(document.body, UserList);
La llamada a m.mount
renderiza el componente especificado (UserList
) en un elemento DOM (document.body
), borrando cualquier DOM que estuviera allí previamente. Abrir el archivo HTML en un navegador ahora debería mostrar una lista de nombres de personas.
En este momento, la lista se ve bastante simple porque no hemos definido estilos. Así que agreguemos algunos. Primero, creemos un archivo llamado styles.css
e incluyámoslo en el archivo index.html
:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Mi Aplicación</title>
<link href="styles.css" rel="stylesheet" />
</head>
<body>
<script src="bin/app.js"></script>
</body>
</html>
Ahora podemos dar estilo al componente UserList
:
.user-list {
list-style: none;
margin: 0 0 10px;
padding: 0;
}
.user-list-item {
background: #fafafa;
border: 1px solid #ddd;
color: #333;
display: block;
margin: 0 0 1px;
padding: 8px 15px;
text-decoration: none;
}
.user-list-item:hover {
text-decoration: underline;
}
Recargar la ventana del navegador ahora debería mostrar algunos elementos con estilo.
Agreguemos enrutamiento a nuestra aplicación.
El enrutamiento consiste en asociar una URL única a cada vista, permitiendo la navegación entre "páginas". Mithril.js está diseñado para Aplicaciones de una Sola Página (SPA), por lo que estas "páginas" no son necesariamente diferentes archivos HTML en el sentido tradicional de la palabra. En cambio, el enrutamiento en las SPA conserva el mismo archivo HTML durante toda su vida útil, pero cambia el estado de la aplicación a través de JavaScript. El enrutamiento del lado del cliente tiene el beneficio de evitar parpadeos de pantalla en blanco entre las transiciones de página y puede reducir la cantidad de datos que se envían desde el servidor cuando se usa junto con una arquitectura orientada a servicios web (es decir, una aplicación que descarga datos como JSON en lugar de descargar fragmentos prerrenderizados de HTML detallado).
Podemos agregar enrutamiento cambiando la llamada a m.mount
por una llamada a m.route
.
// src/index.js
var m = require('mithril');
var UserList = require('./views/UserList');
m.route(document.body, '/list', {
'/list': UserList,
});
La llamada a m.route
especifica que la aplicación se renderizará en document.body
. El argumento "/list"
es la ruta predeterminada. Esto significa que el usuario será redirigido a esa ruta si accede a una URL inexistente. El objeto {"/list": UserList}
declara un mapa de rutas existentes y a qué componentes se asigna cada ruta.
Actualizar la página en el navegador ahora debería agregar #!/list
a la URL para indicar que el enrutamiento está funcionando. Dado que esa ruta renderiza UserList
, aún deberíamos ver la lista de personas en la pantalla como antes.
El fragmento #!
se conoce como hashbang, y es una cadena de uso común para implementar el enrutamiento del lado del cliente. Es posible configurar esta cadena a través de m.route.prefix
. Algunas configuraciones requieren cambios de soporte del lado del servidor, por lo que continuaremos usando el hashbang para el resto de este tutorial.
Agreguemos otra ruta a nuestra aplicación para editar usuarios. Primero, creemos un módulo llamado views/UserForm.js
// src/views/UserForm.js
module.exports = {
view: function () {
// TODO implement view
},
};
Luego podemos require
este nuevo módulo desde src/index.js
// src/index.js
var m = require('mithril');
var UserList = require('./views/UserList');
var UserForm = require('./views/UserForm');
m.route(document.body, '/list', {
'/list': UserList,
});
Y finalmente, podemos crear una ruta que haga referencia a él:
// src/index.js
var m = require('mithril');
var UserList = require('./views/UserList');
var UserForm = require('./views/UserForm');
m.route(document.body, '/list', {
'/list': UserList,
'/edit/:id': UserForm,
});
Observa que la nueva ruta contiene un :id
. Este es un parámetro de ruta; puedes pensar en él como un comodín; la ruta /edit/1
se asignaría a UserForm
con un id
de "1"
. /edit/2
también se asignaría a UserForm
, pero con un id
de "2"
. Y así sucesivamente.
Implementemos el componente UserForm
para que pueda responder a esos parámetros de ruta:
// src/views/UserForm.js
var m = require('mithril');
module.exports = {
view: function () {
return m('form', [
m('label.label', 'Nombre'),
m('input.input[type=text][placeholder=Nombre]'),
m('label.label', 'Apellido'),
m('input.input[placeholder=Apellido]'),
m('button.button[type=submit]', 'Guardar'),
]);
},
};
Y agreguemos algunos estilos más a styles.css
:
/* styles.css */
body,
.input,
.button {
font: normal 16px Verdana;
margin: 0;
}
.user-list {
list-style: none;
margin: 0 0 10px;
padding: 0;
}
.user-list-item {
background: #fafafa;
border: 1px solid #ddd;
color: #333;
display: block;
margin: 0 0 1px;
padding: 8px 15px;
text-decoration: none;
}
.user-list-item:hover {
text-decoration: underline;
}
.label {
display: block;
margin: 0 0 5px;
}
.input {
border: 1px solid #ddd;
border-radius: 3px;
box-sizing: border-box;
display: block;
margin: 0 0 10px;
padding: 10px 15px;
width: 100%;
}
.button {
background: #eee;
border: 1px solid #ddd;
border-radius: 3px;
color: #333;
display: inline-block;
margin: 0 0 10px;
padding: 10px 15px;
text-decoration: none;
}
.button:hover {
background: #e8e8e8;
}
En este momento, este componente no responde a los eventos del usuario. Agreguemos algo de código a nuestro modelo User
en src/models/User.js
. Así es como está el código ahora:
// src/models/User.js
var m = require('mithril');
var User = {
list: [],
loadList: function () {
return m
.request({
method: 'GET',
url: 'https://mithril-rem.fly.dev/api/users',
withCredentials: true,
})
.then(function (result) {
User.list = result.data;
});
},
};
module.exports = User;
Agreguemos código para permitirnos cargar un solo usuario
// src/models/User.js
var m = require('mithril');
var User = {
list: [],
loadList: function () {
return m
.request({
method: 'GET',
url: 'https://mithril-rem.fly.dev/api/users',
withCredentials: true,
})
.then(function (result) {
User.list = result.data;
});
},
current: {},
load: function (id) {
return m
.request({
method: 'GET',
url: 'https://mithril-rem.fly.dev/api/users/' + id,
withCredentials: true,
})
.then(function (result) {
User.current = result;
});
},
};
module.exports = User;
Observa que hemos agregado una propiedad User.current
y un método User.load(id)
que llena esa propiedad. Ahora podemos llenar la vista UserForm
utilizando este nuevo método:
// src/views/UserForm.js
var m = require('mithril');
var User = require('../models/User');
module.exports = {
oninit: function (vnode) {
User.load(vnode.attrs.id);
},
view: function () {
return m('form', [
m('label.label', 'Nombre'),
m('input.input[type=text][placeholder=Nombre]', {
value: User.current.firstName,
}),
m('label.label', 'Apellido'),
m('input.input[placeholder=Apellido]', { value: User.current.lastName }),
m('button.button[type=submit]', 'Guardar'),
]);
},
};
Al igual que en el componente UserList
, oninit
llama a User.load()
. ¿Recuerdas que teníamos un parámetro de ruta llamado :id
en la ruta "/edit/:id": UserForm
? El parámetro de ruta se convierte en un atributo del vnode del componente UserForm
. Por ejemplo, al acceder a /edit/1
, vnode.attrs.id
tendrá el valor "1"
.
Ahora, modifiquemos la vista UserList
para que podamos navegar desde allí a un UserForm
.
// src/views/UserList.js
var m = require('mithril');
var User = require('../models/User');
module.exports = {
oninit: User.loadList,
view: function () {
return m(
'.user-list',
User.list.map(function (user) {
return m(
m.route.Link,
{
class: 'user-list-item',
href: '/edit/' + user.id,
},
user.firstName + ' ' + user.lastName
);
})
);
},
};
Aquí intercambiamos el vnode .user-list-item
por un m.route.Link
con esa clase y los mismos hijos. Agregamos un href
que hace referencia a la ruta que queremos. Lo que esto significa es que al hacer clic en el enlace se cambiaría la parte de la URL que viene después del hashbang #!
(cambiando así la ruta sin descargar la página HTML actual). Internamente, utiliza un elemento <a>
para implementar el enlace, y todo funciona automáticamente.
Si actualizas la página en el navegador, ahora deberías poder hacer clic en una persona y ser dirigido a un formulario. También deberías poder presionar el botón de retroceso en el navegador para regresar del formulario a la lista de personas.
El formulario aún no se guarda al presionar "Guardar". Hagamos que este formulario funcione:
// src/views/UserForm.js
var m = require('mithril');
var User = require('../models/User');
module.exports = {
oninit: function (vnode) {
User.load(vnode.attrs.id);
},
view: function () {
return m(
'form',
{
onsubmit: function (e) {
e.preventDefault();
User.save();
},
},
[
m('label.label', 'Nombre de pila'),
m('input.input[type=text][placeholder=Nombre]', {
oninput: function (e) {
User.current.firstName = e.target.value;
},
value: User.current.firstName,
}),
m('label.label', 'Apellido familiar'),
m('input.input[placeholder=Apellido]', {
oninput: function (e) {
User.current.lastName = e.target.value;
},
value: User.current.lastName,
}),
m('button.button[type=submit]', 'Guardar cambios'),
]
);
},
};
Añadimos eventos oninput
a ambos campos de entrada, que actualizan las propiedades User.current.firstName
y User.current.lastName
a medida que el usuario escribe.
Además, especificamos que se debe llamar al método User.save
cuando se pulse el botón "Guardar". Implementemos este método:
// src/models/User.js
var m = require('mithril');
var User = {
list: [],
loadList: function () {
return m
.request({
method: 'GET',
url: 'https://mithril-rem.fly.dev/api/users',
withCredentials: true,
})
.then(function (result) {
User.list = result.data;
});
},
current: {},
load: function (id) {
return m
.request({
method: 'GET',
url: 'https://mithril-rem.fly.dev/api/users/' + id,
withCredentials: true,
})
.then(function (result) {
User.current = result;
});
},
save: function () {
return m.request({
method: 'PUT',
url: 'https://mithril-rem.fly.dev/api/users/' + User.current.id,
body: User.current,
withCredentials: true,
});
},
};
module.exports = User;
En el método save
, utilizamos el método HTTP PUT
para indicar que estamos actualizando o insertando datos en el servidor, dependiendo de si ya existen.
Ahora intenta editar el nombre de un usuario en la aplicación. Una vez que guardes un cambio, deberías ver el cambio reflejado en la lista de usuarios.
Actualmente, solo podemos volver a la lista de usuarios usando el botón de retroceso del navegador. Lo ideal sería tener un menú o, de forma más general, una estructura donde podamos colocar elementos de la interfaz de usuario (IU) global.
Creemos un archivo src/views/Layout.js
:
// src/views/Layout.js
var m = require('mithril');
module.exports = {
view: function (vnode) {
return m('main.layout', [
m('nav.menu', [m(m.route.Link, { href: '/list' }, 'Usuarios')]),
m('section', vnode.children),
]);
},
};
Este componente es bastante simple: contiene un elemento <nav>
con un enlace a la lista de usuarios. De forma similar a lo que hicimos con los enlaces /edit
, este enlace utiliza m.route.Link
para crear un enlace navegable.
Observa que también hay un elemento <section>
que tiene vnode.children
como elementos secundarios. vnode
es una referencia al vnode que representa una instancia del componente Layout
(es decir, el vnode devuelto por una llamada a m(Layout)
). Por lo tanto, vnode.children
hace referencia a cualquier elemento hijo de ese vnode.
Y actualicemos los estilos una vez más:
/* styles.css */
body,
.input,
.button {
font: normal 16px Verdana;
margin: 0;
}
.layout {
margin: 10px auto;
max-width: 1000px;
}
.menu {
margin: 0 0 30px;
}
.user-list {
list-style: none;
margin: 0 0 10px;
padding: 0;
}
.user-list-item {
background: #fafafa;
border: 1px solid #ddd;
color: #333;
display: block;
margin: 0 0 1px;
padding: 8px 15px;
text-decoration: none;
}
.user-list-item:hover {
text-decoration: underline;
}
.label {
display: block;
margin: 0 0 5px;
}
.input {
border: 1px solid #ddd;
border-radius: 3px;
box-sizing: border-box;
display: block;
margin: 0 0 10px;
padding: 10px 15px;
width: 100%;
}
.button {
background: #eee;
border: 1px solid #ddd;
border-radius: 3px;
color: #333;
display: inline-block;
margin: 0 0 10px;
padding: 10px 15px;
text-decoration: none;
}
.button:hover {
background: #e8e8e8;
}
Modifiquemos el enrutador en src/index.js
para integrar nuestra estructura a la aplicación:
// src/index.js
var m = require('mithril');
var UserList = require('./views/UserList');
var UserForm = require('./views/UserForm');
var Layout = require('./views/Layout');
m.route(document.body, '/list', {
'/list': {
render: function () {
return m(Layout, m(UserList));
},
},
'/edit/:id': {
render: function (vnode) {
return m(Layout, m(UserForm, vnode.attrs));
},
},
});
Sustituimos cada componente por un RouteResolver (que es, básicamente, un objeto con un método render
). Los métodos render
se pueden escribir de la misma manera que las vistas de componentes regulares, anidando llamadas a m()
.
Lo interesante a destacar es cómo se pueden usar los componentes en lugar de una cadena de selector en una llamada a la función m()
. Aquí, en la ruta /list
, tenemos m(Layout, m(UserList))
. Esto significa que hay un vnode raíz que representa una instancia de Layout
y que tiene un vnode UserList
como su único hijo.
En la ruta /edit/:id
, también hay un argumento vnode
que pasa los parámetros de la ruta al componente UserForm
. Entonces, si la URL es /edit/1
, entonces vnode.attrs
en este caso es {id: 1}
, y este m(UserForm, vnode.attrs)
es equivalente a m(UserForm, {id: 1})
. El código JSX equivalente sería <UserForm id={vnode.attrs.id} />
.
Actualiza la página en el navegador y ahora podrás ver la navegación global en cada página de la aplicación.
Esto concluye el tutorial.
En este tutorial, explicamos el proceso de creación de una aplicación muy simple en la que podemos listar usuarios de un servidor y editarlos individualmente. Como ejercicio adicional, intenta implementar la creación y eliminación de usuarios por tu cuenta.
Si deseas ver más ejemplos de código de Mithril.js, consulta la página de ejemplos. Si tienes preguntas, puedes unirte al chat de Mithril.js.