Does Elm have a reflective HTML package?

And would it benefit from having one?

The problem is, that elm/virtual-dom Node is opaque:

So when you get some user-defined view customization, lets say its some UI framework you are writing, and you wanted the user to supply their own Markdown -> Html function

addMarkdown : (Markdown -> Html Never) -> View -> View
addMarkdown userCustomization view =

You may want to parse that user defined Html for some reason.

Perhaps not the best example. In my case, I am interested in offering the ability to render custom views for my text editor work. I need to be able to understand the structure of the user customized view, in order to do coordinate mapping for the cursor - to understand how the cursor moves through that section of the DOM.

A reflectice HTML package would define a non-opaque node type, and a function to turn it into an elm/virtual-dom Node:

type ReflectiveNode = 
    Element String (List Attribute) (List Element)

toNode : ReflectiveNode -> Node

The idea would be to create a full DSL as an exact parallel to the elm/html API, just in a different module name-space.

Disadvantage is, there is obviously an overhead when rendering HTML, and it its a novel HTML representation with no connection to existing packages like elm-css, or elm-ui.

Does such a package already exist? Are there other use cases that it could be useful for?

3 Likes

I have a use case for this!

We’d like to be able to render a “minimap” or “preview” view of our page, while making sure that there are no ids in the minimap render (otherwise you’d have duplicate IDs!). The two approaches we’ve thought of so far are:

  1. Passing some sort of configuration value down the entire view call chain.
  2. Creating a custom element that strips out id from anything inside of it.

A non-opaque node type would be a much better solution!

2 Likes

We use https://package.elm-lang.org/packages/zwilias/elm-html-string/2.0.2/ for rendering html into a string sometimes, we pass that to a JS tooltip library.

Something like this would be nicer as you could hook different renderers at the very end.

1 Like

It’s not exactly a use case of being able to inspect a node, but I’ve found the inability to add attributes to a previously computed node leads to code that might be more complicated than it would otherwise. A simple example, suppose you want all of the buttons on your site to be styled with one of 3 classes, you might do something like:

type ButtonClass
     = Primary
      | Secondary
      | Danger

button : ButtonClass -> msg -> Html msg
button bClass message =
        ... defined in the obvious way ...

Maybe you define this in a Site module, so whenever you need a button you call Site.button and you are guaranteeing that all the buttons on your site have exactly one of the three button classes. You could even in theory write an elm-review rule to avoid direct use of Html.button.

Now suppose you want a button that is disabled, in that case you’re forced to take that in as a parameter to Site.button, because there is no: addAttribute : Attributes msg -> Node msg -> Node msg. Then maybe you find a case where you want to give some button an ‘id’, again you’re going to have to add that in as a parameter. Generally I find that my attempts to write such ‘helper’ functions go awry in this fashion and I end up either abandoning it, or I just take a list of attributes into Site.button. Alternatively I just have a buttonClass : ButtonClass -> Attribute msg helper function.

Perhaps I’m missing a way in which this is helping me, but it feels as if I’m being forced into more complicated/awkward code than otherwise.

Yes, that is also a use case for covering server side rendering as well as client side. You could write a view in the reflective HTML, then render to String or to elm/html.

I’ve bee thinking about this, but taking it further. I was inspired by the Effect pattern in the sense that a really nice* architecture for Elm apps is to build a custom runtime. Currently that mostly relies on a custom Effect type, but one could also have a custom View type. This View type could be composed out of higher level elements out of a design system plus elm-ui style layout primitives:

type View msg
     = Button ButtonStyle msg String
     | Tabset String (List (String, View msg))
     | Column (Attrs msg) (List (View msg))
     | Row (Attrs msg) (List (View msg))
     -- ...

Then the runtime would be be responsible for transforming these into HTML. I imagine this would have the following benefits:

  1. Testing would be simpler, since asserting that a view renders with a particular tab selected is just pattern matching, rather than requiring some specialised library.
  2. Some of the components could be stateful, but the state could be hidden from the caller.
  3. Renderers could be swapped. A custom renderer could be an interesting solution for responsivety for example, but why not render your website directly to PDF?
  4. Reflectivity (as above).

Of course I have no idea how bad the performance penalty would be. Also it’s not clear how mitigation strategies like lazy could be supported in this scheme.

I haven’t used it myself, but I did notice https://package.elm-lang.org/packages/ThinkAlexandria/elm-html-in-elm/latest/ the other day.

You got me thinking, if there were a phantom type parameter, then things like this could even be enforced by the compiler:

type alias ReflectiveHtml msg a = 
    ReflectiveNode msg a

type ReflectiveNode msg a =
   ...

type ReflectiveAttribute msg a = 
   ...

type Always
    = Always

id : String -> ReflectiveAttribute msg { hasId : Always }

stripIds : ReflectiveHtml msg a -> ReflectiveHtml msg { hasId : Never }

minimap : ReflectiveHtml Never { hasId : Never } -> Model -> (Html Msg, Html Msg)

That minimap function accepts a caller defined view that is guaranteed to have no ids in it and the compiler is enforcing a check on that.

However… it would mean an extra type parameter, and I was thinking to keep the API exactly as the Html.Html API is, for familiarity and ease of swapping existing code to/from reflective.

Perhaps there could be 2 APIs, one with phantom types, and one without? Same implementation under-the-hood, the one without just fills in a dummy value.

module Reflective.Html exposing (..)

import Reflective.Phantom.Html

type alias Html msg = 
    Reflective.Phantom.Html.Html msg ()

Trouble is thats a type alias, so pattern matching would not work directly on it, you would need to work with the phantom version for that. :thinking:

A useful reference for implementation. As mentioned above, I was thinking to go with an API that is 1:1 with elm/html for familiarity and swappability. Wonder why this package did not also do that?

I think this will be worth doing, not sure when I will be able to make a start on it, but its definitely in the pipe-line.

This design is heavily used in elm-mdc. So you can add/change attributes. Only at the last minute things are turned into the virtual dom node.

2 Likes

I really love the effect pattern to keep effect more transparent and avoid passing things like API tokens around unnecessarily. I could see a similar approach work with the view with the necessary libraries. One would need a drop-in Html replacement kinda like elm-css has though I assume.

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.