Hi, currently I’m building an app with vanilla Halogen, getting a bit tired of only reusing CSS classes and think is time to make a Theme for my project.
Halogen examples has various methods of componentize things and was wondering about opinions/tradeoff on those?
This is Halogen’s the basic example:
module Example.Basic.Button (component) where
import Prelude
import Halogen as H
import Halogen.HTML as HH
import Halogen.HTML.Events as HE
import Halogen.HTML.Properties as HP
type State = { enabled :: Boolean }
data Action = Toggle
component :: forall q i o m. H.Component q i o m
component =
H.mkComponent
{ initialState
, render
, eval: H.mkEval $ H.defaultEval { handleAction = handleAction }
}
initialState :: forall i. i -> State
initialState _ = { enabled: false }
render :: forall m. State -> H.ComponentHTML Action () m
render state =
let
label = if state.enabled then "On" else "Off"
in
HH.button
[ HP.title label
, HE.onClick \_ -> Toggle
]
[ HH.text label ]
handleAction ∷ forall o m. Action → H.HalogenM State Action () o m Unit
handleAction = case _ of
Toggle ->
H.modify_ \st -> st { enabled = not st.enabled }
This seems like a lot of boilerplate (for themed components), probably has a runtime cost, and is not extensible. If I want to extend the component, I cannot pass more default properties to the button.
I could do something like:
type Input =
{ enabled :: Boolean
, buttonProps :: I.HTMLbutton
, buttonChildren :: Array (HTML w i)
}
But I’m not sure if this is a common practice for Theme components; it is not completely clear to me if “Theme Components build to use with Halogen” should be of type H.Component
or a mix with HTML w i
, because primitives are not encoded as H.Components
, they are “elements”, expecting indexed properties, but H.Components
are often a Record in the docs.
There are variants expressed with an underscore button
→ button_
with empty properties, does it mean that small varials could be just HTML? like buttonPrimary
is just HH.button
with one class applied, like this:
module Theme.Button.Button where
import Prelude
import DOM.HTML.Indexed as I
import Halogen.HTML as HH
import Halogen.HTML.Properties as HP
data Variant = Primary
classFromButton ∷ Variant → HH.ClassName
classFromButton = case _ of
Primary -> HH.ClassName "call_to_action_button"
button ∷ ∀ w i. Variant → HH.Node I.HTMLbutton w i
button variant a b =
HH.button
( [ HP.classes [ classFromButton variant ]
] <> a
)
b
or
module Theme.Button.Button where
import Prelude
import DOM.HTML.Indexed as I
import Halogen.HTML as HH
import Halogen.HTML.Properties as HP
buttonPrimary ∷ ∀ w i. HH.Node I.HTMLbutton w i
buttonPrimary props children =
HH.button
( [ HP.classes [ HH.ClassName "call_to_action_button" ]
] <> props
)
children
(I am still a bit puzzled on how to merge props, like HP.classes
because it seems Halogen only renders the first of every prop)
Then there are the higher order components:
component
:: forall q i o m
. H.Component q i o m
-> H.Component (Query q) i (Message o) m
component innerComponent =
H.mkComponent
{ initialState
, render: render innerComponent
, eval: H.mkEval $ H.defaultEval
{ handleAction = handleAction
, handleQuery = handleQuery
}
}
render
:: forall q i o m
. H.Component q i o m
-> State i
-> H.ComponentHTML (Action o) (ChildSlots q o) m
render innerComponent state
| state.open =
HH.div
[ HP.classes [ H.ClassName "Panel", H.ClassName "Panel--open" ] ]
[ HH.div
[ HP.classes [ H.ClassName "Panel-header" ] ]
[ HH.button
[ HP.classes [ H.ClassName "Panel-toggleButton" ]
, HE.onClick \_ -> Toggle
]
[ HH.text "Close" ]
]
, HH.div
[ HP.classes [ H.ClassName "Panel-content" ] ]
[ HH.slot _inner unit innerComponent state.input HandleInner ]
]
| otherwise =
HH.div
[ HP.classes [ H.ClassName "Panel", H.ClassName "Panel--closed" ] ]
[ HH.div
[ HP.classes [ H.ClassName "Panel-header" ] ]
[ HH.button
[ HP.classes [ H.ClassName "Panel-toggleButton" ]
, HE.onClick \_ -> Toggle
]
[ HH.text "Open" ]
]
]
It is nice to add arbitrary H.Component
children. It behaves similarly to Reactjs children; passing down default properties is a bit complex, but I think I can, and also, I’m not sure if I can access the children in any way. React has things like the Children API, which can be used to transform the children:
import { Children } from 'react';
function RowList({ children }) {
return (
<div className="RowList">
{Children.map(children, child =>
<div className="Row">
{child}
</div>
)}
</div>
);
}
Also things like cloneElement
import { cloneElement } from 'react';
// ...
const clonedElement = cloneElement(
<Row title="Cabbage">
Hello
</Row>,
{ isHighlighted: true },
'Goodbye'
);
console.log(clonedElement); // <Row title="Cabbage" isHighlighted>Goodbye</Row>
to create a new React element using another element as a starting point, heavily used in themes.
A modern approach would be the Context API:
export default function List({ items, renderItem }) {
const [selectedIndex, setSelectedIndex] = useState(0);
return (
<div className="List">
{items.map((item, index) => {
const isHighlighted = index === selectedIndex;
return (
<HighlightContext.Provider key={item.id} value={isHighlighted}>
{renderItem(item)}
</HighlightContext.Provider>
);
})}
For passing down properties, a must in theme components that have an internal state of communication.
Back to how to make components, which is preferred? Higher order components? Is there a benefit in making a simple “Primary Button” a Halogen component if it has no lifecycle?
Also, with Reactjs, passing custom and default props is “easier”, as I can read the custom one’s restructuring and then assign.
import * as React from 'react';
import Stack from '@mui/material/Stack';
import Button from '@mui/material/Button';
export default function BasicButtons() {
return (
<Stack spacing={2} direction="row">
<Button variant="text">Text</Button>
<Button variant="contained">Contained</Button>
<Button variant="outlined">Outlined</Button>
</Stack>
);
}
If I want to pass on more default properties, it is easy to add them and match on the custom ones.
I know at the end of the day, it is a decision what API I provide to the theme, but I wonder how other people are Themefying their apps with Halogen.
Does anyone has some experience in this area?