Hello Solar System
Building on Hello World, this tutorial introduces data fetching with resolves and state parameters. We'll build a list/detail tour of the solar system — the Sun, all eight planets, and one beloved dwarf planet — with real facts and CSS-gradient planet visuals.
Live Demo
What We're Building
- A
planetsstate listing every body in the solar system, ordered by distance from the Sun - A
planetstate showing details of a selected body: kind, distance, diameter, moons, orbital period, and a fun fact - Data is fetched before each state is activated using resolves, from a service with simulated network latency
- The URL includes a parameter for the selected body:
/planets/4is Earth
Resolve Data
What is a Resolve?
A resolve fetches data before a state is entered. The state's component only renders after all resolves have completed. This ensures your component always has the data it needs.
The Solar System Service
First, we create a data service. Each body carries real data plus a CSS gradient used to draw it:
interface SolarBody {
id: number;
name: string;
kind: 'star' | 'rocky planet' | 'gas giant' | 'ice giant' | 'dwarf planet';
distanceAu: number;
diameterKm: number;
moons: number;
orbitalPeriod: string;
funFact: string;
gradient: string;
}
// Ordered by distance from the Sun. (Abbreviated — the example has all ten bodies.)
const solarBodies: SolarBody[] = [
{
id: 1,
name: 'Sun',
kind: 'star',
distanceAu: 0,
diameterKm: 1392700,
moons: 0,
orbitalPeriod: '230 million years around the galactic center',
funFact: "Contains 99.86% of the solar system's mass.",
gradient:
'radial-gradient(circle at 35% 35%, #fff7ae, #ffb703 55%, #d00000)',
},
// ...Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, Neptune, Pluto
];
// Simulated network latency so resolves are observably async.
const delay = <T>(value: T, ms = 300): Promise<T> =>
new Promise((resolve) => setTimeout(() => resolve(value), ms));
const SolarSystemService = {
getAllBodies: (): Promise<SolarBody[]> => delay(solarBodies),
getBody: (id: number): Promise<SolarBody | undefined> =>
delay(solarBodies.find((b) => b.id === id)),
};The delay helper makes each fetch take ~300ms, like a real API call. The router waits for it — notice the pause before each view appears.
Adding Resolves to States
const planetsState: LitStateDeclaration = {
name: 'planets',
url: '/planets',
component: PlanetListComponent,
// Resolve blocks the transition until the async data is ready.
resolve: [
{
token: 'planets',
resolveFn: () => SolarSystemService.getAllBodies(),
},
],
};The resolve block:
- token: A string identifier for the resolved data
- resolveFn: An async function that returns data (or a Promise)
When navigating to planets, the router:
- Calls
SolarSystemService.getAllBodies() - Waits for the Promise to resolve
- Renders
PlanetListComponentwith the data available
Accessing Resolved Data
Components receive resolved data through _uiViewProps:
import { UIViewInjectedProps } from 'lit-ui-router';
@customElement('planet-list')
class PlanetListComponent extends LitElement {
// The router constructs routed components with injected props.
@property({ attribute: false })
_uiViewProps!: UIViewInjectedProps;
constructor(props: UIViewInjectedProps) {
super();
this._uiViewProps = props;
}
// Populated by the `planets` resolve on the list state.
get planets(): SolarBody[] {
return this._uiViewProps.resolves!.planets;
}
render() {
return html`
<h3>Bodies by distance from the Sun</h3>
<ul>
${this.planets.map(
(planet) => html`
<li>
<a ${uiSref('planet', { planetId: planet.id })}>
<span
class="body"
style="width:${dotSize(planet.diameterKm)}px;height:${dotSize(planet.diameterKm)}px;background:${planet.gradient}"
></span>
<span class="name">${planet.name}</span>
<span class="kind">${planet.kind}</span>
</a>
</li>
`,
)}
</ul>
`;
}
}Key points:
_uiViewProps: Injected by<ui-view>, containsresolves,router, andtransition- Constructor parameter: The props are passed to the constructor when the component is created
resolves.planets: Access data using the resolve's token name
Each list item draws its body as a radial-gradient circle. A dotSize helper log-scales the circle by real diameter, so the Sun and Pluto fit on the same screen:
// Log scale keeps the Sun and Pluto on the same screen.
const dotSize = (diameterKm: number): number =>
Math.round(Math.log2(diameterKm / 1000) * 6 + 10);State Parameters
Defining Parameters
The planet state needs to know which body to display. We define a URL parameter:
const planetState: LitStateDeclaration = {
name: 'planet',
url: '/planets/:planetId',
component: PlanetDetailComponent,
// deps injects $transition$ so the resolve can read the route parameter.
resolve: [
{
token: 'planet',
deps: ['$transition$'],
resolveFn: ($transition$: Transition) => {
const planetId = parseInt($transition$.params().planetId);
return SolarSystemService.getBody(planetId);
},
},
],
};:planetId: Defines a URL parameter. For/planets/4,planetIdwould be"4"deps: ['$transition$']: Inject the current transition object$transition$.params(): Access all state parameters
Linking with Parameters
Pass parameters when creating state links:
html`<a ${uiSref('planet', { planetId: planet.id })}>${planet.name}</a>`;The second argument to uiSref is a parameters object. This generates a URL like /planets/4.
The Planet Detail Component
The detail view renders a larger orb plus a definition list of facts:
@customElement('planet-detail')
class PlanetDetailComponent extends LitElement {
@property({ attribute: false })
_uiViewProps!: UIViewInjectedProps;
constructor(props: UIViewInjectedProps) {
super();
this._uiViewProps = props;
}
// Populated by the `planet` resolve, keyed off the :planetId route param.
get planet(): SolarBody | undefined {
return this._uiViewProps.resolves!.planet;
}
render() {
if (!this.planet) {
return html`<p>
Body not found. <a class="back-link" ${uiSref('planets')}>Back</a>
</p>`;
}
const size = dotSize(this.planet.diameterKm) * 2;
return html`
<div>
<h3>${this.planet.name}</h3>
<span
class="body"
style="display:inline-block;width:${size}px;height:${size}px;background:${this.planet.gradient}"
></span>
<dl>
<dt>Kind</dt>
<dd>${this.planet.kind}</dd>
<dt>Distance from Sun</dt>
<dd>${this.planet.distanceAu} AU</dd>
<dt>Diameter</dt>
<dd>${this.planet.diameterKm.toLocaleString('en-US')} km</dd>
<dt>Known moons</dt>
<dd>${this.planet.moons}</dd>
<dt>Orbital period</dt>
<dd>${this.planet.orbitalPeriod}</dd>
</dl>
<p class="fun-fact">${this.planet.funFact}</p>
<a class="back-link" ${uiSref('planets')}>Back to the solar system</a>
</div>
`;
}
}Note the graceful fallback: if the URL contains an unknown id (say /planets/42), the resolve returns undefined and the component renders "Body not found" with a link back to the list.
Router Setup
The router configuration is the same as Hello World, plus the UI Router visualizer so you can watch states and transitions as you click around:
const router = new UIRouterLit();
router.plugin(hashLocationPlugin);
import('@uirouter/visualizer').then(({ Visualizer }) =>
router.plugin(Visualizer),
);
router.stateRegistry.register(planetsState);
router.stateRegistry.register(planetState);
router.urlService.rules.initial({ state: 'planets' });
router.start();Full Source Code
The complete source — including all ten solar bodies and the starfield styling — is in examples/hellosolarsystem/src/main.ts, or browse it in the StackBlitz embed above.
URL Persistence
Notice that the URL contains all the state information:
/#/planets- The full list/#/planets/4- Earth's details
You can bookmark these URLs or refresh the page, and the application will restore to the same state — the resolves re-run and the data is re-fetched. This is one of the key benefits of state-based routing.
Next Steps
Continue to Hello Galaxy to learn about:
- Nested states with parent-child relationships
- Nested ui-views for complex layouts
- Relative state references for navigation within a state hierarchy
- Resolve inheritance between parent and child states