Skip to content

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" in config/routes.rb, mapping to LabController#index.
  • Controller: app/controllers/lab_controller.rb, an empty index action.
  • 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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<% content_for  :title, "Lab — Vera Dispatch Manager" %>

<%= render Components::PageHeader.new(
      title:    "Lab",
      subtitle: "A development-only space for experimenting with map components.",
      breadcrumb: ["Development", "Lab"]
    ) %>

<div class="p-8">
  <div class="rounded-lg overflow-hidden border border-border bg-surface">
    <%= render Vera::Map.new(id: "lab-map", height: "600px" ) %>
  </div>
</div>

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:

  1. The Rails router matched the URL to LabController#index.
  2. The controller action ran (and did nothing — the empty index method).
  3. Rails resolved the view at app/views/lab/index.html.erb and rendered it.
  4. 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.
  5. The component’s HTML included a <div data-controller="vera--map" ...> element with the map’s configuration JSON in a data attribute.
  6. The browser received the HTML and Stimulus connected the vera--map controller to the div.
  7. The vera--map Stimulus 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:

1
2
3
4
5
6
7
8
<div id="lab-map"
     class="..."
     style="width: 100%; height: 600px;"
     data-controller="vera--map"
     data-vera--map-config-value='{"style":"https://...","centre":[133.77,-25.27],"zoom":4,"sources":[],"layers":[],"markers":[],"controls":[],"images":[]}'
     data-turbo-permanent>
  <!-- MapLibre's rendering targets this div -->
</div>

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:

1
2
3
4
5
<%= render Vera::Map.new(
      id:     "lab-map",
      height: "600px",
      style:  :voyager
    ) %>

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:

1
2
3
4
5
6
7
<%= render Vera::Map.new(
      id:     "lab-map",
      height: "600px",
      style: :voyager,
      centre: [-33.8688, 151.2093],
      zoom:   12
    ) %>

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:

1
2
3
4
<%= render Vera::Map.new(
      id:     "lab-map",
      height: "100vh"
    ) %>

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::Map component’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.