Skip to content

Relationships — Parents, Spouses, and Siblings

PersonDetailView is the most complex view in FamilyTree. It shows all of a person’s relationships and provides UI to add, edit, and remove them. Every concept from the course — v-for, v-if, v-model, defineProps, store actions, computed — comes together here.

Before building the UI, complete the store with relationship actions:

// Biological parents
function addParent(personId: string, parentId: string): void {
const person = people.value.find(p => p.id === personId)
if (!person) return
const ids = person.parentIds ?? []
if (ids.includes(parentId) || ids.length >= 2) return
updatePerson(personId, { parentIds: [...ids, parentId] })
}
function removeParent(personId: string, parentId: string): void {
const person = people.value.find(p => p.id === personId)
if (!person) return
updatePerson(personId, { parentIds: (person.parentIds ?? []).filter(id => id !== parentId) })
}
// Spouses
function addSpouse(personId: string, spouseId: string, marriageDate?: string): void {
const person = people.value.find(p => p.id === personId)
const spouse = people.value.find(p => p.id === spouseId)
if (!person || !spouse) return
if ((person.spouses ?? []).some(s => s.personId === spouseId && !s.divorceDate)) return
const rel = { personId: spouseId, marriageDate }
const reverseRel = { personId: personId, marriageDate }
updatePerson(personId, { spouses: [...(person.spouses ?? []), rel] })
updatePerson(spouseId, { spouses: [...(spouse.spouses ?? []), reverseRel] })
}
function recordDivorce(personId: string, spouseId: string, divorceDate?: string): void {
const date = divorceDate || new Date().getFullYear().toString()
const person = people.value.find(p => p.id === personId)
const spouse = people.value.find(p => p.id === spouseId)
if (!person || !spouse) return
updatePerson(personId, {
spouses: (person.spouses ?? []).map(s =>
s.personId === spouseId && !s.divorceDate ? { ...s, divorceDate: date } : s
),
})
updatePerson(spouseId, {
spouses: (spouse.spouses ?? []).map(s =>
s.personId === personId && !s.divorceDate ? { ...s, divorceDate: date } : s
),
})
}

The view is organized into sections — one per relationship type. Each section follows the same pattern:

  1. Show current relationships with v-for
  2. Show an “add” dropdown with v-if + v-model
  3. Show remove buttons
<!-- Biological Parents section (simplified) -->
<section>
<h2>Biological Parents</h2>
<PersonCard
v-for="p in bioParents"
:key="p.id"
:person="p"
role="parent"
:clickable="true"
@select="focusOn"
/>
<span v-if="bioParents.length === 0">None recorded.</span>
<div v-if="eligibleBioParents.length > 0 && bioParents.length < 2">
<select v-model="selectedBioParent">
<option value="">Add biological parent…</option>
<option v-for="p in eligibleBioParents" :key="p.id" :value="p.id">
{{ p.name }}
</option>
</select>
<button :disabled="!selectedBioParent" @click="addBioParent">Add</button>
</div>
</section>

The spouses section demonstrates the power of the SpouseRelationship[] model:

<!-- Current spouse -->
<div v-if="currentSpouse" class="spouse-entry">
<PersonCard :person="currentSpouse" role="spouse" :clickable="true" @select="focusOn" />
<span class="badge">Current</span>
<span v-if="currentSpouseRel.marriageDate">
Married: {{ currentSpouseRel.marriageDate }}
</span>
<button @click="showDivorceForm[currentSpouse.id] = true">Record divorce</button>
<div v-if="showDivorceForm[currentSpouse.id]">
<input v-model="divorceDate[currentSpouse.id]" placeholder="Divorce date" />
<button @click="recordDivorce(currentSpouse.id)">Confirm</button>
</div>
</div>
<!-- Former spouses -->
<div v-for="{ person: sp, rel } in formerSpouseRels" :key="sp.id">
<PersonCard :person="sp" role="spouse" :clickable="true" @select="focusOn" />
<span>{{ rel.marriageDate }} – {{ rel.divorceDate }}</span>
</div>
<!-- Add spouse -->
<select v-model="selectedSpouse">
<option value="">Add spouse…</option>
<option v-for="p in eligibleSpouses" :key="p.id" :value="p.id">{{ p.name }}</option>
</select>
<input v-model="newMarriageDate" placeholder="Marriage date (optional)" />
<button :disabled="!selectedSpouse" @click="addSpouse">Add</button>

The view reads route.params.id and derives everything locally from the people ref:

const id = route.params.id as string
const person = computed(() => people.value.find(p => p.id === id))
const bioParents = computed(() =>
person.value ? people.value.filter(p => (person.value!.parentIds ?? []).includes(p.id)) : []
)
  1. Build the complete PersonDetailView.vue with sections for bio parents, step-parents, spouses (with marriage/divorce dates), bio children, step-children, and siblings.
  2. Add all relationship store actions (see the store actions section above).
  3. Test the full flow: add two people, navigate to one’s detail page, add the other as a biological parent, then as a spouse with a marriage date, then record a divorce.
  4. Add a third person and add them as the new spouse. Confirm the “Former Spouses” section appears.
  • PersonDetailView derives all its data locally via computed from the central people ref.
  • Each relationship section follows: show current → show add dropdown → show remove buttons.
  • The spouse section handles multiple marriages via the SpouseRelationship[] array — divorced spouses move to “Former Spouses” and new ones can be added.
  • Store actions keep mutations in one place — the view just calls them and lets reactivity update the UI.