Skip to content

Instantly share code, notes, and snippets.

@vilterp
Last active May 11, 2016 08:27
Show Gist options
  • Save vilterp/48cc3b4a8422586fcb39 to your computer and use it in GitHub Desktop.
Save vilterp/48cc3b4a8422586fcb39 to your computer and use it in GitHub Desktop.
Html layout as a taskful API

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:

  1. Absolutely position an element based on the position of one or more relatively-positioned elements. E.g:
    1. 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)
    2. 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.
    3. draw a line between two page elements
  2. know whether an element is visible, given the current scroll window
  3. 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.

@GetContented
Copy link

Any update to this? Want to do a set of spans that I can drag-n-drop one out of (0.17.0) and have the rest slide around depending on where I drag that one to (so they push out of the way, and generally can be reordererd). Not sure how to do it given there's no managed size of each thing at present. Or is there, and I just didn't notice it in 0.17.0 ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment