Reusing Umbraco Properties in Umbraco v14

mattbrailsford

Matt Brailsford

Posted on April 23, 2024

Reusing Umbraco Properties in Umbraco v14

When building user interfaces in Umbraco v14, occasionally there comes a time when you need to build a form based on some dynamically defined properties. One such example in Umbraco Commerce is how Payment Providers expose the settings they need for configuration.

Payment Provider Settings

When it comes to capturing this configuration, we ideally don't want to have to define these forms manually. Instead, it would be much better if we could use the exposed setting definitions as a pseudo doctype and dynamically render the form reusing the built in property editors in Umbraco to allow capturing different value types.

Data

I won't in this post go into the server side configuration for the payment provider settings, but needless to say, we have a REST endpoint that returns a JSON configuration for our settings similar to the following:

[
    {
        "alias": "continueUrl",
        "label": "Continue Url",
        "description": "The URL to continue to after the provide has done processing",
        "editorUiAlias": "Umb.PropertyEditorUi.TextBox"
    },
    ...
    {
        "alias": "textMode",
        "label": "Test Mode",
        "description": "Whether to run in test mode or production mode",
        "editorUiAlias": "Umb.PropertyEditorUi.Toggle"
    }
]
Enter fullscreen mode Exit fullscreen mode

In addition, our Payment Method entities which hold the configuration values have a properties collection on it similar to the following:

{
    "id": "e06f29df-74fa-4ec8-8240-acccda44e702",
    "alias" : "invoicing",
    "name": "Invoicing",
    "properties": {
        "continueUrl": "/checkout/continue",
        "cancelUrl": "/checkout/cancel",
        "maxRetries": 12,
        "testMode": true
    }
}
Enter fullscreen mode Exit fullscreen mode

Component

Lets start with a basic component that will be responsible for rendering out form.

@customElement("my-component")
export class MyComponentElement extends UmbElementMixin(LitElement) {
    render() {
        return html`TODO`
    }
}

export default MyComponentElement;

declare global {
    interface HTMLElementTagNameMap {
        "my-component": MyComponentElement;
    }
}
Enter fullscreen mode Exit fullscreen mode

Lets now setup a couple of stateful variables to hold our data structures in and define a constructor that is responsible for populating those properties.

@customElement("my-component")
export class MyComponentElement extends UmbElementMixin(LitElement) {

    @state() 
    _settings: Array<MySettingType> = [];

    @state() 
    _model?: MyEntityType;

    constructor(host: UmbControllerHost) {
        super(host);
        // Some code to fetch and populate the _settings + _model
    }

    render() {
        return html`TODO`
    }
}
...
Enter fullscreen mode Exit fullscreen mode

I'll leave the fetching of the data up to you as it's not the important part, but we can move on with the knowledge we should now have some data in variables in the structures outlines above.

Rendering

The two components that are key to reusing umbraco properties are the umb-property-dataset and the umb-property.

Lets initially setup our rendering function as follows:

render() {
    return html`<umb-property-dataset>
        ${repeat(
            this._settings,
            (itm) => itm.key,
            (itm) => html`<umb-property
                alias=${itm.alias}
                label=${itm.label}
                description=${itm.description}
                property-editor-ui-alias=${itm.editorUiAlias}>
            </umb-property>`)}
    </umb-property-dataset>`
}
Enter fullscreen mode Exit fullscreen mode

I'll get to why we need a umb-property-dataset in a second, but for now we'll just need to know that we must have one wrapped around our rendered properties.

Inside the umb-property-dataset tag, we'll then loop through our setting definitions and use the umb-property component, passing through the alias, label, description and editorUiAlias from our settings.

If we were to now look on our front end you should now see your properties rendering, complete with labels, descriptions and the reusing of umbraco property editors.

Pretty neat for relatively little code 😎

Binding

Right now however, there won't be any values in our properties, and we aren't capturing any value changes either.

This is where the umb-property-dataset comes into play. Rather than needing to set values and handle input events for all properties, we instead populate a single values collection, and listen for a single event from the umb-property-dataset instead.

The first thing we need to do is massage our property data into the expected format. The umb-property-dataset takes in a value of type Array<UmbPropertyValueData> where UmbPropertyValueData is defined as:

export type UmbPropertyValueData<ValueType = unknown> = {
    alias: string;
    value?: ValueType;
};
Enter fullscreen mode Exit fullscreen mode

In our component, lets introduce another stateful variable and populate it after we populated our other settings.

@customElement("my-component")
export class MyComponentElement extends UmbElementMixin(LitElement) {

    ...

    @state() 
    _values: Array<UmbPropertyValueData> = [];

    constructor(host: UmbControllerHost) {
        super(host);
        // Some code to fetch and populate the _settings + _model
        this._values = this._settings.map(setting => ({
           alias: setting.alias,
           value: this._model?.properties[setting.alias]
        }));
    }

    ...
}
...
Enter fullscreen mode Exit fullscreen mode

Here we loop through all our setting definitions, creating a new UmbPropertyValueData entry populating it's alias with the setting alias and then it's value with the value of that setting found in our entity model.

With our property values now in the format we need, we can update our render function as follows:

render() {
    return html`<umb-property-dataset
        .value=${this._values}>
        ${repeat(
            this._settings,
            (itm) => itm.key,
            (itm) => html`<umb-property
                alias=${itm.alias}
                label=${itm.label}
                description=${itm.description}
                property-editor-ui-alias=${itm.editorUiAlias}>
            </umb-property>`)}
    </umb-property-dataset>`
}
Enter fullscreen mode Exit fullscreen mode

Here we have bound our _values variable to the value attribute of the umb-property-dataset component and that is all we need to do to get our store values to display.

Change Handling

The last part of the puzzle is reacting to change and persisting the updated values back.

For this we'll create a single event handler to capture all changes like so.

@customElement("my-component")
export class MyComponentElement extends UmbElementMixin(LitElement) {

    ...

    #onPropertyDataChange(e: Event) {
        // Grab the value
        const value = (e.target as UmbPropertyDatasetElement).value;
        // Convert the value back into an object
        var data = value.reduce((acc, curr)=>({...acc, [curr.alias]: curr.value}), {});
        // Update our model
        this._model.properties = data;
    }

    ...
}
...
Enter fullscreen mode Exit fullscreen mode

Here we get the data set value (which contains values for all our settings) and then we convert it into the same structure of our models property collection, and then reset the value back on the model.

Again, I'll leave the persistence side of things to you here, but for this example I'm just writing back to the main entity model..

Finally, we'll update our render function again to attach our event handler.

render() {
    return html`<umb-property-dataset
        .value=${this._values}
        @change=${this.#onPropertyDataChange}>
        ${repeat(
            this._settings,
            (itm) => itm.key,
            (itm) => html`<umb-property
                alias=${itm.alias}
                label=${itm.label}
                description=${itm.description}
                property-editor-ui-alias=${itm.editorUiAlias}>
            </umb-property>`)}
    </umb-property-dataset>`
}
Enter fullscreen mode Exit fullscreen mode

And that's it. We now have a dynamically generated form that reuses umbraco property editors for input collection and where we can capture and store the user input.

I hope you found this post helpful.

If you'd like to see an example implementation of using the umb-property-dataset and umb-property elements there is an example in the Umbraco BackOffice code base here.

Until next time 👋

💖 💪 🙅 🚩
mattbrailsford
Matt Brailsford

Posted on April 23, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related