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 and ) 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 . I intend to use it for some future articles as well. The live demo is also deployed to . 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 for more details.

For the project, I’m using the latest GitHub version of Surface, which has 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", }>

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 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

  • you know all the object states and they can be translated into multiple CSS classes
  • as often as possible. It will keep your templates clean and readable

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 , and Surface .

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

  • the object state cannot be predefined
  • you prefer using JS rather than Elixir to update the DOM
  • you want to apply complex styles or transitions
  • you want to use external JS libraries for even more complex transformations, physics, or animations. Eg.

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={}>

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

  • you need to deal with basic but highly dynamic style changes
  • you have a personal preference of writing Elixir over JS

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 with your code.

Thanks for reading and happy coding!

elixir dev | | @iac0bs0n