uosis / laminar-web-components

Web Component definitions for Laminar
MIT License
36 stars 8 forks source link

onFocusMount #6

Open mathieuleclaire opened 3 years ago

mathieuleclaire commented 3 years ago

Hi, I am tryiing to reproduce Laminar Hello World example, but the onMountFocus raises a js exception. Is it an exepected behaviour ?

val component = div(
      Textfield(
        _ => onMountFocus,
        _ => value <-- actionVar.signal,
        _ => inContext { thisNode => onInput.mapTo(thisNode.ref.value) --> actionVar },
        _.label := "Name",
        _.outlined := true,
        _.placeholder := "Name"
      ),
      span(
        "Hello, ",
        child.text <-- actionVar.signal.map(_.toUpperCase)
      )
    )

raises the js exception:

ObserverError: TypeError: this.formElement is null

Thanks, and congrats for this work !

raquo commented 3 years ago

Is this "Textfield" of yours a web component?

Check its docs for what it wants about forms, maybe it needs to be put inside a form element or you need to provide some custom attribute linking to the form or something.

If the error really is related to onMountFocus, make sure your web component produces an input element and not some div wrapper.

--Nikita

On Wed., Nov. 25, 2020, 1:35 p.m. Mathieu, notifications@github.com wrote:

Hi, I am tryiing to reproduce Laminar Hello World example https://laminar.dev/examples/hello-world, but the onMountFocus raises a js exception. Is it an exepected behaviour ?

val component = div( Textfield( => onMountFocus, => value <-- actionVar.signal, => inContext { thisNode => onInput.mapTo(thisNode.ref.value) --> actionVar }, .label := "Name", .outlined := true, .placeholder := "Name" ), span( "Hello, ", child.text <-- actionVar.signal.map(_.toUpperCase) ) )

raises the js exception:

ObserverError: TypeError: this.formElement is null

Thanks, and congrats for this work !

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/uosis/laminar-web-components/issues/6, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAECBMAHEJPX6G5DVECIUITSRV2APANCNFSM4UC5OB3A .

mathieuleclaire commented 3 years ago

It is the TextField wrapped in this library: https://github.com/uosis/laminar-web-components/blob/master/material/src/main/scala/material.scala#L3340 The debugger indicates that an input is produced:

<input aria-labelledby="label" class="mdc-text-field__input" type="text" placeholder="Name">
raquo commented 3 years ago

Laminar's onMountFocus correctly calls customElement.focus() after the element is mounted. Specifically, I think that would be this method for this web component: https://github.com/material-components/material-components-web-components/blob/4a5d4eebd1ecf1c54515f7b42c7ec882d4d83470/packages/textfield/mwc-textfield-base.ts#L234

Why this web component's formElement is null at this point, I don't know. Maybe it needs more time to initialize after being mounted or something. I don't know much about how web components are implemented.

mathieuleclaire commented 3 years ago

You're right, a 1ms delay does the trick.

_ => onMountCallback(ctx => {
          js.timers.setTimeout(1) {
            ctx.thisNode.ref.focus()
          }
        })

As, it seems to be a recurent issue (like Material UI and Progresse bar here: https://laminar.dev/examples/web-components), what do you think of proposing timer wrappers for all your onMount methods ? The onMountFocus would become:

val onMountFocus: Modifier[HtmlElement] = onMountFocusAfter(0)

def onMountFocusAfter(delay: Int = 0): Modifier[HtmlElement] = onMountCallback(ctx => {
      js.timers.setTimeout(delay) {
        ctx.thisNode.ref.focus()
      }
    })

Or to set a timeout(1) directly in your mount methods, whatever the component used.

raquo commented 3 years ago

You should probably wrap the focus() call into if (ChildNode.isNodeMounted(ctx.thisNode)) to mitigate the edge case when the component will be unmounted before it has a chance to focus itself. Stuff like this is why I don't like async APIs.

If I were to add these async helpers into Laminar, I'd need a good reason – it's one thing if web components can't possibly be implemented synchronously due to some web platform constraints (and if that's the case, I would need to know what exactly those constraints are so that any potential Laminar helpers would solve the problem in all cases), but if it's just this particular set of web components that are internally poorly implemented, then I'm less inclined to develop ad-hoc helpers for that.

uosis commented 3 years ago

Yeah web components are asynchronous by design - there is WebComponentsReady event, similar to DOMReady, that you need to wait for before you can call methods on web components. See here and here for examples.