Jean Perez
Johan Tique
Profesor¿Cómo integrar Cocoon en formularios dinámicos con Rails?
Integrar Cocoon en tu aplicación de Rails puede transformar la forma en que tus formularios gestionan datos complejos y relaciones anidadas. Si alguna vez te has enfrentado al reto de añadir múltiples registros relacionados en un solo formulario, esta herramienta es tu aliada. En esta guía, aprenderás a integrar Cocoon con una aplicación Rails, utilizando SimpleForm y otras configuraciones para un funcionamiento eficiente y multimodal de tus formularios.
¿Qué pasos seguir para permitir datos anidados en modelos de Rails?
Para trabajar con datos anidados, es crucial configurar correctamente el modelo principal para aceptar estos atributos. Seguir estos pasos garantizará que todo fluya de manera eficiente:
Implementación del método accepts_nested_attributes_for: Añade este método a tu modelo principal para permitir asociaciones anidadas. En el caso de "Task", por ejemplo, configuramos la recepción de datos anidados del modelo "Participant".
class Task < ApplicationRecord has_many :participating_users, dependent: :destroy accepts_nested_attributes_for :participating_users, allow_destroy: true end
Configurar Strong Parameters: Es necesario que el controlador permita la integración de estos parámetros. Modifica el método de parámetros fuertes para incluir las asociaciones y los campos correctos.
class TasksController < ApplicationController private def task_params params.require(:task).permit( :name, :description, :due_date, :category_id, participating_users_attributes: [:id, :user_id, :role, :_destroy] ) end end
¿Cómo se integra Cocoon al formulario?
Una vez configurados los modelos y los parámetros, el siguiente paso es incorporar Cocoon en el formulario HTML para dinamizar el proceso de añadir participantes:
Añadir lógica del frontend con Cocoon: Utiliza el helper link_to_add_association en el formulario para insertar dinámicamente secciones adicionales a medida que se añaden nuevos registros anidados.
<%= simple_form_for @task do |f| %> <%= f.input :name %> <%= f.input :description %> <div id="participants"> <%= f.simple_fields_for :participating_users do |user_f| %> <%= render 'participant_fields', f: user_f %> <% end %> </div> <%= link_to_add_association 'Add Participant', f, :participating_users, data: { association_insertion_node: '#participants' } %> <%= f.submit %> <% end %>
Desactivar Turbolinks para optimizar el JavaScript: Al trabajar con funcionalidades dinámicas como las de Cocoon, desactivar Turbolinks en el application.js puede ser beneficioso.
// Borrando los Turbolinks para evitar problemas con la carga de JavaScript //= require cocoon
¿Qué hacer si ocurre un error al añadir participantes?
Los errores pueden ocurrir al manipular datos anidados. Aquí te ofrecemos un enfoque para resolver problemas comunes:
Verifica los registros de errores (logs): Utiliza la consola para verificar los errores que puedan aparecer al crearse los registros. Un error de "Unpermitted Parameter" señala que el controlador no está configurado para aceptar ciertos parámetros.
Revisar y corregir Strong Parameters: Si visualizas un error en singular cuando debe ser plural, como una relación has_many, asegúrate de que el nombre de la relación es correcto en el método de parámetros fuertes.
Buscar y corregir posibles errores tipográficos: Pequeños errores tipográficos pueden generar grandes inconvenientes. Verifica que todos los detalles en el ejemplo anterior se implementen correctamente.
¿Cómo resolver conflictos de usuarios duplicados?
Permitir que un participante se asocie varias veces al mismo registro no tiene mucho sentido desde una perspectiva práctica. Para resolver esto:
Valida la unicidad en el modelo: Dentro del modelo "ParticipatingUser", puedes incorporar una validación para asegurar que el mismo usuario no sea añadido múltiples veces a la misma tarea.
class ParticipatingUser < ApplicationRecord belongs_to :task belongs_to :user validates :user_id, uniqueness: { scope: :task_id, message: "ya ha sido añadido a esta tarea" } end
Con estas configuraciones, habrás implementado efectivamente un sistema dinámico y robusto de formularios en tu aplicación Rails, ofreciendo una experiencia de usuario enriquecida y eficiente. ¡Continúa innovando y aprendiendo para mejorar tus habilidades en desarrollo web!
Jean Perez
Johan Tique
ProfesorEsteban Saldarriaga Alzate
YEINER CARVAJAL QUINA
braison crece
Johan Donado
Juan Camilo Espitia
Walter Jaramillo
Kenneth Barrientos
Luis Maria Garcia Salado Kühl
Carlos Mauro Cárdenas Fernández
Fredy Arce
Jonathan Fernando Santana Quispillo
David Campos
Carlos Emanuel de la Cruz Rosales
Osmar David Mas Delgado
David Pacheco
Byron Bustamante López
Diego Moreno Valdez
William Guillermo Caranton Mateus
Manuel Alejandro González Cabello
Johan Tique
ProfesorManuel Alejandro González Cabello
Jean Perez
Johan Tique
ProfesorJean Perez
Oscar Diaz
Manuel Alejandro González Cabello
Jesus Junior Campos Vargas
Manuel Alejandro González Cabello
Martín Fernando López Moguel
Jose Ramos
Anibal Español
Johan Tique
ProfesorAnibal Español
Luis David Ramirez Vargas
Fabio Vaccaro
Explicas muy bien, pero llega un punto en que hablas muy muy rapido, en especial en esta parte que se torna un poco mas complicad.
Hola Harry, muchas gracias por la recomendación, quedaremos muy atentos por si tienes alguna pregunta
Buenos videos, pero a modo de concejo se pudieron haber explicado mejor algunas cosas si se hubieran dividido en varios videos para explicarlos mejor
Para la implementación del código para esta clase ulitice la gema vanilla-nested
https://github.com/arielj/vanilla-nested
Ha resultado facil de integrar al proyecto.
# tasks.rb class Task < ApplicationRecord belongs_to :category belongs_to :owner, class_name: 'User' has_many :participating_users, class_name: 'Participant' has_many :participants, through: :participating_users, source: :user validates :participating_users, presence: true validates :name, :description, presence: true validates :name, uniqueness: { case_sensitive: false } validate :due_date_validate accepts_nested_attributes_for :participating_users, reject_if: :all_blank, allow_destroy: true def due_date_validate return if due_date.blank? return if due_date > Date.today errors.add :due_date, I18n.t('tasks.errors.invalid_due_date') end end
# create_participans.rb class CreateParticipants < ActiveRecord::Migration[7.0] def change create_table :participants do |t| t.integer :role t.references :user, null: false, foreign_key: true, on_delete: :cascade t.references :task, null: false, foreign_key: true, on_delete: :cascade t.timestamps end end end
# _form.html.erb <%= simple_form_for(@task) do |form| %> <%= form.error_notification %> <%= form.error_notification message: form.object.errors[:base].to_sentence if form.object.errors[:base].present? %> <div class="form-inputs"> <%= form.input :name, label: t('.name') %> <%= form.input :description, label: t('.description') %> <%= form.input :due_date, label: t('.due_time') %> <%= form.association :category, label: t('categories.category') %> </div> <%= link_to_add_nested(form, :participating_users, '#participants', partial: 'participating_user_fields', link_text: "Agregar") %> <div id="participants"> <%= form.fields_for :participating_users do |g|%> <%= render 'participating_user_fields', form: g %> <% end %> </div> <div class="form-actions"> <%= form.button :submit %> </div> <% end %>
# _participating_user_fields.html.erb <div class="nested-fields"> <%= form.input :user_id, as: :select, collection: User.all - [ current_user ] %> <%= form.input :role %> <%= link_to_remove_nested(form) %> </div>
Muchas gracias por tu aporte yeinercq!!! <3
Me parece demasiado complicado. La idea de las herramientas es agilizar el desarrollo, pero en este caso es más complicado que hacerlo a mano.
esta clase estuvo muy tesa pero a mi punto de vista si la parte del active record, y la forma como generas la relaciones y las posibilidades de modificar el nombre de las mismas si deberían tener unas dos clases mas bien enfocadas me parece super importante entender bien esta parte, a mi punto de vista esta clase quedo un poco cargada.
Total
Se me quitaron las ganas de aprender Rails
Concuerdo con Harry. Explicas muy bien pero en los últimos videos estas hablando muy rápido al punto que se vuelve confuso
eso que lo veo a 2x
No es que esta hablando muy rápido, si no que trata de asimilar muchas cosas en muy poco tiempo por lo que no le da para explicar todo a profundidad
Excelente sus clases profesor Johan Tique. Espero tener muchos cursos de UD.
Los formularios anidados quieren decir que en un solo formulario haya interacción con dos modelos. En este caso será con Task y Participant, con la ayuda de la gema Cocoon.
Para instalar esta gema hay que agregarla en el Gemfile, lo siguiente es agregar una dependencia de Cocoon para que pueda trabajar de forma correcta, esta dependencia se agrega con yarn desde el siguiente repositorio: github:nathanvda/cocoon#c24ba53.
Configuración de cocoon
En el archivo eviroment.js:
const webpack = require('webpack') environment.plugins.prepend('Provide', new webpack.ProvidePlugin({ $: 'jquery', jQuery: 'jquery', 'window.jQuery': 'jquery', Popper: ['popper.js', 'default'], }) )
En el archivo application.js:
import 'cocoon'
Esta configuración es necesaria ya que cocoon tiene como dependencia a jquery.
Una vez hecho esto hacemos la migración, para migrar el modelo Participant (rails db:migrate).
Formulario de tasks
En el formulario de tasks es donde se requiere asociar con los participantes, para esto se usará simple_fields_for, un método que permite asociar un modelo anidado que exista en la estructura de modelos.
= 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 = f.input :name, label: t('tasks.name') = f.input :description, label: t('tasks.description') = f.input :due_date, label: t('tasks.due_date') = f.association :category, label: t('tasks.category') .participants = f.simple_fields_for :paticipating_users
Con el método simple_fields_for se habilita la comunicación con participating_users, que es como se nombró la relación entre tareas y participantes.
Para el formulario principal el constructor es f (primera línea) y para el formulario anidado el constructor es g.
Para presentar el formulario anidado se usará un parcial, que es un archivo creado en la carpeta views>tasks con el nombre _participating_user_fields.html.haml. Y se llamará al formulario principal con la función render dentro del formulario anidado.
.participants = f.simple_fields_for :paticipating_users = render 'participating_users_fields', f: g
Parcial:
.nested-fields = f.input :user_id, as: :select, collections: User.all - [current_user] = f.input :role
En el primer input se inserta un elemento select, para poder elegir entre todos los usuarios, pero le restamos el usuario actual para que no aparezca en la lista. Era necesario poner current_user en un arreglo para que Ruby pudiera realizar las operaciones entre arreglos con User.all (también es un arreglo) sin problemas.
Para los que estan llevando el curso en la actualidad, si presentan algun problema con respecto al boton de "Agregar un participante" y ya revisaron todo el codigo y no tienen ningun error, puede que el problema sea por las dependencias en el package.json
Me lanzaba un error de que no era posible cargar @popper.js/core Y al final el errror es que bootstrap en su version 5 deja de usar jquery, para ello deben de asegurarse de tener el package.json com se los dejo adelante Solo escriben las versiones que tengo y ejecutan - yarn install para descargar todas las dependencias
"dependencies": { "@rails/actioncable": "^6.0.0", "@rails/activestorage": "^6.0.0", "@rails/ujs": "^6.0.0", "@rails/webpacker": "5.4.3", "bootstrap": "^4.5.3", "cocoon": "github:nathanvda/cocoon#c24ba53", "jquery": "^3.5.1", "popper.js": "^1.16.1", "roboto-fontface": "^0.10.0", "turbolinks": "^5.2.0", "webpack": "^4.46.0", "webpack-cli": "^3.3.12" },
Hola a todos, disculpen, tengo una duda de un problema que tengo con la parte de "Agregar participante", es algo que ya comentaron anteriormente pero no me ha funcionado nada de ello. Adjuntaré mi código para un mejor entendimiento. Cabe recalcar que la consola del explorador no me marca nada y la terminan donde está ejecutándose el servidor solo menciona que se ha renderizado:
_form.html.haml
-# 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 = f.input :name, label: t('.name') = f.input :description, label: t('.description') = f.input :due_date, label: t('.due_date') = f.association :category, label: t('.category') #addParticipants = link_to_add_association f, :participating_users, 'data-association-insertion-node' => '.participants .participants-container', 'data-turbolinks' => false do Agregar participante .participants = f.simple_fields_for :participating_users do |g| = render 'participating_user_fields', f: g .participants-container .form-actions = f.button :submit
_participating_user_fields.html
.nested-fields = f.input :user_id, as: :select, collection: User.all - [current_user] = f.input :role
task.rb
class Task < ApplicationRecord belongs_to :category belongs_to :owner, class_name: 'User' has_many :participating_users, class_name: 'Participant' has_many :participants, through: :participating_users, source: :user validates :participating_users, presence: true validates :name, :description, presence: true validates :name, uniqueness: { case_insensitive: false } validate :due_date_validity accepts_nested_attributes_for :participating_users, allow_destroy: true def due_date_validity return if due_date.blank? return if due_date > Date.today end end
yo tambien soy nuevo en esto, pero creo que necesitas separar el "do" y tu: defdue_date_validity debería verse así: def due_date_validity
lo de los "do" a mi me funciona así:
#addParticipants = link_to_add_association f, :participating_users, 'data-association-insertion-node' => '.participants .participants-container', 'data-turbolinks' =>false do agregar participante .participants = f.simple_fields_for :participating_users do |g| = render 'participating_user_fields', f: g .participants-container
Comprueba si cambiando import por require en application.js te funciona:
organizador/app/javascript/packs/application.js
... require("@rails/ujs").start() require("turbolinks").start() require("@rails/activestorage").start() require("channels") require('cocoon') ...
Hola espero puedan ayudarme. Cuando presiono en agregar participoante no hace nada y si quito de la vadilacion accepts_nested_attributes_for :participating_users, allow_destroy:true me dice esto en consola Unpermitted parameter: :participating_users
a mi me pasaba lo mismo, creo que se debe tener corriendo web-packer, mira la gema oficial y sigue los pasos para instalarla https://github.com/nathanvda/cocoon con esto al menos me muestra un error en la consola del navegador
Saludos. Quiero tomar los usuarios del select desde el task_controller . Cree una variable de instancia en el controlador pero he estado revisando como pasarlo al partial como variable local pero no he tenido exito. Alguna idea?
Hola Manuel, estoy asumiendo que quieres pasar tu colección de usuarios desde una variable de instancia a un parcial, y que este parcial acceda a esta información a través de una variable local.
De ser así, tienes varias formas, el sistema de renderización de Rails es muy robusto, incluso te propone varias mecanísmos de optimización, caching y refactorización.
la manera más simple es usar la siguiente forma llamada local_assigns:
= render 'my_partial', my_local_variable: @users
sin embargo, la forma completa del helper render seria asi:
render partial: 'my_partial', locals: { my_local_variable: @users }
la diferencia entre las dos formas, radicaría en la obligación del uso de la variable my_local_variable en la primera forma.
puedes ver mas información aqui revisa también la sección de collections disponible en el artículo que te comparto
Por otro lado, debes tener en cuenta que los parciales tienen una ubicación relativa respecto de su directorio, si tu parcial está por fuera del directorio anclado a la acción del controlador, entonces debes especificar el path completo desde el directorio views
Hola profesor, gracias por su respuesta. Efectivamente lo que estoy haciendo es que en el archivo task_controller.rb en el metodo new estoy tomando de una vez los usuarios sin el current_user, de la siguiente manera:
# GET /tasks/new def new @task = Task.new @users_to_select = User.where.not(id: current_user.id) end
y lo quiero mandar al formulario anidado para la seleccion de los participantes en vez de hacer el User.all en la vista. Hice lo siguiente en la renderizacion del formulario anidado,
_form.hatml.haml de tasks
-# 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 = f.input :name = f.input :description = f.input :due_date = f.association :category #addParticipants = link_to_add_association f, :participating_users, 'data-association-insertion-nod' => '.participants .participants-container', 'data-turbolinks' => false do agregar participante .participants = f.simple_fields_for :participating_users do |g| = render partial: 'participating_user_fields', locals: {f: g , users: @users_to_select} .participants-container .form-actions = f.button :submit
Aqui en el formulario anidado _participating_user_fields.html.haml
.nested-fields = f.input :user_id, as: :select, collection: users = f.input :role
pero obtengo el siguiente error:
ActionView::Template::Error (undefined local variable or method `users' for #<ActionView::Base:0x0000000000f758>): 1: .nested-fields 2: 3: = f.input :user_id, as: :select, collection: users 4: = f.input :role
Estuve leyendo lo que me paso me paso pero no encontré una solución :/ . Muchas gracias por sus respuestas.
Si me gustaria que me ayudaras al llegar aqui
#addParticipants = link_to_add_association f, ':participating_users', 'data-association-insertion-node' => '.participants .participants-container', 'data-turbolinks' => false do agregar
me sale error
Encountered a syntax error while rendering template: check -# 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 = f.input :name, label: t('.name') = f.input :description, label: t('.description') = f.input :due_date, label: t('.due_data') = f.association :category #addParticipants = link_to_add_association f, ':participating_users', 'data-association-insertion-node' => '.participants .participants-container', 'data-turbolinks' => false do agregar .participants = f.simple_fields_for :participating_users do |g| = render 'participating_user_fields', f: g .participants-container .form-actions = f.button :submit
Hola Harry, bien, el problema que tienes es de sintaxis, esto ocurre debido a typos (errores de digitación), codificación extraña de caracteres, bloques de código no cerrados o problemas de indentación.
El código formateado que me presentas, el primer error que encuentro es que no debes escribir ':participating_user' sino usar una de estas dos opciones:
:participating_user'participating_user'El segundo error es que el false está pegado al do así falsedo
Sin emabrgo, esos seguramente no son tus problemas ahora, ya viendo el código que me envias no formateado, podría darte dos posibles razones:
para el primer problema te envío una imagen (sin las etiquetas label) y puedas revisar la indentación (sangrado de las líneas de código) !imagen 1
para el segundo problema, podrías revisar las comillas dobles y simples que estés usando, asegúrate que sean del tipo ' y "
Puedes también revisar que tu indentación sea de dos espacios y no sea definida por tabs dentro de tu editor de texto.
me cuentas cómo te termina de ir
El error que señala es la linea despues del do. Dices que solo debo usar una de estas dos opciones:
:participating_user 'participating_user'
pero en la imagen que anexas esta combinado. De igual manera lo intente y sigue igual.
El do no esta pegado al false.
Gracias de antemano
la explicación muy deficiente no se si es el afán por acabar los vídeos rápido pero parecía una metralleta hablando y llega un punto donde ya es imposible seguirlo
Bueno. Esto de verdad me quito sueño. Pensé que lo había resuelto simplemente colocando esta validación en el modelo Participant:
validates_uniqueness_of :user_id,scope: [:task_id], allow_blank: false, message: "participants cant be repeated in same task"
Pero dejaba pasar a los usuarios y no entendía el porque. Me puse a debugguear y encontré que como en el momento de correr la validación que añadí todavía no se ha seteado el task_id por lo que al momento de la validación lo que tiene es:
user_id: 1 task_id: nil
Por lo que la validación pasa ya que no existe ningun paritcipant con esos valores. Investigue mucho y lo que encontré que sirvió fue crear una migración para hacer la validación desde la base de datos.
class AddConstrainToParticipant < ActiveRecord::Migration[6.1] def change add_index :participants, [:user_id, :task_id], unique: true end end
De esta manera manejo el error directamente en la base de datos. Por lo que cuando se inserte los datos erroneos se hará un rollback en la base de datos y se activará una excepcion que estoy manejando en el task_controller.
rescue_from ActiveRecord::RecordNotUnique do |exception| flash[:alert] = "participants cant be repeated in the same task" redirect_to new_task_path end
Esa es la validación que coloqué en el taskt_controller donde seteo un mensaje y redirijo la vista nuevamente a crear un nuevo participante.
En la vista (_form.html.haml de taskts) hay que agregar el flash. Yo lo hice así:
= flash[:alert] if flash[:alert].present?
De esa manera aparece un mensaje con el error y se tienen que vovler a agregar todos los datos.
Si alguien puede comentarme como lo hizo ojalá exista una manera menos rebuscada.
Una consulta, ¿Donde investigaste eso?
No encuentro exactamente donde lo encontre pero aquí hay varia informacion al respecto
https://mtm.dev/validate-unique-columns-rails-5
https://stackoverflow.com/questions/34424154/rails-validate-uniqueness-of-two-columns-together
Gracias a todos por los comentarios y preguntas! Obtuve varios errores pero luego los encontré leyendo cada uno de los comentarios. 💪🏽
Me da este error, y la linea que me señala no comprendo su sintaxis, ¿alguna idea del origen de este error?
Hola Anibal, task_id no es una columna que deba estar siendo buscada en User, por lo que te diría que hay un posible problema en la configuración de las relaciones. por favor muestrame cómo está definido el modelo Task, Participant y tambien su contraparte en el db/schema.rb
app/models/task.rb
class Task < ApplicationRecord belongs_to :category belongs_to :owner, class_name: 'User' has_many :participating_users, class_name: 'User' has_many :participants, through: :participating_users, source: :user validates :participating_users, presence: true validates :name, :description, presence: true validates :name, uniqueness: { case_sensitive: false } validate :due_date_validity accepts_nested_attributes_for :participating_users, allow_destroy: true def due_date_validity return if due_date.blank? return if due_date > Date.today errors.add :due_date, I18n.t('task.errors.invalid_due_date') end end
-- app/models/participant.rb
class Participant < ApplicationRecord belongs_to :user belongs_to :task end
-- db/schema.rb
ActiveRecord::Schema.define(version: 2020_08_09_061607) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" create_table "categories", force: :cascade do |t| t.string "name" t.text "description" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false end create_table "participants", force: :cascade do |t| t.integer "role" t.bigint "user_id", null: false t.bigint "task_id", null: false t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.index ["task_id"], name: "index_participants_on_task_id" t.index ["user_id"], name: "index_participants_on_user_id" end create_table "tasks", force: :cascade do |t| t.string "name" t.text "description" t.date "due_date" t.bigint "category_id", null: false t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.bigint "owner_id", null: false t.index ["category_id"], name: "index_tasks_on_category_id" t.index ["owner_id"], name: "index_tasks_on_owner_id" end create_table "users", force: :cascade do |t| t.string "email", default: "", null: false t.string "encrypted_password", default: "", null: false t.string "reset_password_token" t.datetime "reset_password_sent_at" t.datetime "remember_created_at" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.index ["email"], name: "index_users_on_email", unique: true t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true end add_foreign_key "participants", "tasks" add_foreign_key "participants", "users" add_foreign_key "tasks", "categories" add_foreign_key "tasks", "users", column: "owner_id" end
Una duda al momento de correr tasks me sale esto: undefined local variable or method `participating_users' for Task:Class Extracted source (around line #17):
15 16 17 belongs_to :owner, class_name: 'User' has_many :participating_users, class_name: 'Participant' has_many :participants, through: participating_users, source: :user
validates :participating_users, presence: true
Luis, me paso lo mismo. Agrega ":" antes de participating_users