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.

@jsdw
Copy link

jsdw commented Sep 3, 2015

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:

type Action
   = Size String (Int,Int)
   | Position String (Int,Int)

view : Address -> Model -> Component
view address model = 
    div [ getSize address (Size "topel"), getPosition address (Position "topel") ] [....]

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..).

@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