Basic behavioral composition

720 views
Skip to first unread message

suttlecommakevin

unread,
Aug 19, 2016, 1:46:30 PM8/19/16
to Elm Discuss
Apologies if this has been posted elsewhere, but I keep coming back to it. 

Let's get basic. Like super basic.

I get a spec from a designer for a button with 3 types of children. 
  1. A button with a label only
  2. A button with an icon only
  3. A button with an icon and a label

In a object-oriented programming environment, you could make a ButtonBase class and extend it. 
In React's bizarro world, they try to promote this Higher-order Components technique, which is really just a function factory.
In Flow, you can at least start making types, and then, share and intersect them. 


ButtonProps.js

// @flow
/* eslint-disable import/prefer-default-export */

export type ButtonProps = {
  type
?: 'button' | 'reset' | 'submit',
  design
: 'primary' | 'secondary',
  className
?: string,
  children
?: Array<HTMLElement>,
  onClick
?: () => void,
  onFocus
?: () => void,
  onmouseover
?: () => void,
  onmouseout
?: () => void,
}


Button.jsx

// @flow

import React from 'react';
import type { ButtonProps } from './ButtonProps';
import './Button.css';

/* eslint-disable flowtype/space-after-type-colon */
const Button = ({
  design
= 'primary',
  className
= 'btn',
  type
= 'button',
  children
} :ButtonProps) =>

 
<button className={[`${design} ${className}`]} type={type}>
   
{children}
 
</button>;

export default Button;


Icon.jsx

// @flow

import React from 'react';
import Button from './Button.jsx';
import type { ButtonProps } from './ButtonProps';
import Icon from '../Icons/Icon.jsx';
import type { IconProps } from '../Icons/IconProps';

type
IconButtonProps = ButtonProps & IconProps;

const IconButton = (props: IconButtonProps) =>
 
<Button
    design
={props.design}
    onClick
={props.onClick}
    className
={`iconBtn ${props.className}`}
 
>
   
<Icon glyph={props.glyph} />
 
</Button>;

export default IconButton;


Notice this line: type IconButtonProps = ButtonProps & IconPropswhich is just a fancy Object.assign() really. 
It's easy to read, easy to understand, but many would claim it doesn't follow "best practices".


My question is, how would Elm/FP handle this? 


Thanks, folks. 

OvermindDL1

unread,
Aug 19, 2016, 2:00:20 PM8/19/16
to Elm Discuss
You could invert it to compose instead of extend, so something like this:

```elm
-- Button with a Label
MyButton.view [ MyButton.label "blah" ]

-- Button with an Icon
MyButton.view [ MyButton.icon "iconId" ]

-- Button with both
MyButton.view [ MyButton.label "blah", MyButton.icon "iconId" ]
```

You can easily enforce the icon to always render before the label is that is a requirement, regardless of position in the list, etc... etc...

This is how the `elm-mdl` package works for example.

suttlecommakevin

unread,
Aug 19, 2016, 2:08:14 PM8/19/16
to Elm Discuss
Very interesting, thank you. Can you explain a bit more on the motives of that architecture and API, please? 

Max Goldstein

unread,
Aug 19, 2016, 2:40:25 PM8/19/16
to Elm Discuss
It sounds like you want a union type to represent the three possible buttons. You can have a view function that does case analysis and renders each possibility. Any shared code can be moved to a "let" definition, or another function.

I mean, unless in missing something...

Richard Feldman

unread,
Aug 19, 2016, 2:45:57 PM8/19/16
to Elm Discuss
This is a great question! I think there's a very clear answer here:

Don't overengineer it.

As you noted, this is basic - super basic - so by default, the best solution is also super basic.

There's a button with 2 configurable options? Cool, let's write a function that accepts that configuration and returns the button we need:

fancyButton : { label : String, icon : String } -> Html msg
fancyButton
{ label, icon } =
   
-- Implementation goes here

Done!

In a language where refactoring is nice, you can and should reach for simple solutions when the problem is simple.

If the problem gets more complex later, you can then revise from a position of knowledge: you'll know precisely how it's more complicated, and that gives you the information you need to end up with the nicest API possible.

Conversely, trying to design an API to fit nebulous theoretical future use cases essentially means going in blind, and it's a recipe for unnecessary suffering in Elm. :)

OvermindDL1

unread,
Aug 19, 2016, 2:47:48 PM8/19/16
to Elm Discuss
`elm-mdl` is doing it that way because it is trying to follow how Google's material library works, and since there is no way to introspect into the virtualnode's to change how they act then it has to wrap things up in that pattern, I.E., VirtualNode limitations and Html.Attributes limitations require it to be this noisy.  :-)

Richard Feldman

unread,
Aug 19, 2016, 2:52:59 PM8/19/16
to Elm Discuss
`elm-mdl` is doing it that way because it is trying to follow how Google's material library works, and since there is no way to introspect into the virtualnode's to change how they act then it has to wrap things up in that pattern, I.E., VirtualNode limitations and Html.Attributes limitations require it to be this noisy.  :-)

Yeah, elm-mdl has very specific and unusual design goals (in part because of ways MDL differs from other UI frameworks, and in part because of the author's goal for how to present MDL in a DSL) that make it substantially different from every other project in the Elm world...I would not look at its design and think "ah, this is probably what I want to do for my project!" because the opposite is far more likely. :)

Nick H

unread,
Aug 19, 2016, 2:58:10 PM8/19/16
to elm-d...@googlegroups.com
I don't think Overmind's and Richard's inputs are at odds. Richard has suggested an API. Overmind's code is what might fit into "-- Implementation goes here".

On Fri, Aug 19, 2016 at 11:52 AM, Richard Feldman <richard....@gmail.com> wrote:
`elm-mdl` is doing it that way because it is trying to follow how Google's material library works, and since there is no way to introspect into the virtualnode's to change how they act then it has to wrap things up in that pattern, I.E., VirtualNode limitations and Html.Attributes limitations require it to be this noisy.  :-)

Yeah, elm-mdl has very specific and unusual design goals (in part because of ways MDL differs from other UI frameworks, and in part because of the author's goal for how to present MDL in a DSL) that make it substantially different from every other project in the Elm world...I would not look at its design and think "ah, this is probably what I want to do for my project!" because the opposite is far more likely. :)

--
You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss+unsubscribe@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

OvermindDL1

unread,
Aug 19, 2016, 4:08:56 PM8/19/16
to Elm Discuss
Precisely, the thing like `MyButton.label` would return an appropriate instanced union holding whatever data you need.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss...@googlegroups.com.

Max Goldstein

unread,
Aug 19, 2016, 5:13:01 PM8/19/16
to Elm Discuss
As much as I love "don't overengineer it" as a principle, I'm not sure Richard's suggestion solves OP's problem. We want the possibility of NO label or NO icon, not just custom values. So that implies Maybe values in the record. Except, that opens the possibility of having neither an icon nor a string. So that leads me back to the union type I suggested above. (A function that takes a list will similarly have to contend with the empty list.)

suttlecommakevin

unread,
Aug 19, 2016, 6:02:38 PM8/19/16
to Elm Discuss
Thanks for the detailed responses so far!

I hope I can shed a bit more light here.

The following concepts are all currently frowned upon in React (not that it matters here, just illustrating a point)
- extending classes
- mixins
- traits (ahem - https://github.com/facebook/flow/issues/2135)
- decorators

In favor of
- "higher-order components" (aka over complicated function factories)
- "functions as children", which seems like a useful concept until you try it
- "container components" - AKA view renderers

Given this breadth of approaches, what does Elm and FP suggest as a best practice for composing shared behaviors, children, interfaces (in OOP-speak), mixins, etc?

debois

unread,
Aug 20, 2016, 3:28:09 PM8/20/16
to Elm Discuss
This is a great question! 

Without internal state

I think the most productive way of working with Elm is to avoid trying to generalise and stick with expressing yourself directly in elm-lang/html whenever possible. If you find yourself doing something a lot, use helper functions to construct high-frequency pieces of `Html m`. Richards' answer above is compatible with this philosophy. 

You'll see elm-mdl take this route in a number of places, i.e., the Card, Elevation, Footer, Grid, Lists and Typography modules contain just functions constructing `Html a` values for you. You add behaviour as you would anything in elm-lang/html by supplying your own attributes, typically Html.Events.on*. 

With internal state

However, all the answers above make sense only if the Button you're building doesn't have internal state. Let's say yours is a a fancy button with animations; someone needs to track the current animation state of the button, and update that state in response to DOM events. You now have just two options for who that someone might be: (1) That someone is you: Stick with working directly in elm-lang/html and manually integrate animation logic for each button in your app's `Msg/update`. (2) That someone is a component: Built a TEA component for your button. (If you think about it a bit, you'll realise that if you don't like (1), whatever else you do, you must do the things TEA components do anyway.)

Composing behaviours

This choice brings us back to the original question: "How do I compose behaviours in Elm"? If you go for (1),  "composing behaviours" is trivial, because you didn't put up a wall in the shape of an API between your fancy button and its surroundings; you just adjust the code from case to case as you need. This emphatically does not mean that you now code by cut-and-paste: As usual, when you find yourself doing the same thing in different places, you factor out that thing into a helper function. As you get more helper functions, perhaps a good API for a button component will emerge.

If you go for (2), full TEA component, "composing behaviours" is much, much harder. You have to chose an API your button, and that API will dictate exactly how it can and cannot interact with the rest of your app. For a button, figuring out this API is maybe not that hard (maybe you can set CSS on it, set its label, set its icon, and give it a Msg to fire when clicked), but for even slightly more complex behaviours, I find producing proper APIs surprisingly hard. A very large portion of the time spent on elm-mdl went to devising APIs for seemingly simple components, like Cards, Lists, and Badges. 

The approach we took in elm-mdl was to mimic elm-lang/html as much as possible: The only way to interact with a component is to customise it through parameters to `view` (think `Attribute m`).  You "compose behaviours" specifically by supplying `Msg`es you'd like to be sent in response to internal events of the component. This approach is compatible with the guidelines in Evan's sortableTable

In elm-mdl, we customise components only through parameters to `view` because that's what elm-lang/html looks like. You could view it as a ruthless, take-no-prisoners realisation of that guideline, though: When we customise only in `view`, the component cannot store any of your data in its model(*). 

So, altogether, this is my preferred approach to "composing behaviour" in elm:

1. Avoid components and work directly with `Html m` if possible. Avoid repeating yourself by ample use of helper functions. "Composing behaviour" is just producing `Html m` values. 
2. When you must make a component, let the Model of the component contain only internal state of that component.
3. When you must make a component, "compose behaviour" with that component by (a) telling it what to look like in `view` and (b) reacting to messages it sends (back) to you. Never touch its Model. Never send it messages.

This approach is not all-encompassing; I had to give up on (3) for the Snackbar component(**). For UI components in general, though, I do think this approach is something to strive for, and I do think it makes sense outside of elm-mdl. 

Cheers,

Søren

(*) Almost can't. 

(**) Snackbar holds a queue of messages to display. That queue has to live in the model. The user can only add messages to the queue by messing with the model or (actual choice) send messages to Snackbar. 

OvermindDL1

unread,
Aug 22, 2016, 11:41:02 AM8/22/16
to Elm Discuss
Fantastic description.

On the Snackbar part though, have you thought about making it an 'effect' so you can send messages to it and subscribe to it (this pattern might make other things easier as well in elm-mdl)?

Wouter In t Velt

unread,
Aug 23, 2016, 9:43:29 AM8/23/16
to Elm Discuss
Thank you for this very clear and concise description @debois!

Coming from React+Flux, I've followed much of the discussions on child-parent communication, components etcetera, but I found it really hard to scale in elm.
The "everything is a component" from react is a hard habit to break, and although I understood (+ learned the hard way) that components are not the way to go in elm, it wasn't really clear to me what the right (or better) way is.

Your post is a very helpful guideline, thanks!

suttlecommakevin

unread,
Aug 23, 2016, 11:03:04 AM8/23/16
to Elm Discuss
Yes, a wonderful and much appreciated breakdown, @debois. Couldn't have said it better @Wouter. 

suttlecommakevin

unread,
Sep 2, 2016, 12:00:31 PM9/2/16
to Elm Discuss
This new doc helps a ton. Thanks for this!

Wouter In t Velt

unread,
Sep 5, 2016, 5:56:48 AM9/5/16
to Elm Discuss
Hadn't noticed this section in the guide before. Great explanation!
Thanks for sharing.

Op vrijdag 2 september 2016 18:00:31 UTC+2 schreef suttlecommakevin:

Rex van der Spuy

unread,
Sep 5, 2016, 9:35:00 AM9/5/16
to Elm Discuss


Hadn't noticed this section in the guide before. Great explanation

... It's just been added :) 
Reply all
Reply to author
Forward
0 new messages