Custom Property Editor (Vue 3 wrapper): Value not persisting on Save

Hi everyone,

I am porting a custom Property Editor from AngularJS to Umbraco 17, using a Vue 3 application wrapped inside a standard Web Component (implementing UmbPropertyEditorUiElement).

The Issue: The UI works fine, and I can edit the values in my Vue app. I am syncing the data back to the Web Component’s .value property. However, when I click “Save” in the backoffice, the data is not persisted to the database. The property remains empty or unchanged.

My setup:

  • Wrapper: A standard HTMLElement extending UmbElementMixin and implementing UmbPropertyEditorUiElement.

  • Internal logic: A Vue 3 app that emits changes to the host.

I know that unlike AngularJS $scope, I need to notify Umbraco about changes. I tried dispatching theproperty-value-change event inside the setter, but it doesn’t seem to trigger the “dirty” state or the actual persistence.

Here is my Web Component code:

import { createApp } from 'vue';
import { UmbElementMixin } from '@umbraco-cms/backoffice/element-api';
import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/property-editor';
import App from './App.vue';

export default class MultiLingualMediaPropertyEditorUiElement 
  extends UmbElementMixin(HTMLElement) 
  implements UmbPropertyEditorUiElement {

  
  app: any;
  private _value: any = {};
  
  get value() {
    return this._value;
  }
  
  set value(newValue: any) {
    this._value = newValue;
    
    // Attempt to notify Umbraco
    this.dispatchEvent(new CustomEvent('property-value-change', {
      bubbles: true,
      composed: true,
      detail: { value: newValue }
    }));
    
    console.log('[Element] Set value & dispatched event:', newValue);
  }

  connectedCallback() {
    super.connectedCallback();
    const mountElem = this.shadowRoot?.querySelector('#app-root');
    if (mountElem) {
      this.app = createApp(App, { mountElem: this });
      this.app.mount(mountElem);
    }
  }
  
  // ... disconnectedCallback implementation
}

And inside my Vue component (App.vue), I sync the data like this:

function syncToWebComponent() {
  // host is a reference to the Web Component
  if (host.value) {
    // This triggers the setter above
    (host.value as any).value = JSON.parse(JSON.stringify(internalData.value));
  }
}

Any help would be greatly appreciated as I’m stuck on how to replace the old AngularJS $scope binding behavior.

As far as I remember, you need to add a getValue function in the MultiLingualMediaPropertyEditorUiElement

  getValue() {
    return this._value;
  }

Umbraco calls this value before saving to get the actual value.

syncToWebComponent seems wrong as well, I think, something more like:

function syncToWebComponent() {
  host.value = JSON.parse(JSON.stringify(internalData.value));
}

Note I’ve not done anything with Vue, so I’m guessing here based on a bit of input from an AI. It suggests a nicer looking function too:

function syncToWebComponent() {
  host.value = structuredClone(internalData.value);
}
2 Likes

Thanks so much! Both suggestions were spot on and fixed the issue completely.

getValue() was critical - you were absolutely right that Umbraco calls this before saving. I had only implemented get/set value but not getValue(). Adding it fixed the empty payload issue:

getValue(): ModelValue {
  return this.#value;
}

structuredClone() worked perfectly - I went with your second suggestion and it’s much cleaner than JSON.parse/stringify. I implemented it with a fallback for older browsers:

function deepClone<T>(obj: T): T {
  try {
    return structuredClone(obj);
  } catch (e) {
    return JSON.parse(JSON.stringify(obj));
  }
}

One additional issue I discovered: I think that Umbraco passes frozen objects to the setter, so I had to clone in the setter too before modifying:

set value(newValue: ModelValue) {
  let cloned = structuredClone(newValue);  
  this.#value = cloned;
}

Without cloning in the setter, I got “Cannot add property culture, object is not extensible” errors.

Final result: data now saves correctly to the database. Your suggestions were exactly what was needed - thanks again!

1 Like

Great to hear that it worked & nice to see that you have success with bringing in a Vue based Web Component.

I have to say, we do not get the value from getValue() but we read it from .value in this case that would be via the getter method. So I’m a little confused if that solved it. Maybe there was something else that fell into place?

I also want to add, your ‘property-value-change’ event should not be composed or bubble. This could cause problems when used inside other Property Editors, like in a Block Editor.
Also, note that we support just ‘change’ as the event name. The ‘property-value-change’ was only used in the early days of the project. (But we will still support it to be backwards compatible)
Also, the value should not be part of the event.

So do this instead:

// Attempt to notify Umbraco
this.dispatchEvent(new CustomEvent('change));

As well, a comment on the frozen value object:
The prettiest way to transform the value would be to first clone it when making a change to it, in this way it becomes a new object for each change. In this partuclar case I cannot guess the data model or what happens inside Vue, so it may just be the easiest for you to clone it up front. And that is perfectly fine. So see it more as a tip.

So a few details to be aware of, but hopefully avoid other mistakes in the future by getting it completely right :slight_smile: Good luck with your project

1 Like