Adressefelt med autocomplete i Vue 3
Skriv en Vue 3-komponent med Composition API, der henter adresseforslag med debounce og afbrudte kald og sender den valgte adresse videre.
Denne guide bygger en single-file component (SFC) til Vue 3 med <script setup> og TypeScript. Komponenten henter forslag fra /autocomplete, håndterer tastaturet og udsender et select-event med den valgte adresse. Der bruges ingen andre biblioteker end Vue.
Nøglen kører i browseren. Den skal derfor være en domænelåst browsernøgle, eller også skal kaldene gå gennem din egen server. Begge dele er beskrevet i Browsernøgler og tilladte domæner og Next.js route handler som proxy. Mønstret med en proxy virker på samme måde i en Nuxt server route.
Nøglen i Vite
En variabel med præfikset VITE_ bliver skrevet ind i bundtet, så alle kan læse den. Det er i orden for en browsernøgle, der er låst til dine domæner. Brug aldrig en ulåst nøgle her.
# .env.local
VITE_DANADRESSE_BROWSER_KEY=dawa_live_din_browsernøgleKomponenten
watch med onCleanup er det, der gør komponenten robust. Hver gang q ændrer sig, kører Vue oprydningen fra den forrige kørsel først. Den stopper timeren (debounce) og afbryder et kald, der stadig er undervejs. Så kan et gammelt, langsomt svar aldrig overskrive et nyere.
<script setup lang="ts"> import { computed, ref, watch } from "vue"; type Forslag = { type: "vejnavn" | "adgangsadresse" | "adresse"; tekst: string; forslagstekst: string; caretpos: number; data: Record<string, any>; }; const emit = defineEmits<{ select: [forslag: Forslag] }>(); const API_KEY = import.meta.env.VITE_DANADRESSE_BROWSER_KEY as string; const uid = `adr-${Math.random().toString(36).slice(2, 8)}`; const q = ref(""); const forslag = ref<Forslag[]>([]); const aktiv = ref(-1); const aaben = ref(false); const fejl = ref(false); const input = ref<HTMLInputElement | null>(null); const vis = computed(() => aaben.value && forslag.value.length > 0); watch(q, (vaerdi, _gammel, onCleanup) => { if (!aaben.value || vaerdi.trim().length < 2) { forslag.value = []; return; } const ctrl = new AbortController(); const timer = setTimeout(async () => { const url = new URL("https://api.danadresse.dk/autocomplete"); url.searchParams.set("q", vaerdi); url.searchParams.set("type", "adresse"); url.searchParams.set("per_side", "8"); try { const res = await fetch(url, { headers: { "X-Api-Key": API_KEY }, signal: ctrl.signal }); if (!res.ok) throw new Error(`HTTP ${res.status}`); forslag.value = await res.json(); fejl.value = false; aktiv.value = -1; } catch (e) { if ((e as Error).name === "AbortError") return; forslag.value = []; fejl.value = true; // feltet virker videre som almindeligt tekstfelt } }, 200); onCleanup(() => { clearTimeout(timer); ctrl.abort(); }); }); function vaelg(f: Forslag) { if (f.type !== "adresse") { // Vejnavn eller opgang: sæt teksten ind og søg videre fra caretpos. q.value = f.tekst; requestAnimationFrame(() => input.value?.setSelectionRange(f.caretpos, f.caretpos)); return; } aaben.value = false; q.value = f.forslagstekst; emit("select", f); } function tast(e: KeyboardEvent) { if (!vis.value) return; const n = forslag.value.length; if (e.key === "ArrowDown") { e.preventDefault(); aktiv.value = (aktiv.value + 1) % n; } else if (e.key === "ArrowUp") { e.preventDefault(); aktiv.value = aktiv.value <= 0 ? n - 1 : aktiv.value - 1; } else if (e.key === "Enter" && aktiv.value >= 0) { e.preventDefault(); vaelg(forslag.value[aktiv.value]); } else if (e.key === "Escape") { aaben.value = false; } } </script> <template> <div class="adr"> <label :for="`${uid}-input`">Adresse</label> <input :id="`${uid}-input`" ref="input" v-model="q" role="combobox" aria-autocomplete="list" :aria-expanded="vis" :aria-controls="`${uid}-list`" :aria-activedescendant="vis && aktiv >= 0 ? `${uid}-${aktiv}` : undefined" autocomplete="off" @input="aaben = true" @keydown="tast" @blur="aaben = false" /> <ul v-if="vis" :id="`${uid}-list`" role="listbox" aria-label="Adresseforslag"> <li v-for="(f, i) in forslag" :id="`${uid}-${i}`" :key="f.data.id ?? f.tekst" role="option" :aria-selected="i === aktiv" @mousedown.prevent="vaelg(f)" > {{ f.forslagstekst }} </li> </ul> <div role="status" aria-live="polite" class="sr-only"> {{ fejl ? "Adresseopslag er utilgængeligt. Skriv adressen manuelt." : vis ? `${forslag.length} forslag` : "" }} </div> </div> </template>
Et par detaljer:
@input="aaben = true"sætter kun listen til åben ved brugerens indtastning. Nårvaelgselv ændrerqefter et endeligt valg, eraabenfalsk, og der laves intet ekstra kald.@mousedown.preventvælger, før feltet mister fokus. Et@clickville komme efter@blur, og så er listen væk.aria-activedescendantpeger på den markeredeli, så skærmlæsere læser forslaget op, mens fokus bliver i feltet. Se Tilgængelig adresse-combobox.
Brug komponenten
Lyt efter select, og gem både teksten og adressens id:
<script setup lang="ts"> import { reactive } from "vue"; import AdresseFelt from "./AdresseFelt.vue"; const ordre = reactive({ adresse: "", adresseId: "", postnr: "", by: "" }); function naarValgt(f: any) { ordre.adresse = f.forslagstekst; ordre.adresseId = f.data.id; ordre.postnr = f.data.postnr; ordre.by = f.data.postnrnavn; } </script> <template> <AdresseFelt @select="naarValgt" /> </template>
data.id er UUID'en for adressen. Den ændrer sig ikke, selv om vejen skifter navn, så den er det rigtige at gemme i din database.
Det kald, komponenten laver
Kør samme forespørgsel fra terminalen, hvis du skal fejlsøge. Svarer den her, men ikke i browseren, er det næsten altid nøglens domæneliste:
curl -s "https://api.danadresse.dk/autocomplete?q=R%C3%A5dhuspladsen%201&type=adresse&per_side=8" \ -H "X-Api-Key: $DANADRESSE_API_KEY"
Grænser i browseren
En browsernøgle bruger kontoens kvote og minutgrænse. Derudover har hver besøgende sin egen grænse på 120 kald i minuttet, og overskrides den, svarer API'et 429 med koden rate_limit_per_visitor. Med 200 ms debounce når en almindelig bruger ikke derop.
Fejlsvar har CORS-headere, så komponenten ser den rigtige status og JSON-svaret, også ved 403 og 500. Retry-After og headerne X-RateLimit-*, X-RateLimit-Minute-* og X-Quota-* kan også læses fra JavaScript. Ved en 429 rate_limit_per_visitor venter du res.headers.get("Retry-After") sekunder før næste kald, viser en kort besked og lader feltet virke som tekstfelt, ligesom komponenten gør ved andre fejl.
Næste skridt
- Browsernøgler og tilladte domæner
- Adressefelt i React — samme logik som hook.
- Tilgængelig adresse-combobox
- Oversigt: /vue-adresse-autocomplete.
Relaterede guides
Markdown-kilde Fejl i guiden? Skriv til os API-reference (Swagger)