Übungsaufgaben zu Fahrer-Sicht und Coding Standards in Vue: eigene Mitfahrgesuche verwalten

https://bildung.social/@oerinformatik/

https://oer-informatik.de/vue-fahrer-sicht-coding-standards-uebungsaufgaben

Vue 3, TypeScript, Rollen, PATCH, ESLint, Prettier, vue-tsc, Coding Standards, Collective Ownership, Vitest, Übungsaufgaben, Prüfungsvorbereitung

tl/dr; (ca. 50 min Lese- und Bearbeitungszeit): In der Web-App des Mitfahrlabors sollen Mitfahrende ihre eigenen Gesuche sehen und offene Gesuche zurückziehen können. In diesen Übungen ordnest Du Regeln und Befunde den Werkzeugen Prettier, ESLint und vue-tsc sowie dem Backend zu. Danach entwickelst Du die Regeln für eigene Gesuche als reine Funktionen und das Zurückziehen per PATCH mit einer Komponente, jeweils mit Tests. Zum Schluss behebst Du die Lint- und Typbefunde in der Komponente eines anderen Paars und begründest jede Änderung [AI-Assisted]. (Zuletzt geändert am 11.11.2026)

Dieser Artikel ist Teil einer Artikelreihe über die Web-App mit Vue 3 und TypeScript: Projektübersicht / XP-Projektstart / TypeScript für Java-Entwickler*innen / Komponenten testgetrieben / API-Client aus OpenAPI / Router und Continuous Integration / Formulare und Validierung / Refactoring mit Pinia / Anmeldung mit Keycloak / Fahrer-Sicht und Coding Standards / Build und Auslieferung / End-to-End-Tests mit Playwright / Change Request Live-Aktualisierung

Überblick: Ein Paar am Rechner schreibt Code im Pair Programming, der Editor formatiert beim Speichern. Die Pipeline prüft Form, Lint-Regeln, Typen und Tests, die Oberfläche blendet Buttons je nach Rolle ein und aus, das Backend prüft jede Anfrage selbst (mit KI erstellt)

Diese Übungsaufgaben gehören zum Artikel Fahrer-Sicht in Vue: Rollen, Coding Standards und Collective Ownership. Alle Aufgaben spielen im Projektteam der Mitfahr-App Mitfahrlabor. Ausgangspunkt für die Aufgaben 2.1, 2.2 und 3.1 ist das Projekt mitfahrlabor-web im Ordner src/09-vue-fahrer-sicht-coding-standards des Repositorys dieser Reihe mit dem Store useSitzungStore (Feld rollen), den Testkonten in src/mocks/anmeldung.ts, Prettier und den Teamregeln in eslint.config.ts. Was für die Gesuche fehlt, steht vollständig in den Aufgaben; die Lösungen der Übungen zu den vorigen Artikeln brauchst Du nicht.

Für die Gesuche nimmt das Team zwei Endpunkte an, die das Backend in einer späteren Version bekommen soll: GET /v1/gesuche liefert alle Gesuche, PATCH /v1/gesuche/{gesuchId} mit dem Body {"status": "zurueckgezogen"} zieht ein Gesuch zurück. Zurückziehen darf nur die Person, die das Gesuch aufgegeben hat; sonst antwortet das Backend mit 403, ohne Token mit 401, bei Erfolg mit 200 und dem geänderten Gesuch.

Hinweise zur Bearbeitung: Bis zur Teil-1-Prüfung der IT-Berufe solltest Du die Basiskompetenz beherrschen, also Aufgabe 1.1. Als FIA, FIDP oder FIDV musst Du zur Teil-2-Prüfung Aufgaben auf Kernkompetenz-Ebene lösen können, das sind Aufgabe 2.1 und Aufgabe 2.2. Von FIA wird zusätzlich erwartet, dass ihnen Aufgaben auf Profilkompetenz-Ebene (Aufgabe 3.1) keine Probleme bereiten. Die Operatoren (zuordnen, vorhersagen, entwickeln, beheben, begründen …) zeigen, wie ausführlich Du antworten sollst. Code schreibst Du in TypeScript bzw. als Single-File-Component von Vue 3 und prüfst ihn mit npm test, npm run type-check, npm run lint und npm run format:check. Schreib Deine Lösung zuerst vollständig auf, bevor Du sie mit der Beispiellösung vergleichst.

Du möchtest eigene Aufgaben zum Üben von einem KI-Sprachmodell generieren lassen?

Kopiere den Text in ein KI-Sprachmodell Deiner Wahl und passe Kompetenzstufe, Anzahl, Bearbeitungszeit und Szenario bei Bedarf an. Wie die Aufgaben aufgebaut sind, beschreibt der Artikel Wie sind die Aufgaben aufgebaut?. Prüfe die Ergebnisse mit dem Lehrartikel: KI-Sprachmodelle machen Fehler.

Erstelle neue Übungsaufgaben zum Thema „Fahrer-Sicht und Coding
Standards in Vue: eigene Mitfahrgesuche verwalten“.

Bauplan: Halte Dich an Aufbau, Kompetenzstufen, Operatoren,
Anforderungsbereiche, Punkte und Zeitschätzung, die im Artikel
https://oer-informatik.de/aufgaben-aufbau beschrieben sind.

Fachliche Grundlage:
- https://oer-informatik.de/vue-fahrer-sicht-coding-standards
- https://oer-informatik.de/szenario-mitfahrlabor
- https://oer-informatik.de/extreme-programming
- Stilbeispiel: https://oer-informatik.de/vue-fahrer-sicht-coding-standards-uebungsaufgaben

Die Aufgaben sollen Inhalte zu diesen Schlagwörtern abdecken:
- Vue 3
- TypeScript
- Rollen
- PATCH
- ESLint
- Prettier
- vue-tsc
- Coding Standards
- Collective Ownership
- Vitest

Kompetenzstufe: Kernkompetenz (alternativ: Basiskompetenz, Profilkompetenz, Expertise)
Anzahl: 1 Aufgabe mit Teilaufgaben
Bearbeitungszeit in Minuten ca.: <5 bis 30 min>
Szenario: eines der Szenarien aus dem Bauplan

Vorgehen:
1. Lies zuerst die verlinkten Artikel. Kannst Du keine Webseiten
   abrufen, sag mir das, statt zu raten.
2. Steht bei einer Angabe noch ein Platzhalter in <…>, frag mich
   zuerst danach. Richte Punkte und Zahl der Teilaufgaben nach der
   Bearbeitungszeit (ca. 0,9 min je Punkt).
3. Gib je Aufgabe Titel mit Kompetenzstufe, Ausgangssituation und
   Teilaufgaben mit Operator und Punktzahl aus, dazu die geschätzte
   Bearbeitungszeit.
4. Gib die Beispiellösungen mit Punkteverteilung erst aus, wenn ich
   danach frage.

Aufgabe 1.1: Wer findet was? (Basiskompetenz)

Im Projekt mitfahrlabor-web prüfen Prettier, ESLint mit den Teamregeln, vue-tsc und das Backend. In der Pipeline laufen die drei Werkzeuge im Job lint.

a) Ordne jeder Beobachtung zu, welche Stelle sie zuerst meldet oder verhindert: Prettier (P), ESLint (E), vue-tsc (T) oder das Backend (B). (6 Punkte)

Beobachtungen zuordnen
Beobachtung Stelle
1: In einer Komponente steht if (gesuch.status == 'offen').
2: Eine Datei ist mit vier Leerzeichen statt zwei eingerückt.
3: Ein Aufruf übergibt { status: 'erledigt' }, der Typ erlaubt nur 'zurueckgezogen'.
4: Kaffeetasse schickt mit curl und ihrem Token ein PATCH für das Gesuch von Rotkehlchen.
5: Eine Liste nutzt v-for ohne :key.
6: Eine Funktion hat einen Parameter vom Typ any.
Zuordnung der Beobachtungen
Beobachtung Stelle Grund
1: == E Teamregel eqeqeq
2: Einrückung P reine Form
3: falscher Wert für status T Typfehler (TS2322)
4: fremdes Gesuch per curl B 403, die Oberfläche ist umgangen
5: v-for ohne :key E vue/require-v-for-key
6: any E @typescript-eslint/no-explicit-any

Bewertung: je richtige Zuordnung 1 Punkt. Bei 6 ist T nicht richtig: vue-tsc akzeptiert any, gerade deshalb verbietet es die Teamregel.

b) Sag voraus, was die Pipeline meldet, wenn ein Paar eine Datei mit falscher Einrückung, aber ohne andere Fehler pusht, und ob die Pipeline die Datei korrigiert. (2 Punkte)

Der Job lint scheitert schon beim ersten Schritt npm run format:check mit „Code style issues found in the above file“ (1 Punkt). Die Pipeline korrigiert nichts, sie prüft nur; das Paar führt npm run format aus und pusht erneut (1 Punkt).

c) Erkläre, warum es keine Rolle „FAHRER“ im Token gibt, obwohl nur Fahrer*innen Anfragen bestätigen dürfen. (2 Punkte)

Ob jemand fährt, hängt nicht am Konto, sondern an der einzelnen Fahrt: Dieselbe Person fährt Angebot 4 und fährt bei Angebot 1 mit (1 Punkt). Das ist eine Beziehung zum Datensatz (fahrerId), die das Backend je Anfrage aus der Datenbank prüft; eine Rolle im Token gilt dagegen überall (1 Punkt).

Aufgabe 2.1: Regeln für eigene Gesuche (Kernkompetenz)

Die Seite „Meine Gesuche“ soll nur die Gesuche der angemeldeten Person zeigen und nur bei offenen eigenen Gesuchen einen Button „Zurückziehen“ anbieten. Wie bei den Angeboten stehen diese Regeln als reine Funktionen in der Domänenschicht. Lege dazu die Datei src/domain/gesuche.ts mit diesem Startcode an:

import type { Benutzer } from '@/auth/anmeldung'
import type { Richtung } from './typen'

export type GesuchStatus = 'offen' | 'vermittelt' | 'zurueckgezogen' | 'abgelaufen'

/** Mitfahrgesuch, wie es GET /v1/gesuche liefert (spätere Version der API) */
export interface Gesuch {
  id: number
  mitfahrerId: number
  haltepunktId: number
  richtung: Richtung
  datum: string // JJJJ-MM-TT
  zeitVon: string // HH:MM
  zeitBis: string // HH:MM
  status: GesuchStatus
}

/** Beispieldaten für die Tests (fiktiv) */
// prettier-ignore
export const beispielGesuche: readonly Gesuch[] = [
  { id: 2, mitfahrerId: 3, haltepunktId: 3, richtung: 'zur-schule', datum: '2026-11-23', zeitVon: '07:00', zeitBis: '07:20', status: 'offen' },
  { id: 6, mitfahrerId: 3, haltepunktId: 3, richtung: 'zur-schule', datum: '2026-11-26', zeitVon: '07:00', zeitBis: '07:30', status: 'offen' },
  { id: 7, mitfahrerId: 8, haltepunktId: 1, richtung: 'zur-schule', datum: '2026-11-26', zeitVon: '06:40', zeitBis: '07:10', status: 'offen' },
  { id: 8, mitfahrerId: 3, haltepunktId: 1, richtung: 'nach-hause', datum: '2026-11-23', zeitVon: '15:00', zeitBis: '16:00', status: 'zurueckgezogen' },
]

Kaffeetasse hat die Nutzer-ID 3, Rotkehlchen die Nutzer-ID 8.

a) Entwickle in src/domain/gesuche.ts die Funktionen istEigenesGesuch(gesuch, benutzer), darfZurueckziehen(gesuch, benutzer) (nur eigene, nur offene Gesuche) und eigeneGesuche(gesuche, benutzer) (eigene Gesuche, späteste Fahrt zuerst). benutzer ist jeweils vom Typ Benutzer | null. (5 Punkte)

/** Gesuch gehört der angemeldeten Person (Beziehung, keine Rolle) */
export function istEigenesGesuch(gesuch: Gesuch, benutzer: Benutzer | null): boolean {
  return benutzer !== null && gesuch.mitfahrerId === benutzer.nutzerId
}

/** Zurückziehen: nur eigene und nur offene Gesuche */
export function darfZurueckziehen(gesuch: Gesuch, benutzer: Benutzer | null): boolean {
  return istEigenesGesuch(gesuch, benutzer) && gesuch.status === 'offen'
}

/** „Meine Gesuche“: eigene Gesuche, späteste Fahrt zuerst */
export function eigeneGesuche(gesuche: readonly Gesuch[], benutzer: Benutzer | null): Gesuch[] {
  return gesuche
    .filter((gesuch) => istEigenesGesuch(gesuch, benutzer))
    .sort((a, b) => b.datum.localeCompare(a.datum) || b.zeitVon.localeCompare(a.zeitVon))
}

Bewertung: istEigenesGesuch mit Vergleich der IDs und Behandlung von null (2 Punkte), darfZurueckziehen mit Status (1 Punkt), eigeneGesuche mit Filter (1 Punkt) und Sortierung nach Datum und Uhrzeit absteigend (1 Punkt). filter() liefert ein neues Array; sort() direkt auf gesuche ginge bei readonly nicht durch die Typprüfung.

b) Entwickle in src/__tests__/gesuche.spec.ts drei Tests: Kaffeetasse sieht ihre Gesuche in der Reihenfolge 6, 8, 2 und nicht Gesuch 7; nur offene eigene Gesuche lassen sich zurückziehen (mindestens je ein Fall für „eigenes offenes“, „eigenes zurückgezogenes“ und „fremdes“ Gesuch); ohne Anmeldung gehört niemandem ein Gesuch. Nutze KAFFEETASSE und testBenutzer() aus src/mocks/anmeldung.ts. (6 Punkte)

import { describe, it, expect } from 'vitest'
import { beispielGesuche, darfZurueckziehen, eigeneGesuche, istEigenesGesuch } from '@/domain/gesuche'
import { KAFFEETASSE, testBenutzer } from '@/mocks/anmeldung'

const gesuch = (id: number) => beispielGesuche.find((g) => g.id === id)!

describe('Gesuche einer Person', () => {
  it('Kaffeetasse sieht ihre Gesuche 6, 8 und 2, späteste Fahrt zuerst, nicht aber Gesuch 7', () => {
    expect(eigeneGesuche(beispielGesuche, KAFFEETASSE).map((g) => g.id)).toEqual([6, 8, 2])
  })

  it('nur offene eigene Gesuche lassen sich zurückziehen', () => {
    expect(darfZurueckziehen(gesuch(6), KAFFEETASSE)).toBe(true)
    expect(darfZurueckziehen(gesuch(8), KAFFEETASSE)).toBe(false) // schon zurückgezogen
    expect(darfZurueckziehen(gesuch(7), KAFFEETASSE)).toBe(false) // gehört Rotkehlchen
    expect(darfZurueckziehen(gesuch(7), testBenutzer(8, 'Rotkehlchen'))).toBe(true)
  })

  it('ohne Anmeldung gehört niemandem etwas', () => {
    expect(istEigenesGesuch(gesuch(6), null)).toBe(false)
    expect(eigeneGesuche(beispielGesuche, null)).toEqual([])
  })
})

Bewertung: je Test 2 Punkte (1 Punkt für sinnvolle Testdaten, 1 Punkt für die richtige Erwartung). Gesuch 8 (23.11., 15:00) steht vor Gesuch 2 (23.11., 07:00), weil bei gleichem Datum die spätere Uhrzeit zuerst kommt.

Aufgabe 2.2: Ein Gesuch zurückziehen (Kernkompetenz)

Aufbauend auf Aufgabe 2.1 bekommt jedes Gesuch in der Liste eine Zeile GesuchZeile.vue. Für die Anfrage an die API nutzt das Team wie bei gibGesuchAuf() ein einfaches fetch, weil der Endpunkt noch nicht im Vertrag steht. Ergänze in src/domain/gesuche.ts:

export type ZurueckziehenErgebnis =
  | { art: 'zurueckgezogen'; gesuch: Gesuch }
  | { art: 'nicht-angemeldet' }
  | { art: 'abgelehnt'; status: number }

a) Entwickle die Funktion ziehGesuchZurueck(gesuchId: number, holeToken: () => string | undefined, basis = '/v1'): Promise<ZurueckziehenErgebnis>. Sie schickt PATCH {basis}/gesuche/{gesuchId} mit dem Body {"status": "zurueckgezogen"} und dem aktuellen Access Token, wertet Erfolg, 401 und alle anderen Statuscodes aus und schickt ohne Token gar keine Anfrage. (5 Punkte)

/** PATCH /v1/gesuche/{gesuchId} mit {"status":"zurueckgezogen"} und dem aktuellen Access Token */
export async function ziehGesuchZurueck(
  gesuchId: number,
  holeToken: () => string | undefined,
  basis = '/v1',
): Promise<ZurueckziehenErgebnis> {
  const token = holeToken()
  if (token === undefined) {
    return { art: 'nicht-angemeldet' }
  }
  const antwort = await fetch(`${basis}/gesuche/${gesuchId}`, {
    method: 'PATCH',
    headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
    body: JSON.stringify({ status: 'zurueckgezogen' }),
  })
  if (antwort.ok) {
    return { art: 'zurueckgezogen', gesuch: (await antwort.json()) as Gesuch }
  }
  if (antwort.status === 401) {
    return { art: 'nicht-angemeldet' }
  }
  return { art: 'abgelehnt', status: antwort.status }
}

Bewertung: ohne Token keine Anfrage (1 Punkt), Methode PATCH, Pfad und Body (1 Punkt), Header Authorization und Content-Type (1 Punkt), Erfolg mit dem Gesuch aus der Antwort (1 Punkt), 401 und andere Statuscodes getrennt (1 Punkt).

b) Entwickle die Komponente src/components/GesuchZeile.vue mit den Props gesuch: Gesuch und basis?: string. Sie zeigt Datum und Zeitfenster, den Button „Zurückziehen“ nur, wenn darfZurueckziehen() mit der angemeldeten Person aus useSitzungStore gilt, meldet einen Erfolg mit dem Ereignis zurueckgezogen (Nutzlast: das geänderte Gesuch) und zeigt bei 403 eine verständliche Meldung. Die Teamregeln aus eslint.config.ts müssen erfüllt sein. (6 Punkte)

<script setup lang="ts">
import { computed, ref } from 'vue'
import { darfZurueckziehen, ziehGesuchZurueck, type Gesuch } from '@/domain/gesuche'
import { useSitzungStore } from '@/stores/sitzung'

const props = defineProps<{ gesuch: Gesuch; basis?: string }>()
const emit = defineEmits<{ zurueckgezogen: [gesuch: Gesuch] }>()
const sitzung = useSitzungStore()

const meldung = ref('')
const sendet = ref(false)
const zeigeButton = computed(() => darfZurueckziehen(props.gesuch, sitzung.benutzer))

async function zurueckziehen() {
  sendet.value = true
  const ergebnis = await ziehGesuchZurueck(props.gesuch.id, () => sitzung.accessToken, props.basis)
  sendet.value = false
  if (ergebnis.art === 'zurueckgezogen') {
    emit('zurueckgezogen', ergebnis.gesuch)
  } else if (ergebnis.art === 'abgelehnt' && ergebnis.status === 403) {
    meldung.value = 'Dieses Gesuch kannst nur Du zurückziehen, wenn es Deins ist.'
  } else {
    meldung.value = 'Das hat nicht geklappt. Bitte versuche es später noch einmal.'
  }
}
</script>

<template>
  <li class="list-group-item d-flex gap-2 align-items-center">
    <span class="me-auto">{{ gesuch.datum }}, {{ gesuch.zeitVon }}–{{ gesuch.zeitBis }} Uhr</span>
    <button
      v-if="zeigeButton"
      type="button"
      class="btn btn-outline-secondary btn-sm"
      :disabled="sendet"
      @click="zurueckziehen"
    >
      Zurückziehen
    </button>
    <span v-if="meldung" role="alert" class="text-danger">{{ meldung }}</span>
  </li>
</template>

Bewertung: Props und Ereignis mit Typen in defineProps/defineEmits (Teamregel vue/require-explicit-emits) (2 Punkte), Button nur bei darfZurueckziehen mit der Person aus dem Store (2 Punkte), Erfolg als Ereignis mit Nutzlast (1 Punkt), Meldung bei 403 bzw. sonstigem Fehler (1 Punkt). Den Button während der Anfrage zu sperren, ist sinnvoll, aber nicht verlangt.

c) Entwickle in src/__tests__/gesuchzurueckziehen.spec.ts Tests mit MSW: Kaffeetasse zieht Gesuch 6 zurück und erhält das Gesuch mit Status zurueckgezogen; für Gesuch 7 meldet die Funktion 403; die Komponente zeigt bei Gesuch 7 keinen Button und meldet bei Gesuch 6 nach dem Klick das Ereignis. Bilde den Endpunkt mit server.use() nach; API, angemeldeteId() und nichtAngemeldet() stehen in src/mocks/handlers.ts. (6 Punkte)

import { describe, it, expect, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import { http, HttpResponse } from 'msw'
import { server } from '@/mocks/server'
import { API, angemeldeteId, nichtAngemeldet } from '@/mocks/handlers'
import { angemeldetesPinia, KAFFEETASSE } from '@/mocks/anmeldung'
import { beispielGesuche, ziehGesuchZurueck } from '@/domain/gesuche'
import GesuchZeile from '@/components/GesuchZeile.vue'

const gesuch = (id: number) => beispielGesuche.find((g) => g.id === id)!

// nachgebildeter Endpunkt der späteren API-Version: nur die Person, die sucht, darf zurückziehen
function gesucheEndpunkt() {
  server.use(
    http.patch(`${API}/gesuche/:gesuchId`, async ({ params, request }) => {
      const id = angemeldeteId(request)
      if (id === undefined) return nichtAngemeldet()
      const vorhanden = gesuch(Number(params.gesuchId))
      if (vorhanden.mitfahrerId !== id) return HttpResponse.json({ status: 403 }, { status: 403 })
      const { status } = (await request.json()) as { status: string }
      return HttpResponse.json({ ...vorhanden, status })
    }),
  )
}

describe('Gesuch zurückziehen', () => {
  it('schickt PATCH mit Token und meldet das geänderte Gesuch', async () => {
    gesucheEndpunkt()
    const ergebnis = await ziehGesuchZurueck(6, () => KAFFEETASSE.accessToken, API)
    expect(ergebnis).toEqual({ art: 'zurueckgezogen', gesuch: { ...gesuch(6), status: 'zurueckgezogen' } })
  })

  it('meldet 403, wenn es nicht das eigene Gesuch ist', async () => {
    gesucheEndpunkt()
    expect(await ziehGesuchZurueck(7, () => KAFFEETASSE.accessToken, API)).toEqual({ art: 'abgelehnt', status: 403 })
  })

  it('zeigt den Button nur am eigenen offenen Gesuch und meldet das Ergebnis per Ereignis', async () => {
    gesucheEndpunkt()
    angemeldetesPinia(KAFFEETASSE)
    const fremd = mount(GesuchZeile, { props: { gesuch: gesuch(7), basis: API } })
    expect(fremd.find('button').exists()).toBe(false)

    const eigen = mount(GesuchZeile, { props: { gesuch: gesuch(6), basis: API } })
    await eigen.get('button').trigger('click')
    await vi.waitFor(() => expect(eigen.emitted('zurueckgezogen')).toHaveLength(1))
    expect(eigen.emitted('zurueckgezogen')![0]![0]).toMatchObject({ id: 6, status: 'zurueckgezogen' })
  })
})

Bewertung: nachgebildeter Endpunkt mit Prüfung von Token und Besitz (2 Punkte), Erfolgsfall (1 Punkt), 403-Fall (1 Punkt), Komponententest mit beiden Gesuchen und Ereignis (2 Punkte). Der 403-Test zeigt, dass die Funktion auch dann richtig reagiert, wenn jemand die Oberfläche umgeht.

Aufgabe 3.1: Lint-Befunde in fremdem Code (Profilkompetenz)

Ein anderes Paar hat für die Seite „Meine Gesuche“ die Komponente src/views/GesuchListe.vue geschrieben und vergessen, vor dem Push npm run lint laufen zu lassen. Die Liste soll die offenen eigenen Gesuche zeigen; steht in der Adresse ?gesuch=6, wird Gesuch 6 hervorgehoben, und ein Klick meldet das gewählte Gesuch an die Seite. Ihr übernehmt die Karte am nächsten Tag (Collective Ownership). Der Code enthält absichtlich Fehler.

<script setup lang="ts">
import { computed, ref } from 'vue'
import { useRoute } from 'vue-router'
import { beispielGesuche, type Gesuch } from '@/domain/gesuche'
import { useSitzungStore } from '@/stores/sitzung'

const sitzung = useSitzungStore()
const route = useRoute()
const auswahl = ref<Gesuch>()

const gesuche = computed(() => beispielGesuche.filter((g) => g.mitfahrerId == sitzung.nutzerId))
const hervorgehoben = computed(() => gesuche.value.find((g: any) => g.id == route.query.gesuch))

function waehle(gesuch: Gesuch) {
  console.log('gewählt', gesuch)
  auswahl.value = gesuch
  emit('gewaehlt', gesuch)
}
</script>

<template>
  <ul class="list-group">
    <li v-for="gesuch in gesuche" v-if="gesuch.status === 'offen'" class="list-group-item">
      <a href="#" @click.prevent="waehle(gesuch)">{{ gesuch.datum }}, {{ gesuch.zeitVon }} Uhr</a>
      <strong v-if="gesuch === hervorgehoben">(aus dem Link)</strong>
    </li>
  </ul>
</template>

Die Pipeline meldet im Job lint (gekürzt):

> eslint .

src/views/GesuchListe.vue
  11:76  error    Expected '===' and instead saw '=='                                     eqeqeq
  12:61  error    Unexpected any. Specify a different type                                @typescript-eslint/no-explicit-any
  12:74  error    Expected '===' and instead saw '=='                                     eqeqeq
  15:3   warning  Unexpected console statement. Only these console methods are allowed: warn, error  no-console
  23:5   error    Elements in iteration expect to have 'v-bind:key' directives            vue/require-v-for-key
  23:35  error    The 'gesuche' variable inside 'v-for' directive should be replaced with a computed property that returns filtered array instead. You should not mix 'v-for' with 'v-if'  vue/no-use-v-if-with-v-for

X 6 problems (5 errors, 1 warning)

> vue-tsc --build

src/views/GesuchListe.vue(17,3): error TS2304: Cannot find name 'emit'.
src/views/GesuchListe.vue(23,41): error TS2339: Property 'gesuch' does not exist on type 'ComponentPublicInstance<…>'.

a) Analysiere die Befunde: Fasse sie zu Ursachen zusammen und erläutere für jede Ursache, welches Fehlverhalten im Browser droht oder warum die Regel trotzdem sinnvoll ist. (6 Punkte)

Befunde nach Ursachen
Ursache Befunde Folge bzw. Grund
v-if und v-for am selben Element 23:35, TS2339 In Vue 3 wird v-if vor v-for ausgewertet; gesuch gibt es dort noch nicht. Die Seite bricht ab bzw. zeigt nichts (vue-tsc meldet die unbekannte Variable).
emit nicht definiert TS2304 Der Klick wirft einen Fehler zur Laufzeit; die Seite erfährt nie, welches Gesuch gewählt ist. Ereignisse gehören in defineEmits.
== statt ===, dazu any 11:76, 12:61, 12:74 == wandelt Typen um und verdeckt hier, dass route.query.gesuch ein Text ist und g.id eine Zahl; any schaltet die Typprüfung ab, sodass vue-tsc das nicht bemerkt.
fehlender :key 23:5 Vue kann Einträge beim Neusortieren nicht zuordnen und verwendet Elemente falsch wieder (z. B. Zustand in der falschen Zeile).
console.log 15:3 Kein Fehlverhalten, aber Daten (Gesuche) in der Konsole und Rauschen beim Suchen nach echten Meldungen.

Bewertung: je Ursache mit zutreffender Folge 1 Punkt, für die Ursachen „v-if/v-for“ und „emit“ je 1 weiterer Punkt für den Hinweis, dass es sich um echte Laufzeitfehler handelt (höchstens 6 Punkte). auswahl als überflüssige Variable zu erkennen, ist ein guter Zusatz.

b) Behebe alle Befunde, sodass npm run lint und npm run type-check keine Meldung mehr zeigen und die Komponente tut, was sie soll. Achte darauf, dass das Hervorheben aus der Adresse danach noch funktioniert. (7 Punkte)

<script setup lang="ts">
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import { beispielGesuche, type Gesuch } from '@/domain/gesuche'
import { useSitzungStore } from '@/stores/sitzung'

const emit = defineEmits<{ gewaehlt: [gesuch: Gesuch] }>()
const sitzung = useSitzungStore()
const route = useRoute()

// Filter im computed statt v-if neben v-for; === mit Zahlen auf beiden Seiten
const offeneGesuche = computed(() =>
  beispielGesuche.filter((g) => g.mitfahrerId === sitzung.nutzerId && g.status === 'offen'),
)
const hervorgehoben = computed(() => offeneGesuche.value.find((g) => g.id === Number(route.query.gesuch)))

function waehle(gesuch: Gesuch) {
  emit('gewaehlt', gesuch)
}
</script>

<template>
  <ul class="list-group">
    <li v-for="gesuch in offeneGesuche" :key="gesuch.id" class="list-group-item">
      <a href="#" @click.prevent="waehle(gesuch)">{{ gesuch.datum }}, {{ gesuch.zeitVon }} Uhr</a>
      <strong v-if="gesuch === hervorgehoben">(aus dem Link)</strong>
    </li>
  </ul>
</template>

Bewertung: defineEmits mit Typ (1 Punkt), Filter für den Status ins computed und v-if entfernt (2 Punkte), :key mit der ID (1 Punkt), === mit Number(route.query.gesuch) und ohne any (2 Punkte), console.log und überflüssiges auswahl entfernt (1 Punkt).

Die Falle: Ersetzt man nur == durch ===, vergleicht der Code eine Zahl mit einem Text und hebt nie etwas hervor. Ohne any meldet vue-tsc das sogar: „This comparison appears to be unintentional because the types ‘number’ and ‘LocationQueryValue | LocationQueryValue[] | undefined’ have no overlap.“ Wer stattdessen any stehen lässt, bekommt grüne Werkzeuge und eine kaputte Funktion. Mit sitzung.nutzerId ist === dagegen direkt richtig, beide Seiten sind Zahlen (bzw. undefined ohne Anmeldung).

c) Das andere Paar schlägt vor, über die Datei /* eslint-disable */ zu schreiben, „weil die Pipeline sonst ständig rot ist“. Nimm Stellung zu dem Vorschlag und nenne, wie das Team künftig verhindert, dass solcher Code überhaupt gepusht wird. (4 Punkte)

Gegen den Vorschlag spricht: Zwei der Befunde sind echte Laufzeitfehler, die Regeln haben sie gefunden, bevor Kund*innen sie sehen. Ein Abschalten für die ganze Datei versteckt auch künftige Fehler und widerspricht den Teamregeln, die Ausnahmen nur einzeln mit Begründung erlauben. Bei Collective Ownership betrifft die Datei alle; ein Paar allein kann die gemeinsamen Regeln nicht aufheben (2 Punkte). Eine einzelne Ausnahme wäre vertretbar, wenn eine Regel im Einzelfall nicht passt, mit // eslint-disable-next-line <regel> -- <Grund>; hält das Team eine Regel generell für falsch, ändert es eslint.config.ts gemeinsam (1 Punkt).

Vorbeugen: Editor formatiert und behebt beim Speichern (.vscode/settings.json), vor dem Push npm run lint und npm run type-check lokal ausführen, im Pair Programming achtet die Navigatorin auf die Meldungen; optional ein Git-Hook vor dem Commit, der die Prüfungen startet (1 Punkt).

Bewertet wird die Begründung, nicht das Ergebnis: Eine abweichende, schlüssig begründete Stellungnahme erhält die Punkte.


Hinweis zur Nachnutzung als Open Educational Resource (OER)

Dieser Artikel und seine Texte, Bilder, Grafiken, Code und sonstiger Inhalt sind - sofern nicht anders angegeben - lizenziert unter CC BY 4.0. Nennung gemäß TULLU-Regel bitte wie folgt: “Übungsaufgaben zu Fahrer-Sicht und Coding Standards in Vue: eigene Mitfahrgesuche verwalten” von oer-informatik.de (H. Stein), Lizenz: CC BY 4.0. Der Artikel wurde unter https://oer-informatik.de/vue-fahrer-sicht-coding-standards-uebungsaufgaben veröffentlicht, die Quelltexte sind in weiterverarbeitbarer Form verfügbar im Repository unter https://gitlab.com/oer-informatik/programmierung/vue. Erstellt mit KI-Assistenz; von mir umfassend redaktionell überarbeitet, geprüft und freigegeben. Stand: 11.11.2026.

[Kommentare zum Artikel lesen, schreiben] / [Artikel teilen] / [gitlab-Issue zum Artikel schreiben]

Kommentare gerne per Mastodon, Verbesserungsvorschläge per gitlab issue (siehe oben). Beitrag teilen: