Mejora de Interfaces con Bootstrap en Aplicaciones Rails
Clase 34 de 36 • Curso de Introducción a Ruby on Rails
Objetivo
En esta clase vamos a tomar las vistas y usando convenciones de clases de estilo, helpers de vistas y algunas refactorizaciones de modelo, mejoraremos y embelleceremos toda nuestra aplicación, obteniendo un producto final como está en la imagen abajo, para esta clase usaremos Bootstrap como framework de componentes gráficos, sin embargo, no es objetivo de la clase hacer un tutorial de Bootstrap o abordarlo con profundidad, sino más bien establecer una conexión entre Rails y este framework.
Layouts
Vamos a retomar nuestra layout por defecto y agregar componentes que podamos reusar en las vistas tal y como te lo mencionaba en la clase de Assets y Layouts.
Vamos a plantear una estructura gráfica de tres secciones en nuestra aplicación: header (cabecera con barra de navegación superior), messages (mensajes) y main container (contenedor principal)
Lo primero que vamos a hacer es transformar el archivo app/views/layouts/application.html.erb
a HAML, para esto podemos: hacerlo manual, usar la tarea de hamlit-rails
, o usar la página https://htmltohaml.com/
En este caso, usaremos la última opción así que copiaremos el contenido del archivo app/views/layouts/application.html.erb
a la sección izquierda de la página mencionada.
Seleccionaremos toda la sección derecha y la pegaremos de vuelta al archivo app/views/layouts/application.html.erb
, por último cambiaremos el nombre del mismo archivo de application.html.erb
a application.html.haml
quedando de la siguiente forma:
-# frozen_string_literal: true -# app/views/layouts/application.html.haml !!! %html %head %meta{:content => "text/html; charset=UTF-8", "http-equiv" => "Content-Type"} %title Organizador = csrf_meta_tags = csp_meta_tag = stylesheet_link_tag 'ap#plication', media: 'all', 'data-turbolinks-track': 'reload' = javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %body = link_to 'cerrar sesión', destroy_user_session_path, method: :delete = yield
Una vez tenemos nuestro layout convertido a HAML, procederemos a editar el %body
del archivo, reemplazando el contenido de la siguiente forma:
%body = render 'layouts/navbar' .container.mt-4.pb-4 .main-flash-messages = render 'layouts/messages' = yield
En el bloque anterior, en el primer render
estamos invocando al parcial encargado de top navbar o de la barra de navegación, el segundo render
está invocando al parcial de los mensajes, que contendrán alertas de procesos CRUD (creación, lectura, actualización o eliminación) o similares en los registros de base de datos de nuestra aplicación, finalmente, el yield
estará encargado de invocar la renderización de nuestras vistas, es decir, el contenido que hemos manipulado hasta ahora en los archivos .haml
de la carpeta app/views
que hemos generado con los scaffold o procesos similares. Debes tener en cuenta que ninguno de los parciales ha sido creado hasta ahora, estos los crearemos en los siguientes bloques
Barra de navegación
La barra de navegación será un parcial que crearemos en el archivo app/views/layouts/_navbar.html.haml
con el siguiente contenido (recuerda que los parciales en Rails comienzan con _ underscore)
-# app/views/layouts/_navbar.html.haml %nav.navbar.navbar-expand-lg.navbar-dark.bg-dark-blue %a.navbar-brand = image_tag 'logo.png', class: 'd-inline-block align-top', width: 85 %button.navbar-toggler{"aria-controls" => "navbarNavDropdown", "aria-expanded" => "false", "aria-label" => "Toggle navigation", "data-target" => "#navbarNavDropdown", "data-toggle" => "collapse", :type => "button"} %span.navbar-toggler-icon #navbarNavDropdown.collapse.navbar-collapse %ul.navbar-nav %li.nav-item.active %a.nav-link{ href: root_path } Inicio %span.sr-only (current) %li.nav-item %a.nav-link{ href: categories_path } Categorías = link_to 'Crear Tarea', new_task_path, class: 'btn btn-light ml-2' .dropdown.dropdown-user %a#userMenu{"aria-expanded" => "false", "aria-haspopup" => "true", "data-toggle" => "dropdown"} = image_tag "https://robohash.org/#{current_user.email}?size=38x38" .dropdown-menu.dropdown-menu-right{"aria-labelledby" => "userMenu"} .dropdown-item.small.text-right= current_user.email .dropdown-divider = link_to destroy_user_session_path, method: :delete, class: 'dropdown-item' do .fas.fa-sign-out Cerrar sesión
Gran parte del contenido, es extraído de una barra de navegación de bootstrap, por primera hemos introducido el helper image_tag
apuntando a una imagen que debemos copiar y colocar dentro del directorio app/assets/images
y que tiene por nombre logo.png:
Por otro lado, es importante mencionar, que por convención el image_tag
helper siempre buscará de forma natural las imágenes dentro de la carpeta app/assets/images
y no debes prefijar la ruta completa hacia la imagen, por lo que image_tag(logo.png)
buscará automáticamente la imagen en la ruta app/assets/images/logo.png
.
Por otro lado, dentro de la barra se encuentran dos enlaces importantes, el enlace para la gestión de categorías y el enlace para la creación de una nueva tarea, haciendo uso del ya conocido link_to
helper. Tanto image_tag
como link_to
son helpers de rails que al final de la renderización de la página en el servidor son traducidos a HTML, en este a las etiquetas <img>
y <a>
respectivamente. Estos enlaces están usando helpers de rutas que son formados automáticamente a partir de la definición de las rutas en el archivo config/routes.rb
de allí podemos usar categories_path
, new_task_path
y destroy_user_session_path
como ya lo habíamos visto antes en pasadas clases.
Finalmente estamos usando la plataforma https://robohash.org (plataforma externa e independiente de Rails) para crear imágenes de perfil asociadas con el email del usuario, y también estamos usando dropdowns de bootstrap para colocar un enlace a cerrar sesión como lo teníamos previamente en el layout.
Mensajes de alerta
Los mensajes de alerta, será un parcial que crearemos en el archivo app/views/layouts/_messages.html.haml
con el siguiente contenido
- flash.each do |name, msg| - if msg.is_a?(String) %br %div{:class => "alert alert-#{name.to_s == 'notice' ? 'success' : 'danger'}"} %button.close{"aria-hidden" => "true", "data-dismiss" => "alert", :type => "button"} × = content_tag :div, msg, :id => "flash_#{name}"
En el anterior bloque estamos introduciendo el objeto flash
de Rails, a través de este, vas a poder pasar de forma temporal algunos tipos de datos entre acciones de rails, los tipos de dato que puedes pasar son: String
, Array
y Hash
, normalmente el objeto flash
se usa para pasar alertas de error o de notificaciones relacionadas a procesos de CRUD de registros, y como es tan común hacer esos envíos de mensajes, Rails ha propuesto flash[:notice] = ‘...’
y flash[:error] = ‘...’
como notificadores de alertas para ser usado comúnmente dentro de los controladores.
Cada que hacemos uso de flash como objeto insertando nuevas alertas, internamente flash almacena la información en un enumerador que permitirá recorrer todas las notificaciones como si se tratase de un arreglo convencional, es por esa razón que podemos usar el método each
en la línea - flash.each do |name, msg|
el resto del código es obtenido de las alertas colapsables de bootstrap.
Configuración de bootstrap
En pasadas clases hemos configurado parcialmente bootstrap como librería, usando yarn y webpacker para eso, así que bootstrap como librería o paquete ya está completamente instalado, pero no está referenciado para su uso en el proyecto, para hacer esto, debemos hacer dos pasos: añadir la relación en el pack de javascript (similar a lo generado con cocoon), y añadir una referencia de importación de las hojas de estilo usando sass y el mismo pack editado anteriormente.
Para el primer paso, debemos ir al archivo app/javascript/packs/application.js
y allí antes de de la importación de cocoon
como módulo, vamos a importar dos paquetes, bootstrap
y robot-fontface
, el primero trae toda la funcionalidad de javascript de bootstrap y el segundo es un paquete externo que ya había sido instalado y nos permitirá usar una fuente tipográfica llamada roboto, por lo que nuestro archivo quedará de la siguiente forma
require("@rails/ujs").start() require("turbolinks").start() require("@rails/activestorage").start() require("channels") import "bootstrap" import "roboto-fontface" import "cocoon"
El segundo paso es añadir la referencia a las reglas de estilo de bootstrap, para esto vamos a seguir usando webpacker y nuestro archivo app/javascript/packs/application.js
y al final de este añadiremos una importación relativa a un nuevo archivo que crearemos en un momento.
import "../src/stylesheets/application"
El nuevo archivo deberá ser creado en la ruta app/javascript/src/stylesheets/application.sass
, y si, tal como lo ves allí, dentro de mi pack puedo añadir referencias explícitas a hojas de estilo, en este caso en formato SASS. este nuevo archivo tendrá el siguiente contenido
@import "~bootstrap/scss/bootstrap"
Hoja de estilo para Layout
Para finalizar con nuestra incursión en la página de Layout, para a añadir una hoja de estilo con una referencia a la librería de iconos font-awesome, que será enlazada al proyecto usando la gema gem 'font-awesome-sass', '~> 5.12.0'
Para instalar esta gema debemos añadir su definición al archivo Gemfile, debajo de la gema gem 'cancancan'
# Gemfile gem 'font-awesome-sass', '~> 5.12.0'
Luego en la raíz del proyecto debes correr el comando bundle install
➜ organizador ~ bi
Una vez instalada la gema, crearemos el archivo layout.sass
en la ruta app/assets/stylesheets/layout.sass
con el siguiente contenido
@import "font-awesome-sprockets" @import "font-awesome" nav.navbar &.bg-dark-blue background-color: #273b47 .navbar-nav .nav-item.active font-weight: bold .dropdown-user img border-radius: 50% background-color: white margin-left: 15px .btn-interaction font-size: small .card border-radius: 10px
Para que puedas ver el resultado de los cambios generados, deberás reiniciar el servidor, debido a que has añadido e instalado una nueva gema, una vez hayas reiniciado el servidor podrás ver la renderización de la página principal de la siguiente forma.
La anterior visualización sólo podrás verla si hay un usuario con sesión iniciada dentro del sistema, por el contrario, si no tienes una sesión iniciada te encontrarás con el siguiente error
Rails te está diciendo que no puede encontrar el método email
de un objeto nil
, esto es normal, debido a que si no hay un usuario con sesión iniciada entonces el valor del método current_user
será nil
, hay varias formas de resolver esta situación, sin embargo, vamos a abordar una que te servirá para conocer otra particularidad de las Layouts, y es que puedes tener varias de estas conviviendo en el mismo espacio.
Por ejemplo, para el inicio de sesión no necesitamos una barra de navegación (que es donde está originado el problema), sólo necesitamos la sección de mensajes y la llamada al contenedor principal a través del yield
, así que crearemos una nueva layout usando un nuevo archivo, que tendrá un nombre especial (debe llamarse así) llamada devise.html.haml
en la ruta app/views/layouts/devise.html.haml
.
La razón de este nombre tan especial, es porque estamos llamando a la layout de la misma forma como se llama el controlador que está a cargo del inicio y registro de usuarios, se trata de Devise, la gema que habíamos abordado en pasadas clases, y que además usa un espacio de nombres llamado devise, así que de forma inteligente Rails sabe que si la layout es llamada devise.html.haml
nos estamos refiriendo a los controladores de Devise como gema. Así es, Devise aporta varios controladores que tú desde el código no puedes ver de forma natural pero conviven en tu proyecto gracias a la gema.
De esta forma, nuestro nuevo archivo app/views/layouts/devise.html.haml
tendrá el siguiente contenido
!!! %html %head %meta{:content => "text/html; charset=UTF-8", "http-equiv" => "Content-Type"}/ %title Tasker = csrf_meta_tags = csp_meta_tag = javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' = stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %body .container .main-flash-messages = render 'layouts/messages' .fullscreen .container = yield
Así que cuando intentes acceder a la página web principal de tu proyecto http://localhost:3000 serás redireccionado al inicio de sesión con la nueva layout y deberás ver lo siguiente:
Embelleciendo nuestras categorías
Las categorías de nuestro proyecto se encuentran en la ruta app/views/categories
allí vamos a embellecer la lista de categorías (acción index), la vista de mostrar (acción show) y nuestros formularios de creación y edición.
Para la lista de categorías, vamos a abrir el archivo app/views/categories/index.html.haml
y a cambiar totalmente su contenido por el siguiente
.categories.index-page .card .card-body %h1 Lista de Categorías %table.table %thead %tr %th Nombre %th Descripción %th %th %th %tbody - @categories.each do |category| %tr %td= category.name %td= category.description %td= link_to t('common.show'), category %td= link_to t('common.edit'), edit_category_path(category) %td= link_to t('common.destroy'), category, :method => :delete, :data => { :confirm => t('common.are_you_sure') } %br .text-right = link_to 'Nueva Categoría', new_category_path, class: 'btn btn-info'
En el anterior bloque hemos añadido una mejor visualización de nuestra lista usando los componentes de bootstrap llamados table
y cards
, además estamos usando el helper link_to
de nuevo, al que le estamos pasando las clases de estilo btn btn-info
, apoyados con la internacionalización de Rails apuntando a la ruta ‘common.<clave de traducción>’
, recuerda que podemos usar la herramienta i18n-tasks
para crear el árbol de traducciones en los archivos de localización de la siguiente forma
➜ organizador ~ i18n-tasks add-missing
Obteniendo como respuesta lo siguiente:
+--------+---------------------+--------------+ | Locale | Key | Value | +--------+---------------------+--------------+ | en | common.are_you_sure | Are you sure | | en | common.destroy | Destroy | | en | common.edit | Edit | | en | common.show | Show | | es | common.are_you_sure | Are you sure | | es | common.destroy | Destroy | | es | common.edit | Edit | | es | common.show | Show | +--------+---------------------+--------------+
Ahora, deberás ir al archivo de traducción ubicado en config/locales/es.yml
, buscar las claves que han sido agregadas por el i18n-tasks
y reemplazarlas por su valor en español
are_you_sure: ¿Está seguro? back: Atrás destroy: Eliminar edit: Editar show: Mostrar
Ahora nuestra sección de categorías se deberá ver de la siguiente forma
Una vez hemos terminado con la página de lista de categorías, abordemos la página de mostrar (acción show), para esto abramos el archivo show.html.haml
en la ruta app/views/categories/show.html.haml
y reemplacemos todo el contenido por el siguiente
.category.show-page .card .card-body %p %b Nombre: = @category.name %p %b Descripción: = @category.description = link_to t('common.edit'), edit_category_path(@category), class: 'btn-interaction' \| = link_to t('common.back'), categories_path, class: 'btn-interaction'
En el anterior bloque de código no hay nada diferente a lo visto hasta ahora, seguimos incorporando componentes gráficos de bootstrap junto con helpers de Rails.
Para concluir con la sección de categorías, abordaremos los archivos de formularios, reemplazando totalmente su contenido por el de la siguiente secuencia:
Archivo app/views/categories/new.html.haml
.category.new-page .card .card-body %h2 Nueva Categoría = render 'form' = link_to t('common.back'), categories_path, class: 'btn-interaction'
Archivo app/views/categories/edit.html.haml
.category.edit-page .card .card-body %h2 Editando categoría = render 'form' = link_to t('common.show'), @category, class: 'btn-interaction' \| = link_to t('common.back'), categories_path, class: 'btn-interaction'
Archivo app/views/categories/_form.html.haml
-# frozen_string_literal: true = simple_form_for(@category) do |f| = f.error_notification = f.error_notification message: f.object.errors[:base].to_sentence if f.object.errors[:base].present? .form-inputs .row .col-12 = f.input :name, label: 'Nombre' .row .col-12 = f.input :description, as: :text, label: 'Descripción' .form-actions = f.button :submit, class: 'btn btn-info'
De igual forma, en los bloques anteriores, hemos abordado los componentes gráficos vistos hasta el momento enfocados en dos vistas y un parcial, en este parcial hemos conservado la estructura base de simple_form
pero se han hemos añadido una pequeña grilla basada en bootstrap
Al final tendremos un formulario renderizado como se muestra aquí
Embelleciendo nuestras tareas
Las páginas de gestión de tareas son más ricas en contenido y tipos de entrada de datos, por lo que separaremos esta sección en tres subsecciones: lista de tareas, mostrar tarea y formulario de tareas
Lista de tareas
En el archivo app/views/tasks/index.html.haml
vamos a reemplazar todo su contenido por el siguiente
.tasks.index-page .display-4 Lista de Tareas %p.text-muted.mb-2 A continuación verás toda la lista de tareas creadas, podrás crear una nueva tarea, editarla o eliminarla si eres el creador de la misma .card.bg-gray .card-body.pb-2 .tasks-container - @tasks.each do |task| .task-container .task-info .task-title %span.small= "[##{task.code}]" = task.name %span.task-category = task.category.name .task-description= task.description .task-dates %b= task.owner.email = "creado el #{l task.created_at, format: :long} / vence el #{task.due_date}" .task-interactions .task-participants - task.participating_users.includes(:user).each do |participant| .task-participant{ class: "task-participant--#{participant.role}" } = image_tag "https://robohash.org/#{participant.user.email}?size=60x60", title: participant.user.email .task-actions .dropdown %a#taskMenu{"aria-expanded" => "false", "aria-haspopup" => "true", "data-toggle" => "dropdown"} .fas.fa-ellipsis-v .dropdown-menu{"aria-labelledby" => "taskMenu"} = link_to t('common.show'), task, class: 'dropdown-item' = link_to t('common.edit'), edit_task_path(task), class: 'dropdown-item' = link_to t('common.destroy), task, :method => :delete, :data => { :confirm => t('common.are_you_sure') }, class: 'dropdown-item'
En el anterior bloque de código hemos transformado totalmente la visualización gráfica de la lista de tareas, sin embargo, seguimos usando los mismos elementos gráficos introducidos en las anteriores secciones de la clase con excepción de dos elementos importantes, el helper de fecha y el método includes
para hacer eager loading (pronunciado [iguer loudin] /ˈiɡər ˈloʊdɪŋ/).
Abordemos primero el helper de decoración de fecha
= "creado el #{l task.created_at, format: :long} / vence el #{task.due_date}"
El helper es invocado con el método l
el cual es un alias del método localize
, este recibe como parámetro un campo de tipo fecha o tiempo, y te devolverá una fecha en un formato más amigable al humano, como estamos usando la gema rails-i18n
y hemos configurado por defecto el idioma español en clases pasadas, esta internacionalización por defecto nos presentará el valor de la fecha y tiempo en español, sin embargo, podemos definir ante este helper en qué formato queremos la salida, nosotros hemos elegido el formato extenso, por lo que verás una salida similar a esta
#=> creado el 2 de junio de 2020 16:28 / vence el 2020-06-17
Ahora abordemos el método includes
en la siguiente sección de código
- task.participating_users.includes(:user).each do |participant| .task-participant{ class: "task-participant--#{participant.role}" } = image_tag "https://robohash.org/#{participant.user.email}?size=60x60", title: participant.user.email
En el anterior bloque de código estamos accediendo desde la tarea a los participantes de la misma, pero debes tener en cuenta que estamos accediendo a los registros de la tabla o modelo intermedio entre la tarea y el usuario, es decir a Participant
, sin embargo, en la línea precedida por el image_tag
se necesita una información de la relación con el modelo User
, necesitamos el campo email
para usar el servicio de robohash.
Si no usamos el método includes
, cada vez que llamemos a la relación user
para obtener el valor campo email
en la iteración, estaríamos llamando a la base de datos por cada más de una vez por cada relación participante / usuario que tengamos, esto no es bueno en términos de rendimiento.
Al usar el método includes(:user)
cuando llamamos a la relación participanting_users
, estamos al mismo tiempo trayendo la información de la relación con user
en memoria, evitando que cuando se acceda a ella más adelante tengamos que hacer una nueva petición a la base de datos.
Por otro lado, la línea mostrada abajo perteneciente al mismo bloque de código anterior, también tiene un aspecto más que es relevante. La introducción del método participant.role
dentro de la interpolación en las clases de estilo del contenedor task-participant
.task-participant{ class: "task-participant--#{participant.role}" }
Actualmente este método participant.role
nos está devolviendo un número, un uno o un dos, sin embargo, en este contexto sería mucho más útil que en lugar de devolver un número nos devolviera un término, como: responsible o follower (responsable o seguidor), para lograr esto vamos a introducir el método enum role: { responsible: 1, follower: 2 }
que nos permitirá hacer una equivalencia clave valor de forma automática entre un atributo de tipo Integer
(nuestro role
) y un arreglo de términos ordenados, en este caso responsible
es 1 y follower
es 2.
Así vamos a abrir el modelo app/models/participant.rb
y añadir la línea mencionada en el anterior párrafo
class Participant < ApplicationRecord enum role: { responsible: 1, follower: 2 } belongs_to :user belongs_to :task end
Una vez modificado este archivo semilla, añadiremos una hoja de estilo para todo nuestra sección de tareas, asi que cambiaremos el nombre del archivo existente app/assets/stylesheets/tasks.scss
a app/assets/stylesheets/tasks.sass
y reemplazaremos todo el contenido de ese archivo por lo siguiente
.tasks &.index-page .card border-radius: 10px border: none &.bg-gray background-color: #cacaca .tasks-container .task-container background-color: white margin-bottom: 25px padding: 10px border-radius: 10px border-left: 8px solid gray .task-info display: inline-block .task-title font-weight: 600 .task-category background-color: yellow padding: 5px font-size: small border-radius: 10px margin-left: 5px .task-description font-weight: 300 font-style: italic .task-dates font-size: small .task-interactions float: right margin-top: 11px .task-participants display: inline-block .task-participant display: inline-block img width: 50px border-radius: 50% background-color: #e9ecef &.task-participant--responsible img background-color: #ef5634 .task-actions display: inline-block .dropdown .fa-ellipsis-v font-size: 34px vertical-align: middle margin-left: 10px cursor: pointer color: #5e676d &.task-status--open border-left-color: 8px solid green &.task-status--close border-left-color: 8px solid green &.show-page .task-participants .task-participants-header padding: 10px background-color: #273b47 color: white font-size: small border-radius: 10px margin-bottom: 10px .task-participant display: inline-block margin-bottom: 15px img width: 50px border-radius: 50% background-color: #e9ecef &.task-participant--responsible img background-color: #ef5634 .notes-container .no-notes font-weight: bold color: #cccccc font-size: 50px margin: 25px text-align: center .notes-container .notes max-height: 300px overflow-y: auto .note-element padding-bottom: 10px margin: 10px 0 .avatar display: inline-block vertical-align: top img border-radius: 50% width: 50px height: 50px background-color: cornflowerblue text-align: center vertical-align: top padding-top: 5px color: white .note padding-left: 10px padding-right: 10px display: inline-block width: 70% width: -webkit-calc(100% - 55px) width: -moz-calc(100% - 55px) width: calc(100% - 55px) .past-time float: right color: gray font-weight: bold font-size: 11px .user color: gray font-weight: bold font-size: smaller .content color: gray
Al finalizar, cuando refresques la página principal de tu aplicación deberás ver algo como lo siguiente
Mostrar tarea
En esta subsección mejoraremos la presentación de la página de mostrar tarea, abramos el archivo app/views/tasks/show.html.haml
y reemplacemos todo su contenido a:
.tasks.show-page .display-4.mt-3.mb-3 = "Tarea" %span.small= "[#{@task.code}]" %h2 Información .card .card-body .table-responsive %table.table.table-striped.table-borderless %thead %tr %th Código %th Nombre %th Categoría %th Creador %tbody %tr %td= @task.code %td= @task.name %td= @task.category.name %td= @task.owner.email %table.table.table-striped.table-borderless %thead %tr %th Descripción %tbody %tr %td.font-weight-light= @task.description .task-participants .task-participants-header .font-weight-bold.text-uppercase= 'Participantes' - @task.participating_users.includes(:user).each do |participant| .task-participant{ class: "task-participant--#{participant.role}" } = image_tag "https://robohash.org/#{participant.user.email}?size=60x60", title: participant.user.email = link_to t('common.edit'), edit_task_path(@task) \| = link_to t('common.back'), tasks_path %h2.mt-2.mb-2 Notas .notes-container .card .card-body - if @task.notes.exists? .notes - @task.notes.order(created_at: :asc).each do |note| .note-element = render 'tasks/notes/note', note: note - else .notes - if can? :add_notes, @task = simple_form_for Note.new, url: task_notes_path(task_id: @task.id), remote: true do |f| = f.input :body, label: false, placeholder: 'Escriba una nota', as: :string, input_html: { autocomplete: :off }
A pesar de que el nuevo bloque de código es extenso, lo único que hemos hecho es añadir estructuras gráficas como tablas, tarjetas, contenedores e imágenes a nuestra ya existente lógica, y hemos usado los mismos componentes que habíamos visto en la secciones pasadas.
Ahora, añadiremos una mejor estructura para nuestras notas en la tarea, por lo que reemplazaremos todo el contenido del archivo app/views/tasks/notes/_note.html.haml
por el siguiente
.avatar = image_tag "https://robohash.org/#{note.user.email}?size=60x60" .note .past-time = time_ago_in_words note.created_at .user = note.user.email .content = note.body
Por último, como nuestro elemento contenedor de nota debe tener la clase .note-element
debemos añadir una nueva línea de código en el archivo de javascript llamado app/views/tasks/notes/create.js.erb
debajo de la línea 3, de esta forma la línea 4 deberá aparecer con el siguiente contenido div.classList.add("note-element");
, en este archivo no debes reemplazar ninguna línea existente, sólo debes añadir la nueva.
Al final obtendremos una página web renderizada de la siguiente forma
Formularios de tarea
En esta subsección nos centraremos en los parciales de los formularios, tanto el principal como el anidado, pero antes de hacerlo debemos cambiar el contenido de los siguientes archivos:
Archivo app/views/tasks/new.html.haml
.tasks.new-page .card .card-body %h2 Nueva Tarea = render 'form' = link_to t('common.back'), tasks_path, class: 'btn-interaction'
Archivo app/views/tasks/edit.html.haml
.tasks.edit-page .card .card-body %h2 Editando tarea = render 'form' = link_to t('common.show'), @task, class: 'btn-interaction' \| = link_to t('common.back'), tasks_path, class: 'btn-interaction'
Ahora reemplacemos totalmente el contenido del archivo app/views/tasks/_form.html.haml
al siguiente
-# frozen_string_literal: true = simple_form_for(@task) do |f| = f.error_notification = f.error_notification message: f.object.errors[:base].to_sentence if f.object.errors[:base].present? .form-inputs .row .col-sm-6 = f.input :name, label: 'Nombre' .col-sm-3 = f.input :due_date, as: :string, input_html: { type: :date }, label: 'Fecha de vencimiento' .col-sm-3 = f.association :category, label: 'Categoría' .row .col-sm-12 = f.input :description #addParticipants %br %h3 .uppercased-font{ style: 'display: inline-block;' } Participantes = link_to_add_association f, :participating_users, class: 'btn btn-info float-right', 'data-association-insertion-node' => '.participants .participants-container', 'data-turbolinks' => false do Agregar un participante .fa.fa-plus-circle .participants = f.simple_fields_for :participating_users do |g| = render 'participating_user_fields', f: g .participants-container .form-actions = f.button :submit, class: 'btn btn-info'
En el bloque anterior, lo que hemos hecho es añadir de nuevo elementos gráficos de grilla y cartas provenientes de bootstrap, la lógica y la interacción con el helper de cocoon
se ha conservado totalmente.
Sin embargo, hemos usado el parámetro input_html
para modificar el tipo de elemento de entrada de la fecha de vencimiento en la línea como es mostrado abajo, de esta manera estoy forzando el tipo de elemento DATE
disponible en HTML5 sobre la etiqueta input
renderizada haciendo uso del constructor de formularios de simple_form
= f.input :due_date, as: :string, input_html: { type: :date }, label: 'Fecha de vencimiento'
Ahora, reemplacemos todo el contenido del archivo app/views/tasks/_participating_user_fields.html.haml
por el siguiente
.nested-fields %hr .text-right .row .col-4 = f.input :user_id, label: 'Usuario', as: :select, collection: User.all - [current_user], label_method: -> (u) { u.email } .col-4 = f.input :role, label: 'Rol', as: :select, collection: Participant.roles, value_method: -> (r) { r[0] } .col-4.text-left .mt-2 = link_to_remove_association f, class: 'btn btn-warning btn-sm btn-flat mt-4' do .fas.fa-trash
En el bloque de código anterior estamos introduciendo dos parámetros muy importantes de simple_form
en términos de renderización de elementos de tipo select
, se trata de los parámetros label_method
y value_method
quienes me permitirán acceder a la colección parametrizada en collection
y acceder a la iteración de forma directa usando métodos Lambda (similar a las funciones anónimas, aunque en Ruby no existe a título propio el concepto de función). Un elemento select
de HTML tiene options
, e internamente estas tienen un sistema de clave valor value:label
, estos parámetros label_method
y value_method
se usan para manipular ese valor en el momento en el que el constructor de formularios esté creando el elemento select
completo
En el primer parámetro label_method
hará que en lugar de imprimir la referencia del usuario en HTML, nos imprima el correo del usuario, un comportamiento similar tiene el segundo parámetro value_method
el cual nos asignará al valor el término numérico del rol definido en el método enum
dentro del modelo Participant
.
Al final, el formulario de creación de una nueva tarea deberá verse de la siguiente forma