Custom Property Editor - Simple Examples

Can anyone point me at a repo / tutorial that will provide a simple starting point for building custom property editors.

I’ve stumbled through the official docs (which need a few updates which I hope to supply) but then I’m not sure of the next steps.

I’m trying to rebuild a simple v13 custom editor that just stored a blob of json from a number of key value paired text fields. I’ve created a models.ts file and made a custom model but not sure how to get this to get and set. Any pointers appreciated.

Apologies as I don’t have a direct answer to this specific questions, but I would recommend, if you need to start building Umbraco extensions, to start with:

Instructions here:

This will set up an environment where you can just start coding, instead of having to figure out what goes where.

It doesn’t include a custom property editor, but I would then have a look at the marketplace and look for something that looks kind of simple and close to what I’m trying to do, for example, here’s a property editor:

The source leads to these few files:

They’re in the structure that was set up from the opinionated starter, so would be a great place to start from and customize for your own needs.

The docs got me through setting up the project and I can happily build out a new property editor - having it as a package is out of scope for me now.

I guess where I’m stuck is more on my complete lack of understanding of Lit / TS! Any other code I’ve found is beyond where I am right now.

Bergmania’s Open Street Map looks great… but too complex for my addled brain right now

So based around the docs -

Here’s the start of my property editor:
ss-translations-property-editor-ui.element.ts

import { LitElement, html, css, customElement, property, state } from "@umbraco-cms/backoffice/external/lit";
import { UmbPropertyEditorUiElement } from "@umbraco-cms/backoffice/property-editor";
import { UmbPropertyValueChangeEvent } from "@umbraco-cms/backoffice/property-editor";
import { SSTranslationModel } from "./models.ts";

@customElement('ss-translations-property-editor-ui')
export default class SSTranslationsPropertyEditorUIElement extends LitElement implements UmbPropertyEditorUiElement {
    //@property({ type: String })
    //public value = "";
    @property()
    value: Partial<SSTranslationModel> = {};


    #onInput(e: InputEvent) {
        this.value.key = "test";
        //this.value = (e.target as HTMLInputElement).value;
     //   this.value = {
     //       key: "test",
     //       value: (e.target as HTMLInputElement).value
     //   };
       // this.value.key = "test";
      // this.value.value = (e.target as HTMLInputElement).value;
        //this.value = {
        //    //...this.value,
        //    key = "test",
        //    value = (e.target as HTMLInputElement).value
                
        //    }
        //};

        //this.value.items[0] = (e.target as HTMLInputElement).value;


        this.#dispatchChangeEvent();
    }

    #dispatchChangeEvent() {
        this.dispatchEvent(new UmbPropertyValueChangeEvent());
    }

    @state()
    private _suggestions = [
        'You should take a break',
        'I suggest that you visit the Eiffel Tower',
        'How about starting a book club today or this week?',
        'Are you hungry?',
    ];

    #onSuggestion() {
        const randomIndex = (this._suggestions.length * Math.random()) | 0;
        this.value = this._suggestions[randomIndex];
        this.#dispatchChangeEvent();
    }

    render() {
        return html`
      <uui-input
        id="suggestion-input"
        class="element"
        label="text input"
        .value=${this.value || ""}
        @input=${this.#onInput}
      >
      </uui-input>
      <div id="wrapper">
        <uui-button
          id="suggestion-button"
          class="element"
          look="primary"
          label="give me suggestions"
          @click=${this.#onSuggestion}
        >
          Give me suggestions!
        </uui-button>
        <uui-button
          id="suggestion-trimmer"
          class="element"
          look="outline"
          label="Trim text"
        >
          Trim text
        </uui-button>
      </div>
    `;
    }

    static styles = [
        css`
    #wrapper {
      margin-top: 10px;
      display: flex;
      gap: 10px;
    }
    .element {
      width: 100%; 
    }
  `,
    ];
}



declare global {
    interface HTMLElementTagNameMap {
        'ss-translations-property-editor-ui': SSTranslationsPropertyEditorUIElement;
    }
}

models.ts - You’ll see above I’ve starting simple trying to just have a single key value pair to try to store a really simple json blob and render it back in two text fields.

export type SSTranslationModel = {
    key: string,
    value: string
};


export type SSTranslationsModel = {
    items: SSTranslationModel[]
};

I think my baby steps are:

  1. Try to change the #onInput to store both a single key and value value to the model.
  2. Store this on save and publish
  3. Be able to then set these values back on load.

I think I need get and set “methods” - or do I?? Totally lost here.

This is probably the simplest example out there:

I think I would be where you are, I’ve played a little bit more but I don’t remember enough I will admit.

  1. Wouldn’t it be best to have the same model? Just store a single or multiple keys, then you don’t need to check whenever you read the value
  2. That should be automatic on this.dispatchEvent(new UmbPropertyValueChangeEvent());
  3. That might be the harder part. I would try to make a model first and apply that during the render always, when the model is empty then it’s the default state, when the model has a saved value then selected items will show

I am not quite sure I can visualize your editor at the moment, so I’m stabbing in the dark a bit.

One other tip, if you’re so inclined: I’ve asked GitHub CoPilot before something like “I need a web component that does x and y, when change a value is needs to dispatch an event and when it loads with a value I need to apply the loaded value”.

That helps get some example code in front of me, from which I can puzzle the rest together.

I feel you, it’s a bit overwhelming at first. I’ll try to be short :wink:

First of all, you need to implement the UmbPropertyEditorUiElement like this:

import { LitElement, ... } from "@umbraco-cms/backoffice/external/lit";
import { UmbPropertyEditorUiElement, ... } from "@umbraco-cms/backoffice/property-editor";

export default class ExamplePropertyEditorUIElement extends UmbElementMixin(LitElement) implements UmbPropertyEditorUiElement
{
}

This interface makes you implement the value property. The value is what is saved to Umbraco and loaded from Umbraco. So if you set something in the property and the user presses save or save and publish in the backoffice, anything in value will be stored. You don’t need to do any custom logic for saving, except setting the value property.

Value will be populated with the saved value once your component loads, so no custom loading code either.

import { LitElement, ... } from "@umbraco-cms/backoffice/external/lit";
import { UmbPropertyEditorUiElement, ... } from "@umbraco-cms/backoffice/property-editor";

export default class ExamplePropertyEditorUIElement extends UmbElementMixin(LitElement) implements UmbPropertyEditorUiElement
{
	/** 
	 * Defines the actual data of the editor that will be saved to Umbraco and get populated automatically when the editor is loaded 
	 * Make sure to call the #dispatchPropertyValueChanged() function when the value changes to notify Umbraco of the change
	 */
	@property({ type: String })
	public value: undefined | ExampleModel;
}

Ok I lied a little for simplicity’s sake. As you can see, the only other thing you need to do, except setting the correct value in to the value property, is dispatching events to let Umbraco know the value changed. If you don’t do this, the changes will not be saved.

import { LitElement, ... } from "@umbraco-cms/backoffice/external/lit";
import { UmbPropertyEditorUiElement, ... } from "@umbraco-cms/backoffice/property-editor";

export default class ExamplePropertyEditorUIElement extends UmbElementMixin(LitElement) implements UmbPropertyEditorUiElement
{
	/** 
	 * Defines the actual data of the editor that will be saved to Umbraco and get populated automatically when the editor is loaded 
	 * Make sure to call the #dispatchPropertyValueChanged() function when the value changes to notify Umbraco of the change
	 */
	@property({ type: String })
	public value: undefined | ExampleModel;

	/**
	 * Updates the value of the editor and dispatches a property value change event.
	 * @param newValue The new value to set the editor to
	 */
	#updatePropertyEditorValue(newValue: undefined | ExampleModel) {
		this.value = newValue;
		this.dispatchEvent(new UmbPropertyValueChangeEvent());
	}
}

I usually just make a function called UpdatePropertyEditorValue (see above) so it’s sets the new value AND dispatches the event.

This is the very basics of the property editor and how it works.

If your property editor has settings, you can implement the config property.

	/** Gets the editor configuration and sets the appropriate variables based on it */
	@property({ attribute: false })
	public set config(config: UmbPropertyEditorConfigCollection) {
		this.allowCustomAspectRatio = config.getValueByAlias("customAspectRatioEnabled") ?? false;
	}
3 Likes

@cheeseytoastie any luck with the examples?

I’m getting closer to my solution. Just having to do this in spare moments so not making the progress I’d like.

I’ll post my property editor as a package as it might help others.

What I’m trying to build is a simple key value list. Based around the multiple text fields property editor. e.g. you can add multiple key value pairs. I’ll add a config so the name is checked for uniqueness as an option - and basic functionality would be drag / sort, delete and add.

Here’s what works;

  • I can now store a json blog and load it again - it’s an array of a simple type I’ve defined.
  • I can add items and on save and publish these are saved to the db.
  • I can load existing items and render
  • I can deal with an empty / new property

Here’s where I’m stuck!

  • Constructor - value is empty - assigning the Umbraco value to internal properties / states. On “load” I want the this.value to be assigned to this._items - the array is empty in the constructor - is there a “umbracoLoaded” or something event that can be hooked into - this.value assignment seems to be a bit mystical to me. How is this bound / assigned?
  • Rendering a list of html fields - more than just a list of a values - I think I need to create a a element item (like the Umbraco-CMS/src/Umbraco.Web.UI.Client/src/packages/core/components/multiple-text-string-input/input-multiple-text-string-item.element.ts at contrib · umbraco/Umbraco-CMS · GitHub - and render an bound HTML “control” item for each item in the list. I’ll need to bind events on these and know the index on change so I can update the list / value
    • I tried copying this example but I can’t work out is what the custom property equivalent of
... extends UUIFormControlMixin(UmbLitElement, '') {  

UUIFormControlMixin doesn’t seem to be available to me. I’m not Lit / Typescript capable enough to know what a simple way of doing this with just html elements?! I’m sure it’s super simple just can’t find an example.

WiP code is below - I’ve commented the bits where I’m stuck - any pointers very appreciated!

import { LitElement, css, html, customElement, property, state, query, repeat } from "@umbraco-cms/backoffice/external/lit";
import { UmbPropertyEditorUiElement } from "@umbraco-cms/backoffice/property-editor";
import { UmbPropertyValueChangeEvent } from "@umbraco-cms/backoffice/property-editor";

/*
This is a work in progress

WHAT WORKS NOW
* Not much - can add items to a list - the save and publish does store to the db and it loads.
* I do check for a new property so that it works to add the first item

TODO:
1) I'm using the this.value directly, I think it should use the _items and just update the value for save and publish / storing the
data. What I can't work out is how I'd init _items as I'm unclear on how this.value is populated in the first place - I seem to declare it and it's magically assigned (perhaps in UmbPropertyEditorUiElement?)
2) Render a list of key value pair text fields with a sorter and a delete button (like the multiple text string controls)
2)a Allow for edits on existing items
2b) Check the key is unique and not null on add 
3) Wire everything up.
4) Maybe a backend property type convertor. - if we use a key value pair then recheck items are unique - what to do if some junk duplicates are there?
*/

// todo - should these be here or inside the custom element> 
type UmbCommunityKeyValue = {
  key: string;
  value: string;
};
type ArrayOf<T> = T[];

@customElement('key-values-property-editor-ui')
export default class UmbCommunityKeyValuesPropertyEditorUIElement extends LitElement implements UmbPropertyEditorUiElement {

  @property()
  public value: ArrayOf<UmbCommunityKeyValue> = [];

  /* this isn't used at the moment - how to init with the values from value ? */
  @state()
  private _items: ArrayOf<UmbCommunityKeyValue> = [];

  @query('#key-value-new-key')
  newNameInp!: HTMLInputElement;

  @query('#key-value-new-value')
  newValueInp!: HTMLInputElement;

  constructor() {
    super();

    // this is empty there - so how do I assign this.value to the _items on init?
    this._items = this.value;
    console.log(this._items.length);
  }

  #onAddRow() {
    const currentInputTyped: UmbCommunityKeyValue = {
      key: this.newNameInp.value,
      value: this.newValueInp.value
    };

    // check the value is an array, the concatenate o/w create an array with this as the first item
    // todo - update to use this._items not value when the items is set on init.
    this._items = Array.isArray(this.value) ? [...this.value, currentInputTyped] : [currentInputTyped];

    this.#updatePropertyEditorValue();
  }

  #updatePropertyEditorValue() {
    this.value = this._items;
    this.dispatchEvent(new UmbPropertyValueChangeEvent());
  }

  renderTranslationList() {
    // this is a simple example of just writing out the values - to be replaced with the fields below
    // todo - use this._items when init issue fixed
    if (this.value?.length) {
      return html`
            <ul>
                ${this._items.map((translation) =>
        html`<li>${translation.key} ${translation.value}</li>`)}
            </ul>`
    } else {
      return html`<span>create an item</span>`;
    }
  }

  renderTranslationFields() {
    // todo - use this._items when init issue fixed
    if (this.value?.length) {
     return html`
           ${repeat(this._items, (item) => item.key, (item, index) => {
             return html`<p>${index}: ${item.key}: ${item.value}</p>`;
     })}
           `;
  
     // todo - do something similar to the umbraco multiple text string - so create an item element and render a list of these.
     // the core example uses UUIInputElement which doesn't seem to be available
  
     //return html`
     //${repeat(
     //    this.value,
     //    (item, index) => index,
     //    (item, index) => html`
     //		<umb-input-multiple-text-string-item
     //			name="item-${index}"
     //			data-sort-entry-id=${item}
     //			required
     //			required-message="Item ${index + 1} is missing a value"
     //			value=${item}>
     //		</umb-input-multiple-text-string-item>
     //	`,
     //    // 	  				    @enter=${this.#onAdd}
     //    // @delete=${(event: UmbDeleteEvent) => this.#deleteItem(event, index)}
     //	//		@input=${(event: UmbInputEvent) => this.#onInput(event, index)}
     //)}
     //`;
   } else {
     return html`<span>You don't have any items yet.</span>`;
   }
  }

  render() {
    return html`
        ${this.renderTranslationList()}
        ${this.renderTranslationFields()}
            <uui-input
                id="key-value-new-key"
                class="element"
                label="text input"
                value=""
            >
            </uui-input>
            <uui-input
                id="key-value-new-value"
                class="element"
                label="text input"
                value=""
            >
            </uui-input>
            <div id="wrapper"> 
                <uui-button
                    id="add-row-button"
                    class="element"
                    look="primary"
                    label="Add a row"
                    @click=${this.#onAddRow}
                >
                    Add a row
                </uui-button>
            </div>
        `;
  }

  static styles = [
    css`
            #wrapper {
                margin-top: 10px;
                display: flex;
                gap: 10px;
            }
            .element {
                width: 100%;
            }
        `,
  ];
}

declare global {
  interface HTMLElementTagNameMap {
    'key-values-property-editor-ui': UmbCommunityKeyValuesPropertyEditorUIElement;
  }
}

I’ve started a repo for this based on the opinioned package doofa.

OK.

I have a dirty but workable property editor. Hurrah!

It basically does the job I set out to do but I’d really love some feedback from people that actually understand this stuff as I’ve just been thrashing around hacking and the code is likely committing all sorts of crimes…

Still some queries:

  1. How do you assign value to your internal properties on “init”? Constructor has an empty object?!
  2. umbConfirmModal - not sure where to get the required host. I’ve hacked a simple js confirm for now.

TODO:

  1. A sorter
  2. Property Editor convertor.
import { LitElement, css, html, customElement, property, state, query, repeat } from "@umbraco-cms/backoffice/external/lit";
import { UmbControllerHost } from "@umbraco-cms/backoffice/controller-api";
//import { umbConfirmModal } from '@umbraco-cms/backoffice/modal';
import { UmbPropertyEditorUiElement } from "@umbraco-cms/backoffice/property-editor";
import { UmbPropertyValueChangeEvent } from "@umbraco-cms/backoffice/property-editor";

/*
This is a work in progress

WHAT WORKS NOW
* You can add, delete and update items - the save and publish stores to the db and it loads.
* I do check for a new property so that it works to add the first item

TODO:
1) I can't work out how I'd init _items (tried in the constructor but it's empty there?) as I'm unclear on how this.value is populated in the first place - I seem to declare it and it's magically assigned (perhaps in UmbPropertyEditorUiElement?)
2) Sorter
3) Check the key is unique and not null on add (perhaps a nice config option)
4) Maybe a backend property type convertor. - if we use a key value pair then recheck items are unique - what to do if some junk duplicates are there?
5) Style and tidy up
6) Fix the constructor - init the _items?
7) Use an umbraco confirm dialog - didn't seem to have the host / controller 
*/

// todo - should these be here or inside the custom element
type UmbCommunityKeyValue = {
  key: string;
  value: string;
};
type ArrayOf<T> = T[];

@customElement('key-values-property-editor-ui')
export default class UmbCommunityKeyValuesPropertyEditorUIElement extends LitElement implements UmbPropertyEditorUiElement {

  @property()
  public value: ArrayOf<UmbCommunityKeyValue> = [];

  /* this isn't used at the moment - how to init with the values from value ? */
  @state()
  private _items: ArrayOf<UmbCommunityKeyValue> = [];

  @query('#key-value-new-key')
  newNameInp!: HTMLInputElement;

  @query('#key-value-new-value')
  newValueInp!: HTMLInputElement;

  //#host: UmbControllerHost;

  constructor(value: ArrayOf<UmbCommunityKeyValue> = [], host: UmbControllerHost) {
    super();

    //this.#host = host;  // host is undefined.
    console.log(host);

    // this is empty there - so how do I assign this.value to the _items on "init"?
    this._items = value;
    console.log(this._items.length);
  }


  #onAddRow() {
    const currentInputTyped: UmbCommunityKeyValue = {
      key: this.newNameInp.value,
      value: this.newValueInp.value
    };

    // check the value is an array, the concatenate o/w create an array with this as the first item
    // todo - update to use this._items not value when the items is set on init.
    this._items = Array.isArray(this.value) ? [...this.value, currentInputTyped] : [currentInputTyped];

    this.#updatePropertyEditorValue();
  }

  #onDelete(index: number) {
    // todo - can't get the host to use the umbConfirmModal here - what should it be?
    // await umbConfirmModal(this.#host, {
    //   headline: `Delete ${this.value || 'item'}`,
    //   content: 'Are you sure you want to delete this item?',
    //   color: 'danger',
    //   confirmLabel: 'Delete',
    // });
   
    // hack use a confirm yes no dialog for now
    if (confirm("Are you sure you want to delete this item?")) {
      this._items = [...this._items.slice(0, index), ...this._items.slice(index + 1)];
      this.#updatePropertyEditorValue();
    }
  }

  private _onEditRowValue(e: InputEvent, index: number) {
    let currentItem = this._items[index];

    const updatedItem: UmbCommunityKeyValue = {
      key: currentItem.key,
      value: (e.target as HTMLInputElement).value
    };

    this._items = [...this._items.slice(0, index), updatedItem, ...this._items.slice(index + 1)];

    this.#updatePropertyEditorValue();
  }

  #updatePropertyEditorValue() {
    this.value = this._items;
    this.dispatchEvent(new UmbPropertyValueChangeEvent());
  }

  renderItemsList() {
    // this is a simple example of just writing out the values - to be replaced with the fields below
    // todo - remove this hack when when assigning value to _items issue fixed
    if (this._items.length === 0 && this.value?.length !== 0) {
      this._items = this.value;
    }

    if (this._items?.length) {
      return html`
      <ul>
        ${repeat(this._items, (item) => item.key, (item, index) => html`
            <li>
              <input type="text" name="${index}" value="${item.key}" disabled="disabled"></input>
              <input type="text" name="${index}" value="${item.value}" @input=${(e: InputEvent) => this._onEditRowValue(e, index)}></input>
              <uui-button
						    compact
						    color="danger"
						    label="remove ${item.key}"
						    look="outline"
						    @click=${() => this.#onDelete(index)}>
						    <uui-icon name="icon-trash"></uui-icon>
					    </uui-button>
            </li> `
      )
        }
      </ul>`;
    } else {
      return html`<span>create an item</span>`;
    }
  }

  render() {
    return html`
        ${this.renderItemsList()}
            <span>Add a new item</span>
            <uui-input
                id="key-value-new-key"
                class="element"
                label="text input"
                value=""
            >
            </uui-input>
            <uui-input
                id="key-value-new-value"
                class="element"
                label="text input"
                value=""
            >
            </uui-input>
            <div id="wrapper">
                <uui-button
                    id="add-row-button"
                    class="element"
                    look="primary"
                    label="Add a row"
                    @click=${this.#onAddRow}
                >
                    Add a key value item
                </uui-button>
            </div>
        `;
  }

  static styles = [
    css`
            #wrapper {
                margin-top: 10px;
                display: flex;
                gap: 10px;
            }
            .element {
                width: 100%;
            }
        `,
  ];
}

declare global {
  interface HTMLElementTagNameMap {
    'key-values-property-editor-ui': UmbCommunityKeyValuesPropertyEditorUIElement;
  }
}

Properties in Web Components will not be assigned in the constructor. You can use the connectedCallback to do that. Lit also has a life-cycle called firstUpdated that only runs once per instance, which might also be appropriate.

class ExampleComponent extends LitElement {

  connectedCallback() {
     super.connectedCallback();
     console.log('this is my value', this.value);
     // do stuff with your properties
  }

}

The host is this if you extend from the UmbLitElement class that might be easiest:

import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { umbConfirmModal } from '@umbraco-cms/backoffice/modal';

class ExampleComponent extends UmbLitElement {

  onClick() {
    umbConfirmModal(this, { headline: 'Hello', content: 'Do you confirm?' })
      .then(() => {
        console.log('they confirmed!')
      })
      .catch(() => {
        console.log('oh no, they did not confirm!')
      })
  }

}

Hint: You can also use an await with a try/catch instead of .then and .catch to flatten it a little bit.

You can find everything you’d ever need about the Lit lifecycle here:

https://lit.dev/docs/components/lifecycle/

1 Like

I checked my own code and I’m indeed using the connectCallback like Jacob said. The value property is initialized there.

1 Like

OK - thanks both.

I’ve updated the code with those two bits now. Works nicely.

Just need to tidy up, add a sorter and config and some more data validation. WiP is in the repo posted above if anyone is following along / trying to do similar.

I think I’ve got this as far as I can - thanks for the help all!

It works, optionally checks for key uniqueness, has a convertor etc etc.

The two remaining bits;

  1. A sorter - UmbSorterController seems to expect a string array - couldn’t see how this would work here. Guessing I have to roll my own?
  2. UmbValidationContext - no idea how to make this work, didn’t seem very flexible (was tied to a single field and sets the input fields as red after a successful input when I reset them.

Code is in the github repo if anyone finds it useful - don’t feel it’s quite marketplace ready yet - not sure I’m confident it’s done in the right Umbraco way. But it works and you can install if if you want.

import { css, html, customElement, property, state, query, repeat } from "@umbraco-cms/backoffice/external/lit";
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { umbConfirmModal } from '@umbraco-cms/backoffice/modal';
import { UmbPropertyEditorUiElement } from "@umbraco-cms/backoffice/property-editor";
import { UmbPropertyValueChangeEvent } from "@umbraco-cms/backoffice/property-editor";
import { type UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor';
//import { UmbValidationContext } from "@umbraco-cms/backoffice/validation";

/*
This is a work in progress

WHAT WORKS NOW
* You can add, delete and update items - the save and publish stores to the db and it loads.
* I do check for a new property so that it works to add the first item
* Can enable / disable a uniqueness check on the key

TODO:
1) Sorter - UmbSorterController expects a string array - how would we use this?
2) Maybe a backend property type convertor. - if we use a key value pair then recheck items are unique - what to do if some junk duplicates are there?
3) Use UmbValidationContext properly - couldn't get it to play nicely
*/

// todo - should these be here or inside the custom element
type UmbCommunityKeyValue = {
  key: string;
  value: string;
};
type ArrayOf<T> = T[];

@customElement('key-values-property-editor-ui')
export default class UmbCommunityKeyValuesPropertyEditorUIElement extends UmbLitElement implements UmbPropertyEditorUiElement {

  @property()
  public value: ArrayOf<UmbCommunityKeyValue> = [];

  @state()
  private _items: ArrayOf<UmbCommunityKeyValue> = [];

  @state()
  private _uniquekeys?: boolean;

  @property({ attribute: false })
  public set config(config: UmbPropertyEditorConfigCollection) {
    this._uniquekeys = config.getValueByAlias("uniquekeys");
  }

  @state()
  private _showKeyErrorEmpty: boolean = false;

  @state()
  private _showKeyErrorNotUnique: boolean = false;

  @query('#key-value-new-key')
  newKeyInp!: HTMLInputElement;

  @query('#key-value-new-value')
  newValueInp!: HTMLInputElement;

  // use the connectedCallback as suggested by Jacob Overgaard as this is where the this.value is available and assigned
  connectedCallback() {
    super.connectedCallback();

    // in connectedCallback the this.value is ready
    this._items = this.value;
  }

  // todo - couldn't make this work
  //#validation = new UmbValidationContext(this);

  #onAddRow() {
    // todo - always comes back as valid?
    //this.#validation.validate().then(() => {
    //  console.log('Valid');
    //}, () => {
    //  console.log('Invalid');
    //});

    // check the key is non-empty
    if (this.newKeyInp.value == '') {
      this._showKeyErrorEmpty = true;
      return;
    }

    let newKeyTrimmed = this.newKeyInp.value.trim();

    // if the config is set check if the value is unique
    if (this._uniquekeys && (this._items?.length ?? false) && this._items.some(i => i.key === newKeyTrimmed)) {
        this._showKeyErrorNotUnique = true;
        return;
    }

    const currentInputTyped: UmbCommunityKeyValue = {
      key: newKeyTrimmed,
      value: this.newValueInp.value
    };

    // check the value is an array, the concatenate o/w create an array with this as the first item
    this._items = Array.isArray(this.value) ? [...this.value, currentInputTyped] : [currentInputTyped];

    this.newKeyInp.value = '';
    this.newValueInp.value = '';

    this.#updatePropertyEditorValue();
  }

  #onDelete(index: number) {
    umbConfirmModal(this, { headline: 'Delete?', content: 'Are you sure you want to delete this item?' })
      .then(() => {
        this._items = [...this._items.slice(0, index), ...this._items.slice(index + 1)];
        this.#updatePropertyEditorValue();
      })
      .catch(() => {
        //console.log('Delete cancelled')
      })
  }

  private _onEditRowValue(e: InputEvent, index: number) {
    let currentItem = this._items[index];

    const updatedItem: UmbCommunityKeyValue = {
      key: currentItem.key,
      value: (e.target as HTMLInputElement).value
    };

    this._items = [...this._items.slice(0, index), updatedItem, ...this._items.slice(index + 1)];

    this.#updatePropertyEditorValue();
  }

  private _onEditNewKey() {
    this._showKeyErrorEmpty = false;
    this._showKeyErrorNotUnique = false;
  }

  #updatePropertyEditorValue() {
    this.value = this._items;
    this.dispatchEvent(new UmbPropertyValueChangeEvent());
  }

  // Prevent valid events from bubbling outside the message element
  #onValid(event: Event) {
    event.stopPropagation();
  }

  // Prevent invalid events from bubbling outside the message element
  #onInvalid(event: Event) {
    event.stopPropagation();
  }

  renderItemsList() {
    // writes out the list with fields to update
    if (this._items?.length) {
      return html`
      <ul>
        ${repeat(this._items, (item) => item.key, (item, index) => html`
            <li>
              <umb-form-validation-message id="validation-message" class="wrapper" @invalid=${this.#onInvalid} @valid=${this.#onValid}>
                <uui-input
                  class="kv-input"
                  label="text input"
                  type="text"
                  name="${index}"
                  value="${item.key}"
                  required=true
                  required-message="A key value is required"
                  disabled="disabled"
                  ></uui-input>
                <uui-input
                  class="kv-input"
                  label="text input"
                  type="text"
                  name="${index}"
                  value="${item.value}"
                  @input=${(e: InputEvent) => this._onEditRowValue(e, index)}>
                </uui-input>
                <uui-button
						      compact
						      color="danger"
						      label="remove ${item.key}"
						      look="outline"
						      @click=${() => this.#onDelete(index)}>
						      <uui-icon name="icon-trash"></uui-icon>
					      </uui-button>
              </umb-form-validation-message>
            </li> `
      )
        }
      </ul>`;
    } else {
      return html`<span>You don't have any items yet</span>`;
    }
  }

  render() {
    return html`
        ${this.renderItemsList()}
            <hr/>
            <div class="wrapper">
              <uui-input
                  id="key-value-new-key"
                  class="kv-input"
                  label="text input"
                  placeholder="key*"
                  value=""
                  @input=${this._onEditNewKey}
                  required=true
                  required-message="A key value is required"
              >
              </uui-input>
              <uui-input
                  id="key-value-new-value"
                  class="kv-input"
                  label="text input"
                  value=""
                  placeholder="value"
              >
              </uui-input>
              <uui-button
                  id="add-row-button"
                  class="kv-input"
                  look="primary"
                  label="Add item"
                  @click=${this.#onAddRow}
              >
                  Add item
              </uui-button>
            </div>
            <span id="kv-new-row-error-empty" class=${this._showKeyErrorEmpty ? 'kv-error show' : 'kv-error'}>Error: Key cannot be empty</span>
            <span id="kv-new-row-error-not-unique" class=${this._showKeyErrorNotUnique ? 'kv-error show' : 'kv-error'}>Error: Key already exists</span>
        `;
  }

  static styles = [
    css`
      .wrapper {
          margin-top: 10px;
          display: flex;
          gap: 10px;
      }
      .kv-input {
        flex: 1;
      }
      ul {
        list-style: none;
        padding-inline-start: 0;
      }
      /* the absolute pain to find this is how to change the disabled font color.. */
      uui-input {
        --uui-color-disabled-contrast: black;
      }
      .kv-error {
        color: var(--uui-color-danger-standalone);
        display: none;
      }
      .kv-error.show {
        display: block;
      }
      `,
  ];
}

declare global {
  interface HTMLElementTagNameMap {
    'key-values-property-editor-ui': UmbCommunityKeyValuesPropertyEditorUIElement;
  }
}

1 Like

Great to see you’ve got something working! I just wanted to give a heads up that I lightly edited your post for readability:

It went from this:

To this:

By adding ts after the three backticks, the forum knows how to pretty print it.

3 Likes