I am trying to recreate a custom property in Umbraco 17 using typescript. I have the component working in Umbraco 13 using Angular.
The issue I have at the moment is I cannot render the html I get back from my API call. either in the override render() on in the template I created. I can render text though :/.
My first guess would be that getHtmlButtonElement() is async and I donât think this works in the render() method, which is not async as far as I know.
Iâd better of getting your data from the API at the connectCallback function and then rerender when the data is available.
Thanks @Luuk. Do you have an example of how this would work. If I override the connectCallback function, would I get the data and then return html? Or do I call update within the connectCallback function which calls the render function
Notice the Lit Task helper makes it easier for you to await a promise and react when itâs fulfilled. Which can be a great help if you like that style of coding, so go ahead and use that solution.
I just wanted to add, looking at your initial code snippet, that it could hint that you havenât fully comprehended how Lit works, so to help you with your future projects, I just wanted to focus a bit on how the render method of Lit works, which I think will help you next time.
The Lit render is executed every time Lit detects a change of the properties of the component. But not any property, only the properties that Lit is watching. To mark properties as reactive(once that Lit will watch) we use the @property & @state decorators.
You already use the @property one for value, so when the value is changed, the render will be exeucted.
In the above case, nothing triggers a render, for when the htmlButtonElement gets set. Simply the htmlButtonElement is not being watched by Lit. I would add a @state decorator on the property declaration. We use @state for internal properties and @property for public properties.
And once thats done, your render method will be executed when it changes. And then no need to call getHtmlButtonElement ()... etc. in your render method.
I hope that explanation helps you understand what is going on and moving forward with more custmozations, good luck.
Thanks @nielslyngsoe yes I am a beginner trying to learn :). I was trying to work out @property and @state were for.
Ok so If I have a public property of ânameâ that uses @property and a private property of âhtmlButtonElementâ that uses a @state. When either of these two values get changed the render method will be called and then when ever I am returning, in my case html with be re rendered on to the page?
When I select either one of the buttons, how do I store that property information to be used in code? For example I want to show this information on the front end of the website and need to know which button the user selected? I cannot see anything in the docs about this.
Thanks, its working and compiling now :D, so I think the first part is complete . Do you know how I store which the user has selected?. In Angular I had to set the model value.
Hi sorry one more question. If I create a method #onClick, how would this work given my html is created in a class called ButtonTemplate?.
What I am confused about is I need to, I think, is add an event to the onclick event of the button. Thus the . So that I can update value which calls the render method and the button the user clicked on in Umbraco is changed. But I cannot work out how I would add the event to the button? as the event needs to be in the MySuggestionsPropertyEditorUIElement class?
Is this correct and does that mean I cannot use the ButtonTemplate? I presume I am missing something ?
The only other way I can think of is to get the button and add an event listener in the constructor on the MySuggestionsPropertyEditorUIElement class and do it that way.
private _buttonTask = new Task(this, {
task: async ([value], { signal }) => {
console.log('Received value', value);
const response = await fetch(`/Theme/ComponentSettings/${value}`, { signal });
if (!response.ok) {
throw new Error(`Error! status: ${response.status}`);
}
const settings = await response.json();
return settings;
},
args: () => [this.value], // <-- Listening for this.value, but can also be empty to autorun with no value
});
Then change your render method to call the ButtonTemplate.render and add the onclick event with the id or whatever you need to save:
Thanks @jacob , now you have mentioned that you can imagine me face palming myself! :P. I donât know how I did not notice this yesterday! Thanks so much again.
So I see what you are saying here, however I am using a HtmlButtonElement. I have tried everything I can thing on. click, onclick, addEventListener to add the event to the HtmlButtonElement with no luck. I have also tried creating the html but that again did not work.
The âSettingsâ object contains multiple ComponentSettingsObjects which contain the button information I need. The âSettingsâ object is just a wrapper.
Inside the ButtonTemplate I am doing a foreach
items.componentSettingsObjects.forEach((value) => {} which returns all the html I need. X number of Buttons.
I hope that makes some sense. I can post code if its easier to understand
suggestions-property-editor-ui.element.ts
import { Task } from '@lit/task';
import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/property-editor';
import { LitElement, customElement, html, property } from '@umbraco-cms/backoffice/external/lit';
import { ButtonTemplate } from './ButtonTemplate';
import type { Settings } from './Settings';
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
@customElement('suggestions-property-editor-ui.element')
export default class MySuggestionsPropertyEditorUIElement extends LitElement implements UmbPropertyEditorUiElement {
@property({ type: String })
public value = '';
private _buttonTask = new Task(this, {
task: async (value) => {
const response = await fetch(`/Theme/ComponentSettings/${value}`);
if (!response.ok) {
throw new Error(`Error! status: ${response.status}`);
}
const settings = await response.json() as Settings;
return settings;
},
args: () => [this.value], // <-- Listening for this.value, but can also be empty to autorun with no value
});
#onClick(buttonId: any) {
this.value = buttonId;
this.dispatchEvent(new UmbChangeEvent());
}
override render() {
return this._buttonTask.render({
pending: () => html`<span>Loading...</span>`,
complete: (settings: Settings) => {
const htmlElement = document.createElement('div');
htmlElement.className = "ccs-component-colour-swatch";
const buttonTemplate = new ButtonTemplate(htmlElement);
const container = buttonTemplate.render(settings);
return html`${container}`;
},
error: (error: any) => html`<span>Error: ${error.message}</span>`,
});
}
}
declare global {
interface HTMLElementTagNameMap {
'my-suggestions-property-editor-ui': MySuggestionsPropertyEditorUIElement;
}
}
You could always build the ButtonTemplate in the property editor itself, but since you are already in the deep end, you can forward the onClick method to your ButtonTemplate:
render(items: Settings, callback: any) {
let outerButtonElement!: UUIButtonElement;
items.componentSettingsObjects.forEach((item) => {
outerButtonElement = document.createElement('uui-button');
outerButtonElement.addEventListener('click', () => callback(item.id)); // <-- Set an event listener and call the callback with an id or something else
});
}
Also, you should consider constructing a LitTemplateResult in the ButtonTemplate like you do in the property editor. Something like this could do it:
The good news @jacob :â), is that your code is brilliant and works perfectly :D. Slightly less good news is that âthisâ in the call back in null, which I have a feeling you may say is terminal and âbuild the ButtonTemplate in the property editor itselfâ