‘Iacta alea est’
In a previous post I hooked up Scrivener with Mote and Syro for validating a form. The HTML for the form is quite verbose and I’d like to demonstrate one way in which I tackled that.
The solution is very targeted at this specific purpose and thus should not be taken as an all-encompassing HTML form generation library but it certainly points one path towards that end.
What I think is useful is that it shows us some of the pieces required for such a library and one way of putting them together. It builds largely on our Scrivener object, extending it to cover a mapping between field and input type alongside the resulting HTML generation.
To jog our memories, this is the Scrivener object we ended up with for a contact form:
class ContactForm < Scrivener
attr_accessor :name
attr_accessor :email
attr_accessor :message
def validate
assert_present :name
assert_present :email
assert_present :message
assert_email :email
end
end
And this was the HTML:
<form action="/contact" method="post">
<ul>
% contact_form.errors.each do |error|
<li>
{{ error.first }}: {{error.last.join(",")}}
</li>
% end
</ul>
<label for="name_field">
Name
</label>
<input
type="text"
name="name"
id="name_field"
value="{{contact_form.name}}">
<label for="email_field">
Email Address
</label>
<input
type="email"
name="email"
id="email_field"
value="{{contact_form.email}}">
<label for="message_field">
Message
</label>
<textarea
name="message"
id="message_field"
value="{{contact_form.message}}">
</textarea>
<input type="submit" value="Send">
</form>
If we take a step back then we see that this form has several attributes: an action, a method, a value for the submit button, a list of form fields and a list of errors. (Form fields could also be grouped with their respective errors, but for now we’ll keep them separate.)
form:
action: "/contact"
method: "post"
errors: []
fields: []
submit: "Send"
We know from Scrivener that an error is a tuple (expressed as a two element Array in Ruby) with a symbol corresponding to an attribute, and a list of symbols corresponding to a set of validation errors.
form:
action: "/contact"
method: "post"
submit: "Send"
errors: [[:email, [:not_present]]]
fields: []
If we operate under the assumption that there will be a one-to-one mapping between attributes defined on a Scrivener object and fields in the form, then we can begin to fill out the structure of a field. Looking at the HTML for the email field:
<label for="email_field">
Email Address
</label>
<input
type="email"
name="email"
id="email_field"
value="">
We can see there are different elements that we need to account for. The for
attribute of label
, and the id
, name
, and value
attributes of input
can all be derived from the Scrivener attribute name. The text content of label
could also be derived, at a push; let’s do that for simplicity. And the key information that is not in Scrivener at all is the type
attribute of the input
.
So a rough YAML structure for all of the form data, including fields, now looks something like this (I’m using symbols for fixed elements corresponding to an exact HTML attribute value versus strings for variable values):
form:
action: "/contact"
method: "post"
submit: "Send"
errors: [[:email, [:not_present]]]
fields:
- type: :email_input # Extra information
name: "email" # Derived from Scrivener attribute
label: "Email" # Title-cased Scrivener object attribute
id: "email_field" # Derived from Scrivener attribute
value: "" # Scrivener attribute value
attribute: :email # Scrivener attribute
Now all that remains is to encode the information that we don’t already have in the Scrivener object. Then we can simply apply a transformation on the Scrivener object to an HTML string. There are different ways that we could do this so I’ll keep it simple for now, simply reproducing the structure above in a form
method.
class ContactForm < Scrivener
attr_accessor :name
attr_accessor :email
attr_accessor :message
def validate
assert_present :name
assert_present :email
assert_present :message
assert_email :email
end
def form
{
action: "/contact",
method: "post",
submit: "send",
errors: errors,
fields: attributes.keys.map do |attr|
{
type: fieldmapping[attr],
name: attr,
label: attr.capitalize,
id: "#{attr}_field",
value: send(attr),
attribute: attr
}
end
}
end
def field_mapping
{
name: :text_input,
email: :email_input,
message: :textarea
}
end
end
The field_mapping
method (or rather Hash) maps a given attribute to a fixed HTML form field tag. In this case :input_text
to <input type="text">
. We still have to write the rendering but this is the one leap that I’ve made and, leaving aside the form details (action
, method
, and submit
), is the only element of a field that is coupled in any way to a concrete HTML representation.
If you have a keen eye, you will notice that what we have done is lift out the form definition as data using a Scrivener object as a base. Once it is encoded as data then it is simply another step to render it to an HTML representation. We’ll take care of that in a future post.
—Thursday 4th March 2021.