I found the official docs very unclear and confusing, unfortunately.
This is for V15 so I don’t think I can do anything server-side regarding registration.
Unfortunately (again) Copilot, ChatGPT and Claude AI all seem to struggle to get the right answer for v15 and I keep going round in circles!
Maybe someone may spot the issue if I post some code here:
store-picker-property-editor.element.ts
// src/property-editors/store-picker/store-picker-property-editor.element.ts
import { html, customElement, property, state, css } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/property-editor';
import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor';
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
interface Store {
id: string;
name: string;
code?: string;
description?: string;
}
@customElement('loop-store-picker-property-editor')
export class LoopStorePickerPropertyEditorElement extends UmbLitElement implements UmbPropertyEditorUiElement {
@property({ type: String })
value?: string = '';
@property({ attribute: false })
public config?: UmbPropertyEditorConfigCollection;
@state()
private _stores: Store[] = [];
@state()
private _loading = false;
@state()
private _error?: string;
async connectedCallback() {
super.connectedCallback();
await this._loadStores();
}
private async _loadStores() {
this._loading = true;
this._error = undefined;
try {
const response = await fetch('/umbraco/backoffice/loop/stores', {
credentials: 'same-origin'
});
if (!response.ok) {
throw new Error(`Failed to load stores: ${response.statusText}`);
}
const stores = await response.json();
this._stores = stores || [];
} catch (error) {
console.error('Error loading stores:', error);
this._error = error instanceof Error ? error.message : 'Failed to load stores';
this._stores = [];
} finally {
this._loading = false;
}
}
private _onStoreChange(event: Event) {
const target = event.target as HTMLSelectElement;
const newValue = target.value;
this.value = newValue;
// Fixed: Use UmbChangeEvent for proper Umbraco integration
this.dispatchEvent(new UmbChangeEvent());
}
private _getSelectedStoreName(): string {
if (!this.value) return 'No store selected';
const selectedStore = this._stores.find(store => store.id === this.value);
return selectedStore ? selectedStore.name : 'Unknown store';
}
render() {
if (this._loading) {
return html`
<div class="store-picker-loading">
<uui-loader></uui-loader>
<span>Loading stores...</span>
</div>
`;
}
if (this._error) {
return html`
<div class="store-picker-error">
<uui-icon name="icon-alert"></uui-icon>
<span>Error: ${this._error}</span>
<uui-button
label="Retry"
@click=${this._loadStores}
look="secondary"
compact>
Retry
</uui-button>
</div>
`;
}
return html`
<div class="store-picker">
<uui-select
.value=${this.value || ''}
@change=${this._onStoreChange}
placeholder="Select a store">
<uui-select-option value="">
-- Select a store --
</uui-select-option>
${this._stores.map(store => html`
<uui-select-option
value=${store.id}
?selected=${this.value === store.id}>
${store.name}
${store.code ? html` (${store.code})` : ''}
</uui-select-option>
`)}
</uui-select>
${this.value ? html`
<div class="store-picker-preview">
<uui-icon name="icon-store"></uui-icon>
<span>Selected: ${this._getSelectedStoreName()}</span>
</div>
` : ''}
</div>
`;
}
static styles = [
css`
:host {
display: block;
}
.store-picker {
display: flex;
flex-direction: column;
gap: var(--uui-size-space-3);
}
.store-picker-loading,
.store-picker-error {
display: flex;
align-items: center;
gap: var(--uui-size-space-2);
padding: var(--uui-size-space-3);
border: 1px solid var(--uui-color-border);
border-radius: var(--uui-border-radius);
background-color: var(--uui-color-surface);
}
.store-picker-error {
color: var(--uui-color-danger);
border-color: var(--uui-color-danger);
}
.store-picker-preview {
display: flex;
align-items: center;
gap: var(--uui-size-space-2);
padding: var(--uui-size-space-2);
background-color: var(--uui-color-surface-alt);
border-radius: var(--uui-border-radius);
font-size: var(--uui-type-small-size);
color: var(--uui-color-text-alt);
}
uui-select {
width: 100%;
}
`
];
}
// Default export for the manifest system
export default LoopStorePickerPropertyEditorElement;
manifests.ts
// src/property-editors/store-picker/manifests.ts
export const manifests = [
{
type: 'propertyEditorUi',
alias: 'Loop.StorePickerPropertyEditorUi',
name: 'Loop Store Picker Property Editor UI',
element: () => import('./store-picker-property-editor.element.js').then(m => ({ element: m.element })),
meta: {
label: 'Loop Store Picker',
propertyEditorSchemaAlias: 'Loop.StorePicker',
icon: 'icon-store',
group: 'pickers'
}
},
{
type: 'propertyEditorSchema',
alias: 'Loop.StorePicker',
name: 'Loop Store Picker',
meta: {
defaultPropertyEditorUiAlias: 'Loop.StorePickerPropertyEditorUi',
}
}
];
manifests.ts
// src/property-editors/manifest.ts
import { manifests as storePickerManifests } from './store-picker/manifests';
export const manifests = [
...storePickerManifests
];
index.ts
// src/index.ts
import { UmbEntryPointOnInit } from '@umbraco-cms/backoffice/extension-api';
import { UMB_AUTH_CONTEXT } from '@umbraco-cms/backoffice/auth';
import { manifests as documentManifests } from './documents/manifest.ts';
import { manifests as sectionManifests } from './section/manifests.ts';
import { manifests as propertyEditorManifests } from './property-editors/manifests.ts';
export const onInit: UmbEntryPointOnInit = (host, extensionRegistry) => {
console.log('Registering manifests:', {
documents: documentManifests,
sections: sectionManifests,
propertyEditors: propertyEditorManifests
});
extensionRegistry.registerMany([
...documentManifests,
...sectionManifests,
...propertyEditorManifests
]);
host.consumeContext(UMB_AUTH_CONTEXT, async (auth) => {
if (!auth) return;
});
};
vite.config.ts
import { defineConfig } from "vite";
export default defineConfig({
build: {
lib: {
entry: "src/index.ts", // your web component source file
formats: ["es"],
},
outDir: "wwwroot/App_Plugins/Loop", // all compiled files will be placed here
emptyOutDir: true,
sourcemap: true,
rollupOptions: {
external: [/^@umbraco/], // ignore the Umbraco Backoffice package in the build
},
},
});