@creative-locator/solid
v0.2.0
Published
Solid.js component bindings for Creative Locator. Renders the headless locator inside a Solid tree with idiomatic onMount/onCleanup lifecycle, forwarding every createHeadlessLocator option as a prop.
Maintainers
Readme
@creative-locator/solid
Solid.js component bindings for Creative Locator.
A thin wrapper around @creative-locator/headless that gives Solid applications two integration shapes:
<DealerLocator />component — drop a single tag into your JSX, pass anapiUrl, render. The component derives the host container from a Solid ref so you never have to managedocument.querySelectorcalls or worry about hydration timing.- Composition primitives —
createDealerLocator/createDealerLocatorState/createDealerEventgive Solid hosts signal-based reactive access to locator state and typed event subscription withonCleanup-driven teardown. See ADR-010 for the design.
Install
pnpm add @creative-locator/core @creative-locator/headless @creative-locator/solid leaflet leaflet.markerclustersolid-js is a peer dependency (^1.8). The package does not bundle Solid.
Usage
import { DealerLocator } from '@creative-locator/solid';
import '@creative-locator/core/styles/style.scss';
export function LocatorPage() {
return (
<DealerLocator
apiUrl="https://example.com/wp-json/creative-locator/v1"
style={{ height: '600px' }}
/>
);
}The component renders a single <div> as the host element. Sizing is the host application's responsibility — apply height via the style prop, the class prop, or a parent container. Without a non-zero height, the locator's map will not be visible.
Props
DealerLocator accepts every option from createHeadlessLocator except container (which is derived from the component's internal ref), plus two layout-shaped props for the wrapping <div>:
| Prop | Type | Default | Notes |
| ----------------------- | ---------------------------------- | -------- | ----------------------------------------- |
| apiUrl | string | — | Required. Creative Locator REST base URL. |
| config | Partial<LocatorConfig> | {} | Merged over defaultLocatorConfig. |
| apiHeaders | Record<string, string> | — | Auth / nonce headers. |
| fetch | typeof globalThis.fetch | global | Override for SSR or test contexts. |
| storage | 'browser' \| 'memory' \| Storage | auto | Auto-detects SSR. |
| enableGoogleAnalytics | boolean | false | GA bridge off by default in headless. |
| licenseTier | 'free' \| 'pro' | 'free' | Gates Pro layouts client-side. |
| adapters | Partial<PlatformAdapters> | — | Per-slot adapter overrides (advanced). |
| class | string | — | Applied to the wrapping <div>. |
| style | JSX.CSSProperties \| string | — | Applied to the wrapping <div>. |
See @creative-locator/headless for the canonical option reference.
Mount-once contract
Like most map libraries, the locator is expensive to initialize. The component intentionally ignores prop changes after the initial mount — re-rendering with a new apiUrl or config does NOT recreate the locator. If you need to swap configuration at runtime, remount the component:
<Show when={apiUrl} keyed>
{(url) => <DealerLocator apiUrl={url()} />}
</Show>This is the same trade-off Leaflet wrappers (e.g. solid-leaflet) make for the same reason.
Composition primitives
For applications that want reactive bindings into locator state (e.g. rendering a result count, a filter summary, or a "pan map to load more" banner in JSX), use the three primitives.
createDealerLocator(containerAccessor, options)
Mount a locator into a host element returned by an accessor thunk. Returns { locator } — an Accessor<DealerLocator | null> that returns null before onMount fires and immediately after the owning root disposes.
import { createDealerLocator } from '@creative-locator/solid';
import '@creative-locator/core/styles/style.scss';
export function LocatorPage() {
let container!: HTMLDivElement;
const { locator } = createDealerLocator(() => container, {
apiUrl: 'https://example.com/wp-json/creative-locator/v1',
config: { layout: 'classic' },
});
return <div ref={container} style={{ height: '600px' }} />;
}Teardown registers via onCleanup, so the primitive works correctly in components, manual createRoot() blocks, route navigation, and HMR. The same mount-once contract as the <DealerLocator /> component applies — to swap apiUrl / config at runtime, toggle the mounting <Show> with a fresh key wrapper.
createDealerLocatorState(locator)
Reactive bridge to the four orchestration-relevant locator state fields:
import { Show } from 'solid-js';
import { createDealerLocator, createDealerLocatorState } from '@creative-locator/solid';
export function LocatorPage() {
let container!: HTMLDivElement;
const { locator } = createDealerLocator(() => container, { apiUrl: '...' });
const { allDealers, currentRadius, isViewportMode, currentFilters } =
createDealerLocatorState(locator);
return (
<>
<div ref={container} style={{ height: '600px' }} />
<p>
Showing {allDealers().length} dealers within {currentRadius()} mi.
</p>
<Show when={isViewportMode()}>
<p>Pan the map to load more.</p>
</Show>
</>
);
}Each returned Accessor is auto-updated after every results:updated / filter:changed / search:start event tick. Internally the primitive uses createRenderEffect so state writes settle synchronously with the locator-attach signal write — setLocator(instance) followed by state.allDealers() reads the post-attach snapshot in the same task.
createDealerEvent(locator, event, handler)
Typed event subscription with automatic teardown. The event parameter is constrained to one of the 10 documented public event slugs; the handler payload type is narrowed automatically with no cast:
import { createDealerLocator, createDealerEvent } from '@creative-locator/solid';
export function LocatorPage() {
let container!: HTMLDivElement;
const { locator } = createDealerLocator(() => container, { apiUrl: '...' });
createDealerEvent(locator, 'marker:click', (payload) => {
// `payload` is typed { location: DealerSummary }
console.log('clicked', payload.location.id);
});
createDealerEvent(locator, 'results:updated', ({ results, radius }) => {
// `results` is DealerSummary[], `radius` is number
});
return <div ref={container} />;
}The subscription is released on owner disposal, when the locator accessor returns null, and when the locator accessor swaps to a different instance.
SolidStart
The component touches window and document (via Leaflet), so it must run on the client. In SolidStart, gate it behind clientOnly:
import { clientOnly } from '@solidjs/start';
const Locator = clientOnly(() => import('./Locator'));
export default function LocatorPage() {
return <Locator />;
}Where ./Locator.tsx is the file containing <DealerLocator />. clientOnly ensures the import only runs in the browser.
License
GPL-2.0-or-later
