Shapes — Dynamic Styles with Phoenix Live View

Exploring (simple) ways to generate dynamic styling for your Phoenix Live View projects

Working on a game as my hobby project brought up some very interesting challenges. While the game is mostly static/table-top style, many of the challenges were related to the multiplier/backend area. But still, some dynamic aspects of the frontend (built with Phoenix Live View and Surface) are needed. This is how I ended up exploring various ways to create dynamic styles for the HTML components.

The Setup

For this article, I created a demo project called Playground. I intend to use it for some future articles as well. The live demo is also deployed to Heroku. The context dedicated to the present article is called Shapes. Our demo will use some squares that should change the color, size, and location in a dynamic way.

The project is using Phoenix 1.5 with Liveview and Surface.

Surface is my default go-to when speaking of personal projects built with Phoenix. However, all different approaches detailed below will work also with the default Live View, but the code will be slightly different. As a side note, some of the Surface functionalities are getting integrated into the core Live View. Check this article for more details.

For the project, I’m using the latest GitHub version of Surface, which has a new syntax. At the time when I write this, version 0.5 is not yet published.

That being said let’s get to work!

1. Dynamic CSS Classes

The first example goal is to change the shape’s color on the user's radio-button selection.

This is by far the easiest approach. It involves creating multiple classes in the CSS file, then dynamically changing the element class in the HTML template, reacting to the user input. For our example:

.green {
background-color: green;
}
.blue {
background-color: blue;
}
.red {
background-color: red;
}
}

In the template, Surface makes it as easy as:

<div class={"shape", @shape_color}>

where the @shape_color is the dynamic class passed into the assigns. Behind the scenes, on user selection, a change_color event is triggered. This updates the Live Component state with the selected color. You can find the full implementation in the repo.

This demo code is also very basic. For example, we could have multiple dynamic classes for the same element. Also, Surface offers another option of allocating dynamic classes, by providing a keyword list:

<div class={class_one: my_function(@assign_one), class_two: @boolean_assign, ... etc.}>

One may ask, and for good reasons: if that is the easiest solution, why not use it every time? Why do we need to look further? As it may respond to most of your needs, there are certain cases that this approach cannot handle.

The most important limitation is that the classes are predefined. And while it can handle some level of dynamicity (see responsiveness for example), it cannot adapt to custom user input, for example.

When to use it

2. JS Hooks

This example addresses custom user input. The user drags a slider that adjusts the shape size.

The dynamic CSS example is not practical in this case. We do not know the state, we cannot create a class for each value the user may choose. Fortunately, Live View comes with excellent JS interoperability, and Surface makes it even easier.

There are 2 major things going on in the template:

  1. a JS hook for the element that wraps the shape:
<div class="playground" id="resize_playground" :hook="DynamicStyle">
<div class="shape" id="resize_shape">
</div>
</div>

The shape also has an id that makes it easy to find in the JS Hook

2. On change, the slider triggers a change_size event. To prevent a huge amount of events, this is debounced with opts={phx_debounce: 100}.

The Live Component handles the event, and pushes an event to the JS hook:

def handle_event("change_size", %{"style" => %{"size" => size}}, socket) do
size = String.to_integer(size)
socket = push_event(socket, "change_size", %{size: size})
{:noreply, assign(socket, :shape_size, size)}
end

Finally, the hook knows how to find the shape and changes its size according to the received value:

let DynamicStyle = {
mounted() {
this.handleEvent('change_size', ({ size }) => {
const shape = document.getElementById('resize_shape');
shape.style.height = px(size);
shape.style.width = px(size);
});
},
};
const px = (val) => `${val}px`;export { DynamicStyle };

When to use it

3. Elixir-only

The third and last example uses Elixir code to update the styles. There are two sliders this time. They correspond to the X and Y axes of the container. Dragging them changes the shape's position inside the parent element.

First of all, the JS Hook approach above would work fine for this example as well. But there may be people (for example myself) that would prefer to handle the transformation in the Live Component.

One nice thing about Surface is that you can pass a keyword list to an element style like: [left: “10px”, top: “10px”]. So it is very easy to compose the style in the component and pass it in the assigns.

<div class="shape" style={@style}>

In pure Live View, the same approach will require some string manipulation.

On change, the slider triggers a change_position event. That is handled in the component:

data style, :list, default: [left: "0px", top: "0px"]def handle_event("change_position", %{"style" => %{"x_axis" => x, "y_axis" => y}}, socket) do
style = [left: x <> "px", top: y <> "px"]
{:noreply, assign(socket, style: style)}
end

When to use it

Conclusion

We covered the basics of three potential approaches to add dynamic styles to the Live View templates. Worth mentioning, the 3 models are not mutually exclusive. You can combine them however you see it fit.

That being said, if you use or know other ways of handling dynamic styles, please leave a comment and/or submit a PR with your code.

Thanks for reading and happy coding!

elixir dev | dorian.iacobescu@gmail.com | @iac0bs0n