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.
Du kan melde deg på turen dersom du
- har vært fast ansatt i Oslo kommune i mer enn 1 år
- ikke er redd for høyder
- har godt humør
For å fylle ut skjemaet trenger du dokumentasjon som viser hvor lenge du har vært ansatt i Oslo kommune
Informasjon om deg
Postnummeret er ikke gyldig. Du må skrive et postnummer med 4 tegn.
Poststedet er ikke gyldig. Du må skrive et poststed med minst 2 tegn.
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.
- Adressen du har skrevet er ikke gyldig. Skriv adressen inn på nytt
- Postnummeret er ikke gyldig. Du må skrive et postnummer med 4 tegn.
- Poststedet er ikke gyldig. Du må skrive et poststed med minst 2 tegn
- Du må velge en reisemetode
Send inn påmelding
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>
</>
)
}