Nicd

Nicd

Structuring a LiveView with many similar (dynamic?) components

So I have this page and I’m thinking of making a new version of it with LiveView:

As you can see, there are many different graphs on the page. Currently on the frontend I have a JS system where each graph is its own component and defines what kind of data it is interested in. Since many graphs can be interested in the same data (such as the two language lists), I don’t want to duplicate queries.

Additionally, the page is live updated, so all the graphs get the new data and decide what to do with it.

Now I’m wondering how to structure this with LiveView. I just started hammering it directly and got the top bar and top languages list working, but obviously the LV module will become a mess with all the data retrievals. So I want to structure it in a cleaner way. I have some ideas but would be nice to get yours.

A final aim here is that some time in the future, the specific graphs and their positions could be configurable, so if possible, I don’t want to hardcode them in the template. But I don’t know what LV’s change tracking would think of that.

My plan is to do something like this for now (pseudo-ish):

defmodule ProfileLive do
  @graphs [
    TopBarGraph,
    Last2WeeksGraph,
    TopLanguagesGraph,
    ...
  ]

And then somehow render the stuff based on that list (then later it could be made configurable). The modules would have something like

defmodule TopBarGraph do
  def wants_data(), do: MapSet.new([:user, :total_xp, :recent_xp, :date_xps])
  ...
end

and then the LV module would combine the needed datas and retrieve those, providing them to the graphs. Does that sound reasonable?

But if the data needs are dynamic like that, I would need to render the component in a generic way like

<%= live_component(@socket, TopBarGraph, data: @TopBarGraph_data) %>

right? I can think of a couple other ways too, but I’m not sure which of them would work with LV’s change tracking so it doesn’t have to render everything on every update.

I think I can get the live updates working once I figure out a good structure for the basic setup. So, any ideas welcome. :slight_smile: Have you done anything similar?

Most Liked

mindok

mindok

I’ve just been doing something similar, building pluggable charts for borehole data.

What I ended up doing was:

  • Pluggable data extractor modules (basically a “read_data” function in each module that receives a keyword list for context - e.g. dates - in my case a borehole identifier)
  • A small piece of configuration (currently hard-coded, but in future will be user-configurable) that maps named datasets to the data extractor modules
  • On mount or handle_params, I iterate the data extraction configuration to build a map of extracted datasets using the different extractor modules and put them on the socket as socket.assigns.dataset (in my case about 20Mb per liveview process - thankfully there’s only ever a handful of users!). There are also some derived datasets generated from the base ones (e.g. moving averages) - the data extraction process runs the base ones first, then generates the derived ones.

In the case where you are listening for live updates from other parts of the system I would just update the data held in socket.assigns.datasets in the appropriate handle_info

That sorts out the data extraction.

I also have pluggable renderers for each different type of visualisation I want to show (mostly based on Contex FWIW - they emit SVG so no JS hooks are required).

I then have a definition for each display element that defines the named dataset to use, the pluggable rendering module and any settings to control the rendering. This is added to the socket as display_blocks

Finally I have a component that is embedded in the main liveview along the lines of what you have:

<%= live_component(@socket, MyLayoutComponent, datasets: @datasets, display_blocks: @display_blocks, other_stuff: @other_stuff, id: "some-id") %>

MyLayoutComponent handles organising height & width of all the sub-components based on the settings in the passed display_blocks, passing in the correct dataset and settings and invoking the rendering. The whole thing re-renders when anything changes at the moment (data, settings or other things like the currently selected item - aka other_stuff), which is a bit inefficient but actually performs ok. I feel the code and approach is reasonably well organised and easy enough to extend with additional visualisations.

I hope this makes sense!

paulstatezny

paulstatezny

Nicd

Nicd

I hacked on it last week and came up with a first iteration, the meat of it is here: lib/code_stats_web/profile_live · f6ba35970a16f8fff89c0cbc38d80ecce9f53b16 · CodeStats / code-stats · GitLab

There are no docblocks yet and there’s a couple of unused functions laying around, but it’s a start. I’ll describe here how it works so that I can copy these to the docblocks later. :grin:

The building blocks here are Graphs, DataProviders, SharedData, and the live view itself. The relationships are something like:

ProfileLive <--1..n-- Graphs <--1..n-- DataProviders <--1..n-- SharedData

So the live view has many graphs, these are managed by Graphs.Catalogue. The graphs’ responsibility is to receive data and render it, and they are normal live components (not yet sure if they will need to be stateful or stateless). They also have a function render_wrapper that is used to render them into the live view, so that I don’t need to do just data: @data assigns, but I can instead get accurate assigns for each graph. Example from UserInfo:

<%=
  live_component(
    @socket,
    __MODULE__,
    user: assigns[DataProviders.UserInfo],
    total_xp: assigns[DataProviders.TotalXP].total_xp,
    recent_xp: assigns[DataProviders.TotalXP].recent_xp,
    last_day_coded: assigns[DataProviders.LastDayCoded].last_day_coded
  )
%>

I don’t actually know yet how this affects change tracking, because the live updates aren’t implemented, but I’m hoping for the best. And I think it’s nicer that the component only gets the data it needs and not all of it.

Now the graphs in turn have a set of DataProviders that they get data from. The purpose of the providers is to retrieve the initial dataset, and to update it (in the future) when any events come in. Each graph can get data from many providers and the providers can be used for many graphs.

Now, since some providers rely on the same data (like the last 12 hours of events) and I don’t want to repeat the queries, there is finally the SharedData module. Providers specify what shared data they need and SharedData is responsible for requesting it.

Finally we get to combining all of this in the live view:

# Get all the graphs available
graphs = Graphs.Catalogue.graphs()
# Get data providers required by the graphs (now that I think of this, this should take in the graphs as argument)
data_providers = Graphs.Catalogue.data_providers()
# Get the shared data requirements of the providers
required_shared_data = DataProviders.Utils.required_shared_data(data_providers)
# Retrieve the shared data
shared_data = DataProviders.SharedData.get_data(required_shared_data, user)
# Retrieve the initial data to show in the graphs based on the shared data
initial_data = Graphs.Catalogue.get_initial_data(shared_data, data_providers, user)
# Given the graphs, providers, and initial data, build the socket assigns using the module names as keys
socket = Graphs.Catalogue.assign_datas(socket, graphs, data_providers, initial_data)

Then in the template, I use

<%= Graphs.Catalogue.render_graphs(assigns) %>

which in turn is just a for statement that calls the render_wrapper of all the graphs.

Now, this all works great currently, but I don’t have the live updates implemented yet, so it’s not a full featured prototype. Additionally, I have a couple of open questions still:

  • Currently, like shown above, I don’t use render but instead just call the functions directly. I wonder if this affects LV’s change tracking ability? I don’t see any other reason to use render because it would require a view and extra hassle.
  • I have functions that look like this:
    def render_graphs(assigns) do
      ~L"""
      <%= for graph <- @graphs do %>
        <%= graph.render_wrapper(assigns) %>
      <% end %>
      """
    end
    
    and I wonder if the ~L wrapping is necessary or if I could just run the code directly.

I know that’s a lot to read and I don’t expect anyone to invest too much time into this, but if you have any comments or ideas or insults, I’d be glad to hear them. :slight_smile:

Where Next?

Popular in Questions Top

chokchit
** (DBConnection.ConnectionError) connection not available and request was dropped from queue after 2733ms. You can configure how long re...
New
aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New
Harrisonl
We have an ECS cluster with 4 services, where each task joins a single cluster, via discovery ECS discovery service. Currently when I de...
New
minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
jaysoifer
Is there a way to rollback a specific migration and only that one (“skipping” all the other ones)? Would mix ecto.rollback -v 200809061...
New
SoCreat
i’m a new one to elixir which editor can i use vs code? or atom? Thanks! :smiley:
New
vonH
When I run the Plug and I recompile I wind up having to use Ctrl C to quit iex and start again. Witht the help of rlwrap I can use the cu...
New
sergio_101
I am VERY much an elixir newbie. I have taken one elixir course and one phoenix course on Udemy. During that course, I saw the instructor...
New
WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
New

Other popular topics Top

sen
Hi All, I set a environment variables in dev.exs , like below code. when i start server, how can i set the ${enable} value? thanks. d...
New
JeremM34
Hello, how can I check the Phoenix version ? Thanks !
New
minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
JakeBecker
TL;DR: I’ve just released an implementation of Microsoft’s IDE-independent Language Server Protocol for Elixir. It adds language support ...
1144 53690 245
New
JorisKok
I have a server on AWS, and was running a load test using artillery. When looking at the Phoenix dashboard I see the Ports going to 100% ...
New
Emily
I have VueJS GUIs with the project generated using Webpack. I have Elixir modules that will need to be used by the VueJS GUIs. I forese...
New
gausby
I asked this very same question on twitter and got some interesting feedback, but I thought it would be a good question to ask here as we...
1207 39297 209
New
Qqwy
Original source of discussion: This topic on the Pragmatic Programmers’ Functional Web Development with Elixir, OTP, and Phoenix forum. ...
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
romenigld
I am trying to run a deploy with docker and I successfully runned with this command: docker build -t romenigld/blog-prod . but when I t...
New

We're in Beta

About us Mission Statement