marcamillion
marcamillion

Reputation: 33755

How do I dynamically populate a partial based on a link click?

So I have a partial called _node.html.erb. That is rendered in different views (and from different controllers) throughout my app. On one particular view, what happens is I generate a list of events that are linkable (and unique).

What I want to happen is, whenever someone clicks one of those links, it shows the partial and populates it with the correct info. The way I was initially trying to do it is via passing data-attributes from the link to some JS that does the updating of the partial. This quickly runs into a wall when we reach the node.comments, where the partial should load all of the comments associated with the node being rendered.

For example, here is some sample code of what my _node.html.erb may look like:

  <!-- Card Meta -->
  <div class="card__meta">
    <aside>
      <%= image_tag node.user.avatar.url, class: "card__author-avatar", alt: "" %>
    </aside>
    <section>
      <h6 class="card__author-name"><%= node.user.name %></h6>
      <time class="card__date"><%= node.created_at.strftime("%B %d, %Y") %></time>
      <p class="card__description"><%= node.media.try(:description) %> 
      <textarea class="card-form__description" name="" id="" cols="30" rows="10">Aenean lacinia bibendum nulla sed consectetur.</textarea>
    </section>

  </div>
  <!-- End Card Meta -->

  <!-- Card Comments -->
  <div class="card-comments-container">
    <h4 class="card-comments__title"><%= pluralize(node.comments_count, "Comment") %></h4>
    <a class="card-comments__button"><i class="icon-arrow-down"></i></a>

    <div class="card-comments">
      <%= render partial: "nodes/comment", collection: node.comments.includes(:user).order(created_at: :desc) %>
    </div>
  </div>
  <!-- End Card Comments -->

Here is a sample of the link that a user can press, or rather the Rails code that generates the link:

<div class="item-wrapper">
  <div class="item_comment">
    <h5 class="item_comment-title"><%= event.action %> on <%= link_to event.eventable.node.name, "#", data: {toggle_card: "", card_id: event.eventable.node.id, card_title: event.eventable.node.media.title, card_description: event.eventable.node.media.description, card_time: event.eventable.node.media.created_at, card_comments: event.eventable.node.comments_count, card_favorites: event.eventable.node.cached_votes_total } %></h5>
    <p class="item_comment-text"><%= event.eventable.message %></p>
  </div>
</div>

This is an example of the type of jQuery I was using to do it originally:

$(document).ready(function() {
  var overlay  = $('.card-overlay'),
      $card     = overlay.find('.card'),
      triggers =  $('a[data-toggle-card]');

  overlay.hide();

  overlay.click(function(e) {
    if( e.target != this ) {

    } else {
      overlay.hide();
    }
  });

  triggers.click(function() {
    var trigger = $(this);
    var tr = trigger;

    overlay.show();

    var card = {
      id: tr.data('card-id'),
      title: tr.data('card-title'),
      description: tr.data('card-description'),
      time: tr.data('card-time'),
      comments: tr.data('card-comments'),
      favorites: tr.data('card-favorites'),
    };

    $card.attr('id', 'card-' + card.id);
    $card.find('.card__content').attr('style', 'background-image: url(' + card.image + ')');
    $card.find('.card__favorite-count').html('<i class="icon-heart"></i> ' + card.favorites);
    $card.find('.card__comment-count').html('<i class="icon-speech-bubble-1"></i> ' + card.comments);
    $card.find('.card__title').text(card.title);
    $card.find('.card__date').text(card.time);
    $card.find('.card__description').text(card.description);
    $card.find('textarea').attr('id', 'card-input-field-' + card.id);


    var player = videojs("example_video_" + card.id, {}, function() {});

    player.src(
      {
        src: card.video.mp4, type: 'video/mp4'
      },
      {
        src: card.video.webm, type: 'video/webm'
      },
      {
        src: card.video.ogv, type: 'video/ogv'
      }
    );
    player.load();

    player.ready(function() {
        console.log("READY")
    });
    return false;
  });
});

How do I do this in a more "Railsy" way without relying on too much JS/jQuery?

Edit 1

After attempting Rich Peck's suggestion, I got this error:

05:46:31 web.1              | Unexpected error while processing request: JS, accepted HTTP methods are OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE, CONNECT, PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, LOCK, UNLOCK, VERSION-CONTROL, REPORT, CHECKOUT, CHECKIN, UNCHECKOUT, MKWORKSPACE, UPDATE, LABEL, MERGE, BASELINE-CONTROL, MKACTIVITY, ORDERPATCH, ACL, SEARCH, and PATCH

To be clear, this is what I did:

Modified my routes to have this:

  resources :events do
    get :node, action: :node
  end

Then created an action in my EventsController called node like this:

  def node
    @node = current_user.nodes.find(params: [node_id])
    respond_to do |format|
      format.js
    end
  end

Then I created a app/views/events/node.js.erb, that looks like so:

$overlay = $('.card-overlay');
$node = $('<%= j render "nodes/node", locals: {node: @node} %>');
$overlay.append($node);

Then I modified the link_to tag like so:

<%= link_to event.eventable.node.name, event_node_path(event.eventable.node), remote: true, method: :js  %></h5>

Upvotes: 1

Views: 368

Answers (2)

Richard Peck
Richard Peck

Reputation: 76774

Okay there are two problems here:

  1. You're trying to load & populate a partial without any extra data (IE node.comments)

  2. Secondly, you're heavily relying on JQuery to update various attributes on the page. Although there is nothing wrong with this in itself, it does become an issue if you're looking to change page layouts etc.

A more "railsy" way to do this would be to remotely load the node directly from the server, much as Alex Lynham recommended:


#config/routes.rb
resources :cards do
   get :node_id, action: :node #-> url.com/cards/:card_id/:node_id
end

#app/controllers/cards_controller.rb
class CardsController < ApplicationController
   def node
      @card = Card.find params[:card_id]
      @node = @card.nodes.find params[:node_id]
      respond_to do |format|
         format.js #-> app/views/cards/node.js.erb
      end
   end
end

#app/views/cards/node.js.erb
$node = $("<%=j render "cards/node", locals: {node: @node} %>")
$('body').append($node);

#app/views/cards/index.html.erb
<% @cards.each do |card| %>
   <%= link_to card.name, card_node_path(@card), remote: true, format: :js %>
<% end %>

This will keep the code clear and concise.

You can put all your options directly in the _node.js.erb partial. Although it still means there are some changes you have to make, it will give you the ability to change your partial & implementation without all the JQuery specialization you have now.

Upvotes: 1

Alex Lynham
Alex Lynham

Reputation: 1318

You need to make an AJAX request to the server, then use a js.erb partial to return what you are looking for to the frontend, probably. That's the most 'Rails-y' way I can think of.

Personally, I'd ask for JSON and then use a bit of jQuery to wrangle the return values much as you have done in your example. By calling out to the backend is the only sane way you'll be able to get the associated records, the .comments bit, which is I believe where you're stuck, if I've understood your question correctly.

EDIT:

This is quite a good tutorial on how AJAX-y stuff can be accomplished with Rails, in case you've not had much cause to do so in the past.

Upvotes: 1

Related Questions