I'm using Lustre framework to rebuild the Admin area of this blog. When implementing the form for editing blog post, I'm surprised how tricky to handle the checkbox, which may look simple at first.
In CRUD applications, people often use the checkbox to represent a boolean field. The "checked" status is for True and unchecked is for False. Take this form as example, when I want to publish a post, I tick the "Published" checkbox and save. If I want to unpublish, I untick and save.

Most of people don't see any issue with this usage. When I make frontend apps with VueJS, with an edit form like this, I often bind each <input> element with a reactive variable, via v-model:
<form
v-if='post'
method='post'
@submit.prevent='onSubmit'
>
<HorizontalFormField
v-model='post.is_published'
class='mt-2'
widget-type='checkbox'
label='Published'
/>
</form>
Vue, and maybe other frameworks of the same kind (React, Svelte etc.), supports this use case extremely well.
When I make frontend apps with Lustre, a framework with quite different architecture from the above names, I choose the approach which looks more like in backend side. I just collect the data that a HTML <form> submitted, passing it to a form validation object (I use formal for this), then taking the ouput of this validation to call API. MUV (Model-Update-View) frameworks like Lustre has a central Msg type and a central update() function to process those messages, so I don't want to inflate this Msg. If we bind each <input> like in VueJS, we will create a lot of variants for Msg and the central update() will be very long! With the "process form as a whole" approach, I only need one variant of Msg:

And that is when I realized the quirk of checkbox: If I uncheck the is_published checkbox, the data submitted by <form> will miss the is_published field. The data that we then send to API to "patch" the Post object also miss this field and doesn't make change to the Post object. At the end, the Post is still published even though we as user already unticked the checkbox.
The "only include if being checked" behavior is actually what HTML checkbox is designed from the beginning. We are just bending it to our custom use.
My resolution is to add one more field to the app Model to track the boolean status of the checkbox. It is similar to what we do in a Vue app, but we only do for checkboxes, not all <input>.
We define a Checkboxes type to track all checkboxes we will have in the app and give it a seat in Model:
pub type CheckBoxes {
CheckBoxes(is_published: Bool)
}
pub type Model {
Model(
route: Route,
...
post_form: Option(Form(PostEditablePart)),
checkboxes: CheckBoxes,
)
}
We define a message that will be emitted when user tick / untick the checkbox:
pub type Msg {
...
UserToggledIsPublishedCheckbox(Bool)
}
The message name is weirdly long. I haven't thought of a better message collection to deal with multiple checkbox yet.
When we render the checkbox, we base on the checkboxes tracking data to make the widget checked or unchecked:
fn render_is_published_field(is_published: Bool) {
h.div([a.class(class_row)], [
h.label([a.class(class_label)], [h.text("Published")]),
h.div([a.class("pt-2")], [
h.input([
a.name("is_published"),
a.type_("checkbox"),
a.checked(is_published),
ev.on_check(UserToggledIsPublishedCheckbox),
]),
]),
])
}
When handling this message, we just need to update the tracking data in model:
fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
case msg {
UserToggledIsPublishedCheckbox(checked) -> {
let model = Model(..model, checkboxes: CheckBoxes(is_published: checked))
#(model, effect.none())
}
}
}
At the step of validating submitted data, with formal package, normally we use form.add_values to feed the new data to the Form object to process. But we should note that when the checkbox is unchecked, the new value for that field is missing, the Form will use old data (which was previously added) instead and may produce unwanted result. Unfortunately, formal doesn't have a function to remove old data that it is keeping. We have to use form.set_values to overwrite all fields.
I have done presenting tips to deal with HTML checkboxes. Hope it helps!