Lesson 3 — Your first map
This is the lesson where the foundation pays off. By the end of it, you’ll have edited the Lab page to render a real map of Australia in your browser. No data on it yet, no interactions, just the canvas — but it’s a real MapLibre map, declared through the Vera gem’s Phlex component, with no JavaScript you wrote yourself.
What’s already there
The chassis ships with a developer-only Lab section in the
sidebar. Sign in to your dispatch app, look at the bottom of the
left sidebar, and you should see a “Development” section header
with a “Lab” link beneath it. The link is only visible in
Rails.env.development? — it’s invisible in production.
The route, controller, and a placeholder ERB view are already in place too:
- Route:
get "/lab"inconfig/routes.rb, mapping toLabController#index. - Controller:
app/controllers/lab_controller.rb, an emptyindexaction. - View:
app/views/lab/index.html.erb, an ERB placeholder showing an “Empty for now” message.
Activity 1 — Visit the Lab placeholder
Make sure bin/dev is running. Click the “Lab” link in the
Development section of the sidebar (or visit
http://localhost:3000/lab directly).
You should see a placeholder page with a beaker icon, a heading that says “Empty for now,” and explanatory text about what this page becomes in this lesson.
That’s the starting state for this lesson. We’ll edit the ERB view to render a real map.
Pages and components
Before we make the edit, a quick word about the pattern we’ll use across the tutorial. The dispatch deck uses a hybrid approach:
- Pages are ERB. Every file in
app/views/{controller}/is.html.erb. Pages are mostly markup with small islands of Ruby; ERB lets HTML look like HTML. - Components are Phlex. Every file in
app/components/is a Phlex class. Components encapsulate reusable UI with typed properties, conditional rendering, and composition.
Pages render components freely. The Lab placeholder you just
visited already does this — its ERB view renders the chassis’s
Components::PageHeader and Components::Icon via the
standard <%= render %> helper.
The Vera gem’s Vera::Map is a Phlex component, just like any
other. We render it from an ERB view exactly the way we’d render
any chassis component: <%= render Vera::Map.new(...) %>.
Rendering the map
Open app/views/lab/index.html.erb. The placeholder content
inside the <div class="p-8"> is what we’ll replace. The page
header, content_for, and overall structure stay; only the
content within the main wrapper changes.
Replace the file with:
|
|
That’s it. Three changes from the placeholder: the placeholder
card has been removed, and a single <%= render %> call drops
in a Vera::Map component. As we promised in Lesson 2 - a map with a single
line of ruby code!
A few things to notice:
The Vera::Map.new(...) call is the heart of the lesson. We pass
an id: (so the map element has a stable identifier for outlets
and morph protection — more on this in Module 5) and a height:
(so the map has a vertical size; without it, the container would
collapse).
Notice what we’re not passing. No centre:, no zoom:, no
style: — the gem’s defaults are sensible, and they happen to
be exactly what we want for the dispatch deck: Australia
centred, zoom 4, positron-styled. We’ll see in Lesson 4 what
those values are and how to change them.
The component renders inside a div with rounded corners and a
subtle border. This isn’t required for the map to work, but it
gives the map a frame that matches the rest of the dispatch
deck’s visual style.
Activity 2 — Reload the Lab page
Save the file and refresh the browser. You should see:
- The chassis’s top bar at the top of the page
- The dark slate sidebar on the left, with the Lab item now highlighted
- A page header showing “Lab”
- A 600-pixel-tall map of Australia, light-grey-styled, with state outlines and ocean
The map should be centred over the middle of Australia at a zoom level that shows the whole continent.
What just happened
When you reloaded /lab, a chain of things ran:
- The Rails router matched the URL to
LabController#index. - The controller action ran (and did nothing — the empty
indexmethod). - Rails resolved the view at
app/views/lab/index.html.erband rendered it. - The ERB view ran, and inside it the
<%= render Vera::Map.new(...) %>call instantiated the Phlex component and inserted its HTML output into the page. - The component’s HTML included a
<div data-controller="vera--map" ...>element with the map’s configuration JSON in a data attribute. - The browser received the HTML and Stimulus connected the
vera--mapcontroller to the div. - The
vera--mapStimulus controller read the configuration from the data attribute, instantiated a MapLibre map with it, and rendered to the div.
Steps 1–5 are server-side. Steps 6–7 are client-side. The data attribute is the boundary between them. It’s worth remembering this shape — it’s the fundamental architecture for every map in the rest of the tutorial. The Phlex component generates a configuration; Stimulus initialises MapLibre with it; the result appears on the page.
Activity 3 — Inspect the map element
In the browser, open the developer tools and inspect the map
element. Look for the outer <div> that wraps the map. You’ll
see something like:
|
|
The data-controller attribute tells Stimulus which controller
to attach. The data-vera--map-config-value attribute carries
the serialised configuration — this is the JSON the Phlex
component generated. The data-turbo-permanent attribute opts
the element out of Turbo morph, so the map’s state is preserved
across page updates (more on this in Module 5).
Notice that the JSON’s centre value is [133.77, -25.27] —
longitude first, then latitude. The DSL accepts [lat, lng]
when you write Ruby (because that matches how most people write
coordinates: latitude first), but it stores them as
[lng, lat] because that’s MapLibre’s native format. Lesson 4
explains why this matters.
Customising the map
The defaults are sensible, but you’ll want to change them as you build different views. Try a few variations now to get a feel for the parameters.
To use a different style:
|
|
The gem ships with several style names you can pass as symbols:
:positron, :positron_no_labels, :dark_matter, and
:voyager. You can also pass a full URL string to use any
MapLibre-compatible style. For the tutorial we’ll vary it based on
what we think best suits the situation, but remember you can change it on any map if you choose to.
To centre on Sydney at city zoom level:
|
|
Note that the centre: parameter takes [latitude, longitude]
— the natural Ruby ordering. Sydney’s at lat -33.87, lng 151.21
(southern hemisphere is negative latitude, eastern hemisphere
is positive longitude).
To make the map taller:
|
|
100vh (100% of the viewport height) makes the map fill the
full browser window — useful for full-bleed map pages we’ll
build later.
Activity 4 — Try a few configurations
Change the render call in app/views/lab/index.html.erb to
centre on different locations with different zoom levels. Try:
- Sydney at zoom 12:
[-33.8688, 151.2093] - Brisbane at zoom 11:
[-27.4698, 153.0251] - The whole world at zoom 1:
[0, 0] - Antarctica at zoom 3:
[-83, 0]
Reload after each change and observe how the map moves to the new location.
When you’re done, set the centre back to default Australia (or
remove the centre: parameter entirely) so the next lesson can
build from a known state.
Where this leaves us
You now have:
- A working Lab page rendering a real MapLibre map
- A clear sense of the server-to-browser pipeline that produces it
- Familiarity with the
Vera::Mapcomponent’s parameters
What’s coming next:
Lesson 4 explains the coordinate ordering convention in detail
— why the gem flips between [lat, lng] (Ruby-natural) and
[lng, lat] (MapLibre’s native format), and what to watch for
when copying coordinates from elsewhere. This is one of the
most common sources of mapping bugs in any application. A
short, focused chapter.
Lesson 5 adds the corner controls — navigation, scale, attribution — that make the Lab map look like a real piece of UI rather than a stub. By the end of Module 2, the map will be styled, centred, zoomed, and ready for data, which arrives in Module 4.