I’m updating an app from 1.5 to 1.7 and changing to components/heex, and it’s led me to a question about what the Right Way™ to do something is… Here is the current state of things:
I have a component that generates SVG code representing a gauge. Think speedometer 0-60 kinda thing. This object is updated every 15 seconds with fresh data and redraws.
Currently the way I have things organized to have a module in AppWeb/views/gauge_arc.ex, which contains a struct, a type, and helper functions, including a “build_from_values()” function which takes the numerical values, and populates the remaining fields of the struct with values like “svg_bg_path_string” and “svg_fill_path_string” and “main_label_text”.
In my LiveView I fetch the data, create a new %GaugeArc{} with the basic values, pass that to GaugeArc.build_from_values() to fill in the computed values, and store the entire %GaugeArc{} into the assigns in a variable indicating what the gauge represents. (my page has several GaugeArcs on it)
The LiveView then calls a LiveComponent for each arc, passing in the correct struct from assigns, and the component pulls out the values it needs from the computed fields via @gaugeArc.svg_bg_path_string and the like.
All in all, in phx1.5 land, it’s actually been a pretty reasonable way to move a bunch of data into a liveview, from a code organization perspective.
My questions though:
I feel like passing a 30-field struct into @assigns for a component is probably… not recommended? The final LiveComponent only uses 13 of them, 3 of which should never change for the life of the component. My assumption is that the correct way to do this in 1.7 would be to create a component with 13 individual attrs, but this still leaves the problem of how to handle getting that data into the assigns in the LiveView itself. 13 attrs each for 4 or more individual arcs. I could have mount/1 create @arc1_val1, @arc1_val2, … variables, but that seems inelegant. Is that the recommended solution for something like this, or was I on the right track with a custom struct to pass into @assigns?
In 1.7 LiveComponent world, where is the preferred place for the code for these sorts of custom structs or helpers? They are firmly display-only, and generate HTML/SVG code, so I felt that they belonged in AppWeb, not over in App, but I’m not quite sure where to put them in the directory structure. Putting the struct/type/functions directly inside the app_web/components/gauge_arc.ex didn’t seem right. A “helpers” directory perhaps? Curious to see what others do for this.
It’s worth noting that this is a very low-impact project, it will never have more than maybe 10 concurrent users, so sacrificing some performance was never a problem. I’m just trying to think through the Right Way™ so that if I ever run into a bigger project with similar needs I’ve got some ideas, and a better understanding of how te work with change tracking rather than against it.
This should actually be fine, if the struct is not also large in terms of memory usage. The important bit for change tracking is more around how you access data out of assigns and also that you precompute everything, so template just pull out values directly from the assigns (even if nested) and not do computations. You seem to be doing that.
I personally am not a big fan of phoenix having gotten rid of the views folder, which to me is where this stuff belongs. Good thing: The folders used by generators are suggestions, not rules. Add whatever you think makes sense.
If a helper generates HTML there is no reason not to make it a component AFAIC. It can then live anywhere in your lib/my_app_web/components/ directory.
This should actually be fine, if the struct is not also large in terms of memory usage. The important bit for change tracking is more around how you access data out of assigns and also that you precompute everything, so template just pull out values directly from the assigns (even if nested) and not do computations. You seem to be doing that.
Okay, that’s what I was thinking. I wasn’t sure how much extra work change tracking was doing for those struct fields that aren’t actually getting used. (Is it doing a deep comparison on the struct, for example, or does it just see struct and assume it’s changed and toss the whole thing is an “changed”?)
It’s premature optimization in this case, but if it’s doing a deep comparison then having a 2nd struct that was just the necessary computed results might actually be a good idea.
I personally am not a big fan of phoenix having gotten rid of the views folder, which to me is where this stuff belongs. Good thing: The folders used by generators are suggestions, not rules. Add whatever you think makes sense.
Yeah, I generally would just create a folder “helpers” or “support” and put things like the struct definitions in there. I know it works, I was just wondering if there was really a standard place for this sort of stuff in the new components organizational structure. One does try to adhere to standards so other people can read the code.
The problem is that the code used, the %GaugeArc{} struct and it’s functions and type, aren’t specific to the component itself. The LiveView (dashboard/index.ex) has to call them. Calling GaugeArcComponent.build_from_values() and then storing the result into assigns and then passing that result into <.live_component module={GaugeArcComponent}> seemed somehow wrong, which is why I’ve got the %GaugeArc{} broken out into its own module currently.
I certainly was not suggesting anything like that!!! I’m not 100% on what you are after but it seems you have created a struct specifically for converting other structs into a normalized struct that can then be passed to a live component. In that case, I may actually be suggesting something like that… If that is the case, don’t be afraid to be a bit OO and instead of calling GaugeArcComponent.build_from_values() and passing that result to <.live_component ... />, just pass the original struct to <.live_component ... /> and convert each case inside the component itself (ie, call build_from_values(%Polymorphic{} = guage_arc). If I’m not completely misunderstanding your usecase, this is basically what views would have been doing in that a view provides specialized presentation logic for a specific slice of UI.
I may be way off base, though. Seeing some more concrete code would really help if you haven’t already figured something out!
I’m also curious as to your use-case for live components. Could they just be function components in this case or no?
That’s an interesting callout… I realized while answering your next question that I should probably look at moving as much of that logic as possible into the component. Something that got forgotten when I moved to the LiveComponent!
I’m building a dashboard that renders a number of widgets. Each widget could be on the page multiple times with different data, and may be on different pages. Each widget has several controls on it to allow it to display its data in different ways, (Metric/Imperial unit toggle for example.) the setting for which should only change the display for the widget the control was changed for, not for all widgets on the page.
To my understanding this basically screams “LiveComponent”. (especially considering that I have no resource constraint and no concerns about scaling.) However I’m more than happy to be wrong if there’s a Better Way™.
Naw, definitely a case for a live component! I asked because I wasn’t sure whether you really need to keep all your state in the LiveView itself or not. If you did then a live component might not be needed.