Phoenix LiveView highlight text


I’ve been playing around with Phoenix LiveView lately and am really enjoying it so far!

I’m trying to write a simple site where users can highlight selections in a body of text. One way I can think of doing that would be to have a list of tuples and iterating through each character to add spans for the selections. The problem with that is I have to go parse the entire document every time there’s an update, which I’d prefer not to do.

Is there a more efficient way of (1) generating the view and (2) making sure that updates don’t require me to re-render the entire document when a new highlight is added?


1 Like

Hi @babaloo,

Not sure if I got your problem, so it’s possible, my suggestion is completely off, but if I understood you correclty, you have a body of text, where certain parts can/should be highlighted (change color basically). Can you offload highlighting part to javascript?

I think LiveView is awesome, but javascript is powerful too, and the best way is to leverage power of both.

In your case, you could have your body of text, and parts that need to be highlighted, you could send as array or json object in separate div. and with javascript highlight parts of your text based on that data.

In that case, if your text does not change, you can send only the new highlights, and text will stay unchanged.
For example

   actual text
<div class="hidden">

and with live hooks you can control, when you receive new highlights and then run javascript to rerender actual text

If I understand you well he wants to achieve what we can do in Medium articles, highlight a part of the text in a sentence and persist it in the backend so that the user always see it in the future.

I am just starting now with LiveView, and for what I understand and read about, it only sends a diff to the browser. So by other words it only sends the data that have changed inside an html tag, not the entire html, neither the entire data that goes between each html tag.

If I am wrong, then please anyone is free to correct me.

Hey @Exadra37,

you are partially right. Phoenix will send only updates of the page instead of sending the whole rerendered page. But it means it will send updates that you pass as assign to your socket, and rest of HTML will not be sent.

So in the case of babaloo, if he has

  def mount(_params, _session, socket) do
    post = get_post()
    {:ok, socket, post: post}

  def handle_event("add_highlights", _value, socket) do
    post = make_highlighted_post(
    {:noreply, assign(socket, post: post)}

every time there is an update, entire post will be sent down. of course, it will not send other parts of your html (head, body, navbar etc), but only the post, if the post is big chunk of the page, then that’s problematic.

I’m suggesting to basically separate part that doesn’t change, and part that will be updated.

  def mount(_params, _session, socket) do
    post = get_post()
    highlights = get_highlights()
    {:ok, socket, post: post, highlights: highlights}

  def handle_event("add_highlights", _value, socket) do
    highlights = get_highlights
    {:noreply, assign(socket, highlights: highlights)}

In this case, onlysmall fraction will be sent, and the highlighting part needs to be handlend on javascript side

hi, yes a realtime version of what medium (or google docs) is sort of what I’m after.

I can do it in javascript and that might be a better solution honestly, but my goal is to try to avoid javascript as much as possible (mainly for my own learning). It’s definitely necessary for the window selection api, but I’m hoping that’s all I use it for.

The challenge that I’ve run into so far is that just given highlights (structs that define start and ends of a highlight), constructing the ui with eex is difficult, so I had to create a second data structure that made this easier that I called a chunk for now.

defmodule Qda.Chunk do
  defstruct content: "", highlight: false
  def chunkify(content, highlights), do: chunkify(content, 0, highlights)
  def chunkify("", _offset, _highlights), do: []
  def chunkify(suffix, _offset, []), do: [%__MODULE__{content: suffix, highlight: false}]
  def chunkify(content, offset, highlights) do
    [ highlight | tail ] = highlights
    {prefix, highlight_start} = String.split_at(content, highlight.start_offset - offset)
    {highlighted_content, suffix} = String.split_at(highlight_start, highlight.end_offset - highlight.start_offset)
    case prefix do
      "" -> []
      str -> [%__MODULE__{content: str, highlight: false}]
    ++ [%__MODULE__{content: highlighted_content, highlight: true}]
    ++ chunkify(suffix, String.length(prefix) + String.length(highlighted_content) + offset, tail)

There I have a function that given some text and some highlights, computes the chunks for the document. Then in my eex I can do something like this to render all the chunks (with the selections bolded):

<div id="contents" phx-hook="DocumentContents">
  <%= for chunk <- @chunks do %>
    <%= if chunk.highlight == true do %>
      <b><%= chunk.content %></b>
    <% else %>
      <span><%= chunk.content %></span>
    <% end %>
  <% end %>

If I add a new chunk, will live view know not to push the other chunks down the wire (and not rerenader the other parts of the dom)?

Is this the right way to approach the problem? I’m new to elixir + phoenix, so if anyone has suggestions, I’d love to hear them! Thanks :slight_smile:

I believe this is not possible because LiveView tracks changes based on the assigns in the socket. So if you have @content in your liveview template to render the page’s content, any changes to this variable will re-render the part of the LiveView that depends on this variable, which in your case, will be re-rendering the entire document.

Yeah, I understand what you mean, with not wanting to use javascript. If that’s for learning purposes only, so that you will find what are the limits of the tool you are learning, then it is totally fine. If it is for something, that will go to production, I think, you definately should not try to avoid javascript. It is an amazing tool and not using it (where it shines) will hurt only your product. Thats imho :slightly_smiling_face:

Regarding the question,

If your chunks are only appended, then there is a possibility to do that, but I think in your case, different chunks that were already sent can be updated as well. in that case Phoenix will not deduce which ones are updated, it will sent like @shankardevy described earlier.

For example, if you have

chunks = [{content: "one", highlight: false},{content: "two", highlight: false}, {content: "three", highlight: false}]

sent down to client, and then you always have new chunks

chunks=[{content: "four", highlight: true}]

then you can make Phoenix to send only that portion (and you stop monitoring previous parts). More on this here

However, if in your use case you need to do something like

# initial render
chunks = [{content: "one", highlight: false},{content: "two", highlight: false}, {content: "three", highlight: false}]

# updates
chunks = [{content: "one", highlight: true},{content: "two", highlight: false}, {content: "three", highlight: false}]

meaning, any part of those chunks can be updated any time, then phoenix will send the whole structure