Stimulus Discourse

Correct way to append HTML from server?


#1

Hello - I’m wondering what the “right” way to insert markup from the server is. I have a pretty vanilla Rails setup and I’m using unobtrusive javascript to send form data to my server (a form with data-remote=true).

I’m trying to create a basic task list, I have a form for creating a new Task - when it’s successful, I want to append the markup from the server into my tasks list. I have my server respond with a javascript snippet (from create.js.erb), that does something along the lines of:

document.querySelector('.tasks').innerHTML += "<%= j render @task%>" 

which renders a template along the lines of:

<!--_task.html.erb -->
<div class="task" data-controller="task" data-task-id="<%=task.id %>">
  <%= task.description %>
</div>

This works fine, but I’m trying to also trigger an event when a task is added (that contains the task id). Initially I added code to handle this through listening to the unobtrusive js ajax:success event, but quickly realized that event doesn’t have any data from the server in a format that I can easily use since the server is returning JS, not JSON.

So, I realized that my Stimulus TaskController should automatically call connect() when my new DOM element is added. Maybe I could trigger my event there, since that has access to data attributes? For example:

// task_controller.js
export default class extends Controller {
  connect() {
    this.element.dispatchEvent(new CustomEvent('task:add', {bubbles: true, detail: { id: () => this.data.get('id')}}))
  }
}

To my surprise however, I noticed that when innerHTML is called, all my existing Stimulus controllers in the task list are disconnected, and then new ones (I presume) are initialized/connected. Therefore, putting my event dispatch code in the connect() method is always resulting in n events, where n is the number of tasks in the list (rather than just the 1 event, which is what I want).

Adding Task
(5) TaskController Disconnect
TaskController Initialize
TaskController Connect
...
TaskController Initialize
TaskController Connect

I assume this is happening because innerHTML is replacing all the child DOM nodes in the .tasks node, rather than appending to it?


Ok I actually found an answer to my question right before I was about to submit this, but I was able to solve this by using the JS API insertAdjacentHTML, rather than innerHTML. So my rails create.js.erb file now looks like this:

document.querySelector('.tasks').insertAdjacentHTML('beforeend',"<%= j render @task %>")

Awesome! I’m still going to post this because it might help out someone else.

Thanks!


#2

Using rails_ujs’s ajax:success event seems to work well with Stimulus.

tasks_controller.rb

  def index
    @task = Task.new
    @tasks = Task.all # or whatever query you need
  end

  def create
    @task = Task.new(task_params)  
    if @task.save
      render @task, layout: false
   else
      # handle errors
   end
  end

Tasks Form

<%= form_for @task, 
             remote: true,
             data: {
               action: 'ajax:success->tasks#onCreateSuccess' 
             } do |f| %>
  <%= f.submit "Create Task" %>
<% end %>

Tasks partial

<div data-controller="tasks" data-target="tasks.tasks">
  <%= render @tasks %>
</div>

Tasks Controller

export default controller TasksController extends Controller {
  static targets = ['tasks'];

  onCreateSuccess(event) {
    const [data, status, xhr] = event.detail;
    this.tasksTarget.insertAdjacentHTML('beforeend', xhr.response);
  }
}