Parenteser

Betraktninger fra Mat-teamets grønne enger

Et fint, lite system for skjemaer

I bloggposten om Førsteklasses parametere i praksis lovet jeg å komme tilbake til hvordan vi håndterer skjemaer i klienten vår. Det er nemlig ganske stilig.

Sånn kan et skjema se ut hos oss, etter at styling er strippet bort:

<form data-form-id="skjema/sjekk-adresse">
  <input type="text" name="gate/navn">
  <input type="text" name="adresse/nummer">
  <input type="text" name="adresse/bokstav">
  <input type="text" name="postnummer/nummer">
  <button type="submit">Sjekk adressen</a>
</form>

Som du ser er det et helt vanlig HTML-skjema, bortsett fra at det mangler method og action. Det er fordi vi har tenkt å håndtere dette skjemaet i klienten. Likevel er det ingen onSubmitform-en, eller – gud forby – onClickbutton-en.

†: Det er jo flere måter å submitte en form - slikt som å trykke enter i et av tekstfeltene.

Istedenfor har vi en data-form-id som plukkes opp av maskineriet. Sånn omtrent ser den koden ut:

(defn hijack-form-submits []
  (js/document.body.addEventListener
   "submit"
   (fn [event]
     (when (some-> event .-target (.getAttribute "data-form-id"))
       (.preventDefault event)
       (submission-center/handle-form-submit (.-target event))))))

Det er altså én sentral event handler for alle form submits - og hvis den ser data-form-id-attributtet, så håndterer den eventen selv.

Hver side har sin egen liste med skjemadefinisjoner:

{:page/forms
  [{:form/id :skjema/sjekk-adresse
    :form/fields {:gate/navn {:validations [{:kind :required}]}
                  :adresse/nummer {:validations [{:kind :required}]}
                  :adresse/bokstav {}
                  :postnummer/nummer {:validations [{:kind :required}]}}
    :form/handler #'sjekk-adresse}]}

Det er denne definisjonen som brukes av det sentrale maskineriet til å håndtere eventen.

Attributtet i form-taggen …

<form data-form-id="skjema/sjekk-adresse">

… brukes til å finne skjemaet :skjema/sjekk-adresse over. 👆

Deretter finner maskineriet verdiene i input-felter i skjemaet ved å løpe over new FormData(form) og bygge opp et map:

{:gate/navn "Bingeveien"
 :adresse/nummer 1
 :postnummer/nummer "3158"}

La du merke til at :adresse/nummer er et tall, ikke en streng? Det er fordi alle attributtene blir vasket av systemet vårt for førsteklasses parametere, som sørger for at vi har full kontroll på våre velkjente parameternavn.

Deretter sjekkes alle :validations. Om noen feiler, så blir valideringsfeil tilgjengeligjort som data for UI-koden, slik at den kan vises frem til brukeren. Det kan se omtrent sånn ut:

[:div
 [:input {:type "text" :name "gate/navn"}]
 (when-let [error (submission/get-validation-error state :gate/navn)]
   [:p.error (:text error)])]

Hvis alt går bra, blir endelig :form/handler-funksjonen kalt. I vårt tilfelle, sjekk-adresse:

(defn sjekk-adresse [_ form-data]
  [[:action/query
    {:query/name :adressevask/sjekk-adresse
     :query/kind :query/analyser-adresse
     :query/data form-data}]])

Vi jobber for å holde så mye kode som mulig i den funksjonelle kjernen. Det inkluderer handler-funksjoner for skjemaene våre. Det betyr at funksjonene er rene, og bare returnerer data: en liste med actions som skal gjennomføres av maskineriet.

‡: Vi må åpenbart skrive mer om arkitekturen vår (functional core, imperative shell) på denne bloggen.

I dette tilfellet blir det kjørt en query, som vil hente noe data fra serveren. Adressen blir analysert og sammenstilt med informasjon i kartverket sine adressedata.

Det er mange andre actions tilgjengelige også:

  • :action/execute-command - når man vil sende endringer til serveren
  • :action/go-to-location - navigerer til en annen side
  • :action/copy-to-clipboard - kopierer tekst for brukeren

Det kule her er at dette er akkurat de samme actions som er tilgjengelige for event-handlere i DOM-en, som onClick og onChange. Det føyer seg inn i mønsteret hvor vi prøver å løse ting én gang.

Det kule

Kort fortalt:

  • Vi lener oss på helt standard forms. Det stryker nettleseren medhårs og gir oss god UU.

  • Vi skriver skjemaer med data og rene funksjoner. Det gir oss bedre tester, og flytter bevegelige deler ut til maskineriet.

  • Vi får full glede av førsteklasses, velkjente parametere. Det lar oss unngå masse konverteringskode og håndsøm for kjente greier.

  • Vi bruker de samme byggeklossene som ellers i systemet. Det gjør det lettere å lære, og lettere å skjønne hvordan ting henger sammen.

  • Vi løser problemer sentralt én gang, istedenfor mange steder rundt i koden, hver gang.

Ganske stilig.

Magnar

Om FK/IS, Framsideutvikling og Design