Correct way to append HTML from server?

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.description %>

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: () =>'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.


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


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

  def create
    @task =  
      render @task, layout: false
      # handle errors

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 %>

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);