Skjema
Skjema-malen inneholder alle skjemaelementene i Punkt, basert på fiktivt innhold. Målet med sidemalen er å lage et utgangspunkt for å sette opp dine egne skjemaer.
Ta i bruk malen
Du finner malen i Figma og i kode. Alt i malen kan endres for å tilpasse din løsning eller tjeneste.
Innhold
Følg retningslinjene for gode skjemaer, vi har laget en hel artikkel om det her.
I eksempelet har vi tatt utgangspunkt i en fiktiv fjelltur til Rondane. Du kan ta utgangspunkt i både plassering og innhold, men husk å tilpasse det til ditt behov eller tjeneste.
Responsivitet
På mobil brekker elementene under hverandre.
Husk på
- Land først på “Hvorfor trenger vi egentlig et skjema?”
- Alt i oppsettet kan justeres, dette er ment som inspirasjon
- Les mer om gode skjemaer i artikkelen om skjemadesign
Implementasjon i kode
I skjema-malen er det hovedsakelig fokus på å vise hvordan felter kan settes opp. Det er lagt til tilstandshåndtering og validering på noen av feltene for å gi noen eksempeler på hvordan dette kan gjøres.
Disse eksemplene kan sees ved feltene for “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.
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">Digitaliseringsetaten</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}
{SectionAboutTheTrip}
<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>
</>
)
}