Reduce duplication in Counter and the Blade view #64

Closed
opened 2026-08-16 12:12:23 +02:00 by myrmidex · 1 comment
Owner

Refactor pass over the code v0.4.0 left behind. The whole application is 286 lines, so none of this is urgent — but there is one real piece of duplication and a few inconsistencies worth settling before they get copied.

The two forms in counter.blade.php are near-identical

resources/views/livewire/counter.blade.php has an onboarding form (line 6) and a set-value form (line 62). Both contain:

  • The same <input type="number" min="0" step="1" autofocus wire:model="value"> with an identical 200-character Tailwind class string
  • The same @error('value') block
  • A submit button with an identical class string

The class strings appear twice each, verbatim. Only three things actually differ: the label text, the wire:submit handler, and whether a cancel button is present.

Roughly 40 of the view's 100 lines are duplicated. Extract a Blade component — something like <x-counter-form :action="..." :label="..." :cancel="true|false"> — so the terminal styling lives in one place. It is the styling that matters here: it is the app's whole visual identity and currently has two copies that can drift.

Tracker::current() and null-guard repeated four times

app/Livewire/Counter.phpinitialise(), increment(), save() and mount() all open by fetching the tracker and handling its absence. A private accessor would collapse that, though note the four cases are not identical:

  • mount() uses null to set needsOnboarding
  • initialise() uses ?? to adopt an existing tracker
  • increment() and save() return early

Worth checking the abstraction actually simplifies rather than adding a layer over four slightly different needs. If it does not, leave it.

Small inconsistencies in Counter

  • save() writes $this->count = (int) $this->value; but initialise() writes $this->count = $tracker->count; — both correct, differently expressed
  • increment() resyncs $this->value = $this->count afterwards; initialise() does not. Harmless today because onboarding transitions away immediately, but the asymmetry is the kind of thing that becomes a bug when someone adds a fourth path

app/Http/Controllers/Controller.php is an empty abstract with no subclasses

#52 removed the last controller. The file is eight lines and framework convention, so it is probably worth keeping for whenever a controller reappears — but it is currently dead. Decide deliberately rather than leaving it unexamined.

Not in scope

AppServiceProvider is empty but registered in bootstrap/providers.php; that is framework wiring, not dead code. Leave it.

Acceptance criteria

  • The input and button styling exists in exactly one place
  • counter.blade.php no longer contains two copies of the same form
  • Counter's tracker access is consistent, or documented as deliberately varied
  • All three gates still pass: Pint, PHPStan level 7, PHPUnit
  • The rendered page is visually unchanged — verify in the browser, not only via tests
Refactor pass over the code v0.4.0 left behind. The whole application is 286 lines, so none of this is urgent — but there is one real piece of duplication and a few inconsistencies worth settling before they get copied. ## The two forms in `counter.blade.php` are near-identical `resources/views/livewire/counter.blade.php` has an onboarding form (line 6) and a set-value form (line 62). Both contain: - The same `<input type="number" min="0" step="1" autofocus wire:model="value">` with an identical 200-character Tailwind class string - The same `@error('value')` block - A submit button with an identical class string The class strings appear **twice each**, verbatim. Only three things actually differ: the label text, the `wire:submit` handler, and whether a cancel button is present. Roughly 40 of the view's 100 lines are duplicated. Extract a Blade component — something like `<x-counter-form :action="..." :label="..." :cancel="true|false">` — so the terminal styling lives in one place. It is the styling that matters here: it is the app's whole visual identity and currently has two copies that can drift. ## `Tracker::current()` and null-guard repeated four times `app/Livewire/Counter.php` — `initialise()`, `increment()`, `save()` and `mount()` all open by fetching the tracker and handling its absence. A private accessor would collapse that, though note the four cases are not identical: - `mount()` uses null to set `needsOnboarding` - `initialise()` uses `??` to adopt an existing tracker - `increment()` and `save()` return early Worth checking the abstraction actually simplifies rather than adding a layer over four slightly different needs. If it does not, leave it. ## Small inconsistencies in `Counter` - `save()` writes `$this->count = (int) $this->value;` but `initialise()` writes `$this->count = $tracker->count;` — both correct, differently expressed - `increment()` resyncs `$this->value = $this->count` afterwards; `initialise()` does not. Harmless today because onboarding transitions away immediately, but the asymmetry is the kind of thing that becomes a bug when someone adds a fourth path ## `app/Http/Controllers/Controller.php` is an empty abstract with no subclasses #52 removed the last controller. The file is eight lines and framework convention, so it is probably worth keeping for whenever a controller reappears — but it is currently dead. Decide deliberately rather than leaving it unexamined. ## Not in scope `AppServiceProvider` is empty but registered in `bootstrap/providers.php`; that is framework wiring, not dead code. Leave it. ## Acceptance criteria - [ ] The input and button styling exists in exactly one place - [ ] `counter.blade.php` no longer contains two copies of the same form - [ ] `Counter`'s tracker access is consistent, or documented as deliberately varied - [ ] All three gates still pass: Pint, PHPStan level 7, PHPUnit - [ ] The rendered page is visually unchanged — verify in the browser, not only via tests
myrmidex added this to the v0.4.1 milestone 2026-08-16 12:12:23 +02:00
myrmidex added the
enhancement
label 2026-08-16 12:12:23 +02:00
myrmidex self-assigned this 2026-08-16 12:12:24 +02:00
Author
Owner

Done — e5c257d

The duplicated forms

New resources/views/components/counter-form.blade.php. The input, its 200-character class string, the @error block and the submit button now exist in exactly one place. Props: action, label, submit, id, cancel.

counter.blade.php went 100 → 57 lines; the two forms became two component calls.

Counter consistency

Added a private syncFrom(Tracker). Before, the same intent was written four ways:

  • mount() — ternary on null
  • initialise() — set $count, left $value stale
  • increment() — set both
  • save()(int) $this->value cast

Now every mutation ends with syncFrom($tracker), so $count and $value cannot drift. The tracker is the single source of truth.

Verified visually unchanged

Beyond the three gates, I diffed the rendered HTML before and after:

45c45
<   data-csrf="UCT2HQuAkscjbV8EJznLktdLhKmkCcW1oihleQE0"
>   data-csrf="f9yq0uYzEg5otRvcHLzB9JySRv3MSo1P2FpRCRDH"

A per-request CSRF token is the only difference — the markup is otherwise byte-identical. The compiled CSS also stayed at exactly 13.58 kB, confirming Tailwind produced the same class set from the extracted component.

Added test_the_edit_form_renders_with_both_actions, since the edit form sits behind $editing and appeared in no rendered page the tests checked. 18 tests now.

Declined: the shared tracker accessor

The ticket flagged that this might not simplify, and it does not. The four Tracker::current() call sites need different things — mount() uses null to set a flag, initialise() uses ?? to adopt an existing tracker, increment() and save() return early. A shared accessor would return null and leave every caller guarding anyway, hiding control flow without removing it. Left as-is.

Declined: removing Controller.php

The empty abstract stays. Eight lines, framework convention, and the natural place for a controller if one returns.

Acceptance criteria

  • Input and button styling exists in exactly one place
  • counter.blade.php no longer contains two copies of the same form
  • Counter's tracker access is consistent — via syncFrom(); the four fetch sites are deliberately varied, documented above
  • Pint, PHPStan level 7, PHPUnit all pass
  • Rendered page visually unchanged — verified by HTML diff, not only tests
## Done — `e5c257d` ### The duplicated forms New `resources/views/components/counter-form.blade.php`. The input, its 200-character class string, the `@error` block and the submit button now exist in exactly one place. Props: `action`, `label`, `submit`, `id`, `cancel`. `counter.blade.php` went **100 → 57 lines**; the two forms became two component calls. ### `Counter` consistency Added a private `syncFrom(Tracker)`. Before, the same intent was written four ways: - `mount()` — ternary on null - `initialise()` — set `$count`, left `$value` stale - `increment()` — set both - `save()` — `(int) $this->value` cast Now every mutation ends with `syncFrom($tracker)`, so `$count` and `$value` cannot drift. The tracker is the single source of truth. ### Verified visually unchanged Beyond the three gates, I diffed the rendered HTML before and after: ``` 45c45 < data-csrf="UCT2HQuAkscjbV8EJznLktdLhKmkCcW1oihleQE0" > data-csrf="f9yq0uYzEg5otRvcHLzB9JySRv3MSo1P2FpRCRDH" ``` A per-request CSRF token is the only difference — the markup is otherwise byte-identical. The compiled CSS also stayed at exactly 13.58 kB, confirming Tailwind produced the same class set from the extracted component. Added `test_the_edit_form_renders_with_both_actions`, since the edit form sits behind `$editing` and appeared in no rendered page the tests checked. 18 tests now. ### Declined: the shared tracker accessor The ticket flagged that this might not simplify, and it does not. The four `Tracker::current()` call sites need different things — `mount()` uses null to set a flag, `initialise()` uses `??` to adopt an existing tracker, `increment()` and `save()` return early. A shared accessor would return null and leave every caller guarding anyway, hiding control flow without removing it. Left as-is. ### Declined: removing `Controller.php` The empty abstract stays. Eight lines, framework convention, and the natural place for a controller if one returns. ### Acceptance criteria - [x] Input and button styling exists in exactly one place - [x] `counter.blade.php` no longer contains two copies of the same form - [x] `Counter`'s tracker access is consistent — via `syncFrom()`; the four fetch sites are deliberately varied, documented above - [x] Pint, PHPStan level 7, PHPUnit all pass - [x] Rendered page visually unchanged — verified by HTML diff, not only tests
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lvl0/incr#64
No description provided.