Mar 2021
Lessons from HTML passed Over The Wire in Rails
Our app is a web chat - so a variety of data needs to be broadcasted directly to a user in real time.
This is handled via WebSockets - instant sending and receiving of data. Rails implementation of WS is called Action Cable
.
Incoming Requests
For this chat application, users can request chats with pros.
So a pro will receive an incoming request on her dashboard.
Before request:
After request:
As mentioned above, we need to send this data directly to the pro, without the pro needing to refresh her page.
Let’s look in the Controller that handles chat sessions.
ChatSessions Controller
When a user requests a chat, we route to the create
action of the ChatSessionsController
# ChatSessionsController
def create
...
# append new student to pro buddy list
SessionChannel.broadcast_to(
@pro,
action: 'create',
new_student: new_student(@chat_session),
... more stuff ...
)
...
end
private
def new_student(session)
ApplicationController.renderer.render(partial: 'shared/list_user_pro_dash', locals: { session: session, session_status: 'active' })
end
The SessionChannel
is the Action Cable channel where we handle chat sessions throughout their lifecycle (request
, accept
, cancel
, end
).
In this case, we are broadcasting an HTML partial (list_user_pro_dash.html.erb
) directly to the @pro
’s dashboard.
This partial generates the HTML we see in the image above — a link with the username of the incoming request.
Receiving the Partial
The Javascript component of the SessionChannel
lives in session_channel.js
, and we receive
the data that was broadcasted from the controller.
#session_channel.js
import consumer from "./consumer"
consumer.subscriptions.create("SessionChannel", {
connected() {},
disconnected() {},
received(data) {
if (data.action == 'create'){
// append data.new_student to correct location in DOM
}
})
Appending data.new_student
data.new_student
is a string that contains the HTML
corresponding to this requested chat. Just a minute ago, this was rendered via ApplicationController.renderer
in our ChatSessionsController
.
To append it to the pro’s dashboard, we could do something like this:
const new_student = data.new_student
// active sessions live in #active element
const active = document.getElementById('active')
active.append(new_student)
However, this won’t work because new_student
is a string (and not a Node element), so append
will treat it like a string.
In other words, we will see HTML, instead of experiencing HTML.
In other words, actually, in images, our view will look something like:
instead of what we want:
Strings or Nodes
So at this point, we either need to turn our HTML string into a Node object (because append
can also append nodes) or somehow ask Javascript to interpret our string as HTML.
The latter option probably would have been the easiest. All we would have to do is something like this:
const new_student = data.new_student
// active sessions live in #active element
const active = document.getElementById('active')
active.insertAdjacentHTML('beforeend', new_student)
insertAdjacentHTML
will parse a string as HTML.
However, I decided to stick with append
to see if we could get it to work.
To do this, we do a little hack: we create an element, then fill it with our HTML string. At this point, our string magically becomes a node object. And we can extract it from our temporary element (temp_obj
) via firstChild
.
// create temp Node object
const temp_obj = document.createElement('div')
temp_obj.innerHTML = data.new_student
const new_student = temp_obj.firstChild
// append to list
const active = document.getElementById('active')
active.append(new_student)
At this point we have a working solution. We turn a string of HTML from our Controller into a node, and then append it to the correct element.
The Application Breaks
Initially, the partial we append (new_student
) looked something like this:
<li>Stuff in here</li>
Then, we make a seemingly innocuous change to the partial.
<% if something? %>
<li>Stuff in here</li>
<% else %>
<li>Different stuff in here</li>
<% end %>
Now the application is broken.
Huh?
It seemed bizarre that the addition of a conditional in a partial on the backend would break the broadcasting elsewhere in the site.
It turns out that the broadcast wasn’t broken, but our Javascript was unable to deal with the new HTML string from our partial.
What’s new in this partial?
It would seem that the partial should render a virtually identical result — a simple <li>
element containing some info.
However, there is one difference. As is standard in code syntax, we indent the nested line inside of a conditional.
And because of this, a whitespace is created before our <li>
element.
FirstChild
Now back to our Javascript. Our data.new_student
now contains a single whitespace, and then an <li>
element.
// temporary object
const temp_obj = document.createElement('div')
temp_obj.innerHTML = data.new_student
const new_student = temp_obj.firstChild
const active = document.getElementById('active')
active.append(new_student)
When we temporarily created the element and then grab the firstChild
, we are actually grabbing the whitespace instead of the <li>
.
So, we are appending something, but we are appending whitespace.
Fixes
At this point we have a couple of fixes.
The easiest would be to use firstElementChild
instead of firstChild
, as that will grab the <li>
and ignore text(whitespace).
Alternatively, we can be more specific than firstElementChild
and instead look directly for an <li>
element.
// temporary element
const wrapper = document.createElement('div')
wrapper.innerHTML = data.new_student
const new_student = wrapper.querySelector("li")
const active = document.getElementById('active')
active.appendChild(new_student)
The End
Be careful when using firstChild
as whitespace could potentially find its way to the front of the child you were initially targeting.