Files
fuel-price/docs/superpowers/specs/2026-04-07-fuelfinder-subcomponent-split-design.md
2026-04-07 21:27:56 +01:00

6.8 KiB

FuelFinder Sub-Component Split

Date: 2026-04-07 Status: Approved Scope: Split the monolithic FuelFinder Livewire component into focused sub-components communicating via browser events.


Motivation

FuelFinder currently owns search state, API calls, map data, station results, and prediction — all in one component. Every search re-renders the entire page including the map container. Splitting into focused components allows the map, station list, and recommendation to update independently without re-mounting Leaflet or triggering unnecessary re-renders.


Architecture

FuelFinder (shell)                   — layout only, no state
├── <livewire:fuel.search />         — search form, filters, geolocation, API calls
├── <livewire:fuel.map />            — relays stations-found to Alpine/Leaflet
├── <livewire:fuel.station-list />   — renders station cards
├── <livewire:fuel.recommendation /> — renders prediction card
└── <x-fuel.forecast />             — Blade component, unchanged

Components

FuelFinder (shell)

Class: App\Livewire\Public\FuelFinder View: resources/views/livewire/public/fuel-finder.blade.php

Retains #[Layout('layouts.shell')]. Holds no state and no methods. The view contains only the page chrome (header, main scroll area, bottom nav) and the four <livewire:fuel.*> tags.


fuel.search

Class: App\Livewire\Public\Fuel\Search View: resources/views/livewire/public/fuel/search.blade.php

Owns all search state and logic migrated from FuelFinder:

Properties:

  • search: string — postcode/town input
  • fuelType: string — default 'petrol'
  • radius: int — default 5
  • sort: string — default 'reliable'
  • apiError: ?string
  • hasSearched: bool

Methods:

  • findStations() — validates, calls /api/stations, calls /api/prediction, dispatches stations-found
  • updatedFuelType(), updatedRadius(), updatedSort() — re-run findStations() if hasSearched

Dispatches:

$this->dispatch('stations-found',
    results: $results,
    meta: $meta,
    prediction: $prediction,
    radius: $this->radius,  // needed by Map for Leaflet zoom level
);

The view contains the search input, geolocation Alpine component, filter selects, and error display — extracted from the current fuel-finder.blade.php.


fuel.map

Class: App\Livewire\Public\Fuel\Map View: resources/views/livewire/public/fuel/map.blade.php

Thin relay. Listens for stations-found on the PHP side and re-dispatches as a browser event for Alpine/Leaflet to consume. No re-render required.

#[On('stations-found')]
public function handle(array $results, array $meta, int $radius = 5, ?array $prediction = null): void
{
    $this->dispatch('map-update', results: $results, meta: $meta, radius: $radius);
}

The view contains the <x-fuel.station-map> component wrapped in wire:ignore. The Alpine stationMap function in station-map.js is updated to listen for the map-update browser event instead of $watch-ing an entangled Livewire property.

station-map.js change:

init() {
    // ...existing Leaflet init...
    window.addEventListener('map-update', (e) => {
        this.results = e.detail.results;
        this.meta    = e.detail.meta;
        this.radius  = e.detail.radius;
        this._plotMarkers();
    });
}

station-map.blade.php props change from @entangle(...) to static defaults — Map holds no state:

<div x-data="stationMap([], {}, 5)" class="h-56 w-full ..."></div>

fuel.station-list

Class: App\Livewire\Public\Fuel\StationList View: resources/views/livewire/public/fuel/station-list.blade.php

Listens for stations-found, stores results, re-renders station cards.

Properties:

  • results: array
  • meta: array
  • hasSearched: bool

Methods:

#[On('stations-found')]
public function handle(array $results, array $meta): void
{
    $this->results    = $results;
    $this->meta       = $meta;
    $this->hasSearched = true;
}

The view contains the "Stations Nearby" heading, result count, @forelse station card loop, and the empty-state message — extracted from fuel-finder.blade.php.


fuel.recommendation

Class: App\Livewire\Public\Fuel\Recommendation View: resources/views/livewire/public/fuel/recommendation.blade.php

Listens for stations-found, stores prediction, re-renders.

Properties:

  • prediction: ?array

Methods:

#[On('stations-found')]
public function handle(array $results, array $meta, ?array $prediction = null): void
{
    $this->prediction = $prediction;
}

The view renders <x-fuel.recommendation :prediction="$prediction" /> when prediction is set. Currently commented out in the parent view — this component makes it easy to uncomment when the scoring engine is wired.


Event Contract

Event name: stations-found

Payload:

[
    'results'    => array,   // station records from /api/stations
    'meta'       => array,   // lat, lng, count from API response
    'prediction' => ?array,  // from /api/prediction, nullable
    'radius'     => int,     // user-selected radius in miles, for Leaflet zoom
]

All listener components must accept ?array $prediction = null and int $radius = 5 as defaults to avoid errors when optional fields are absent.


File Changes

New files:

  • app/Livewire/Public/Fuel/Search.php
  • app/Livewire/Public/Fuel/Map.php
  • app/Livewire/Public/Fuel/StationList.php
  • app/Livewire/Public/Fuel/Recommendation.php
  • resources/views/livewire/public/fuel/search.blade.php
  • resources/views/livewire/public/fuel/map.blade.php
  • resources/views/livewire/public/fuel/station-list.blade.php
  • resources/views/livewire/public/fuel/recommendation.blade.php

Modified files:

  • app/Livewire/Public/FuelFinder.php — strip all state/methods, keep layout attribute
  • resources/views/livewire/public/fuel-finder.blade.php — replace inline sections with <livewire:fuel.*> tags
  • resources/js/maps/station-map.js — replace $watch with map-update event listener
  • resources/views/components/fuel/station-map.blade.php — remove @entangle props

Moved tests:

  • FuelFinderTest search/API logic tests → SearchTest

New tests:

  • SearchTest — validates, dispatches stations-found, handles API errors
  • StationListTest — responds to stations-found, renders cards, handles empty state
  • RecommendationTest — responds to stations-found, renders when prediction set, hidden when null

What Does Not Change

  • x-fuel.forecast — remains a Blade component
  • station-map.js map logic — only the data-binding mechanism changes
  • All fuel/ Blade components (station-card, recommendation, forecast, etc.)
  • The visual layout in fuel-finder.blade.php
  • Route definition