Skip to content

Adopt Vue composables for stateful non-rendering logic, starting with TemplateSection's scroll-arrows #134

Description

@moodyjmz

TL;DR: This app has no composables yet — stateful logic that doesn't render anything (dialog state, view-mode persistence, scroll-arrow tracking) currently lives inline in <script setup> blocks alongside markup-driving state. Introduce the pattern with one small, real extraction first, establish the conventions it needs, then let it spread opportunistically per AGENTS.md's existing "a component that keeps absorbing new responsibilities... extract the next one that lands" guidance — not a big-bang refactor.

Detail

Why now

Found while investigating a recurring test-coverage gap across #126/#127/#130 (tracked separately in #133): the same file keeps absorbing new interactive elements and derived state without ever being split up. Composables are the piece "extract it" doesn't cover on its own — component extraction splits render output, composables split stateful logic with no direct template dependency. This project is small (5 Vue files, ~1000 lines total) and young, so establishing the convention now is cheap; it gets more expensive the more code lands on the current shape.

First target: TemplateSection.vue's scroll-arrows cluster

list (element ref) + canScrollLeft/canScrollRight + updateArrows() + scrollByStep() + the onMounted/onUnmounted wiring is a self-contained "scrollable region with arrow buttons" state machine — reusable, no dependency on other in-flight work, and the same region already has an open a11y gap (#59: no aria-label identifying it as a scrollable region) worth closing while in there.

Check @vueuse/core before hand-rolling this one. Verified directly against its shipped type declarations: useScroll(element) already returns arrivedState.{left,right,top,bottom} (the inverse of canScrollLeft/canScrollRight) plus reactive x/y, and handles the listener attach/detach and resize-observation internally. The extraction should be a thin wrapper over useScroll, not new hand-rolled logic — this is the same "reuse before you write" ladder AGENTS.md already applies to @nextcloud/* packages and @nextcloud/vue components, extended to general-purpose Vue utilities. @vueuse/core@14.4.0's only peer dependency is vue@^3.5.0, which this app already satisfies.

Other candidates identified, for later extractions (not in scope for the first PR)

  • OfficeOverview.vue: the create-from-template dialog cluster (showCreateDialog, newFileName, pendingCreator, pendingTemplate, creating, createError, createInput)
  • OfficeOverview.vue: the view-mode toggle + persistence cluster (viewMode, toggleViewMode(), getOverviewGridView/setOverviewGridView)
  • OfficeOverview.vue: the active-creator/routing-sync cluster landing via feat: add navigation actions for office #130 (activeCreator, routeCreatorId, the URL-sync watch)

Each is its own extraction, its own commit, done opportunistically the next time that region is touched — not bundled into one refactor PR.

Conventions to establish with the first extraction, not invent per-composable later

  1. Check @vueuse/core first. Before hand-rolling a composable, check whether it already exists there (as with useScroll above). Prefer it the same way an existing @nextcloud/vue component or design token is preferred over a hand-rolled equivalent.
  2. Return individual refs, not a reactive() object. A composable that returns reactive({...}) breaks reactivity silently if the caller destructures it. Return individual refs (or toRefs() on an internal reactive object) so destructuring is always safe.
  3. A shared test helper for composables using lifecycle hooks. onMounted/onUnmounted throw outside an active component instance, so testing needs a small reusable helper — a trivial host component whose setup() just returns the composable — living in src/test-utils/, written once with the first such composable rather than reinvented slightly differently by the second and third.
  4. Same new-unit-plus-sibling-spec rule as src/utils//src/components/ today. src/composables/useScrollArrows.ts lands with src/composables/useScrollArrows.spec.ts in the same commit — this is also where the src/composables/ directory convention itself gets established.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions