Using Webcomponents in LiveView - Pros and Cons

I am considering using Overview (shoelace.style) for the front-end part of the application.
Has anyone used web components as part of the LiveView? Will it slowdown the application significantly? Yes - handling forms might be unnecessarily complicated. Any other downsides? Is there any upside?

2 Likes

I have no experience using web components with LiveView, but there was a good talk about it at ElixirConf 2021: ElixirConf 2021 - Chris Nelson - LiveView and Web Components - YouTube

3 Likes

I use custom components for small jobs in combination with live view. It worked very well so far.
When I want to give the full control of the tag content to the custom component, I put an phx-update=“ignore“ attributed at it. To pass data to the component via live view I use attributes and their change callbacks.

I would post example code here, but I am on vacation and my laptop had to stay at home.

8 Likes

I am investigating using shoelace with liveview currently.

The plan is to use Chris Nelsons live_elements with Shoelace web components.

Reason for this selection:

  • Lack of a comprehensive and professional phoenix component library. No, petal components doesn’t cut it.
  • Web components don’t require buying into a JavaScript framework ecosystem
  • Shoelace is framework agnostic so I can use it everywhere, that means static sites, dead views, liveview, react, vue, svelte etc.
  • Shoelace has excellent design tokens making it easy to broadly theme an app, and also specifically target parts within web-components (which use a shadow DOM) and to do so in a way that is not subject to churn within the component. Components can basically remain a black box and still have their sub elements styled without fragility.
  • very approachable styling and design tokens in Shoelace using CSS variables, nothing complicated about it.
  • live_elements makes working with web components much easier, in particular passing data and receiving events in your liveview.
  • I can wrap anything I need in a web component and use it like any other html element without buying into a complicated javascript framework.
  • Not reliant on live view hooks for fancy stuff, anything sophisticated becomes a web component
  • With web components I can get imrooved reactivity in my liveview apps

I am also aware that tailwind is not aligned with web components as it doesn’t support styling shadow DOM using parts of a web component (as there is no tag to hang a class on).

So your back to using CSS with @apply:

/* parts can be targeted using @apply */
sl-button::part(base) {
  @apply py-2 px-4 bg-blue-500 text-white font-semibold rounded-lg shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-opacity-75;
}

At this point I feel one may as well embrace the shoelace approach to CSS which in a way is better. Tailwind is the antithesis of broad app wide theming and I hate baking theme decisions in my code. I believe tailwind is at least one reason there isn’t a a compelling UI component library for Phoenix as they have little choice but to bake theme into the component markup and then it’s a pain to use them.

7 Likes

Maybe with Tailwindcss 3.4, and the planned Oxide engine, things will change? I feel the reason for lack of a good UI component library is due to the absence of a library like Framer.
One thing is for sure - there are many takers for a good quality UI component library in LiveView. It is necessary for good Developer Experience. Let us see, from how and where we get it.

2 Likes

I think the first step IMO is a headless UI. The great thing about a headless UI is it provides a uniform contract and robust accessibility. Styling and theming are completely separate concerns.

Imagine if we could build liveview apps with headless component contracts. The ecosystem would have something to anchor to, tools, generators UI kits and CSS themes would emerge for Phoenix and Phoenix itself would be inherently CSS agnostic, anchoring to headless components.

Currently all we have are the core components stop gap for generators and they bake in tailwind styles. The current situation is kind of a mess.

I was also really keen on using shadcn-svelte with live-view but ran into some issues with live-svelte.

shadcn-svelte is basically a theming wrapper of bits-ui (a headless set of compnents) which is in turn composed using melt UI which is a headless component builder.

My reservation with webcomponents is SSR (server side rendering) as that is a requirement for many sites. For a SaaS app it’s less important as you’re not concerned with web crawlers and SEO.

I will see how the experiment goes with shoelace and report my experience with it back here.

3 Likes

I’m currently experimenting with lit and it’s going very well so far.

I was about to give up on LiveView because that extra layer of UI polish was almost impossible with just hooks. But lit has opened it back up again.

I didn’t want to bring in a framework, things don’t always play nicely with esbuild. But I don’t need any fancy stuff just import lit!

And it’s possible to not use the shadow dom at all by doing createRenderRoot() { return this } so I can keep using tailwind easily.

2 Likes

Yes this is the last chance for liveview for me too. If I don’t get a good outcome I’ll be moving forward with SvelteKit and shadcn-svelte. If I go this route I will also look at live-state by Chris Nelson as an alternative to liveview and graphql.

Yes that’s correct if you’re wrapping your own components you can use the light DOM to expose the internals rather than the internals being private and selectively defining a contract for styling using the part:: selector.

Shoelace can still use tailwind for styling if you choose to but specific overrides to internal component parts require use of @apply to target the parts.

I believe use of @appply is still required for light DOM web components as you still can’t put classes on those internal web component elements in your markup when using the component, but all the internals are exposed for styling with CSS via @apply. I much prefer the parts approach than having the fragile dirty internals of a web component reflected in my styling.

I have a few form related components which have hidden inputs so I turn off the shadow dom for those.

I’m still using hooks as a first resort where possible, but if it’s too complicated for a hook. I’ve (so far) found a nice escape hatch with lit.

Do you mind sharing a link to lit? This is the first I’ve heard of it.

window._hooks = {};

export default {
  mounted() {
    window._hooks[this.el.id] = this;
  },

  destroyed() {
    delete window._hooks[this.el.id];
  },
};
<my-thing … phx-hook=“Lit” />

You can also combine them with hooks with a little hack so you can use all the hook methods in the web components if needed.

1 Like

The Elixir Conf talk by Chris Nelson shows use of web components and how we can easily wrap anything. He also uses Lit (by Google) but there are many other choices, but Lit seems to be the most popular approach for making web components:

1 Like

shoelace is pretty impressive I can see myself adopting something like that :smiley:

Nice one, aside from that and the live element hook for event ergonomics that is all the hooks one ever needs to do anything with custom elements/web components.

I am also looking at some other interesting ideas from @superchris, in particular his live_state solution for delivering state to custom elements over web sockets / phoenix channels as a total replacement for liveview.

My research suggests we can deliver state from our Phoenix backed to the front end in about the most streamlined fashion possible with live_state, and then on the front end we can deliver that state into web components using lit-state for full reactivity on the front end with whatever web components / custom elements we choose to use.

In fact I am also looking at going full PWA for my front end using create-lit-pwa and live_state (which also delivers events into our phoenix backend just like LiveView but it’s not LiveView) with no need for graphql whilst keeping my phoenix backend the same and just interacting with my contexts via live_state rather than LiveView.

The goal is web standards based UI, high adherence to web acccessibility with established web components and zero javascript framework nonsense, avoiding web API layers, and with a total overhead of just 5kb for my client app shell with PWA support.

If you think about it, all a front end needs is state delivered from the backend to render and the ability to send events to the backend and receive updated state to re-render. We don’t need complex API layers and bloated client side web frameworks with the available web standards already supported in all the browsers.

I feel every day that I’m getting closer to my utopia for web development and it’s likely LiveView will not be in that future but the similar idea applied to sharing state to the front end via live-state might certainly be.

In any event I will be publishing my learnings with some documented starter projects based on my explorations for others to follow.

6 Likes

Hi, I still owe you an example. This is production code, that works since 4 years now.

The example’s purpose is to show a localized date time.

Starting with the custom component:

class MyLocalDatetime extends HTMLElement {
  format (name) {
    return ({
      long: { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' },
      short: {}
    })[name]
  }

  constructor () {
    super()
    this._format = 'short'
    this._date = null
  }

  _rerender () {
    this.innerHTML = new Date(this._date).toLocaleString([], this.format(this._format))
  }

  static get observedAttributes () {
    return ['iso-datetime', 'format']
  }

  attributeChangedCallback (name, _oldValue, newValue) {
    (({
      'iso-datetime': (newValue) => { this._date = newValue },
      'format': (newValue) => { this._format = newValue }
    })[name])(newValue)
    this._rerender()
  }
}

window.customElements.define('my-local-datetime', MyLocalDatetime)

I wrapped it in a live_view component:

  @doc """
    Renders a DateTime to a local datetime

    opts:
    - date: DateTime struct
  """
  attr :datetime, DateTime, required: true
  attr :format, :string, default: "short", values: ["short", "long"]
  attr :attrs, :global

  def my_local_datetime(assigns) do
    ~H"""
    <my-local-datetime {@attrs} format={@format} iso-datetime={@datetime} phx-update="ignore"><%= @datetime %></my-local-datetime>
    """
  end

When you change the datetime attribute in your live view, then the attributeChangedCallback is triggered and the content is rerenderd.

I try to avoid big frameworks, because they come and go and they all come and go with their own complexity. So I mostly write vanilla JS.
But you can also use this method to glue js libraries in you project. I have done this for maps to be integrated in live view, for instance too.

9 Likes

After a few attempts, I managed to wrap the shoelace color picker component.

https://www.npmjs.com/package/phoenix-custom-event-hook makes things a lot simpler, but required some tweaking so I could access the new color value.

Sharing here in case anyone else is looking to do the same (feedback also appreciated as I’m very new to pheonix :))

Live component:

defmodule DemoWeb.ColorPicker do
  use DemoWeb, :live_component

  @impl true
  def render(assigns) do
    ~H"""
    <div>
      <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.12.0/cdn/themes/dark.css" />
      <script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.12.0/cdn/components/color-picker/color-picker.js"></script>
      <sl-color-picker
        id={@id}
        phx-hook="PhoenixCustomEvent"
        phx-send-events="sl-change"
        class="sl-theme-dark demo-color-picker"
        value={@value}
        inline={@inline}
        >
      </sl-color-picker>
      <style>
        .demo-color-picker::part(base) {
          background: rgba(0, 0, 0, 0.0);
          border: none;
        }
      </style>
    </div>
    """
  end
end

Add and tweak the custom hook in app.js:

import PhoenixCustomEvent from "phoenix-custom-event-hook";
PhoenixCustomEvent.serializeEvent = (event) => {
    return {
        value: event.target.value,
        detail: event.detail,
    };
};

Live view:

<.live_component module={DemoWeb.ColorPicker} id="this-picker" inline={:true} value={@my_color} />
...
  def handle_event("sl-change", params, socket) do
    {:noreply, assign(socket, my_color: params["value"])}
  end
1 Like

There is this related discussion:

Not about web components but a more generic approach.

(And also: a friendly reminder that I’m biting my nails till you publish some demo for Adobe Spectrum/shoelace + LV)

2 Likes

TW3.4 is out and there is no change in this regard.

My mistake. I think it is Tailwindcss 3.5, which comes with the Oxide engine. It is going to greatly simplify the build system it seems.

1 Like