Focus management in Elm

1,020 views
Skip to first unread message

Irakli Gozalishvili

unread,
Jun 23, 2016, 2:34:56 PM6/23/16
to elm-dev
Hi,

As far as I can tell after talking to ppl on slack and after looking around, there is no library for focus management in Elm. I have being considering to write an effect manager with native bindings to do that, but would like raise the question here before going ahead with it. On of the challenges associated with it aport from needing a native code is how to communicate with native code which DOM element needs to get focused, most solutions I've seen so far use some sort of unique identifier and CSS selector to let JS know which element to focus. That's not ideal & pretty painful for my personal use case due to elements I need to focus being at the very different level of the component tree, so I ether need to thread through unique IDs across many layers, or use task to generate unique IDs which is also not ideal because then my `uuid` field would have to be of maybe type and that introduces extra ceremony not to mention that focusing element on init becomes impossible.

I think there might be a better options to go about it:

1. Back focus management into Virtual DOM layer. Say as a special `focus` attribute. This is also not ideal and his own quirks, like what if two different parts of UI claim to have focus simultaneously ? It's not possible in DOM but would be possible to express in Virtual DOM.

2. Introduce some notion of `refs` maybe something like `ref : model -> HTML.Attribute msg` that could be used in Virtual DOM for elements you may want to focus. That way instead of managing unique ids manually you'll be able to use `model` associated with a view itself.


I am also vey interested to know if Elm core team is working or even thinking about this problem before I end up building a solution that I'm even unable to publish.

P.S.: Also interested in input selection management that seems to have more or less same problems as focus management, although doing it at Virtual DOM layer via special attribute seems to not have same problems. 

Søren Debois

unread,
Jun 24, 2016, 8:36:14 AM6/24/16
to elm-dev
From an API-perspective, I like the idea of a special "focus" attribute (similarly for selected state, especially for input elements). E.g., 

   div
     
[]
     
[ button
         
[ onClick MyClickMsg
         
, focused
         
]
     
, ...
     
]
 

I also don't see a good way to avoid users accidentally specifying focus more than once, but I don't think that's a major disaster. I don't see any other immediate problems?

Richard Feldman

unread,
Jul 19, 2016, 7:22:02 PM7/19/16
to elm-dev
Would the proposed `ref` function be referentially transparent? It seems to me like it wouldn't be, but maybe I'm missing something.

Michael Bylstra

unread,
Jul 19, 2016, 10:14:16 PM7/19/16
to elm-dev
I've raised some questions here https://gist.github.com/rtfeldman/5f015adbdfbba541c7e7e1409b6efeef#gistcomment-1829693.

To continue the discussion, I think there are some (hopefully not completely unsolvable) problems with generating Cmds based on node refs in Elm:

  • you can only return a Cmd from an update function, but the update function (rightly so) has no knowledge of the view(s).
  • you are unable to respond to Msgs in view functions, and you are unable to return Cmds from a view function. The problem is that these imperative commands are a view related concern, not a model/update related one.
  • "firing a command" by way of an element attribute raises a lot of questions (as raised in the gist response)
  • You might be able to generate a ref based on a model, but models are independent of views. What if a model is represented in multiple views (eg: my name+photo block might appear in the top right menu bar as well as next to a comment). The basic Elm Architecture is Model-Update-View, but it doesn't dictate that you can't have multiple views of a model. How would you know which view should be focused?

Also, should this conversation be moved to elm-discuss?

Irakli Gozalishvili

unread,
Jul 20, 2016, 1:34:27 AM7/20/16
to elm-dev
On Tuesday, July 19, 2016 at 4:22:02 PM UTC-7, Richard Feldman wrote:
> Would the proposed `ref` function be referentially transparent? It seems to me like it wouldn't be, but maybe I'm missing something.

In terms of API it is referentially transparent. Now implementing such function is tricky though only options I can see are:

1. Using weak maps so `ref x` would either read value from map:
function ref(x) { return wm.has(x) ? wm.get(x) : wm.set(x, guid()) }
It is impure but referrentially transparent. That being said weak maps is probably no go.

2. Built object `guid` support it into Elm itself. In nutshell it's a same as with weak maps but using internal field instead:

guid : a -> string

function guid (x) {
if (x.__elm_guid == null) {
x.__elm_guid = ++nextGUID
}
return x.__elm_guid
}

Again impure but referrentially transparent, as long as nothing else can read __elm_guid field. But then I am not sure if this approach is acceptable.

3. Use some sort of object hashing algorithm. But that seems too expensive to be practical.

To be clear I'm not married about the proposed API. I just think there needs to be a way to referernce a DOM element that corresponds to specific `Html msg` element that wont require manual approach like css selectors.

Maybe Html.Ref similar to Html.Keyed is a better direction, but I have not managed to come up with a better API myself

Irakli Gozalishvili

unread,
Jul 20, 2016, 5:28:07 PM7/20/16
to elm-dev


On Tuesday, July 19, 2016 at 7:14:16 PM UTC-7, Michael Bylstra wrote:

 

To continue the discussion, I think there are some (hopefully not completely unsolvable) problems with generating Cmds based on node refs in Elm:

  • you can only return a Cmd from an update function, but the update function (rightly so) has no knowledge of the view(s).
  • you are unable to respond to Msgs in view functions, and you are unable to return Cmds from a view function. The problem is that these imperative commands are a view related concern, not a model/update related one.
  • "firing a command" by way of an element attribute raises a lot of questions (as raised in the gist response)
  • You might be able to generate a ref based on a model, but models are independent of views. What if a model is represented in multiple views (eg: my name+photo block might appear in the top right menu bar as well as next to a comment). The basic Elm Architecture is Model-Update-View, but it doesn't dictate that you can't have multiple views of a model. How would you know which view should be focused?

That is a valid concern. I wish I had a good answer for it but all I have is this:

Say you have a `model` that is viewed as a `tab` and a `content`  for it. Then using `ref model` on both views will be problematic due to conflict. What you could do instead (which is what I've being doing) is allocate a "handle" which is part of you `model`, you can think of it as an ID. In other words in such scenario you can use `ref model.tab` and `ref model.content` to refer to views instead of  just using `ref model`. Arguably you'd want to have dedicated field on your model to be used with `ref` anyhow as `model` is likely will be changing and there for `ref model` will return different value even though identity will be the same. But if you allocate a field on the model only on `init` and never update it, you got yourself an identity which can be used in conjunction with a `ref`.
 

Also, should this conversation be moved to elm-discuss?

The reason I started it here was because if this were to be implemented it would require some changes to Elm itself, but I'll leave this call up to Richard.

Richard Feldman

unread,
Jul 20, 2016, 5:37:27 PM7/20/16
to elm-dev
That is a valid concern. I wish I had a good answer for it but all I have is this:

Say you have a `model` that is viewed as a `tab` and a `content`  for it. Then using `ref model` on both views will be problematic due to conflict. What you could do instead (which is what I've being doing) is allocate a "handle" which is part of you `model`, you can think of it as an ID. In other words in such scenario you can use `ref model.tab` and `ref model.content` to refer to views instead of  just using `ref model`. Arguably you'd want to have dedicated field on your model to be used with `ref` anyhow as `model` is likely will be changing and there for `ref model` will return different value even though identity will be the same. But if you allocate a field on the model only on `init` and never update it, you got yourself an identity which can be used in conjunction with a `ref`.
 
Yeah, the thing is once you're generating globally unique identifiers, you're one id attribute away from querySelector being a more flexible version of the same thing. (If you want nicer type-checking around that, you can always write a wrapper around querySelector and/or id that only accepts your GUIDs so you'll get compiler errors if you accidentally try to use something invalid.)

Also, should this conversation be moved to elm-discuss?

The reason I started it here was because if this were to be implemented it would require some changes to Elm itself, but I'll leave this call up to Richard.

Thanks for asking! Good instinct. I do think this particular discussion makes sense for elm-dev though. :) 

Irakli Gozalishvili

unread,
Jul 20, 2016, 5:50:20 PM7/20/16
to elm-dev
 
Yeah, the thing is once you're generating globally unique identifiers, you're one id attribute away from querySelector being a more flexible version of the same thing.

Sorry I fail to understand how is `querySelector` more flexible version of the same thing ? What I would like to achieve with this is to let Elm platform do an identifier generation so it will be guaranteed to be unique and there for be free of collision hazards. Also worth pointing out that Elm already generates unique identifiers for somewhat similar reasons:
https://github.com/elm-lang/core/search?utf8=✓&q=guid&type=Code

 
(If you want nicer type-checking around that, you can always write a wrapper around querySelector and/or id that only accepts your GUIDs so you'll get compiler errors if you accidentally try to use something invalid.)

If there is a way to avoid naming conflicts so that manual name-spacing concerns  can be resolved, I'm all up for it. But personally I do not see how that can be done without lazy accessor for central ID generator. If you could illustrate solution I'd be very keen on seeing it.

Irakli Gozalishvili

unread,
Jul 20, 2016, 6:09:12 PM7/20/16
to elm-dev
Ok to be more concrete I guess this is more or less what I had in mind:

This would allow `Ref.guid` to be used with any record and there for record identity used as an identifier. It is referentially transparent as same record will always return same id & it also leverages Elm runtime to avoid ID coordination & namespacing concerns associated with manual ID management.

It does not solve problem of what if I use same identifier for two unrelated things, but that is logic error and nothing else. This drafts also walks away from `ref : model -> Html.Attribute a` API proposed initially as that can be build on top or `Ref.guid thing` can be used as an actual element id. What's important IMO is to have a way to identify things somehow without manually assigning unique identifiers to them.

That being said it could be that introducing identity to Elm is not desired. For example should `Ref.guid { x: 1 }` in module `Foo` and `Ref.guid { x: 1 }` in module `Bar` return same value ?

Irakli Gozalishvili

unread,
Jul 20, 2016, 6:46:43 PM7/20/16
to elm-dev
To be more clear this is what I really hope we could have in Elm - Way to control elements focus / scroll / selection same as we control component we embed.


Idea is that `Focus` model will do all the focus management for you if you just delegate messages coming from it to it. Or you could intercept those messages with your own custom logic. Or if you want to just every now end then focus an element and let `Focus` module deal with staying in sync with user interactions that also becomes trivial you just send `Focus` / `Blur` actions to the `Focus.update`. Again this is an Elm way to embed children and control / uncontrol them and it makes total sense to do so for focus / selection / scroll etc. To me `Ref` is just a foundation that would allow that, but that's implementation detail I care the least in the grand scheme of things.

Alternative would be to let `Focus` module perform a task on `init` to generate a unique `id` for it. The problem with that approach is though what if happens with focus / blur commands in the meantime ? Do you queue them up and run once you have an ID ? That seems like an overkill, which is why I prefer `Ref` based solution, but it also could be that it's just fine.

Hope this provides more context helps here.



On Thursday, June 23, 2016 at 11:34:56 AM UTC-7, Irakli Gozalishvili wrote:

Richard Feldman

unread,
Jul 20, 2016, 7:57:32 PM7/20/16
to elm-dev
That being said it could be that introducing identity to Elm is not desired. For example should `Ref.guid { x: 1 }` in module `Foo` and `Ref.guid { x: 1 }` in module `Bar` return same value ?

I think it is undesirable, yeah.

Alternative would be to let `Focus` module perform a task on `init` to generate a unique `id` for it. The problem with that approach is though what if happens with focus / blur commands in the meantime ? Do you queue them up and run once you have an ID ? That seems like an overkill, which is why I prefer `Ref` based solution, but it also could be that it's just fine.

Or to have it accept a hashing function allowing it to generate one from a `model`, yeah?

That's what I mean about it being decoupled - you can implement https://gist.github.com/Gozala/7f1e3f63f5711756a4d159c13b96cffd using `querySelector` (and maybe if you don't want to use `id`, a custom `attribute` like `data-ref-value` along with a `querySelector` that looks for that attribute) if that's the API you want to interact with.

Irakli Gozalishvili

unread,
Jul 20, 2016, 8:24:56 PM7/20/16
to elm-dev


On Wednesday, July 20, 2016 at 4:57:32 PM UTC-7, Richard Feldman wrote:
That being said it could be that introducing identity to Elm is not desired. For example should `Ref.guid { x: 1 }` in module `Foo` and `Ref.guid { x: 1 }` in module `Bar` return same value ?

I think it is undesirable, yeah.

Fare enough.
 

Alternative would be to let `Focus` module perform a task on `init` to generate a unique `id` for it. The problem with that approach is though what if happens with focus / blur commands in the meantime ? Do you queue them up and run once you have an ID ? That seems like an overkill, which is why I prefer `Ref` based solution, but it also could be that it's just fine.

Or to have it accept a hashing function allowing it to generate one from a `model`, yeah?

Hashing function isn't a good option actually, because it is equality based and there for `model.focus.ref` will return same hash across all the models that embed `Focus`.
 

That's what I mean about it being decoupled - you can implement https://gist.github.com/Gozala/7f1e3f63f5711756a4d159c13b96cffd using `querySelector` (and maybe if you don't want to use `id`, a custom `attribute` like `data-ref-value` along with a `querySelector` that looks for that attribute) if that's the API you want to interact with.

I am afraid I'll repeat myself here. The only reason I dislike `querySelector` approach is because requires one to employ name-spacing convention to work correctly. I have being using querySelector based approach with namespacing conventions and it was painful enough that we're migrating to less error prone approach where following convention isn't required. My over experience with Elm has being that provided solutions make mistakes really difficult and I fear that reliance on using unique querySelectors will make mistakes easy and harm modularity.

Irakli Gozalishvili

unread,
Aug 2, 2016, 2:09:53 PM8/2/16
to elm-dev
I just posted this comment Dom.elm gist https://gist.github.com/rtfeldman/5f015adbdfbba541c7e7e1409b6efeef#gistcomment-1840433 but I realize now that it probably belonged here instead. So I'm reposting it here as well:

I have being thinking more about the problem of CSS selectors as poor way to reference Elements in the DOM and Declarative VS Task based approach for updating certain DOM properties. And I had a thought that there could be a way to solve both problems in a neat way. What if something along this lines was added to elm-html:

taskAttribute : (DOMElement -> Task Never msg) -> Html.Attribute msg

focus : DOMElement -> Task SelectorError msg
blur : DOMElement -> Task SelectorError msg
scrollLeft : Float -> DOMElement -> Task SelectorError Float

That way those task could be turned into attributes and elm-html can take care of running them with an appropriate DOM elements. Better yet it could actually recognize such tasks and run them in the same tick to avoid races we've talked about.

It not fully fleshed out idea, but I though I'd share it anyway to get other minds think about it.


To be totally clear I don not suggest that Elm should expose raw DOMElement to Elm programs. It most definitely should not, but there could be a type like DOMElement (or Ref or whatever you want to call it) that is never given to an Elm program but can be used to connect different native APIs in this case connecting elm-html diff / patch process with with tasks like focus / blur that also likely will be implemented in native.

Richard Feldman

unread,
Aug 2, 2016, 11:51:22 PM8/2/16
to elm-dev
So https://github.com/evancz/elm-todomvc currently sets focus through a port using a Cmd/querySelector interface.

How would that code look if it used this API?

Irakli Gozalishvili

unread,
Aug 3, 2016, 2:45:15 PM8/3/16
to elm-dev


On Tuesday, August 2, 2016 at 8:51:22 PM UTC-7, Richard Feldman wrote:
So https://github.com/evancz/elm-todomvc currently sets focus through a port using a Cmd/querySelector interface.

How would that code look if it used this API?

Here is the commit with changes illustrating how all that may look like. Please note that I have also slightly updated proposed API as after more thinking I came to conclusion that users should not really deal with errors associated with DOM manipulation same as they don't deal with them currently if updating attribute or property change fails. In fact only errors from proposed task API was related to selectors anyhow. As of task success it's also irrelevant and in fact DOM already produces errors when you change focus / blur so no point in that.

With all this in mind I think this is more or less how refined idea looks like:

-- Setter here is somewhat similar to hooks from VirtualDOM library in that
-- that they are passed value & corresponding HTMLElement and they return
-- task which presumably does something to that HTMLElement.
-- https://github.com/Matt-Esch/virtual-dom/blob/master/docs/hooks.md#hooks
-- Note that task can not error neither it's return anything, it's somewhat
-- similar to HTML library itself, if it fails to reflect view onto DOM that
-- is it's own problem not a consumers.
type alias Setter value = value -> HTMLElement -> Task Never ()
-- Setting just takes setter and value it supposed to set on an HTML element
-- that contains it.
setting : Setter value -> value -> Attribute msg
-- Most users should not care about settings or setters as Html library will provide
-- settings to cover the web platform.
focused : boolean -> Attribute msg
focused = setting setElementFocus
-- Finally focusSetter is somethig that will have native implementation.
focusSetter : boolean -> HTMLElement -> Task Never ()
-- Which will likely be something along these lines:
focusSetter = function(value, element) {
var task = _elm_lang$core$Native_Scheduler.nativeBinding(function run(callback) {
if (value) {
element.focus();
// If element did not get focused, it is because this is initial render and
// in such task is run before element are in the document tree and there for
// calls to`.focus()` has no effect. In such case we just reschedule the task
if (element.ownerDocument.activeElement !== element) {
_elm_lang$core$Native_Scheduler.spawn(task)
}
} else {
element.blur()
}
callback(succeed(_elm_lang$core$Native_Utils.Tuple0))
})
return task

Evan Czaplicki

unread,
Aug 3, 2016, 4:54:37 PM8/3/16
to elm-dev
The following API has two problems:
taskAttribute : (DOMElement -> Task Never msg) -> Html.Attribute msg
First, this means you can only set focus in response to user events. Never based on servers or timers or anything else.

Second, that is not actually true because this allows you to get DOMElement values anywhere in a program. You can say (taskAttribute Task.succeed) and suddenly your message is a DOMElement. You can hold on to it for a million years. Do whatever you want with it. All while virtual DOM stuff is changing. Maybe that node does not even exist anymore.

-------------------

Here's how I look at this whole thing. The DOM is not pure. Just like a server is not pure. We send it data. Complex data sometimes, but that does not guard us from the fact that it is managing state of its own. And because there are two versions of the same state, it can get out of sync. That's just a fact about duplicating state.

In the case of servers, we acknowledge that it's not pure and send tasks. We don't want to refer to values on the server directly because those values may change in silly ways. We want their bad ideas to stay over there.

I personally do not like the query selector idea because you can't cache it easily. Getting things by ID, I think I could cache that in the virtual DOM implementation. I don't know how fast or slow getElementById or getQuerySelector are, but I think it's worth considering that getting the answer to a query a ton of times definitely costs more than getting IDs. I'd be curious to get more info on this aspect.



--
You received this message because you are subscribed to the Google Groups "elm-dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-dev+u...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/elm-dev/a68f4809-418b-4a3d-95b7-a94dba1b91b2%40googlegroups.com.

For more options, visit https://groups.google.com/d/optout.

Evan Czaplicki

unread,
Aug 3, 2016, 4:56:28 PM8/3/16
to elm-dev
Also, I think I said things in a potentially misleading way. It's not that I don't like the use of query selectors. It is more accurate to say, when considering that design (which is my favorite so far) I have had concerns about performance and optimization. It would be good to look into those.

Brad Grzesiak

unread,
Aug 3, 2016, 5:08:33 PM8/3/16
to elm...@googlegroups.com
If and only if the purpose is to set/change focus, should we care much about high performance over, say, developer happiness?




For more options, visit https://groups.google.com/d/optout.


Brad Grzesiak
CEO, Bendyworks
mobile: 608-347-5689
office: 608-230-6788
106 E. Doty St, Suite 200
Madison, Wisconsin 53703
http://bendyworks.com/

Irakli Gozalishvili

unread,
Aug 3, 2016, 5:17:28 PM8/3/16
to elm-dev


On Wednesday, August 3, 2016 at 1:54:37 PM UTC-7, Evan Czaplicki wrote:
The following API has two problems:
taskAttribute : (DOMElement -> Task Never msg) -> Html.Attribute msg
First, this means you can only set focus in response to user events. Never based on servers or timers or anything else.

I don't believe that is true. Or maybe I misunderstand your point. Presumably server response, timer and other things will send message and your update can change a state change, like `isFocused = true` and view will just reflect that.

 

Second, that is not actually true because this allows you to get DOMElement values anywhere in a program. You can say (taskAttribute Task.succeed) and suddenly your message is a DOMElement. You can hold on to it for a million years. Do whatever you want with it. All while virtual DOM stuff is changing. Maybe that node does not even exist anymore.


Fare enough! But again DOMElement does not necessarily needs to be actual DOM element just a handle that could only be unboxed internally by Elm. Here is a simple illustration of the technique:

var neverExposedInternalUnboxKey = {}
function box(thing) {
 
return function unbox(key) {
   
if (key === neverExposedInternalUnboxKey) {
     
return thing
   
}
 
}
}

Point I'm trying to make you could pass references to the raw DOM Elements such that Elm programs won't be able to get hold of actual DOM Element. You could also make it so that GC won't be an issue.

 

-------------------

Here's how I look at this whole thing. The DOM is not pure. Just like a server is not pure. We send it data. Complex data sometimes, but that does not guard us from the fact that it is managing state of its own. And because there are two versions of the same state, it can get out of sync. That's just a fact about duplicating state.

In the case of servers, we acknowledge that it's not pure and send tasks. We don't want to refer to values on the server directly because those values may change in silly ways. We want their bad ideas to stay over there.


Not sure I completely follow this. You do already have a way to refer to them via virtual dom, it's just handful of cases are not covered by virtual-dom so task based approach is proposed as an alternative. What I'm trying to say there is no real difference between `input.value` or other HTMLElement properties and `input.focus` other than inconsistent underlaying DOM interface so how do we draw the line where it's better to use tasks and where it's bettor to use virtual dom abstraction ?
 

I personally do not like the query selector idea because you can't cache it easily. Getting things by ID, I think I could cache that in the virtual DOM implementation. I don't know how fast or slow getElementById or getQuerySelector are, but I think it's worth considering that getting the answer to a query a ton of times definitely costs more than getting IDs. I'd be curious to get more info on this aspect.


At the end of the day `focused : Bool -> Attribute msg` can also be implemented at the virtual dom level without exposing any of the proposed task approach.

Irakli Gozalishvili

unread,
Aug 3, 2016, 6:02:06 PM8/3/16
to elm-dev
Slight off topic - I think this discussion illustrates some course correction in Elm architecture. Pleas don't get annoyed with me, in fact feel free to stop reading here as it's just some impressions of an observer.

I've being struggling since (Model, Effects msg) became a pattern, mostly because of asymmetric interface. But apart from that it just felt odd that some IO (that dealt with HTML) was scheduled declaratively via `view` function. And other IO end up more imperative that manually had to be scheduled as an Effect. I even attempted to propose alternative similar to `view` that would schedule effects in separate step:  `fx : Model -> Task Msg Msg`.

Now I get an impression that explicit IO scheduling becomes preferred pattern over declarative approach. In wonder if manual redraw scheduling is more inline with that direction. Maybe there should be a `draw : Html msg -> Task Never msg` for scheduling DOM updates from update instead of current `view` functions ?



On Thursday, June 23, 2016 at 11:34:56 AM UTC-7, Irakli Gozalishvili wrote:

Richard Feldman

unread,
Aug 3, 2016, 6:02:27 PM8/3/16
to elm-dev
querySelector vs getElementById brings up some interesting API scope questions. I thought through this and came to a surprising conclusion.

Two use cases come to mind:

1. Third-party jQuery Date Picker

At work we have a jQuery date picker that we interop with using ports. The date picker creates text inputs that do not have IDs.

Today if I want to focus that text field, I have to use a port. With querySelector, I could focus it using Elm code. With getElementById, I could not.

This is arguably a vote in favor of querySelector, as it means I can write more Elm code and less JS code. More Elm code is a good thing!

However it is also arguably a vote for getElementById. It's pretty easy to write a selector that works today but breaks in the future. I already think of my JS interop code as "the danger zone" part of my code base, where I need to be extra careful because things aren't as reliable.

Arguably in the cases where querySelector works and getElementById does not work, they are cases where things are in fact more brittle, and writing port code is appropriate because that code belongs in "the danger zone."

2. Third-party Elm Date Picker

Suppose someone writes a date picker and publishes it on elm-package. It has a text input, and does not expose a way to focus it.

Here the distinction is more stark. If I only have getElementById, I cannot focus that input area. I am writing Elm code, using someone else's Elm code, and I have no way to focus their text area. That sucks, right?

However, people publish libraries that don't support things all the time. The usual answer is "if that's a good idea, they should expose a way to do it." And I agree! The library author should have exposed it.

Here's where things get interesting. If getElementById is the only option, how could that library author possibly expose an API for focusing the date picker? What if the date picker is used in multiple places on the page? They'll have the same IDs unless somehow the caller passes in a unique identifier!

What's interesting is that if querySelector is available, this is still true - it's just less obvious.

Suppose I'm writing a date picker that can be used multiple places on the page, and I want to expose a function that focuses the date picker's input. I write DatePicker.focus : Task x () and implement it as focus ".awesome-date-picker". This works fine when I test it on my single example date picker, but then as soon as an end user tries using it multiple times on a single page, it breaks. Not only that, but it breaks in a way where I have to make a MAJOR breaking API change to fix it.

The reality is that if people want to publish something that supports focus, it needs to expose an API that works like Html.Keyed - where the caller provides a unique identifier to associate with a particular instance. The only sane API to expose would be DatePicker.focus : String -> Task x () - and what the function would do is prepend whatever library-specific namespace prefix it also prepended to the id attribute it used in the view (e.g. "awesome-date-picker-" ++ key) to avoid global collisions in the page's id namespace.

If someone writes a date dicker library that does not expose this, then if I write a querySelector to work around their decision not to expose it, I am engaging in an unreliable hack. What if the author changes their class structure in a PATCH version bump, and now my code suddenly breaks? querySelector makes it easy for miscommunications to happen between library author and consumer, whereas getElementById forces you to take the right things into account in order to use it.

Richard Feldman

unread,
Aug 3, 2016, 6:06:46 PM8/3/16
to elm-dev
The tl;dr of my revious post:

  • getElementId seems to be a better API choice than querySelector, even if they have identical performance.
  • This is because querySelector encourages writing unreliable hacks in Elm code, whereas "the right thing" if you want reliability is always to use getElementById.
  • It is a feature for unreliable hacks to have to live on the other side of ports.

Irakli Gozalishvili

unread,
Aug 3, 2016, 6:16:46 PM8/3/16
to elm-dev

+1 on ID over general selector as that reduces surface for errors.

Worth noting that unlike with Html.Keyed where unique key is required only among siblings and there for contained with in the module, here every ancestor needs to pass down it's unique key to the children to ensure absolute uniqueness.
Reply all
Reply to author
Forward
0 new messages