Reuse property editor UI for custom property editor

In Umbraco 13 we create a custom property editor, which re-used configuration from MNTP, but a different property editor to implement a different reference via GetReferences() instead of default reference.

In Umbraco 17 it looks something like this:

[DataEditor(
    //name: "Alternative Page Relation Picker",
    alias: Constants.PropertyEditors.Aliases.AlternativePageRelationPicker,
    //view: "contentpicker",
    //Icon = "icon-mindmap",
    ValueType = ValueTypes.Text)
    //Group = Umbraco.Cms.Core.Constants.PropertyEditors.Groups.Pickers)
]
public class AlternativePageRelationPickerPropertyEditor : DataEditor
{
    private readonly IIOHelper _ioHelper;

    public AlternativePageRelationPickerPropertyEditor(
        IDataValueEditorFactory dataValueEditorFactory,
        IIOHelper ioHelper)
        : base(dataValueEditorFactory)
    {
        _ioHelper = ioHelper;
        SupportsReadOnly = true;
    }

    /// <inheritdoc />
    protected override IConfigurationEditor CreateConfigurationEditor() =>
        new MultiNodePickerConfigurationEditor(_ioHelper); // Just reuse the MNTP configuration.

    protected override IDataValueEditor CreateValueEditor() =>
        DataValueEditorFactory.Create<AlternativePageRelationPickerPropertyValueEditor>(Attribute!);

    public class AlternativePageRelationPickerPropertyValueEditor : DataValueEditor, IDataValueReferenceFactory, IDataValueReference
    {
        public AlternativePageRelationPickerPropertyValueEditor(
            IShortStringHelper shortStringHelper,
            IJsonSerializer jsonSerializer,
            IIOHelper ioHelper,
            DataEditorAttribute attribute)
            : base(shortStringHelper, jsonSerializer, ioHelper, attribute)
        {
        }

        public IDataValueReference GetDataValueReference() => this;

        public IEnumerable<string> GetAutomaticRelationTypesAliases() => [Constants.RelationTypes.RelatedAlternativePageAlias];

        public bool IsForEditor(IDataEditor? dataEditor) => dataEditor?.Alias == Constants.PropertyEditors.Aliases.AlternativePageRelationPicker;

        public IEnumerable<UmbracoEntityReference> GetReferences(object? value)
        {
            var asString = value == null ? string.Empty : value is string str ? str : value.ToString();

            if (string.IsNullOrEmpty(asString))
            {
                yield break;
            }

            var udiPaths = asString!.Split(',');

            foreach (var udiPath in udiPaths)
            {
                if (UdiParser.TryParse(udiPath, out Udi? udi))
                {
                    yield return new UmbracoEntityReference(udi, Constants.RelationTypes.RelatedAlternativePageAlias);
                }
            }
        }
    }
}

However view is moved to client side, so we have something like this:

import { MCB_ALTERNATIVE_PAGE_RELATION_PICKER_PROPERTY_EDITOR_UI_ALIAS } from './constants.js';
import { manifest as schemaManifest } from './property-editor-schema.js';

export const manifests: Array<UmbExtensionManifest> = [
	{
		type: 'propertyEditorUi',
		alias: MCB_ALTERNATIVE_PAGE_RELATION_PICKER_PROPERTY_EDITOR_UI_ALIAS,
		name: 'Alternative Page Relation Picker Property Editor UI',
		element: () => import('./alternative-page-relation-picker.element.js'),
		meta: {
			label: 'Alternative Page Relation Picker',
            propertyEditorSchemaAlias: 'MCB.AlternativePageRelationPicker',
            icon: 'icon-mindmap',
			group: 'pickers',
			supportsReadOnly: true,
			settings: {
				properties: [
					{
						alias: 'filter',
						label: 'Allow items of type',
						description: 'Select the applicable types',
						propertyEditorUiAlias: 'Umb.PropertyEditorUi.ContentPicker.SourceType',
					},
				],
			},
		},
	},
	schemaManifest,
];

Is there any way to re-use umb-property-editor-ui-document-picker?

I tried elementName: "umb-property-editor-ui-document-picker", but it seems requiring to import this component, which I think isn’t exported in modules - or if we could create a custom property editor UI extending UmbPropertyEditorUIDocumentPickerElement, but I think this isn’t available in modules either.

We can implement <umb-input-document> as suggested in linked post below and used inside the core property editor UI, but it probably requires to implement much of the same logic.

Is there a better/simpler approach to reuse property editor UI, when one just want a slightly different change backend wise, in this case different implementation of GetReferences() and GetAutomaticRelationTypesAliases() in data value editor?

@warren asked a similar question here:

I think the approach is my answer in that thread you mentioned:

I had a look at umb-property-dataset previously, but IIRC it required the umb-property and render label/description here, which isn’t what a want inside a custom property editor.

So let me see if I get this right: you want to create a custom property editor that reuses the editor UI for the MNTP, but has custom handling of the data?

It is mainly the same feature as MTNP, just to set a different relation type in tracked reference instead if the default as I can use IRelationService to get these specific references for a content node (and not any other pickers where this node is picked).

However I want to remain the standard MNTP feature for other pickers, so I don’t want to replace core property value converter for the standard picker.

I tried if I just in migration of old picker could re-use Umb.PropertyEditorUi.ContentPicker since I basically don’t need any changes in this.

internal sealed class UpdatePropertyEditorUiAlias : AsyncMigrationBase
{
    public UpdatePropertyEditorUiAlias(IMigrationContext context) : base(context)
    {
    }

    protected override async Task MigrateAsync()
    {
        var dataTypes = await Database.Query<DataTypeDto>()
            .Where(x => x.EditorAlias == "MCB.Umbraco.AlternativePageRelationPicker").ToListAsync();

        foreach (var dataType in dataTypes)
        {
            dataType.EditorAlias = dataType.EditorAlias switch
            {
                "MCB.Umbraco.AlternativePageRelationPicker" => "MCB.AlternativePageRelationPicker",
                _ => dataType.EditorAlias
            };

            dataType.EditorUiAlias = dataType.EditorAlias switch
            {
                "MCB.AlternativePageRelationPicker" => "Umb.PropertyEditorUi.ContentPicker", // Reuse core property editor UI.
                _ => dataType.EditorUiAlias
            };

            await Database.UpdateAsync(dataType);
        }
    }
}

However this migrates picker to this:

So the standard MTNP converter willl be used for this.

However if I basically copy this into my custom property editor UI

and use MCB.PropertyEditorUi.AlternativePageRelationPicker instead in migration, then it works:

Ah so realistically you want to use the UI of the MNTP but have a custom DataEditor to handle the data differently? I think you approach in itself is sound. You don’t need to copy the editor ui file though.

The UmbPropertyEditorUIContentPickerElement class is not part of the public package exports of @umbraco-cms/backoffice, so you cannot do import { UmbPropertyEditorUIContentPickerElement } from '@umbraco-cms/backoffice/content-picker' and extend it. But the custom element tag umb-property-editor-ui-content-picker is registered globally as soon as the built in manifest loads, which happens at backoffice startup well before your extension renders anything. So you can just use the tag inside your own thin wrapper element, like this:

import { html, customElement, property } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry';

@customElement('mcb-property-editor-ui-alternative-page-relation-picker')
export class McbAlternativePageRelationPickerUiElement
  extends UmbLitElement
  implements UmbPropertyEditorUiElement {

  @property({ attribute: false }) value: string | undefined;
  @property({ attribute: false }) config: any;

  #onChange = (e: CustomEvent) => {
    this.value = (e.target as any).value;
    this.dispatchEvent(new CustomEvent('change'));
  };

  render() {
    return html`<umb-property-editor-ui-content-picker
      .value=${this.value}
      .config=${this.config}
      @change=${this.#onChange}>
    </umb-property-editor-ui-content-picker>`;
  }
}

Then your propertyEditorUi manifest with alias MCB.PropertyEditorUi.AlternativePageRelationPicker points its element at this file. You inherit every fix and feature added to the built in picker, and your extension stays at a few lines instead of the full element.

I don’t think in that case it’s possible to re-use the existing property value converter because that actually binds to the DataEditorAlias. And since you have a custom alias it won’t trigger….

1 Like

Ah wait, the Property Value Converter is not sealed, so I think you can actually do something like this:

public class AlternativePageRelationPickerValueConverter : MultiNodeTreePickerValueConverter
{
    public override bool IsConverter(IPublishedPropertyType propertyType) =>
        propertyType.EditorAlias.Equals("MCB.AlternativePageRelationPicker");
}

Yes, it was quite simple in old back office using view, but e.g. different converter or omit some configuration properties - eventually setting defaults.

I will have a look at this.

Not sure it it would work as I think is ConvertSourceToIntermediate() and ConvertIntermediateToObject() only handle this editor aliases.

Oh that is really lame…. that’s what the

public override bool IsConverter(IPublishedPropertyType propertyType) =>
propertyType.EditorAlias.Equals(Constants.PropertyEditors.Aliases.MultiNodeTreePicker);

is for!

This seems to work well and simplifies it a bit.
Just using UmbInputDocumentElement and UmbChangeEvent instead.

import { customElement, html, property } from '@umbraco-cms/backoffice/external/lit';
import { UmbInputDocumentElement } from "@umbraco-cms/backoffice/document";
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
import type {
	UmbPropertyEditorUiElement,
} from '@umbraco-cms/backoffice/property-editor';
import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation';

@customElement('mcb-alternative-page-picker')
export class McbAlternativePageRelationPickerElement
	extends UmbFormControlMixin<string, typeof UmbLitElement>(UmbLitElement)
	implements UmbPropertyEditorUiElement {
		
	@property({ attribute: false }) config: any;

	#onChange(event: CustomEvent & { target: UmbInputDocumentElement }) {
		this.value = event.target.value;
		this.dispatchEvent(new UmbChangeEvent());
	}

	override render() {
		return html`<umb-property-editor-ui-content-picker
		.value=${this.value}
		.config=${this.config}
		@change=${this.#onChange}>
		</umb-property-editor-ui-content-picker>`;
	}
}

export default McbAlternativePageRelationPickerElement;

declare global {
	interface HTMLElementTagNameMap {
		'mcb-alternative-page-relation-picker': McbAlternativePageRelationPickerElement;
	}
}