Skjema mal

Om koden

I skjema malen er det hovedsaklig vært fokus på å vise hvordan felter kan settes opp. Det er kun tilstandshåndtering og validering på et par av feltene for å vise et eksempel på hvordan man kan gjøre dette. Dette gjelder “adresse”, “postnr”, “poststed” og “hvordan skal du reise?” - spørsmålet om transport. Se koden for sidemal helt nederst.

Kodeeksempel

Påmelding til fjelltur

Meld deg på bedriftsidrettslagets årlige fjelltur til Rondane. Påmeldingsskjemaet har 5 steg, og det tar 3-4 minutter å fylle det ut.

Informasjon om deg


Om turen


Når du krysser av samtykker du til at Oslo kommune behandler personopplysninger elektronisk og formidler opplysninger til de ansatte som arrangerer turen.

Under er et forslag på hvordan man kan implementere tilstandshåndtering og validering i skjemaet i Vue og React.

Se kodeeksempel i fanene for Vue og React
<template>
  <page-main class="pkt-container pkt-container--laptop">
    <h1>Mal for skjema</h1>
    <p class="pkt-txt-22-light">
      Dette er en mal for et skjema. Kun adresse, postnr, poststed og spørsmålet
      om transportmiddel har validering. Dette er kun for å vise et eksempel på
      hvordan de kan valideres. ✨
    </p>

    <div
      :class="[responsivePhabletGrid, 'pkt-grid--gap-size-32', 'pkt-grid--gap-size-48-phablet-up']"
    >
      <div :class="cellClass(12)">
        <h1 class="pkt-txt-28 pkt-txt-54--phablet-up mb-size-24">
          Påmelding til fjelltur
        </h1>
        <p class="pkt-txt-20-light pkt-txt-24-light--phablet-up mb-size-0">
          Meld deg på bedriftsidrettslagets årlige fjelltur til Rondane.
          Påmeldingsskjemaet har 5 steg, og det tar 3-4 minutter å fylle det ut.
        </p>
      </div>

      <PktMessagebox
        :class="cellClass(12)"
        skin="blue"
        title="Før du fyller ut"
      >
        Du kan melde deg på turen dersom du
        <ul>
          <li>har vært fast ansatt i Oslo kommune i mer enn 1 år</li>
          <li>ikke er redd for høyder</li>
          <li>har godt humør</li>
        </ul>
        For å fylle ut skjemaet trenger du dokumentasjon som viser hvor lenge du
        har vært ansatt i Oslo kommune
      </PktMessagebox>
      <section
        :class="[cellClass(10), responsivePhabletGrid, responsiveSectionGap]"
      >
        <h2
          :class="[cellClass(12), 'pkt-txt-22', 'pkt-txt-24--phablet-up', 'mb-size-0']"
        >
          Informasjon om deg
        </h2>
        <PktTextinput id="firstName" label="Fornavn" :class="cellClass(12)" />
        <PktTextinput id="lastName" label="Etternavn" :class="cellClass(12)" />

        <PktTextinput
          id="birthdate"
          label="Fødselsdato (dd.mm.åååå)"
          :class="cellClass(12)"
          placeholder="datepicker"
        />
        <PktTextinput
          id="address"
          label="Adresse"
          :class="cellClass(12)"
          helptext="Adressen trenger vi for å sende deg informasjon om turen."
          @input="(event) => setAddress(event.target.value)"
          @blur="(event) => setAddressError(!isAddressValid(event.target.value))"
          v-model="address"
          errorMessage="Adressen du har skrevet er ikke gyldig. Skriv adressen på nytt."
          :hasError="hasAddressError"
        />
        <div
          :class="[cellClass(12), responsivePhabletGrid, 'pkt-grid--gap-size-8']"
        >
          <PktTextinput
            id="postalCode"
            label="Postnr."
            class="pkt-cell pkt-cell--span4 pkt-cell--span3-tablet-up"
            :hasError="hasPostalCodeError"
            @input="(event) => setPostalCode(event.target.value)"
            @blur="(event) => setPostalCodeError(!isPostalCodeValid(event.target.value))"
            v-model="postalCode"
          />
          <PktTextinput
            id="postalArea"
            label="Poststed"
            class="pkt-cell pkt-cell--span8 pkt-cell--span9-tablet-up"
            :hasError="hasPostalAreaError"
            @input="(event) => setPostalArea(event.target.value)"
            @blur="(event) => setPostalAreaError(!isPostalAreaValid(event.target.value))"
            v-model="postalArea"
          />
          <PktAlert
            v-if="hasPostalCodeError"
            :class="cellClass(12)"
            skin="error"
            aria-live="assertive"
            id="postalCode-error"
            compact
          >
            Postnummeret er ikke gyldig. Du må skrive et postnummer med 4 tegn.
          </PktAlert>
          <PktAlert
            v-if="hasPostalAreaError"
            :class="cellClass(12)"
            skin="error"
            aria-live="assertive"
            id="postalArea-error"
            compact
          >
            Poststedet er ikke gyldig. Du må skrive et poststed med minst 2
            tegn.
          </PktAlert>
        </div>
        <PktSelect
          :class="cellClass(12)"
          label="I hvilken virksomhet jobber du?"
          helptext="Her velger du hvilken virksomhet du jobber i. For eksempel tilhører alle som jobber på en skole Utdanningsetaten."
          id="business"
          v-model="business"
          @change="(event) => setBusiness(event.target.value)"
        >
          <option selected>Velg virksomhet</option>
          <option value="1">Origo</option>
          <option value="2">Bymiljøetaten</option>
          <option value="3">Finn</option>
          <option value="4">Handelsbanken</option>
        </PktSelect>
      </section>

      <hr :class="[cellClass(12), 'pkt-hr']" />

      <section
        :class="[cellClass(10), responsivePhabletGrid, responsiveSectionGap]"
      >
        <h2
          :class="[cellClass(12), 'pkt-txt-22', 'pkt-txt-24--phablet-up', 'mb-size-0']"
        >
          Om turen
        </h2>
        <PktInputWrapper
          :class="cellClass(12)"
          label="Hvordan skal du reise?"
          helptext="Velg alternativet som passer best"
          forId="transportRadioGroup"
          hasFieldset
          :hasError="hasTransportError"
          errorMessage="Du må velge en reisemetode"
        >
          <PktRadiobutton
            v-for="option in transportOptions"
            :key="option.id"
            :id="option.id"
            :label="option.value"
            v-model="transport"
            name="transportMethod"
            :hasError="hasTransportError"
          />
        </PktInputWrapper>
        <PktInputWrapper
          :class="cellClass(12)"
          label="Hva slags rom ønsker du?"
          helptext="Velg romtypen som passer deg best. Du kan velge flere alternativer."
          optionalTag
          optionalText="Valgfritt"
          forId="roomCheckboxGroup"
          hasFieldset
        >
          <PktCheckbox
            id="single-room"
            label="Enkeltrom (koster 200 kr ekstra)"
          />
          <PktCheckbox id="double-room" label="Dobbeltrom" />
          <PktCheckbox id="three-person-room" label="Tremannsrom" />
          <PktCheckbox id="multiple-people" label="Soverom (12 sengeplasser)" />
        </PktInputWrapper>
        <PktTextarea
          :class="cellClass(12)"
          id="foodPreferences"
          label="Har du noen allergier eller matønsker?"
          placeholder="Gluten, laktose, spiser ikke kjøtt etc."
          helptext="Hvorfor spør vi om dette?"
          helptextDropdown="Vi vil sørge for at alle får et godt måltid. Dersom du har allergier eller matønsker, kan du skrive det her. "
        />
        <PktTextinput
          id="uploadFiles"
          label="Last opp dokumentasjon"
          :class="cellClass(12)"
          helptext="Her laster du opp dokumentasjon som viser hvor lenge du har jobbet i kommunen "
          placeholder="Kun et tekstfelt, ingen filopplasting enda. "
        />
      </section>
      <hr :class="[cellClass(12), 'pkt-hr']" />
      <section :class="[cellClass(12), responsivePhabletGrid]">
        <p :class="[cellClass(12), 'pkt-txt-16-light', 'm-size-0']">
          Når du krysser av samtykker du til at Oslo kommune behandler
          personopplysninger elektronisk og formidler opplysninger til de
          ansatte som arrangerer turen.
        </p>
        <PktCheckbox
          :class="cellClass(12)"
          id="checboxDataAgreement"
          label="Jeg samtykker til at Oslo kommune innhenter mine opplysninger"
        />
      </section>
      <PktAlert
        :class="cellClass(12)"
        v-if="hasAnyError"
        skin="error"
        aria-live="assertive"
        id="summary-error"
        title="Vi mangler informasjon fra deg"
      >
        <ul>
          <li v-if="hasAddressError">
            Adressen du har skrevet er ikke gyldig. Skriv adressen inn på nytt
          </li>
          <li v-if="hasPostalAreaError">
            Postnummeret er ikke gyldig. Du må skrive et postnummer med 4 tegn.
          </li>
          <li v-if="hasPostalCodeError">
            Poststedet er ikke gyldig. Du må skrive et poststed med minst 2 tegn
          </li>
          <li v-if="hasTransportError">Du må velge en reisemetode</li>
        </ul>
      </PktAlert>
      <PktButton
        class="pkt-cell pkt-cell--span6-phablet-up pkt-cell--span12"
        skin="primary"
        variant="label-only"
        type="submit"
        :style="{ width: 'fit-content' }"
      >
        Send inn påmelding
      </PktButton>
    </div>
  </page-main>
</template>

<script>
  import PageMain from "@/dev-components/PageMain.vue";
  import { PktAlert } from "@/components/alert";
  import { PktButton } from "@/components/button";
  import { PktCheckbox } from "@/components/checkbox";
  import { PktInputWrapper } from "@/components/inputwrapper";
  import { PktMessagebox } from "@/components/messagebox";
  import { PktRadiobutton } from "@/components/radiobutton";
  import { PktSelect } from "@/components/select";
  import { PktTextarea } from "@/components/textarea";
  import { PktTextinput } from "@/components/textinput";

  export default {
    name: "TemplateForm",
    components: {
      PageMain,
      PktAlert,
      PktButton,
      PktCheckbox,
      PktInputWrapper,
      PktMessagebox,
      PktRadiobutton,
      PktSelect,
      PktTextarea,
      PktTextinput,
    },
    data: () => {
      return {
        responsivePhabletGrid: "pkt-grid pkt-grid--phablet",
        responsiveSectionGap:
          "pkt-grid--gap-size-24 pkt-grid--gap-size-32-phablet-up",
        address: "",
        business: "",
        hasAddressError: false,
        hasPostalAreaError: false,
        hasPostalCodeError: false,
        hasTransportError: false,
        postalArea: "",
        postalCode: "",
        transport: "Buss",
        transportOptions: [
          { id: "bus", value: "Buss" },
          { id: "car", value: "Egen bil" },
          { id: "train", value: "Tog" },
        ],
      };
    },
    computed: {
      hasAnyError() {
        return (
          this.hasAddressError ||
          this.hasPostalAreaError ||
          this.hasPostalCodeError ||
          this.hasTransportError
        );
      },
    },
    methods: {
      // Styling
      cellClass(spanNo) {
        return `pkt-cell pkt-cell--span12 pkt-cell--span${spanNo}-phablet-up`;
      },

      // Validators
      isAddressValid(address) {
        // Enkel regex validering.  Kun tall, bokstaver, mellomrom og ".,-" er tillatt.
        const regex = /^[a-zA-Z0-9\s.,-]*$/;
        console.log(address, regex.test(address));
        return regex.test(address);
      },
      isPostalAreaValid(postalArea) {
        // Tillater kun tall og må være 4 tegn langt.
        const regex = /^[a-zA-ZæøåÆØÅ\s]{2,}$/;
        console.log(postalArea, regex.test(postalArea));
        return regex.test(postalArea);
      },
      isPostalCodeValid(postalCode) {
        // Tillater alle bokstaver og mellomrom, samt æøå og ÆØÅ. Strengen må også være minst 2 tegn lang.
        const regex = /^[0-9]{4}$/;
        console.log(postalCode, regex.test(postalCode));
        return regex.test(postalCode);
      },

      // State handling
      setAddress(address) {
        this.address = address;
      },
      setPostalCode(postalCode) {
        this.postalCode = postalCode;
      },
      setPostalArea(postalArea) {
        this.postalArea = postalArea;
      },
      setBusiness(business) {
        this.business = business;
      },
      // Error handling
      setAddressError(isError) {
        this.hasAddressError = isError;
      },
      setPostalAreaError(isError) {
        this.hasPostalAreaError = isError;
      },
      setPostalCodeError(isError) {
        this.hasPostalCodeError = isError;
      },
    },
  };
</script>
import React, { useEffect } from 'react'
import {
  PktAlert,
  PktButton,
  PktCheckbox,
  PktInputWrapper,
  PktMessagebox,
  PktRadioButton,
  PktSelect,
  PktTextarea,
  PktTextinput,
} from '..'

/\*\*

- TemplateSchema
-
- @description
- Dette er en mal for et skjema. Skjemaet er delt inn i flere seksjoner, og inneholder ulike typer input-felter.
- I en reell applikasjon hadde det vært ideelt og mer leselig å dele opp skjemaet i flere komponenter.
- Man kunne for eksempel hatt en komponent for hver seksjon og kalt de "About You", "About the trip" og "Data agreement".
-
- For å vise et eksempel på hvordan validering kan gjøres i et skjema, har vi lagt til en enkel validering av adressen.
- Ellers er skjemaet uten tilstandshåndtering og validering.
  \*/

const isAddressValid = (address: string): boolean => {
// Enkel regex validering. Kun tall, bokstaver, mellomrom og ".,-" er tillatt.
const regex = /^[a-zA-Z0-9\s.,-]\*$/
return regex.test(address)
}

const isPostalCodeValid = (postalCode: string): boolean => {
// Tillater kun tall og må være 4 tegn langt.
const regex = /^[0-9]{4}$/
return regex.test(postalCode)
}

const isPostalAreaValid = (postalArea: string): boolean => {
// Tillater alle bokstaver og mellomrom, samt æøå og ÆØÅ. Strengen må også være minst 2 tegn lang.
const regex = /^[a-zA-ZæøåÆØÅ\s]{2,}$/
return regex.test(postalArea)
}

export default function TemplateForm() {
  // State
  const [business, setBusiness] = React.useState<string>('')
  const [address, setAddress] = React.useState<string>('')
  const [postalCode, setPostalCode] = React.useState<string>('')
  const [postalArea, setPostalArea] = React.useState<string>('')
  const [hasAddressError, setAddressError] = React.useState<boolean>(false)
  const [hasPostalCodeError, setPostalCodeError] = React.useState<boolean>(false)
  const [hasPostalAreaError, setPostalAreaError] = React.useState<boolean>(false)

/\*\*

- Radio knapp grupper bør alltid ha en default verdi. Dermed
- Dermed vil det ikke være noe behov for å validere om en verdi er valgt.
- For å vise et eksempel på hvordan radioknapper kan valideres,
- har vi satt default verdi til 'hasTransportError' til true. I en reell applikasjon,
- ville dette vært satt til false.
- \*/
  const [transport, setTransport] = React.useState<string>('bus')
  const [hasTransportError, setTransportError] = React.useState<boolean>(true)

// Styles
const cellClass = (spanNo: number) =>
`pkt-cell pkt-cell--span12 pkt-cell--span${spanNo}-phablet-up`
const responsivePhabletGrid = 'pkt-grid pkt-grid--phablet'
const responsiveSectionGap = 'pkt-grid--gap-size-24 pkt-grid--gap-size-32-phablet-up'

const SectionAboutYou = (

<section className={`${cellClass(10)} ${responsivePhabletGrid} ${responsiveSectionGap}`}>
<h2 className={`${cellClass(12)} pkt-txt-22 pkt-txt-24--phablet-up mb-size-0`}>
Informasjon om deg
</h2>
<PktTextinput id="firstName" label="Fornavn" className={cellClass(12)} />
<PktTextinput id="lastName" label="Etternavn" className={cellClass(12)} />
<PktTextinput
        id="birthdate"
        label="Fødselsdato (dd.mm.åååå)"
        className={cellClass(12)}
        placeholder="datepicker"
      />
<PktTextinput
id="address"
label="Adresse"
className={cellClass(12)}
helptext="Adressen trenger vi for å sende deg informasjon om turen."
onChange={(event: React.FocusEvent<HTMLInputElement>) => setAddress(event.target.value)}
onBlur={(event: React.FocusEvent<HTMLInputElement>) =>
setAddressError(!isAddressValid(event.target.value))
}
value={address}
errorMessage="Adressen du har skrevet er ikke gyldig. Skriv adressen på nytt."
hasError={hasAddressError}
/>
<div className={`${cellClass(12)} ${responsivePhabletGrid} pkt-grid--gap-size-8`}>
<PktTextinput
id="postalCode"
label="Postnr."
className="pkt-cell pkt-cell--span4 pkt-cell--span3-tablet-up"
hasError={hasPostalCodeError}
onChange={(event: React.FocusEvent<HTMLInputElement>) =>
setPostalCode(event.target.value)
}
onBlur={(event: React.FocusEvent<HTMLInputElement>) =>
setPostalCodeError(!isPostalCodeValid(event.target.value))
}
value={postalCode}
/>
<PktTextinput
id="postalArea"
label="Poststed"
className="pkt-cell pkt-cell--span8 pkt-cell--span9-tablet-up"
hasError={hasPostalAreaError}
onChange={(event: React.FocusEvent<HTMLInputElement>) =>
setPostalArea(event.target.value)
}
onBlur={(event: React.FocusEvent<HTMLInputElement>) =>
setPostalAreaError(!isPostalAreaValid(event.target.value))
}
value={postalArea}
/>
{hasPostalCodeError && (
<PktAlert
className={`${cellClass(12)}`}
skin="error"
aria-live="assertive"
id={`postalCode-error`}
compact >
Postnummeret er ikke gyldig. Du må skrive et postnummer med 4 tegn.
</PktAlert>
)}
{hasPostalAreaError && (
<PktAlert
className={`${cellClass(12)}`}
skin="error"
aria-live="assertive"
id={`postalArea-error`}
compact >
Poststedet er ikke gyldig. Du må skrive et poststed med minst 2 tegn.
</PktAlert>
)}
</div>

      <PktSelect
        className={cellClass(12)}
        label="I hvilken virksomhet jobber du?"
        helptext="Her velger du hvilken virksomhet du jobber i. For eksempel tilhører alle som jobber på en skole Utdanningsetaten. "
        id="business"
        value={business}
        defaultValue={business}
        onChange={(event: React.ChangeEvent<HTMLSelectElement>) => setBusiness(event.target.value)}
      >
        <option selected>Velg virksomhet</option>
        <option value="1">Origo</option>
        <option value="2">Bymiljøetaten</option>
        <option value="3">Finn</option>
        <option value="4">Handelsbanken</option>
      </PktSelect>
    </section>

)
const transportOptions: { id: string; label: string }[] = [
{ id: 'bus', label: 'Buss' },
{ id: 'car', label: 'Egen bil' },
{ id: 'train', label: 'Tog' },
]

const SectionAboutTheTrip = (

<section className={`${cellClass(10)} ${responsivePhabletGrid} ${responsiveSectionGap}`}>
<h2 className={`${cellClass(12)} pkt-txt-22 pkt-txt-24--phablet-up mb-size-0`}>Om turen</h2>
<PktInputWrapper
        className={cellClass(12)}
        label="Hvordan skal du reise?"
        helptext="Velg alternativet som passer best"
        forId="TransportRadioGroup"
        hasFieldset
        hasError={hasTransportError}
        errorMessage="Du må velge en reisemetode"
      >
{transportOptions.map((option) => (
<PktRadioButton
id={option.id}
label={option.label}
name="selectedTransport"
hasError={hasTransportError}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => setTransport(event.target.id)}
checked={transport === option.id}
/>
))}
</PktInputWrapper>
<PktInputWrapper
        className={cellClass(12)}
        label="Hva slags rom ønsker du?"
        helptext="Velg romtypen som passer deg best. Du kan velge flere alternativer."
        optionalTag
        optionalText="Valgfritt"
        forId="roomCheckboxGroup"
        hasFieldset
      >
<PktCheckbox id="single-room" label="Enkeltrom (koster 200 kr ekstra)" />
<PktCheckbox id="double-room" label="Dobbeltrom" />
<PktCheckbox id="three-person-room" label="Tremannsrom" />
<PktCheckbox id="multiple-people" label="Soverom (12 sengeplasser)" />
</PktInputWrapper>
<PktTextarea
        className={cellClass(12)}
        id="foodPreferences"
        label="Har du noen allergier eller matønsker?"
        placeholder="Gluten, laktose, spiser ikke kjøtt etc."
        helptext="Hvorfor spør vi om dette?"
        helptextDropdown="Vi vil sørge for at alle får et godt måltid. Dersom du har allergier eller matønsker, kan du skrive det her. "
      />
<PktTextinput
        id="uploadFiles"
        label="Last opp dokumentasjon"
        className={cellClass(12)}
        helptext="Her laster du opp dokumentasjon som viser hvor lenge du har jobbet i kommunen "
        placeholder="Kun et tekstfelt, ingen filopplasting enda. "
      />
</section>
)

return (

<>
<main className="page-main pkt-container">
<h1>Mal for skjema</h1>
<p className="pkt-txt-22-light">
Dette er en mal for et skjema. Kun adresse, postnr, poststed og spørsmålet om
transportmiddel har validering. Dette er kun for å vise et eksempel på hvordan de kan
valideres. ✨
</p>

        <div
          className={`${responsivePhabletGrid} pkt-grid--gap-size-32 pkt-grid--gap-size-48-phablet-up`}
        >
          <div className={cellClass(12)}>
            <h1 className="pkt-txt-28 pkt-txt-54--phablet-up mb-size-24">Påmelding til fjelltur</h1>
            <p className="pkt-txt-20-light pkt-txt-24-light--phablet-up mb-size-0">
              Meld deg på bedriftsidrettslagets årlige fjelltur til Rondane. Påmeldingsskjemaet har
              5 steg, og det tar 3-4 minutter å fylle det ut.
            </p>
          </div>
          <PktMessagebox className={cellClass(12)} skin="blue" title="Før du fyller ut">
            Du kan melde deg på turen dersom du
            <ul>
              <li>har vært fast ansatt i Oslo kommune i mer enn 1 år</li>
              <li>ikke er redd for høyder</li>
              <li>har godt humør</li>
            </ul>
            For å fylle ut skjemaet trenger du dokumentasjon som viser hvor lenge du har vært ansatt
            i Oslo kommune
          </PktMessagebox>

          {SectionAboutYou}

          <hr className={`pkt-hr ${cellClass(12)}`} />

          {SectionAboutTheTrip}

          <hr className={`${cellClass(12)} pkt-hr`} />

          <section className={`${cellClass(12)} ${responsivePhabletGrid} `}>
            <p className={`${cellClass(12)} pkt-txt-16-light m-size-0`}>
              Når du krysser av samtykker du til at Oslo kommune behandler personopplysninger
              elektronisk og formidler opplysninger til de ansatte som arrangerer turen.
            </p>
            <PktCheckbox
              className={cellClass(12)}
              id="checboxDataAgreement"
              label="Jeg samtykker til at Oslo kommune innhenter mine opplysninger"
            />
          </section>
          <PktAlert
            className={`${cellClass(12)}`}
            skin="error"
            aria-live="assertive"
            id={`summary-error`}
            title="Vi mangler informasjon fra deg"
          >
            <ul>
              {hasAddressError && (
                <li>Adressen du har skrevet er ikke gyldig. Skriv adressen inn på nytt</li>
              )}
              {hasPostalAreaError && (
                <li>Postnummeret er ikke gyldig. Du må skrive et postnummer med 4 tegn.</li>
              )}
              {hasPostalCodeError && (
                <li>Poststedet er ikke gyldig. Du må skrive et poststed med minst 2 tegn</li>
              )}
              {hasTransportError && <li>Du må velge en reisemetode</li>}
            </ul>
          </PktAlert>
          <PktButton
            className="pkt-cell pkt-cell--span6-phablet-up pkt-cell--span12"
            skin="primary"
            variant="label-only"
            type="submit"
            style={{ width: 'fit-content' }}
          >
            Send inn påmelding
          </PktButton>
        </div>
      </main>
    </>

)
}