Looking for people experienced with elm-html's "key" function

1,275 views
Skip to first unread message

Evan Czaplicki

unread,
Feb 29, 2016, 1:38:56 PM2/29/16
to elm-d...@googlegroups.com
You use Html.Attribute.key when you have a sequence of items where you may be inserting and removing from the middle. I took this API pretty much directly from the virtual-dom implementation that backs the currently released version of elm-html. I am doing a rewrite for various reasons, and realized that the current API clashes with the laziness optimizations.


Problem

If you have a big list, you’d likely want to make each entry lazy. It would suck to have to build 1000 virtual nodes. If you want keys, you need to add them as an attribute, but lazy takes no attributes! This means you need to add an extra DOM node with a key where it has one child that’s lazy.


Alternative

I am considering getting rid of Html.Attribute.key entirely and replacing it with this function:

lazyDict
  : String
  -> List Attribute
  -> (a -> Html)
  -> Dict comparable a
  -> Html
lazyDict tagName attributes viewItem items = ...

It is somewhat similar to the normal Html.node function in that it is just about creating virtual DOM nodes, but in this case you give a Dict instead of a List. You might create a news feed like this:

viewNewsFeed : Dict Time Story -> Html
viewNewsFeed stories =
  lazyDict "div" [class "news-feed"] viewStory stories

Each entry would also be done lazily, so you only need to create the Html if the dictionary entries are not reference equal.

This means it would be easy to treat the stories as a dictionary in your model. When you want to add something, you add it with a timestamp. If it's sorted by name, you have a (Dict String Person) or whatever.


Question

If you have personally used keys for a specific scenario, would this new API cover things as well? Would it be better or worse? Can you elaborate on the details of your case?

Note: I am looking for specific experience. If you don't have a concrete example, you are off-topic. Take it to another thread. If you are reading this and the thread is long, feel free just to respond to my initial question. The goal here is to gather data, not opinions.

Ossi Hanhinen

unread,
Feb 29, 2016, 2:50:38 PM2/29/16
to Elm Discuss
Considering you can't really sort a Dict with anything besides its keys, I find the alternative a bit problematic.

Our scenario was this: the long list was somewhere around 50 content blocks, to and from which the user could drag articles. These content blocks could also be dragged up and down the list to rearrange the contents. 

Each content block had a key, and the articles inside were lazy. In our case, we used the database ID for the content block's key. Obviously it would be guaranteed to be unique in the list, but the thing is that the database ID had nothing to do with the order of the blocks in the list. In order to make the "manual order" work with the proposed alternative, we would need to generate the Dict on-the-fly based on a List using something like contentBlocks |> List.indexedMap (,) |> Dict.fromList. This, on the other hand would break the keying and re-introduce the unnecessary diffing the whole thing was trying to solve. Or am I missing something obvious here?

Evan Czaplicki

unread,
Feb 29, 2016, 4:52:12 PM2/29/16
to elm-d...@googlegroups.com
Ossi, that's a nice example: list of 50 items with drag to reorder.

So I agree that dictionary sucks for that. For the sake of keeping the initial post simple, I left out the idea of having lazySet and lazyZipList for other cases. This sounds like a case where zip-lists would be the ideal representation, so imagine a function like this:

lazyZipList
  : String
  -> List Attribute
  -> (a -> Html)
  -> ZipList a
  -> Html
lazyZipList tagName attributes viewItem items = ...

This would mean all the logic about moving items in the list around would be more like constant time. So I guess the questions I'd ask back are:
  • Ignoring the view, would a zip-list be a good representation for the dragging logic?
  • Can you describe how the dragging logic worked? Like, how did you know when to do a swap?
So my response is basically that there are a probably a handful of data structures that cover these cases, and I need help figuring out exactly which data structures those might be.

P.S. Another possible function is something like this:

lazyList
  : String
  -> List Attribute
  -> (a -> String)
  -> (a -> Html)
  -> List a
  -> Html
lazyList tagName attributes itemToKey viewItem items = ...

Basically build the keying into the parent rather than having it be the responsibility of each individual item.


--
You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Richard Feldman

unread,
Feb 29, 2016, 6:09:40 PM2/29/16
to Elm Discuss
Dreamwriter's Open menu uses key.

Some interesting things about how I'm using it in this particular case:
  1. I currently sort the docs by last modified timestamp and then render them in that order.
  2. To maintain this sorting, I'd want to incorporate last modified timestamp into each key within the Dict.
  3. Since last modified timestamp isn't necessarily unique per document, I'd also need to incorporate the document's id into each key.
  4. Presumably the best way to do this would be to represent these things as a Dict ( Date, DocId ) Doc
  5. I think my update functions would be happiest if I stored docs in the model as a Dict DocId Doc instead, since then I could avoid doing stuff along the lines of findById when I only care about an individual entry.
  6. Basically the only reason I'm not storing them as a Dict DocId Doc right now is performance concerns.
Based on the above, I wonder if it would be useful to decouple sorting from keys.

Like maybe lazySortedDict (but with a better name) could accept a sorting function, and I could take the sorting function I'm currently passing to List.sortBy and pass it to lazySortedDict instead. That would collapse two function calls into one, and would result in better performance and a more nicely-organized Model.

Overall I really like the idea of replacing key with something like this.

Ossi Hanhinen

unread,
Mar 1, 2016, 3:52:44 PM3/1/16
to Elm Discuss
Basically build the keying into the parent rather than having it be the responsibility of each individual item.

Sure, in principle this is something I can definitely get behind. I think in our case all the keyed things essentially use an equivalent of .databaseId as the extracting function. 

Ignoring the view, would a zip-list be a good representation for the dragging logic?

I suppose so. This is the first time I come across the data structure but it does seem to fit the bill.
 
Can you describe how the dragging logic worked? Like, how did you know when to do a swap?
 
I am not entirely sure what you are asking me, but let me try to explain all the same. So I wrote this little JS script to handle the tricky HTML5 DnD thing. For each event, it would send something like 
{eventType : String, draggedElement : Id, fromElement : Id, toElement : Maybe Id} 
through a port to the Elm app. Now this encodes all the information we need to figure out what is being dragged, from where it came and where it is going to. This was used to update the DragState, as we called it. On a drop event we would create an action for trying to move. Then we would simply use the latest DragState and determine if it was actually possible to move the dragged element to where the user was pointing. If yes, set off a Task for that to happen (in the backend), if no, just fail and set the DragState to Nothing. 

Irakli Gozalishvili

unread,
May 3, 2016, 12:22:13 PM5/3/16
to Elm Discuss
Evan I think you're missing one crucial detail about key attribute. Indeed it's primary use is to optimize list of items updates, but there is more. If you have an element who's children may change in response to a state change (for example you have an input field and only when something is typed clear button is revealed). Unless revealed child is added to the end of the child list you'll end up recreating all children that follows it, which is problematic if those children include certain elements like iframe or video / audio elements.

That being said you could likely avoid problems by just hiding element that get revealed on updates or employ Dict / ZipList or other data structure for representing children. My only worry is that problem may not be apparent neither solution is obvious. 

Mark Hamburg

unread,
May 3, 2016, 10:46:01 PM5/3/16
to Elm Discuss
I have used logic very much like key in other systems. In particular, when viewing lists of photos, these will often be sorted by a variety of criteria but will all have unique database ids that get used as keys during updates. The keys then allow items to be automatically animated by the OS on insertion or removal and on changes in layout — e.g., an updated photo gets cropped and becomes narrower.

Within Elm, I've been looking at using key to force the creation of a DOM node so that a CSS animation can run thereby triggering some JavaScript code to run and decorate the DOM node for dropzone.js. This is grotesque all the way through — particularly using a CSS animation as a way to detect node creation — but there isn't even really an option to do this in Elm since as far as I can tell one can't write a binary uploader in Elm. (I'm being less than fully assertive about having done this and it working because it's my planned replacement for our current approach which involves the hack of making the DOM node go away and then reappear.)

As for the proposed replacements, in the first case the lazyList ought to work and I suspect that lazyZipList would work but I need to understand it better. In the second case, almost anything would work as long as the replacement of a dropzone for one key by a dropzone for another key could trigger creating a new DOM node. (I'd really like to just replace the latter code with an Elm-based uploader, but it's probably still a useful hack for cases where there exists a JavaScript component that already does exactly what one wants.)

Mark

Mark Hamburg

unread,
May 5, 2016, 3:05:27 PM5/5/16
to Elm Discuss
On Tuesday, May 3, 2016 at 7:46:01 PM UTC-7, Mark Hamburg wrote:
Within Elm, I've been looking at using key to force the creation of a DOM node so that a CSS animation can run thereby triggering some JavaScript code to run and decorate the DOM node for dropzone.js. This is grotesque all the way through — particularly using a CSS animation as a way to detect node creation — but there isn't even really an option to do this in Elm since as far as I can tell one can't write a binary uploader in Elm. (I'm being less than fully assertive about having done this and it working because it's my planned replacement for our current approach which involves the hack of making the DOM node go away and then reappear.)

Just implemented this and though it was grotesque to use CSS animations to detect DOM node creation,  it simplified the communication between Elm and the JavaScript logic, so I count it as a win. I realized as I was implementing it, however, that I don't need the full matching power of the key attribute. I just need a way to say "even though these nodes have the same type, don't let them match". That seems easier to implement within the virtual DOM logic. For accessing system animations for list updates, I would need more but for this simple case, I just need a way to inhibit spurious matches.

The list updates case could actually get most of the way there with simply a way to inhibit spurious matches. Run an LCS algorithm (e.g., http://www.xmailserver.org/diff2.pdf) to decide which nodes correspond and then everything else is an insertion or a deletion. This means that keys can't drive re-arrangement animations but I at least could live without those.

Mark

Richard Lupton

unread,
May 12, 2016, 12:05:34 PM5/12/16
to Elm Discuss
Here is a simple example of when "key" is needed when using CSS transitions:

https://gist.github.com/ricklupton/3992f45742c4bd8dc5b097c24b0feb18

There is a list of 3 circles with CSS transitions. When you drop the first 1 from the list, you would like it to disappear and the remaining 2 circles to stay where they are. In fact, the first DOM node becomes the second circle, the second DOM node becomes the third, and everything jumps around. As I understand it, this is exactly the intended use for "key".

At the moment, it doesn't seem to be possible to do this in Elm 0.17. Am I missing something? I don't see how the introduction of lazyDict & co would help in this case either.

(obviously in this very simple example, a solution would be to remove the CSS transition -- but that's not the point).

Thanks,
Rick

Michael Niessner

unread,
May 12, 2016, 6:59:37 PM5/12/16
to Elm Discuss
We have an app that displays a list of items for the user to process. Each item has a globally unique id. 

The list is unstable and changes due to the activity of other user’s in the system.

A notification from the server may include any or all of
1) Items in the list may be removed.
2) Items in the list may be added.
3) Items in the list may have been updated.

Additionally, the list may be sorted or filtered by the user.

To manage this we store two things.

1) A sorted List of ids that the user should currently see.
2) A Dict of components by id.

When we receive an update notification we reduce over the list of ids and either create a new component model or update an existing component model as necessary.

If the user filters or sorts we generate a new list of sorted ids.

Then on display we map over the list of ids to get each component.

Seems like your lazyList would work well for us.

Thanks,
Michael

debois

unread,
May 18, 2016, 12:24:17 PM5/18/16
to Elm Discuss
I was using virtualdom key for two things, both of which I cannot do in 0.17.

1. Instructing virtual-dom whether or not to reset scrolling-state of an element. Try it out here: https://debois.github.io/elm-mdl/: Scroll to the bottom, then switch tab. Because virtual-dom is re-using the div containing the main contents, it does not reset scrolling state, and so when you switch tab, you retain scrolling state. In 0.16, I could specify different keys for the old and new tabs, which would make virtual-dom clear scrolling-state on change. I can't find a way to do this in 0.17.

2. Controlling CSS transitions in dynamically generated list of elements. See @Richard's post above. Here is an example from the elm-mdl demo.

Proper behaviour: https://debois.github.io/elm-mdl/. On the "snackbar" tab, click the "Snackbar" button a few times, then click "Undo".
 
0.17 behaviour: https://debois.github.io/elm-mdl/broken-css.html. Try the same thing. Note boxes accidentally resizing. What happens is that virtualdom re-appropriates a div of width 0 for an element that wants to be width 200px; CSS transitions kick in. 
  

Cheers,

Søren
 
 



Mark Hamburg

unread,
May 19, 2016, 6:07:25 PM5/19/16
to elm-d...@googlegroups.com
Some members of my team just raised the concern that not providing for item identity could cause focused text fields to lose focus and a bunch of edit state. I said I would pass it on.

Sylvain Falardeau

unread,
May 20, 2016, 9:10:21 AM5/20/16
to Elm Discuss


On Wednesday, May 18, 2016 at 12:24:17 PM UTC-4, debois wrote:
I was using virtualdom key for two things, both of which I cannot do in 0.17.

1. Instructing virtual-dom whether or not to reset scrolling-state of an element. Try it out here: https://debois.github.io/elm-mdl/: Scroll to the bottom, then switch tab. Because virtual-dom is re-using the div containing the main contents, it does not reset scrolling state, and so when you switch tab, you retain scrolling state. In 0.16, I could specify different keys for the old and new tabs, which would make virtual-dom clear scrolling-state on change. I can't find a way to do this in 0.17.


I have the same problem with a scrolling element that loses it's current scrolling position because another element is removed before it. In 0.16, adding the "key" attribute fixed the problem but it does not work anymore in 0.17.

While hiding the element in this case may be an option, it certainly makes the virtual-dom abstraction more "leaky". You have to understand the limitations and the ways the algorithm works to correct the problem. While the "key" attribute also asked to understand the problem, the fix is easy for newcomers. As far as I know, the idea of virtual-dom was to simplify the reasoning behind DOM changes but without "key" to minimize changes, you may have to rethink your DOM content ordering which seems fragile as you develop your application.

Message has been deleted

flip101

unread,
May 21, 2016, 8:23:22 AM5/21/16
to Elm Discuss
Here are two examples were key is needed https://langrisa.eu/virtual-dom-pitfalls/ . I don't understand the new lazy proposal so i can't judge if it would work well in these cases. Why can't the compiler just infer where this is needed? Thinking about key is a HARD problem for your brain.

Evan what's your conclusion so far after gathering this data?

ge...@theoldmonk.net

unread,
May 23, 2016, 8:41:56 AM5/23/16
to Elm Discuss
I'm not sure if the lazyDict approach applies, but I'm hitting a much simpler (IMO) issue that I've reported at https://github.com/elm-lang/html/issues/21.

I'm honestly surprised that no one has run into it yet, specially since the documentation for `key` in 0.16 specifically provides this exact issue as the reason for its existence.

It's entirely possible and highly likely that I'm missing something basic. I would be glad to receive any pointers, but I would also argue that the documentation could be improved in that case.

Cheers,
--gera.


On Monday, February 29, 2016 at 7:38:56 PM UTC+1, Evan wrote:

Kris Jenkins

unread,
May 24, 2016, 6:40:18 AM5/24/16
to Elm Discuss
I've just run into this too, but for me it causes a runtime exception. I've added a demo to reproduce (based on Gera's code) to the same issue:

  https://github.com/elm-lang/html/issues/21
Reply all
Reply to author
Forward
0 new messages