javascriptruby-on-railscoffeescriptbatman.js

Accepting Nested Attributes in Batman.js


I have this relationship working using HTML and HTTP, but I'm completely lost as to make this work with Batman.js. I've made a plethora of attempts, but none have been successful. I've also attempted to implement the steps in this guide with no luck. Can anyone point me in the right direction?

My routes and models are the Batman.js equivalent of the rails files, aside from the controller, which is where I anticipate I will need to implement some fancy Batman.js

Routes

resources :tasks do
  resources :task_entries
end

Models

class Task < ActiveRecord::Base
  has_many :task_entries

  accepts_nested_attributes_for :task_entries

  # must have at least one entry, built in controller
  validates :task_entries, presence: true
end


class TaskEntry < ActiveRecord::Base
  belongs_to :task
  has_one :item

  accepts_nested_attributes_for :item

  # must have an item, built in controller
  validates :item, :task, presence: true
end

class Item < ActiveRecord::Base
  belongs_to :parent, polymorphic: true

  validates :title, presence: true
end        

Controller

class TasksController < ApplicationController
  def new
    @task = Task.new
    @task.task_entries.build(item: Item.new)
  end

  def task_params
    returned_params = params.require(:task).permit(
      task_entries_attributes: [
        :status, :due_at, :defer_until, :estimated_duration, :completed_at, 
        item_attributes: [
          :title, :description, :flagged, :visibility, :difficulty
        ]
      ]
    )
  end
end

Form that works

tasks/_form.html.slim
= form_for @task do |f|
  = f.fields_for :task_entries do |e|
    = e.fields_for :item do |i|
      = i.text_field :title
      = i.text_area :description
      = i.check_box :flagged
      = i.select :visibility, Item.visibility.options
      = i.select :difficulty, Item.difficulty.options
    = e.select :status, TaskEntry.status.options
    = e.datetime_select :due_at
    = e.datetime_select :defer_until
    = e.number_field :estimated_duration
    = e.datetime_select :completed_at
  = f.submit

HTTP example that works

POST /tasks HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Host: localhost:3000

task[task_entries_attributes][0][item_attributes][title]=help+me+stack+overflow%2C+you%27re+my+only+hope

Solution

  • First, I'd say until this PR is merged, I wouldn't count on batman.js to encode your attributes. Instead, let batman.js encode the data its own way, then transform it in your Rails controller. (That's how we do it in Planning Center Check-Ins.)

    To transform the params in your Rails controller, update the task_params method:

      def task_params
        # first, permit the params without the _attributes suffix:
        returned_params = params.require(:task).permit(
          task_entries: [
            :status, :due_at, :defer_until, :estimated_duration, :completed_at, 
            items: [
              :title, :description, :flagged, :visibility, :difficulty
            ]
          ]
        )
    
        # then, reassign them to different keys in the params hash (and delete the old keys)
        if returned_params[:task_entries].present?
          task_entries_params = returned_params.delete(:task_entries)
          returned_params[:task_entries_attributes] = task_entries_params
          # do the same for the items params
          if returned_params[:task_entries_attributes][:items].present?
            items_params = returned_params[:task_entries_attributes].delete(:items)
            returned_params[:task_entries_attributes][:items_attributes] = items_params
          end
        end
      end
    

    This is assuming batman.js models that look like this:

    class MyApp.Task extends Batman.Model 
      # ...
      @hasMany "task_entries", saveInline: true 
    
    class MyApp.TaskEntry extends Batman.Model 
      # ...
      @encode "status", "due_at", "defer_until", "estimated_duration", "completed_at"
      @hasOne "item", saveInline: true, polymorphic: true, as: "parent"
    
    class MyApp.Item extends Batman.Model 
      # ...
      @encode "title", "description", "flagged", "visibility", "difficulty"
      @belongsTo "parent", polymorphic: true
    

    I'd agree that transforming the params isn't ideal. Hopefully that will be cleared up in batman.js 0.17.