⌘K

Core

TourStep

One stop on a tour — the element hook it points at, how it is narrowed to one element among many, and what the panel beside it says.

On this page

One stop on a Tour: the element it points at, and what the panel beside that element says. A step names an element hook, the data-wire name the framework writes on its markup, and never a CSS selector. To find the name of the thing you want to point at, open devtools and read it off the element, or list them in bulk — Theming → Finding a name shows both.

use NyonCode\WireCore\Tours\TourStep;

How It Works

What a step points at. TourStep::make('table-search') becomes the selector [data-wire="table-search"]. The browser looks that selector up when the tour starts, and again before each step is shown. The first element that matches is the one the panel points at, as long as it is showing. Much of the framework's markup is rendered and then hidden until it is needed: the bulk-action bar until rows are selected, a dropdown until it opens. An element like that is in the page, but it has no size, so the step is treated as if the element were missing.

Why a hook and not a selector. A hook name is public API. The theming guide promises that a name may be added and is not renamed or removed in a minor release, and npm run hooks:verify holds the framework to it. A class, an id or a structural selector like .mt-4 > div has no such promise, so a tour written against one could break in a release nobody thought was breaking. It is not data-testid either. The two carry the same name where both are present, but a test id is free to change.

Checked when it is made. make() throws a TourDefinitionException for a name that is not a well-formed hook name (kebab-case: lowercase letters and digits, single hyphens, starting with a letter). The check is there because the browser skips a step whose element is missing or hidden. That is on purpose, so a tour keeps working when an application hides a control. Without the check, a typo would be skipped the same way and the tour would get shorter without anyone noticing. The check can only catch a malformed name. A well-formed name that nothing on the page renders is skipped, like any missing element.

Where it runs. Nothing about a step reaches the server after the page has rendered. Its selector, heading, text and placement are sent with the page, and the browser does the rest.

Defaults. No heading, no text, placement bottom, no narrowing.

Basic Usage

TourStep::make('table-search')
->heading('Find a row')
->text('Type here to narrow the table down.');

Content

heading() is the bold line at the top of the panel, and text() is the one or two sentences under it. Either can be left out, or cleared with null. They are plain text, not HTML:

TourStep::make('table-column-toggle')
->heading('Your columns')
->text('Hide the ones you never read. The table remembers your choice.');

Translate them as you would any other string:

TourStep::make('admin-sidebar')->text(__('tours.sidebar'));

Placement

Where the panel sits relative to its element. The value is passed to Floating UI unchanged: top, right, bottom or left, optionally followed by -start or -end. If there is no room on the side you asked for, Floating UI moves the panel to the opposite side.

TourStep::make('admin-sidebar')->placement('right-start'); // beside a tall element, top-aligned
TourStep::make('admin-user')->placement('bottom-end'); // under something in the top-right corner

The value is not validated. Only an empty value is replaced, with bottom. Anything else is passed through as written.

Narrowing to One Element

One hook name can be on many elements at once. Every entry in the sidebar is an admin-nav-item. where() adds a second attribute that picks one of them out:

TourStep::make('admin-nav-item')->where('resource', 'orders');
// [data-wire="admin-nav-item"][data-resource="orders"]

The attribute name is written without its data- prefix and must be a well-formed hook-style name, or where() throws. Its value is escaped, so a key containing a quote cannot break the selector. Several where() calls combine with AND, in the order you wrote them.

This works for admin-nav-item because its view writes data-resource next to the hook. Narrow only by attributes that sit on the same element as the hook.

On Another Page

on() puts a step on another page of the same zone, named by the resource key and the page the panel routes it under:

TourStep::make('table-search')
->on('orders') // the orders list — the page defaults to 'index'
->heading('Every list works like this');
 
TourStep::make('form-actions')->on('orders', 'create');

When the step before it is finished, the browser navigates to that page with the tour in the query string, and the tour carries on at this step. "Back" from it goes to the step before, on whatever page that one is. The address comes from the same owner every other page link does, in the zone the tour is running in, so a step never leads out of its zone.

A step whose page is not routed in that zone is skipped like a missing element, and so is one whose page this person may not open. That is asked of the page's route: the can: middleware wire-panels writes from a page's permission() and its zone's, put to the Gate. Somebody without access to the orders list gets a tour one step shorter, not a 403. A step whose element the other page does not render is found out only on arrival: the tour moves on to the next step there, and if there is none, it stops without being recorded. The progress counter is worked out once, on the page the tour started on, and carried along, so it does not change halfway through.

Keep a tour that spans pages short, because every page change is a full navigation. Somebody who leaves it on another page by clicking elsewhere meets it again on the page it starts on, at the last step before the one they left, so "Next" takes them back (see Tour, left halfway).

Extended Example

A first-run tour whose steps follow the page from left to right, including one that points at a single entry among many in the sidebar:

<?php
 
namespace App\Providers;
 
use Illuminate\Support\ServiceProvider;
use NyonCode\WireCore\Tours\Tour;
use NyonCode\WireCore\Tours\Tours;
use NyonCode\WireCore\Tours\TourStep;
 
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
$this->app->make(Tours::class)->register(
Tour::make('orders-first-run')
->resource('orders')
->page('index')
->steps([
TourStep::make('admin-nav-item')
->where('resource', 'orders')
->heading('Orders')
->text('You are here. Every order the shop has taken is in this list.')
->placement('right'),
 
TourStep::make('table-search')
->heading('Find one')
->text('Search by customer, number or e-mail.')
->placement('bottom-start'),
 
TourStep::make('table-bulk-bar') // hidden until rows are selected — skipped
->text('Select several rows to act on them at once.'),
 
TourStep::make('table-column-toggle')
->heading('Your columns')
->text('Hide the ones you never read.')
->placement('bottom-end'),
]),
);
}
}

table-bulk-bar is on the page but hidden until rows are selected, so on a first visit the step is skipped and the counter shows three steps, not four.

TourStep API

TourStep::make(string $anchor) // an element-hook name; throws if it is not a well-formed one
->heading(?string $heading) // bold line at the top of the panel — default none
->text(?string $text) // the body, plain text — default none
->placement(string $placement) // 'top'|'right'|'bottom'|'left', optionally '-start'|'-end' — default 'bottom'
->where(string $attribute, string $value) // narrow by data-<attribute>="<value>"; the name must be hook-shaped
->on(string $resource, string $page = 'index') // put the step on another page of the same zone — default the tour's own page //
->getAnchor(): string
->getHeading(): ?string
->getText(): ?string
->getPlacement(): string
->getSelector(): string // the CSS selector the browser resolves, narrowing included
->getResource(): ?string // the resource key from on(), or null
->getPage(): ?string // the page from on(), or null
->isElsewhere(): bool // whether on() was called