Simplifying Halogen components with rows

I’m currently working on overhauling the child slot mechanism for components in Halogen to use extensible rows which (hopefully) simplifies how you use child components. Previously there was quite a bit of boilerplate and ceremony around ChildPaths and such, which is largely eliminated. It’s also led to a cascade of other simplifications, such as removing the variations on the component constructor (child, parent, lifecycle, etc).

8 Likes

@natefaubion I spent some time today reworking purescript-halogen-select to use rows for child slots and tested it out in our main application as well.

There’s a WIP of my commit to update to rows here:

Some nice things

  1. I love that this mostly kills the boilerplate around child slot / child path. It’s especially nice that you can export a component’s slot when you define the component, rather than forcing the parent to define slots each time, and that you can use this to also provide a helper function that handles mounting the component given the right input, slot id, and handler. It’s especially nice in modules like our router where we have so many routes it turns into this enormous swathe of childpath-related code.

  2. I’m also a big fan that lifecycleParentComponent, parentComponent, lifecycleComponent, andcomponentcan all just be reduced to one,component, and that all the various type synonyms can be removed in favor of justComponentHTML,HalogenM, andComponent`.

  3. It feels nicer to use a symbol proxy for the row when querying (H.query _select unit ...) than to use the childpath. It always felt awkward to have to remember what, exactly, cp1 corresponded to – not informative.

Some caveats

  1. We have some components that accept their render function via their Input type. With the old method, H.ComponentHTML Query covered it for the render function, but now all render functions have the result H.ComponentHTML Query ChildSlots m. If we don’t want to have to parameterize our render function by a polymorphic m and the component has no children, then the workaround is to require a render function like this: forall m. State -> H.ComponentHTML Query () m, which recreates the old behavior.

  2. It’s not yet clear to me why H.Slot requires the child component’s query, message type, and slot type. Previously childpaths connected queries and slot types, but I don’t immediately see the reason to require the message type as well. @natefaubion any notes on this?

Overall, A+++ on this change. Can’t wait to put it into production.

2 Likes

Currently there is actually a “bug” (misfeature?) where the output handler for a component cannot be rebound once the component is mounted. This is unlike handlers for DOM events which are rebound on render, which lets you close over state and such. This is because the output type is not part of the slot. Since it is existentially hidden in the ComponentSlot, there is no way to safely bind a new handler to the existing lifecycle. We did not despise our users quite enough to make them stitch together another type for outputs as well, and keep it sync with the Query and Slot types. If you thought ChildPaths were bad before… whew…

Since all these types can now be kept together in a single mapping, I opted to fix it, but this obviously requires tracking the output type. This means that output handlers and event handler now work identically.

3 Likes

Interesting. I haven’t encountered this case before, but it’s good to know about this. It’s easy to put the output type in the slot when you export it from your component because you’ve just defined it in the same module – so it’s not a hassle.

As a side note: are there blocks that will prevent a new release with this feature? Stability / testing alone?

2 Likes