I have been using htmx and _hyperscript lately and love how much it simplifies building a web site. However, I was having a bit of trouble getting a dynamic form to work properly as there is a little bit of extra work needed. This post describes my solution.
Context
By ‘dynamic form’ I mean an HTML form where the inputs can change based on how the user populates the form. I tend to need this when there is an array type in the data and the number of elements in the array is specified by the user, or when there is a union type in the data and I want to change the inputs shown based on the case selected by the user. An example is worth a thousand more words.
Domain
Let’s model a frequency for some type of scheduler. I typically use F# so that I can leverage it’s amazing type system, and in particular discriminated unions.
type Frequency =
| FixedDays of {| days:int |}
| SpecificDates of {| dates:DateTimeOffset[] |}
Web
Let’s create a form for our
Frequency
type.<h1>Frequency Form</h1>
<form>
<div class="group">
<label for="frequencyType">Frequency</label>
<select name="frequencyType" _="on change take .active from .activate for #{my value}">
<option value="fixedDays">Fixed Days</option>
<option value="specificDates">Specific Dates</option>
</select>
</div>
<div class="group activate active" id="fixedDays">
<label for="days">Days</label>
<input name="days" type="number">
</div>
<div class="group activate" id="specificDates">
<label for="specificDate">Dates</label>
<div id="specificDate">
<div id="specificDateInputs"></div>
<button type="button" _="on load set :idx to 0
on click increment :idx
then put `
<div style='display:flex'>
<input id='specific-date-${:idx}' name='specificDate' type='date'>
<button _='on click remove closest <div/>'>Remove</button>
</div>`
at the end of #specificDateInputs
then call _hyperscript.processNode(#specificDateInputs)">
+ Add Date
</button>
</div>
</div>
</form>
.group {
margin-top: 10px;
}
label {
display: block;
}
.activate {
display: none;
}
.activate.active {
display: block;
}
In the first group of the form we use a
<select>
element to select the case of our union type. We then use the hyperscript code on change take .active from .activate for #{my value}
to remove the active
class from any elements with the class activate
and add it to the element with the id of the option value (my
represents the current element) whenever there is a change
event. In our CSS we then only display elements with the activate
class if they also have the active
class. This allows us to dynamically show form inputs based on the value of a select element.In the second two groups we display the actual inputs. The first input group
fixedDays
is pretty straight forward and just uses a number input. The second group however represents an array of dates and so we need some code to add and remove elements. On the add button we use the hyperscript code on load set :idx to 0
on click increment :idx
then put `
<div style='display:flex'>
<input id='specific-date-${:idx}' name='specificDate' type='date'>
<button _='on click remove closest <div/>'>Remove</button>
</div>`
at the end of #specificDateInputs
then call _hyperscript.processNode(#specificDateInputs)
Which breaks down ‘when the element loads initialize the
idx
variable to 0 and when the button is clicked increment the idx
variable append this new <div>
element to the end of the element with id="specificDateInputs"
then reprocess any hyperscript code in the element with id="specificDateInputs"
’. The reason we have to reprocess the elements is because the <div>
we are adding includes the hyperscript code on click remove closest <div/>
which translates to ‘when this button is clicked find the nearest parent <div>
element and remove it from the DOM.Another thing to note is that I use
type="button"
on the <button>
elements so that they do not trigger a submit
event.When we submit the form the request will contain all the values from the inputs, and we can take the ones we need by inspecting the value of the
frequencyType
.