benkimpel

benkimpel

Improve Support for Web Components in Forms with "Element Adapters"

Background

I work at a hedge fund and our traders need highly dynamic UIs (think splitters, tabbed panels, tree lists, enormous data grids, pop-outs, etc.) and we don’t want to have to build it all from scratch nor do we want to switch to React.

I had thought we found an 80% solution in Shoelace (https://shoelace.style) – a really well done web component library – and I got very far with using it in our main Phoenix app (just a few tricks to make reflected attributes work), but I hit the wall hard when it came to forms. I found that there are several (perfectly reasonable) assumptions baked into the LiveView JavaScript that don’t support web components (and, really, how could they? Web components are custom by nature).

Proposal

Phoenix LV already works well with web components, but I think we can take it even further. If we get it right, then the chances that someone will have to eject and switch to React are much, much lower. Since web components are markup-first and generally use attributes as their api they could be a nearly perfect fit for advanced, server-managed UI. I’m familiar with phx-update="ignore", but I think that is an escape hatch that we don’t need to use as often as we might think AND it doesn’t help for web component-based form controls at all.

My proposal is to introduce an element adapter and adapter registration mechanism which will allow Phoenix LV JavaScript to code to a standard interface for interacting with an element and will also allow Phoenix users to provide adapters for web components.

Phoenix LV would ship with adapters for all standard html elements and they would implement the logic that is in place today. Adapters would be assigned by tagName since this is an element-level responsibility. Element adapters would most likely be passed to the LiveSocket constructor.

The gist of the proposal is “When it comes to html elements, ask instead of inspect/assume.”

Some Examples

In DOM.js there are a few good candidates: hasSelectionRange, isTextualInput, isFormInput. There are other examples elsewhere, but these seemed reasonably illustrative.

Example: hasSelectionRange

Here’s the definition for hasSelectionRange in DOM.js

hasSelectionRange(el) {
  return el.setSelectionChange && (el.type === "text" || el.type === "textarea)
}

Here’s a function in DOM.js that calls hasSelectionRange.

restoreFocus(focused, selectionStart, selectionEnd){
  if(focused instanceof HTMLSelectElement){ focused.focus() }
  if(!DOM.isTextualInput(focused)){ return }

  let wasFocused = focused.matches(":focus")
  if(focused.readOnly){ focused.blur() }
  if(!wasFocused){ focused.focus() }
  if(this.hasSelectionRange(focused)){
    focused.setSelectionRange(selectionStart, selectionEnd)
  }
}

Here’s how adding an element adapter might look for the Shoelace input element <sl-input> which does have its own setSelectionRange, but without this we’d have no way to tell Phoenix LV about that. (A better example would have been isFormInput, but you get the idea I hope.)

// Defining/assigning an adapter
const liveSocket = new LiveSocket("/live", Socket, {
  elementAdapters: {
    // If these adapters are the same for a group of elements
    // the adapter function can be defined outside and assigned
    // to multiple tagNames here.
    //
    // The default adapters for native html elements would obviously live
    // inside phoenix liveview js
    "SL-INPUT": (el) => {
      // NOTE: I think doing this extraction work might tighten up this interface a bit.
      return {
        el: el,
        isTextualInput: true,
        hasSelectionRange: true,
        // ... Use sl-input's setSelectionRange function
        setSelectionRange: el.setSelectionRange,
        isFocusable: true,
        // sl-inputs can get focus by calling focus
        focus: el.focus,
        // isFormControl: true
        // value: () => {}
        // ... etc.
      }
    }
  }
})

Usage within the same example code above, would then become…

restoreFocus(focused, selectionStart, selectionEnd){
  // Maybe we just pass the wrapped el around instead...
  const ela = elAdapter(focused);

  if (ela.isFocusable) { ela.focus() }
  if (!ela.isTextualInput) { return }

  let wasFocused = focused.matches(":focus")
  if(ela.readOnly){ ela.blur() }
  if(!wasFocused){ ela.focus() }
  if(ela.hasSelectionRange()){
    ela.setSelectionRange(selectionStart, selectionEnd)
  }
}
// The elAdapter function is just a lookup on the registered adapters by tagName
elAdapter(el) {
  return someAdapterRegistry[el.tagName](el);
}

Benefits

  • Expose and standardize phx operations against html elements by coding to an interface
  • Should make phoenix element interactions more testable
  • Allow web components to better participate in the finer parts of PHX DOM management (has focus, merge attrs, phx private, etc).

Thoughts?

If there’s any interest in this then I’d be happy to do a proof of concept. I’d start by writing adapters for the native html elements and moving the current html inspection/interaction into the adapters.

If there’s no interest OR if there’s an obvious reason why this could never work, then let me know. I’ve poked around quite a lot in the existing JS and it seems possible to me. I’ve seen much of the record-keeping via phxPrivate, phx-has-focused, and so on and I don’t think any of that is a blocker.

Marked As Solved

Sebb

Sebb

I have given up on this.

reason: you can’t really use a webcomponets-component-library with LV right now.

Also Liked

chrismccord

chrismccord

Creator of Phoenix

We are currently exploring more direct WC integration in the form of a callback that well invoke for any custom element that implements the interface. Made up name at the moment, but imagine if any WC instance implemented liveViewMounted/1 and received the hook object to do whatever they needed like pushing events to the server, listening for server events, etc. You’re talking about lower level integration where the WC couples do our internal DOM patching stuff, but I’m not yet sure about that. We can start at the top level and work down if needed.

15
Post #3
Sebb

Sebb

Good point about Fast being build with blazor in mind could make it a better fit for LV.

I’m making good progress with wrapping https://ix.siemens.io/ for LV.
When I’m done I plan to post a guide how to integrate other webcomponent libraries into LV.

benkimpel

benkimpel

It’s a fair question, but it probably has as many answers as there are developers. For me, specifically, my team is very comfortable with Elixir/Phoenix/LV/etc. and not with JS/React. I’m quite comfortable in both, but if we can stay in Phoenix I’d rather do so for the sake of the team, maintenance, build pipelines, testing, etc. I’m not a JS/React hater by any means, but I do prefer Phoenix LV.

I think it can. It’s pretty close already. we should try at least! ;^D

I think that might actually be more likely if we were to start doing one-offs in react. What’s up for discussion now is still almost entirely server-side state. WCs have some internal state, but it is generally exposed through attributes which should be perfect for LV.

Where Next?

Popular in Proposals: Ideas Top

manhvu
In a large repo, working with module need to add alias too much is quite annoyed and not good for organizing code. I think better add su...
New
hst337
iex&gt; map = %{x: 1} iex&gt; map.x 1 iex&gt; map.x() 1 Why would anyone use the map.x() syntax for getting map value? I’d suggest depre...
New
nocksock
LiveView has spoiled me and now I would LOVE to be able to write controllers like this: defmodule DeadViewWeb.ItemController do use De...
New
jakeprem
Goal: To make JS.patch and JS.navigate more interoperable with JS.push. Scenario: Imagine making a reusable Phoenix component and you wa...
New
mortenlund
Hi! I would like to suggest a new callback in the lifecycle of the Live Component which is unmount. Sometimes it is nice to be able to ...
New
bartblast
This could resolve to {[a: 1, b: 2]}. Was it ever considered to allow such syntax? Notice this: {:abc, a: 1, b: 2} and this: my_fun(:abc,...
New
pejrich
I propose adding compact_map/2 to the Enum module. What is it? Sometimes you want to map over a collection, but sometimes you want to ma...
New
mxgrn
As one edits a Phoenix LiveView form in a modal, it’s very easy to accidentally press ‘Esc’ and lose all the edits. Similar to data-conf...
New
dimitarvp
To @jonatanklosko and @the-mikedavis: I see that there is a Rust crate at crates.io: Rust Package Registry but it is pointing at https:/...
New
eagle-head
Hi everyone, I’ve been researching Content Security Policy Level 3 support in Phoenix and wanted to share my findings and a proposal for...
New

Other popular topics Top

Nvim
Anybody knows a comprehensive comparison of Django and Phoenix, thanks for the help. Where are they similar? Where do they differ the m...
New
fireproofsocks
Forgive me if this is obvious, but how does one delete a database record WITHOUT selecting it first? Ecto.Repo — Ecto v3.14.0 has exampl...
New
Lily
In templates/appointment/index.html.eex: &lt;%= for appointment &lt;- @appointments do %&gt; &lt;tr&gt; &lt;td&gt;&lt;%= appoi...
New
hariharasudhan94
lets say i have a sample like a = 20; b = 10; if (a &gt; b) do {:ok, "a"} end if (a &lt; b) do {:ok, b} end if (a == b) do {:ok, "equa...
New
jay1
Why is it that the mnesia database isn’t the most preferred database for use in Elixir/Phoenix?
New
SoCreat
i’m a new one to elixir which editor can i use vs code? or atom? Thanks! :smiley:
New
sergio_101
I am VERY much an elixir newbie. I have taken one elixir course and one phoenix course on Udemy. During that course, I saw the instructor...
New
jason.o
In the code below, if the create action is not set to accept “extra_key” as an input, it errors out with a message shown above. Is there ...
New
Brian
What is the proper way to load a module from a file in to IEX? In the python world, doing something like this pretty standard: from ....
New
AstonJ
We’ve put together this wiki for Phoenix LiveView - please feel free to add any info you feel is worth including. What is Phoenix LiveV...
New

We're in Beta

About us Mission Statement