Updated: Unpoly + Nested attributes in Rails: A short overview of different approaches

Posted . Visible to the public. Auto-destruct in 57 days

With the new unpoly client side templates Show archive.org snapshot (available since 3.10) there's another way to substitute the ids of inserted nested attribute forms which I added to this card.

Changes

  • -This card describes two variants, that add a more intuitive workflow when working with nested attributes in Rails + Unpoly.
  • +This card describes four variants, that add a more intuitive workflow when working with nested attributes in Rails + Unpoly:
  • +
  • +1. [Without JS](#section-js)
  • +2. [With HTML template and JS](#section-adding-nested-records-via-template-html-js)
  • +3. [With HTML template and JS using dynamic Unpoly templates](#section-adding-nested-records-via-template-html-js-dynamic)
  • +4. [Adding Records via XHR and JS](#section-adding-nested-records-via-xhr-js)
  • # Example
  • ...
  • ----
  • +## Adding nested records via template HTML with JS using dynamic Unpoly templates
  • +This approach uses unpoly's support for [templates with dynamic variable substition](https://unpoly.com/templates#dynamic) available since [Unpoly version 3.10.0](https://unpoly.com/changes/3.10.0)
  • +
  • +```haml
  • +- if flash[:notice]
  • + = flash[:notice]
  • +
  • +%h1 Edit User
  • +
  • += form_with(model: @user, url: variant_2_user_path(@user)) do |form|
  • + - if @user.errors.any?
  • + %h2= pluralize(@user.errors.count, "error") + " prohibited this user from being saved:"
  • + %ul
  • + - @user.errors.full_messages.each do |message|
  • + %li= message
  • +
  • + = form.label :full_name
  • + = form.text_field :full_name
  • +
  • + %h3 Tasks
  • + .div(nested-records)
  • + = form.fields_for :tasks do |task_form|
  • + %div
  • + = task_form.label :title
  • + = task_form.text_field :title
  • + = task_form.check_box :_destroy
  • + = task_form.label :_destroy, "Remove task"
  • +
  • + = form.fields_for :tasks, Task.new, child_index: "NESTED_RECORD_ID_PLACEHOLDER" do |task_form|
  • + %template(nested-records--template)
  • + %div(nested-records)
  • + %div
  • + = task_form.label :title
  • + = task_form.text_field :title
  • + = task_form.check_box :_destroy
  • + = task_form.label :_destroy, "Remove task"
  • +
  • + %a(up-target="[nested-records]:after" up-document="[nested-records--template]" role="button" href="#") Add task
  • + = form.submit
  • +```
  • +
  • +Compared to the previous version, some changes are necessary:
  • +- the nested records need to be in their own container so that `up-target` can correctly insert new clones at the end via the `up-target="[nested-records]:after"` selector
  • +- `up-document` specifies the template to be cloned
  • +- the template needs to contain the targeted container element (`[nested-records]`) so that unpoly can correctly place it
  • +- `form.fields-for` allows setting the ids of created fields via `child-index` which we set to a placeholder value (`NESTED_RECORD_ID_PLACEHOLDER`)
  • +
  • +The last part to make this work is a custom unpoly template "engine" which replaces these placeholder values with a unique id during cloning.
  • +
  • +```js
  • +up.on('up:template:clone', '[nested-records--template]', (event) => {
  • + const template = event.target.innerHTML
  • + const result = template.replaceAll('NESTED_RECORD_ID_PLACEHOLDER', new Date().valueOf())
  • + event.nodes = up.element.createNodesFromHTML(result)
  • +})
  • +```
  • ## Adding nested records via XHR with JS
  • ...
Daniel Schulz
Last edit
Daniel Schulz
License
Source code in this card is licensed under the MIT License.
Posted by Daniel Schulz to makandra dev (2025-04-25 12:24)