Reusing Umbraco Properties in Umbraco v14
4 min read
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.
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"
}
]
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
}
}
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;
}
}
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`
}
}
...
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>`
}
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;
};
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]
}));
}
...
}
...
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>`
}
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;
}
...
}
...
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>`
}
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 👋