Skip to content

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 is the atomic unit. It receives a person object and a role, displays the name and optional dates, and emits select when clicked:

src/components/PersonCard/PersonCard.vue
<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>
src/components/FocusView/FocusView.vue
<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>

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.

  1. Build PersonCard.vue and FocusView.vue from the code above.
  2. Add <FocusView /> to HomeView.vue.
  3. In the browser, use store.addPerson() from a button (even a temporary one) to add a few people, then connect them via parentIds.
  4. Confirm clicking a parent card in the tree shifts focus to that person.
  • PersonCard is the atomic reusable card — accepts a Person prop, emits select with the id.
  • FocusView reads from the store’s computed getters and renders the full tree.
  • The reactive loop: store action → computed updates → template re-renders automatically.
  • v-for + :key renders lists; v-if guards conditional sections.