Dynamic forms with hyperscript

Created: 4/23/2022

Updated: 5/18/2022

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.

To play around with this you can check out this codepen.