Aplicativo Simples
Vamos desenvolver um aplicativo simples que mostra como fazer a maioria das coisas que você precisaria lidar ao usar o Mithril.
Um exemplo interativo do resultado final pode ser visto aqui
Primeiro, vamos criar um ponto de entrada para a aplicação. Crie um arquivo index.html
:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Minha Aplicação</title>
</head>
<body>
<script src="bin/app.js"></script>
</body>
</html>
A linha <!doctype html>
indica que se trata de um documento HTML5. A primeira meta tag charset
define a codificação de caracteres do documento, enquanto a meta tag viewport
configura como os navegadores móveis devem dimensionar a página. A tag title
contém o texto a ser exibido na aba do navegador para esta aplicação, e a tag script
indica o caminho para o arquivo JavaScript que controla a aplicação.
Poderíamos criar toda a aplicação em um único arquivo JavaScript, mas isso dificultaria a navegação no código mais tarde. Em vez disso, vamos modularizar o código e criar um bundle bin/app.js
.
Existem diversas ferramentas para criar bundlers, mas a maioria é distribuída via npm. De fato, a maioria das bibliotecas e ferramentas JavaScript modernas, incluindo o Mithril, são distribuídas dessa forma. Após instalar o Node.js e o npm, abra o terminal (linha de comando) e execute o seguinte comando:
npm init -y
Se o npm estiver instalado corretamente, um arquivo package.json
será criado. Este arquivo conterá a estrutura básica de metadados do projeto. Você pode editar as informações do projeto e do autor neste arquivo.
Para instalar o Mithril.js, siga as instruções na página de instalação. Com a estrutura do projeto e o Mithril.js instalados, podemos começar a desenvolver a aplicação.
Vamos começar criando um módulo para armazenar nosso estado. Vamos criar um arquivo chamado src/models/User.js
// src/models/User.js
var User = {
list: [],
};
module.exports = User;
Agora, vamos adicionar o código necessário para buscar dados de um servidor. Para comunicação com o servidor, utilizaremos o utilitário XHR do Mithril.js, m.request
. Primeiro, incluímos o Mithril.js no módulo:
// src/models/User.js
var m = require('mithril');
var User = {
list: [],
};
module.exports = User;
Em seguida, criaremos uma função para realizar a requisição XHR. Vamos chamá-la de loadList
:
// src/models/User.js
var m = require('mithril');
var User = {
list: [],
loadList: function () {
// TODO: fazer chamada XHR
},
};
module.exports = User;
Então, adicionaremos uma chamada a m.request
para fazer uma requisição XHR. Neste tutorial, utilizaremos a API REM (link MORTO, FIXME: https //rem-rest-api.herokuapp.com/), uma API REST mock criada para prototipagem rápida, para realizar as requisições XHR. Esta API retorna uma lista de usuários através do endpoint GET https://mithril-rem.fly.dev/api/users
. Utilizaremos m.request
para realizar a requisição XHR e preencher nossos dados com a resposta do endpoint.
Nota: cookies de terceiros podem ter que ser habilitados para que o endpoint 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;
A opção method
especifica o método HTTP a ser utilizado. Para obter dados do servidor sem causar alterações, utilizaremos o método GET
. url
é o endereço do endpoint da API. withCredentials: true
indica que cookies serão enviados na requisição (necessário para a API REM).
m.request
retorna uma Promise que, quando resolvida, contém os dados do endpoint. Por padrão, Mithril.js interpreta a resposta HTTP como JSON e a converte automaticamente em um objeto ou array JavaScript. O callback .then
será executado após a conclusão da requisição XHR. Neste caso, o callback atribui o array result.data
à propriedade User.list
.
Note que também retornamos a Promise em loadList
. Essa é uma boa prática ao trabalhar com Promises, pois permite encadear outros callbacks que serão executados após a conclusão da requisição XHR.
Este modelo expõe duas propriedades: User.list
(um array contendo objetos de usuário) e User.loadList
(um método para preencher User.list
com dados do servidor).
Agora, vamos criar um módulo de visualização para exibir os dados do nosso modelo User.
Crie o arquivo src/views/UserList.js
. Nele, importe o Mithril.js e o modelo User, pois ambos serão necessários:
// src/views/UserList.js
var m = require('mithril');
var User = require('../models/User');
Em seguida, criaremos um componente Mithril.js. Um componente é um objeto que possui um método view
:
// src/views/UserList.js
var m = require('mithril');
var User = require('../models/User');
module.exports = {
view: function () {
// TODO adicionar código aqui
},
};
Por padrão, as views do Mithril.js são definidas utilizando hyperscript. Hyperscript oferece uma sintaxe concisa e mais fácil de indentar do que HTML para tags complexas. Além disso, por ser JavaScript puro, permite o uso de diversas ferramentas do ecossistema JavaScript. Por exemplo:
- Você pode utilizar Babel para transpilar código ES6+ para ES5 (compatível com o Internet Explorer) e para transpilar JSX (uma extensão de sintaxe similar ao HTML) em chamadas hyperscript.
- Você pode usar ESLint para linting fácil sem plugins especiais.
- Você pode usar Terser ou UglifyJS (somente ES5) para minimizar seu código facilmente.
- Você pode usar Istanbul para cobertura de código.
- Você pode usar TypeScript para análise de código fácil. (Existem definições de tipo suportadas pela comunidade disponíveis, então você não precisa criar as suas próprias.)
Vamos começar utilizando hyperscript para criar uma lista de itens. Hyperscript é a forma mais comum de usar Mithril.js, mas JSX também funciona de maneira similar.
// src/views/UserList.js
var m = require('mithril');
var User = require('../models/User');
module.exports = {
view: function () {
return m('.user-list');
},
};
A string ".user-list"
é um seletor CSS e, como esperado, .user-list
define uma classe. Quando a tag não é especificada, o elemento div
é utilizado por padrão. Portanto, essa view é equivalente ao seguinte HTML: <div class="user-list"></div>
.
Agora, vamos utilizar a lista de usuários do modelo (User.list
) para gerar a lista dinamicamente:
// 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);
})
);
},
};
Como User.list
é um array JavaScript e as views hyperscript são JavaScript puro, podemos iterar sobre o array utilizando o método .map
. Isso cria um array de vnodes (nós virtuais) representando uma lista de elementos div
, cada um exibindo o nome de um usuário.
O problema é que ainda não chamamos a função User.loadList
. Portanto, User.list
permanece vazio e a view renderizará uma página em branco. Para que User.loadList
seja executado quando o componente for renderizado, podemos utilizar os métodos de ciclo de vida do 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);
})
);
},
};
Isso significa que, quando o componente for inicializado, User.loadList
será chamado, disparando a requisição XHR. Quando o servidor responder, User.list
será preenchido com os dados.
Note que não utilizamos oninit: User.loadList()
(com parênteses). A diferença é que oninit: User.loadList()
executaria a função imediatamente, enquanto oninit: User.loadList
a executa apenas quando o componente é renderizado.
Essa é uma diferença crucial e um erro comum para iniciantes em JavaScript: executar a função imediatamente fará com que a requisição XHR seja disparada assim que o código for interpretado, mesmo que o componente nunca seja renderizado. Além disso, se o componente for recriado (por exemplo, ao navegar para frente e para trás na aplicação), a função não será executada novamente como esperado.
Vamos renderizar a view a partir do arquivo de entrada src/index.js
que criamos anteriormente:
// src/index.js
var m = require('mithril');
var UserList = require('./views/UserList');
m.mount(document.body, UserList);
m.mount
renderiza o componente especificado (UserList
) dentro do elemento DOM (document.body
), substituindo qualquer conteúdo existente. Ao abrir o arquivo HTML no navegador, você deverá ver uma lista de nomes.
A lista está sem formatação, pois ainda não definimos nenhum estilo. Vamos adicionar alguns estilos. Primeiro, vamos criar um arquivo chamado styles.css
e incluí-lo no arquivo index.html
:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Minha Aplicação</title>
<link href="styles.css" rel="stylesheet" />
</head>
<body>
<script src="bin/app.js"></script>
</body>
</html>
Agora podemos aplicar estilos ao 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;
}
Ao recarregar a página, os elementos estilizados devem ser exibidos.
Vamos adicionar roteamento à nossa aplicação.
Roteamento significa associar um componente a um URL específico, permitindo a navegação entre diferentes "páginas" da aplicação. Mithril.js é projetado para Single Page Applications (SPAs), onde as "páginas" não correspondem a arquivos HTML distintos, como em aplicações tradicionais.
Em SPAs, o roteamento mantém o mesmo arquivo HTML durante toda a sessão, alterando o conteúdo da página dinamicamente através de JavaScript. O roteamento no lado do cliente evita o "flash" de tela branca durante a transição entre páginas e pode reduzir a quantidade de dados transferidos do servidor, especialmente quando utilizado em conjunto com uma arquitetura orientada a serviços web (onde os dados são obtidos via JSON em vez de HTML pré-renderizado).
Para adicionar roteamento, substituiremos a chamada m.mount
por m.route
:
// src/index.js
var m = require('mithril');
var UserList = require('./views/UserList');
m.route(document.body, '/list', {
'/list': UserList,
});
m.route
especifica que a aplicação será renderizada no elemento document.body
. O argumento "/list"
define a rota padrão. Isso significa que o usuário será redirecionado para essa rota caso acesse um URL inexistente. O objeto {"/list": UserList}
define um mapeamento entre as rotas e os componentes correspondentes.
Ao atualizar a página, #!/list
deve ser adicionado ao URL, indicando que o roteamento está funcionando corretamente. Como essa rota renderiza o componente UserList
, a lista de usuários deve ser exibida como antes.
O trecho #!
é conhecido como hashbang e é comumente utilizado para implementar o roteamento no lado do cliente. É possível configurar esse prefixo através da propriedade m.route.prefix
. Algumas configurações exigem alterações no servidor, portanto, continuaremos utilizando o hashbang neste tutorial.
Vamos adicionar uma nova rota para permitir a edição de usuários. Primeiro, vamos criar um módulo chamado views/UserForm.js
// src/views/UserForm.js
module.exports = {
view: function () {
// TODO implementar visualização
},
};
Em seguida, importaremos o novo módulo em 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,
});
Finalmente, criaremos uma rota que referencia o novo módulo:
// 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,
});
Note que a nova rota contém :id
. Este é um parâmetro de rota, um "coringa". A rota /edit/1
direcionaria para o componente UserForm
com o parâmetro id
igual a "1"
. Da mesma forma, /edit/2
também direcionaria para UserForm
, mas com id
igual a "2"
.
Vamos implementar o componente UserForm
para que ele possa receber e utilizar esses parâmetros de rota:
// src/views/UserForm.js
var m = require('mithril');
module.exports = {
view: function () {
return m('form', [
m('label.label', 'First name'),
m('input.input[type=text][placeholder=First name]'),
m('label.label', 'Last name'),
m('input.input[placeholder=Last name]'),
m('button.button[type=submit]', 'Save'),
]);
},
};
E vamos adicionar mais alguns estilos para 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;
}
Atualmente, este componente não responde a nenhuma interação do usuário. Vamos adicionar código ao modelo User
em src/models/User.js
. Veja como o código está agora:
// 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;
Vamos adicionar o código necessário para carregar os dados de um único usuário.
// 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;
Note que adicionamos a propriedade User.current
e o método User.load(id)
, que preenche essa propriedade com os dados do usuário. Agora podemos preencher a view UserForm
utilizando esse novo 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', 'First name'),
m('input.input[type=text][placeholder=First name]', {
value: User.current.firstName,
}),
m('label.label', 'Last name'),
m('input.input[placeholder=Last name]', { value: User.current.lastName }),
m('button.button[type=submit]', 'Save'),
]);
},
};
Assim como no componente UserList
, oninit
chama User.load()
. Lembra do parâmetro de rota :id
na rota "/edit/:id": UserForm
? Esse parâmetro se torna um atributo do vnode do componente UserForm
. Ao acessar /edit/1
, vnode.attrs.id
terá o valor "1"
.
Agora, vamos modificar a view UserList
para permitir a navegação para o componente 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
);
})
);
},
};
Aqui, substituímos o vnode .user-list-item
por um componente m.route.Link
com a mesma classe e conteúdo. Adicionamos um atributo href
que aponta para a rota desejada.
Isso significa que, ao clicar no link, a parte do URL após o hashbang #!
será alterada (mudando a rota sem recarregar a página HTML). Internamente, o componente utiliza um elemento <a>
para criar o link, e o roteamento é feito de forma transparente.
Ao atualizar a página no navegador, você poderá clicar em um nome e será redirecionado para o formulário de edição. Você também poderá utilizar o botão "Voltar" do navegador para retornar do formulário para a lista de usuários.
O formulário ainda não salva os dados quando você clica em "Salvar". Vamos implementar essa funcionalidade:
// 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', 'First name'),
m('input.input[type=text][placeholder=First name]', {
oninput: function (e) {
User.current.firstName = e.target.value;
},
value: User.current.firstName,
}),
m('label.label', 'Last name'),
m('input.input[placeholder=Last name]', {
oninput: function (e) {
User.current.lastName = e.target.value;
},
value: User.current.lastName,
}),
m('button.button[type=submit]', 'Save'),
]
);
},
};
Adicionamos eventos oninput
aos campos de entrada para atualizar as propriedades User.current.firstName
e User.current.lastName
à medida que o usuário digita.
Além disso, definimos que o método User.save
será executado ao clicar no botão "Salvar". Agora, vamos implementar esse 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;
No método save
, utilizamos o método HTTP PUT
para indicar que estamos atualizando os dados do usuário no servidor.
Agora, tente editar o nome de um usuário no aplicativo. Após salvar a alteração, você deverá vê-la refletida na lista de usuários.
Atualmente, só conseguimos voltar para a lista de usuários usando o botão "Voltar" do navegador. Idealmente, teríamos um menu ou, de forma mais geral, um layout onde pudéssemos incluir elementos globais da interface do usuário (UI).
Vamos criar um arquivo 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' }, 'Users')]),
m('section', vnode.children),
]);
},
};
Este componente é bem simples: ele possui um <nav>
com um link para a lista de usuários. De forma similar ao que fizemos com os links /edit
, este link utiliza m.route.Link
para criar um link que gerencia as rotas da aplicação.
Note que também existe um elemento <section>
que recebe vnode.children
como seus elementos filhos. vnode
é uma referência ao vnode que representa uma instância do componente Layout
(ou seja, o vnode retornado por uma chamada m(Layout)
). Portanto, vnode.children
se refere a todos os elementos filhos desse vnode.
E vamos atualizar os estilos mais uma vez:
/* 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;
}
Vamos modificar o roteador em src/index.js
para integrar o novo layout:
// 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));
},
},
});
Substituímos cada componente por um RouteResolver (que é essencialmente um objeto com um método render
). Os métodos render
podem ser escritos da mesma forma que as views de componentes regulares, aninhando chamadas m()
.
Um ponto importante é observar como os componentes podem ser utilizados no lugar de uma string de seletor em uma chamada m()
. Aqui, na rota /list
, temos m(Layout, m(UserList))
. Isso significa que existe um vnode raiz representando uma instância de Layout
, que contém um vnode UserList
como seu único elemento filho.
Na rota /edit/:id
, existe também um argumento vnode
que passa os parâmetros da rota para o componente UserForm
. Assim, se a URL for /edit/1
, vnode.attrs
será {id: 1}
, e m(UserForm, vnode.attrs)
é equivalente a m(UserForm, {id: 1})
. O código JSX equivalente seria <UserForm id={vnode.attrs.id} />
.
Atualize a página no navegador e você verá a barra de navegação global em todas as páginas da aplicação.
Isto conclui o tutorial.
Neste tutorial, abordamos o processo de criação de uma aplicação simples que permite listar usuários de um servidor e editá-los individualmente. Como um exercício extra, tente implementar a criação e exclusão de usuários por conta própria.
Se você tiver alguma dúvida, participe da sala de bate-papo do Mithril.js.