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.
The store actions you need
Section titled “The store actions you need”Before building the UI, complete the store with relationship actions:
// Biological parentsfunction 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) })}
// Spousesfunction 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 ), })}PersonDetailView structure
Section titled “PersonDetailView structure”The view is organized into sections — one per relationship type. Each section follows the same pattern:
- Show current relationships with
v-for - Show an “add” dropdown with
v-if+v-model - 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 spouse section — multiple marriages
Section titled “The spouse section — multiple marriages”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>Local computed for this person
Section titled “Local computed for this person”The view reads route.params.id and derives everything locally from the people ref:
const id = route.params.id as stringconst person = computed(() => people.value.find(p => p.id === id))const bioParents = computed(() => person.value ? people.value.filter(p => (person.value!.parentIds ?? []).includes(p.id)) : [])Exercise
Section titled “Exercise”- Build the complete
PersonDetailView.vuewith sections for bio parents, step-parents, spouses (with marriage/divorce dates), bio children, step-children, and siblings. - Add all relationship store actions (see the store actions section above).
- 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.
- Add a third person and add them as the new spouse. Confirm the “Former Spouses” section appears.
PersonDetailViewderives all its data locally viacomputedfrom the centralpeopleref.- 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.