FocusView and the Component Tree
FocusView is the heart of FamilyTree. It renders a visual tree centered on one person — their parents above, their spouse beside them, their children below. Clicking any person shifts the focus, navigating the tree.
PersonCard component
Section titled “PersonCard component”PersonCard is the atomic unit. It receives a person object and a role, displays the name and optional dates, and emits select when clicked:
<script setup lang="ts">import type { Person } from '../../types'
const props = defineProps<{ person: Person role: 'focus' | 'parent' | 'step-parent' | 'child' | 'step-child' | 'spouse' | 'sibling' clickable?: boolean}>()
const emit = defineEmits<{ select: [id: string] }>()</script>
<template> <div class="person-card" :class="[role, { clickable }]" @click="clickable && emit('select', person.id)" > <span class="name">{{ person.name }}</span> <span v-if="person.birthYear" class="year"> {{ person.birthYear }}<template v-if="person.deathYear"> – {{ person.deathYear }}</template> </span> </div></template>FocusView component
Section titled “FocusView component”<script setup lang="ts">import { useRouter } from 'vue-router'import { useFamilyStore } from '../../stores/familyStore'import PersonCard from '../PersonCard/PersonCard.vue'
const store = useFamilyStore()const router = useRouter()
function focus(id: string) { store.setFocus(id)}
function goToDetail(id: string) { router.push({ name: 'person', params: { id } })}</script>
<template> <div class="focus-view"> <div v-if="store.people.length === 0" class="focus-view__empty"> <p>No people in your family tree yet.</p> <RouterLink to="/add">Add the first person</RouterLink> </div>
<template v-else-if="store.focusPerson"> <!-- Parents row --> <div class="focus-row focus-row--parents"> <PersonCard v-for="parent in store.parents" :key="parent.id" :person="parent" role="parent" :clickable="true" @select="focus" /> <span v-if="store.parents.length === 0" class="placeholder"> No parents recorded </span> </div>
<div class="connector connector--down" />
<!-- Main row: siblings + focus + spouse --> <div class="focus-row focus-row--main"> <PersonCard :person="store.focusPerson" role="focus" @click="goToDetail(store.focusPerson!.id)" style="cursor: pointer" /> <template v-if="store.currentSpouse"> <div class="connector connector--horizontal" /> <PersonCard :person="store.currentSpouse" role="spouse" :clickable="true" @select="focus" /> </template> </div>
<!-- Children row --> <div v-if="store.children.length > 0" class="connector connector--down" /> <div v-if="store.children.length > 0" class="focus-row focus-row--children"> <PersonCard v-for="child in store.children" :key="child.id" :person="child" role="child" :clickable="true" @select="focus" /> </div> </template> </div></template>Key patterns in FocusView
Section titled “Key patterns in FocusView”v-for with :key renders each parent and child as a PersonCard. The store’s parents and children computed properties filter people to only the relevant ones.
v-if/v-else shows the empty state, the tree, or individual sections (like spouse) conditionally.
Event flow: PersonCard emits select(id) → FocusView calls store.setFocus(id) → store.focusPerson computed updates → template re-renders with the new focus person. No explicit re-render call needed.
Exercise
Section titled “Exercise”- Build
PersonCard.vueandFocusView.vuefrom the code above. - Add
<FocusView />toHomeView.vue. - In the browser, use
store.addPerson()from a button (even a temporary one) to add a few people, then connect them viaparentIds. - Confirm clicking a parent card in the tree shifts focus to that person.
PersonCardis the atomic reusable card — accepts aPersonprop, emitsselectwith the id.FocusViewreads from the store’s computed getters and renders the full tree.- The reactive loop: store action → computed updates → template re-renders automatically.
v-for+:keyrenders lists;v-ifguards conditional sections.