A problem that comes up periodically is that people want to know the position of Html elements on the page after they've been laid out, for example so they can:
- Absolutely position an element based on the position of one or more relatively-positioned elements. E.g:
- make a popup seem to "come out of" a certain element (this could be done by adding a child of the div which is positioned relative to it, but is easier if you can just get the laid-out coordinates of the div and position the popup relative to that)
- draw a circle around an element, to highlight it (e.g. for a first-time app walkthrough tutorial). Again, maybe possible to do with purely relative layout, but much easier if you can get element position.
- draw a line between two page elements
- know whether an element is visible, given the current scroll window
- get mouse events on an element which are relative to that element's top-left origin (e.g. for a mouse-interactive Graphics.Collage embedded in HTML)
Elm-html cannot tell us anything about the position and size of elements on the page, because it doesn't know, because the layout isn't computed until VirtualDom does its diff, mutates the DOM, and triggers the browser to re-layout the page. Thus, because layout information is computed as an effect of mutating the DOM, it seems any API to get HTML layout information must be Task-based.
Here are a couple stabs at such an API:
Stab 1 (simple API, probably slow):
type LayoutTree =
LayoutNode {
attributes : List (String, String),
boundingBox: BoundingBox,
children : List LayoutNode
}
type alias BoundingBox =
{ top : Float, left : Float, width : Float, height : Float }
render : Html -> Task x LayoutTree
querySelectorAll : CssSelector -> LayoutTree -> List (LayoutTree) -- or something similar to get the nodes you're interested in
Why slow: Since the return type of a task has to be immutable, we would have to snapshot the entire layout tree on render, unless there was an API like this:
renderAndGetBBox : Html -> CssSelector -> Task x BoundingBox
Additionally, it does seem apt that rendering (i.e. mutating the DOM) should be a Task like any other, i.e. main would no longer be a special case. Something like this:
html : Signal Html
html = Signal.map view state
-- could push these to a mailbox if you need them
layoutTrees : Signal (Task x LayoutTree)
layoutTrees : Signal.map Html.render html
port renders : Signal (Task x ())
port renders = Signal.map (Task.map (always ())) layoutTrees
This doesn't seem very beginner-friendly though.
Stab 2 (ugly API, probably fast):
To relieve the need to snapshot the entire DOM on each render, Html.render
could return a reference to the stateful DOM node itself, and the function to access its bounding box could be taskful.
type LayoutTree =
LayoutTree
render : Html -> LayoutTree
querySelectorAll : CssSelector -> LayoutTree -> List (LayoutTree) -- or something
getBoundingBox : LayoutTree -> Task x (Maybe BoundingBox)
-- ^^ `Maybe` because this element may no longer be in the DOM
This just doesn't seem nice.
Long range ideas (maybe crazy):
Maybe at some point either
- browsers will expose their layout algorithms such that we can use them as a pure function from Elm: layout : Html -> LayoutTree
- we will re-implement layout in pure Elm (Facebook did it here in JS for Flexbox: https://github.com/facebook/css-layout)
Does anyone have thoughts about (a) how to make an API that's both performant and nice or (b) whether this seems like a good step towards a world where renderers are pluggable and Elm can be run in non-graphical contexts (i.e. server-side)?
This may seem like a rare case, but it does come up in advanced apps, and seems really hard to work around in Elm as it is now.
Thanks for thinking about this. This is the same thing I hit a few days ago and also asked on elm-discuss in the context of infinite scrolling. I want to be able to "display" thousands of items in some container, and actually creating all of those elements is hellishly slow. I was pondering how one might go about exposing this in elm.
Using the current wiring for events like onClick, which already forms a nice feedback loop from component to effect back to component, I could see something like:
where getSize and getPosition send the element size and position back around to the component just as
onClick
would send the click event (except that it happens on initial render as well). These could then be stored in the model and used to dictate rendering stuff quite easily.I am probably not thinking about various caveats but offhand this is the sort of approach I would look towards, and it seems to fit nicely with how elm generally does things (though I say this with 2 days of Elm experience to date..).