Lesson 4 — Extracting a map component
The Lab page’s inline map block has grown. We have one source,
two layers, and hover state — about a dozen lines of DSL inside
the Vera::Map block. It’s still readable, but it’s reaching
the boundary of what’s pleasant to keep on a view template.
Lesson 5 will add depot points, which would push the inline
block well past comfortable.
This is the right moment to extract a component. We’ll move the
map’s logic out of app/views/lab/index.html.erb and into a
proper Phlex component at app/components/service_area_map.rb.
The Lab page becomes a single render call again, and the
component file holds everything map-shaped.
This is the pattern Module 2 hinted at — pages render Phlex components, components encapsulate their internal complexity. The inline approach was right earlier (one map, simple configuration); the extraction is right now (the map’s configuration warrants its own home).
What we’re moving where
The Lab page currently looks something like this:
|
|
We’ll move everything inside the Vera::Map.new(...) do |m| ... end
block plus its initialisation arguments into a Phlex component.
The page header, the wrapping divs, the Tailwind classes — all
of that stays on the Lab page, since they’re page-level chrome,
not map concerns.
After the extraction, the Lab page will look like:
|
|
Single render call, name describes what the map shows. Anyone reading this template knows immediately that there’s a map showing service areas; they can dive into the component file when they want to know how it’s configured.
Creating the component
Create app/components/service_area_map.rb:
|
|
A few things worth noting.
prop :id and prop :height with defaults. The component
takes two arguments, both optional. The defaults match what was
previously hardcoded on the Lab page — same DOM ID, same
height. If a different page later wants the same map at a
different size or under a different ID, it can pass those in.
prop :id, String, default: -> { … } — typed properties from the literal gem, which Phlex 2 uses to declare component arguments. The argument becomes available as @id inside view_template.
The view_template method. Phlex 2’s convention. It’s
where the component renders. We render Vera::Map.new(...) with
the same DSL block we had inline before — just moved here.
No new logic. The component doesn’t add anything; it relocates. Same source, same layers, same paint, same hover overrides. The point of the extraction isn’t to introduce new behaviour — it’s to put the existing behaviour somewhere appropriate. Pure refactoring.
Updating the Lab page
Replace the inline map block on app/views/lab/index.html.erb
with a single component render:
|
|
The <div class="rounded-lg overflow-hidden border border-border bg-surface">
wrapping div stays — it’s page-level chrome (the rounded
border, the background colour) that’s about how the map
sits on the Lab page, not about the map itself.
A different page later might render the same component without that wrapping treatment, or with a different one. The component itself takes no view on chrome; the page applies whatever frame is appropriate for the context.
Activity 1 — Verify nothing has changed visually
Refresh the Lab page. You should see exactly the same map you saw at the end of Lesson 3 — translucent fills, blue outlines, hover state working. Functionally identical.
If anything has broken:
- Component-not-found errors usually mean the file path or
class name doesn’t match. The file at
app/components/service_area_map.rbshould defineComponents::ServiceAreaMap. Zeitwerk reads filename and expects matching class structure. - Style or hover differences usually mean a paint key got miscopied. Check the component’s paint hash against what Lesson 3 had inline.
Activity 2 — Try changing the component
Make a small visible change in the component to confirm it’s
genuinely the source of truth now. Open app/components/service_area_map.rb
and change the line color in the outline layer:
|
|
Refresh the Lab page. The polygon outlines should now be red.
This confirms the extraction worked: the Lab page is no longer
involved in the map’s styling decisions. Anything map-related
goes through Components::ServiceAreaMap. Change the component,
the Lab page reflects it.
Revert the colour back to #1e3a5f when you’re done — we’ll
keep the dispatch deck’s primary blue for the rest of the
tutorial.
What this opens up
A few things become natural now that the map is a component:
Reuse. Other pages can render Components::ServiceAreaMap
without duplicating any logic. Module 6’s dispatcher page will
use a closely-related variant; Module 9’s monitoring page
another. Each gets its own component, but they all live in
app/components/ next to this one and follow the same shape.
Per-page customisation through props. The component can
grow new optional props as needs arise — for_state: to scope
to a single state, with_legend: to add a legend overlay,
zoom: to change the initial zoom. The Lab page can keep
calling Components::ServiceAreaMap.new with no arguments;
other pages pass what they need.
Testability. A Phlex component can be rendered to a string in a test, asserted against, exercised in isolation. The inline form was much harder to test because it lived inside an ERB template.
Encapsulation of complexity. When Lesson 5 adds depot pins, the new logic goes inside the component. The Lab page doesn’t change. The component grows; everything else stays the same.
Where this leaves us
We’ve finally landed the component-extraction pattern. The Lab page is back to a clean single-line render. The component holds everything map-shaped — sources, layers, paint, hover state — in a single named place.
Lesson 5 takes this component and adds depots: a second source,
a circle layer to render the depot points, popup behaviour
when one is clicked. All of those changes happen inside
app/components/service_area_map.rb. The Lab page stays
identical.