Files
Reservair/FORMS.md
T
2026-06-16 10:33:05 +02:00

5.8 KiB

Forms

The reservation is mostly created by filling in a form. For that reason this plugin has it's own form definition feature.

The form definition is an array of element it contains. Each element has a type, for example: input-text, input-reservation, etc.

Keep the form structure and content separate -> when working with the form, take time to figure, if you are working with structure, or existence of something within.

Rendering

The rendering currently happens on the backend, but that might be wrong. It renders out structure, but interactive components gets rendered on the frontend anyway.

Submitting

When user submits the form, the RsvFormSubmitter.js is called. It collects the values, which we describe in more detail, and then sends it using fetch as POST to reservations/forms/{form_id}.

Collecting

The collecting is a process with <form> DOM subtree as an input and valid input JSON for the defined form at form_id as output.

We decided to only consider linearized form, therefore an single-dimension array of fields. We find little to no value to define JSON structure in the form itself, but rather define linear array of inputs, where each input value is a JSON itself. We explaing why that is beneficial.

  1. Even atomic values like "hello" or 69 are valid JSON format. This means the collector can assume every value is a JSON and use JSON.parse(input.value).
  2. We can change the form structure, but the output remains the same. The use-case is for example, grouping fields for First and Last name into one row, but still keep them separate attributes in final JSON. On the other hand, the country code and telephone number are two fields on the same row, but should be one attribute in the final JSON. Both cases reflect only in the DOM structure.

Contract

For element to be collected, it has to comply to a contract. Namely, it must have a rsv-form-field class and have a value attribute. The tagging with rsv-form-field class allows that by default, no custom defined component is collected. Therefore you can compose new elements from existing ones and only tag the outer-most as rsv-form-field. For example:

<rsv-reservation-collector class="rsv-form-field">
  <rsv-calendar/>
  
  <rsv-time-slot-selector/>
</rsv-reservation-collector>

If the collector would collect exact elements, like <input>, it would: a) require a register b) be unpredictable c) require some form of filtering

Element handlers

Element handler is a PHP class extending RsvFormElementHandler class. The parent class has two abstract methods: draw & submit. The draw method is called when the form is being rendered on the backend for the user. The caller is the RsvFormRenderer that does not try hard to catch all errors, so be careful with putting logic to draw.

The other method submit is called by the RsvFormProcessor. It definitely should validate the value, but it can also do other things. For example, the element for reservation saves the reservation to the database.

You might have noticed in a reservation example one major flaw. What happens, when any of the next elements fail and cause the whole form unworthy of submission? The error handling itself and propagating back to the user is described later. For now let's focus on handling the error correctly.

We thought of two approaches: separate validation & submission steps and rollback. The first approach will not actually solve the issue. It might eliminate some cases, but sometimes error slips through and cause exception in the submission step. For example suppose creating reservation. The validation step can decide it is okay and the time block is available. But right after that another request creates the reservation in the same time block.

We could either do same validation in the submission step, but then, what is the point of the validation step. Another solution is to create a token for the time block. The second solution requires one important thing, the element must know it is an element, to implement the safety gates correctly.

The second approach is using a rollback, that does so when any of the next elements fail and cause the whole form unworthy of submission. This way only the element handler has to implement the safe gate.

TODO: generalize it using a property the element submission must have

Register

Register is a string to object mapping, that maps element IDs to instantiated objects of the handlers. This allows to customize the handler by changing it's state. For example, there is a handler for input text. The constructor can have a predicate lambda that does the validation and allow simple implementation of email, telephone number or any other validation. Or it can be wrapped with regular expression.

Error handling

When an error occurs, the backend should send back an error response, so that user knowns what he did wrong. The response should contain element's ID, that detected the issue and error ID. The reason why not use a message is for internationalization. The form processor does not know and must not know the user's culture. The internationalization is a separate concern and should be done mapping error ID to a particular message.

Last thing inspired by Problem Details RFC are extensions. The response can contain a dictionary mapping strings to strings that contains structured data about the error. For example, only one time slot of multiple can be occupied. The extensions could contain value of the occupied time slot and the response handler could use it to provide a more detailed error message.

Success handling

Even success must be handled. The user must know that the submission is successfully finished. What is shown is a templated HTML code that is defined by user. It can contain custom elements, like <reservation-summary>.