Using jQuery Tokeninput within a nested form partial

311 views Asked by At

I'm using jQuery Tokeninput as shown in this Railscast. I'd like to combine this functionality in a nested form but get the error

undefined method `artists' for #<SimpleForm::FormBuilder:0x007febe0883988>

For some reason its not recognizing the track parameter in my form builder which is stopping me to get a hold of albums I have on record.

<div class="input">
  <%= f.input :artist_tokens, label: 'Featured Artists', input_html: {"data-pre" => f.artists.map(&:attributes).to_json} %>
</div>

Keep in mind this works in my track form but just not in my album form since its nested. What should I do to get this to work?

class ArtistsController < ApplicationController
    def index
      @artists = Artist.order(:name)
      respond_to do |format|
          format.html
          format.json {render json: @artists.tokens(params[:q])}
      end
    end
end

Models

class Artist < ActiveRecord::Base
    has_many :album_ownerships
    has_many :albums, through: :album_ownerships

    has_many :featured_artists
    has_many :tracks, through: :featured_artists


    def self.tokens(query)
      artists = where("name like ?", "%#{query}%")

      if artists.empty?
        [{id: "<<<#{query}>>>", name: "Add New Artist: \"#{query}\""}]
      else
        artists
      end
    end

    def self.ids_from_tokens(tokens)
      tokens.gsub!(/<<<(.+?)>>>/) {create!(name: $1).id}
      tokens.split(',')
    end
end

class Albums < ActiveRecord::Base
    attr_reader :artist_tokens

    accepts_nested_attributes_for :tracks, :reject_if => :all_blank, :allow_destroy => true

    has_many :albums_ownerships
    has_many :artists, through: :albums_ownerships

    def artist_tokens=(ids)
        self.artist_ids = Artist.ids_from_tokens(ids)
    end
end

class Track < ActiveRecord::Base
    attr_reader :artist_tokens

    belongs_to :album

    has_many :featured_artists
    has_many :artists, through: :featured_artists

    def artist_tokens=(ids)
        self.artist_ids = Artist.ids_from_tokens(ids)
    end
end


class AlbumOwnership < ActiveRecord::Base
    belongs_to :artist
    belongs_to :album
end

class FeaturedArtist < ActiveRecord::Base
    belongs_to :artist
    belongs_to :track
end

Album Form

<%= simple_form_for(@album) do |f| %>
  <div class="field">
    <%= f.label :name %><br>
    <%= f.text_field :name %>
  </div>

  <h1>Tracks</h1>

  <%= f.simple_fields_for :tracks do |track| %>
    <%= render 'track_fields', :f => track %>
  <% end %>
  <div id='links'>
    <%= link_to_add_association 'Add Field', f, :tracks %>
  </div>

  <div class="actions">
    <%= f.submit %>
  </div>
<% end %>

Track Partial

<div class="field">
  <%= f.input :name %><br>
</div>

<div class="input">
  <%= f.input :artist_tokens, label: 'Featured Artists', input_html: {"data-pre" => f.artists.map(&:attributes).to_json} %>
</div>

JS

$(function() {
    $('#track_artist_tokens').tokenInput('/artists.json', {
        prePopulate: $("#track_artist_tokens").data("pre"),
        theme: 'facebook',
        resultsLimit: 5
    });
});

UPDATE

As mentioned by nathanvda, I needed to use f.object in order for the artists to be recognized. So in my partial I now have:

<%= f.input :artist_tokens, label: 'Featured Artists', input_html: {"data-pre" => f.object.artists.map(&:attributes).to_json, class: 'test_class'} %>

In my js I also needed to call the token input method before/after insertion:

$(function() {  
    $('.test_class').tokenInput('/artists.json', {
        prePopulate: $(".test_class").data("pre"),
        theme: 'facebook',
        resultsLimit: 5
    });


    $('form').bind('cocoon:after-insert', function(e, inserted_item) {
        inserted_item.find('.test_class').tokenInput('/artists.json', {
            prePopulate: $(".test_class").data("pre"),
            theme: 'facebook',
            resultsLimit: 5
        });
    });
});

The only remaining issue I have is the the tracks_attributes not being saved. I ran into an issue similar to this in the past in this post but the two main difference is the second level of nesting involved and that I used a join table within my nested form. I'm not entirely sure if or how any of that code would translate over but I believe this is most likely problem. As far as the permitted params of my albums_controller here's what they looks like.

def album_params
  params.require(:album).permit(:name, :artist_tokens, tracks_attributes: [:id, :name, :_destroy, :track_id])
end
2

There are 2 answers

10
nathanvda On BEST ANSWER

If you need to acces the object of a form, you need to write f.object, so I think you should just write f.object.artists.

6
John On

Your "data-pre" => f.artists... is calling the artists method on f which is the form builder and doesn't have an #artists method.

Try this instead:

In the album form, change the render partial line to this:

<%= render 'track_fields', :f => track, :artists => @artists %>

And then use this in the track partial:

<%= f.input :artist_tokens, label: 'Featured Artists', input_html: {"data-pre" => artists.map(&:attributes).to_json} %>

UPDATED

Let's back up a step. From your code it looks like you need to populate a data-pre attribute with the attributes of a collection of artists.

The problem is you're calling f.artists where f is the FormBuilder and doesn't know anything about artists. This is why you're getting undefined method 'artists'...

The solution is make a collection of artists available to the view and its partials. One way to do this:

class AlbumsController < ApplicationController
  ...
  def new
    @album = Album.new
    @artists = Artist.order(:name) # or some other subset of artists
  end

  ...

  def edit
    @album = Album.find params[:id]
    @artists = Artist.order(:name) # or perhaps "@artists = @album.artists", or some other subset of artists
  end
end

and then in new.html.erb and edit.html.erb, pass @artists to the form partial:

... # other view code
  <%= render 'form', album: @album %>
... # other view code

and then in your form partial:

... # other view code
<%= f.simple_fields_for :tracks do |track_form| %>
  <%= render 'track_fields', :f => track_form %>
<% end %>
... # other view code

finally, in your track partial:

... # other view code
<div class="input">
  <%= f.input :artist_tokens, label: 'Featured Artists', input_html: {"data-pre" => @artists.map(&:attributes).to_json} %>
</div>
... # other view code

Does that make sense?