UP | HOME

No Javascript Clientside Phoenix Live View Modals

Use the css checkbox hack to open and keep the state of a modal on the client-side without any javascript or server round-trips.

1 Intro

Client-side modals are the probably the trickiest part of using Phoenix Live View. Normally, modals require a full round trip, but that doesn’t make for the greatest experience on the client-side.

There are a few existing blog posts about the topic which use alpine:

I actually used alpinejs for a while, but it got complicated. My use-case requires a form inside the modal and having the main page update seamlessly. Maybe someone else could figure it out, but I needed to keep state on the server-side anyway to ensure the modal stayed open on a page state change. But there’s a different way that doesn’t involve any js at all.

The CSS checkbox hack. Apparently, this is ancient, but as someone who learnt css in the past year, this is super-neat.

2 Basic modal with no JS

Essentially, we store the open/close state of the modal inside a checkbox in the dom and control it via a css selector rule.

  1. Create an input checkbox which keeps the client-side state and add phx-update=“ignore”.
  2. Create a div for the modal and set the default style to “style=’display:none’”.
  3. Add a css selector rule for the parent/child/sibling to be able to set the style when the checkbox is clicked.

I use the following template which I’ve simplified and added comments to generate all the client-side modals. On re-reading this code, we can actually add the checkbox outside of the div and adjust the css selector rule, but I suspect I didn’t do so because of the Away hook in the next section. Or something. Who knows, it took me a bit of fiddling to get this.

    <style>
    /* Step 1 - creating an input checkbox */
    #<%= @component_id %>_<%= @name %>_toggle { display: none }

    /* Step 2 - set the default modal to not display  */
    #<%= @component_id %>_<%= @name %>_modal {display: none; }

    /* Step 3 - css selector rule for a sibling */
    #<%= @component_id %>_<%= @name %>_toggle:checked ~ #<%= @component_id %>_<%= @name %>_modal {display: grid;}
    </style>

    <div style="position: relative"; id="<%=  @component_id %>_<%= @name %>" <!-- phx-hook="ModalCloseAway" -->>
        <input type="checkbox" id="<%= @component_id %>_<%= @name %>_toggle" phx-update="ignore" role="button" >
        <!-- below is the button which when clicked controls the checkbox -->
        <label class="button config_button" for="<%= @component_id %>_<%= @name %>_toggle"
           id="<%= @component_id  %>_<%=  @name %>_button" phx-hook="ModalOpen"
        >
            Config
        </label>
        <div id="<%= @component_id %>_<%= @name %>_modal" class="settings_modal">
           <%= @modal_content %>
        </div>
    </div

3 Close modal on away

Now, if you wanted to get a bit more fancy and close the modal when the user clicks anywhere outside of the modal, you’ll need to add a hook. In the previous section, I commented out the hook, which you will need to add back.

And here’s the following code which I spliced together from various sources. Warning though, if you use this code and want to open the modal from anywhere but the “config” button in the template which is part of the main div, you’re going to have a bad time. You’ll likely need to use something like event.stopPropagation() to successfully do so, otherwise, this hook will execute afterwards and close the modal.

Hooks.ModalCloseAway = {
    // https://stackoverflow.com/questions/152975/how-do-i-detect-a-click-outside-an-element
    mounted() {
        function hideOnClickOutside(element) {
            const isVisible = elem => !!elem && !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length ) // source (2018-03-11): https://github.com/jquery/jquery/blob/master/src/css/hiddenVisibleSelectors.js
            const outsideClickListener = event => {
                try {
                    var toggle_element = document.getElementById(element.id + '_toggle')
                    //console.log(toggle_element.checked)

                    // we hit escape, so just set  false;
                    if(event.keyCode === 27) {
                        toggle_element.checked = false;
                    }
                    if (!element.contains(event.target) && toggle_element.checked == true) {
                        toggle_element.checked = false;
                    }
                }
                catch(e) {
                    console.log("outside click listener can't find " + element.id + "_toggle")
                }
            }

            const removeClickListener = () => {
                document.removeEventListener('click', outsideClickListener)
            }

            document.addEventListener('click', outsideClickListener)
            document.addEventListener("keydown", outsideClickListener)
        }
        hideOnClickOutside(this.el)
    }
}

4 Conclusion

I’m not an expert in front-end, but using a checkbox to keep modal state on the frontend seems to work pretty decently for phoenix live view. I’ve been using this in production for a few months and it works on Firefox, Chrome, and Safari.

Footnotes:

1

My use-case requires a form inside the modal and having the main page update seamlessly. Maybe someone else could figure it out, but I needed to keep state on the server-side anyway to ensure the modal stayed open on a page state change.

2

Apparently, this is ancient, but as someone who learnt css in the past year, this is super-neat.

3

On re-reading this code, we can actually add the checkbox outside of the div and adjust the css selector rule, but I suspect I didn’t do so because of the Away hook in the next section. Or something. Who knows, it took me a bit of fiddling to get this.

4

Warning though, if you use this code and want to open the modal from anywhere but the “config” button in the template which is part of the main div, you’re going to have a bad time. You’ll likely need to use something like event.stopPropagation() to successfully do so, otherwise, this hook will execute afterwards and close the modal.