Creating your own UI extension points in Umbraco v14 - Part 4: Interchangeable Elements

6 min read

In the last post we looked at how we can swap out our "quick action" button behavior whenever a button is clicked.

Quick Action Buttons

In this post we'll take a look at how we could allow someone to completely replace the button element with some other user control.

Element Extensions

To allow our actions to support a interchangeable elements we first need to update our manifest definition again.

export interface ManifestQuickAction extends ManifestElementAndApi<QuickActionElement, QuickActionApi> {
    type: 'quickAction';
    meta: MetaQuickAction;
}

export interface QuickActionElement extends UmbControllerHostElement {
    manifest: UcManifestEntityQuickAction;
    api?: QuickActionApi;
}

Here again we swap out the base interface and now extend from ManifestElementAndApi<>. We also define a QuickActionElement interface which lays out the required properties that any element used as a quick action must support. This interface, along with our previously defined API interface, are passed to the ManifestElementAndApi<> interface as generic arguments.

If we take a look at the ManifestElementAndApi<> interface we can see that our manifest now supports a couple of extra properties.

export interface  ManifestElementAndApi<ElementType, ApiType> extends ManifestBase {
    element?: ElementLoaderProperty<ElementType>;
    elementName?: string;
    ...
}

Default Element Updates

With our element interface defined, we can go back to our default button implementation and ensure this now implements our required interface.

@customElement("quick-action")
export class QuickActionElement extends UmbElementMixin(LitElement)
    implements QuickActionElement {

    @property({ type: Object, attribute: false })
    manifest!: ManifestQuickAction;

    @property({ type: Object, attribute: false })
    api?: QuickActionApi;

    #onClick(e: Event) {
        this.api?.execute();
    }

    render() {
        return html`<uui-button 
                look=${this.manifest.meta.look ?? 'secondary'}
                @click=${this.#onClick}>
                ${this.manifest.meta.label}
           </uui-button>`
    }
}

export default QuickActionElement;

declare global {
    interface HTMLElementTagNameMap {
        "quick-action": QuickActionElement;
    }
}

Overriding the Default Element

With everything configured developers can now override the default button element by firstly creating their own component that implements our interface.

@customElement("my-quick-action")
export class MyQuickActionElement extends UmbElementMixin(LitElement)
    implements QuickActionElement {

    @property({ type: Object, attribute: false })
    manifest!: ManifestQuickAction;

    @property({ type: Object, attribute: false })
    api?: QuickActionApi;

    #onClick(e: Event) {
        this.api?.execute();
    }

    render() {
        return html`RENDER YOUR COMPONENT HERE`
    }
}

export default MyQuickActionElement;

declare global {
    interface HTMLElementTagNameMap {
        "my-quick-action": MyQuickActionElement;
    }
}

Then updating the manifest to use this component instead.

export const quickActionManifests: ManifestQuickAction[] = [
    {
        type: 'quickAction',
        alias: 'Mb.QuickAction.SendEmail',
        name: 'Send Email Quick Action',
        weight: 200,
        element: MyQuickActionElement,
        api: SendEmailQuickActionApi,
        meta: {
            label: "Send Email",
            look: "primary"
        }
    },
    {
        type: 'quickAction',
        alias: 'Mb.QuickAction.ChangeStatus',
        name: 'Change Status Quick Action',
        weight: 100,
        element: MyQuickActionElement,
        api: ChangeStatusQuickActionApi,
        meta: {
            label: "Change Status"
        }
    }
]

What's next?

In this post we've looked out how to allow developers to override the default element of an extension.

In the next post we'll take a look at how we can reduce some repetition in our manifest definitions by creating reusable 'kinds'.

Until then 👋