tobi
March 24, 2025, 11:39am
1
Hi
I’m upgrading a large v13 site to v15 and we have a lot of custom property editors.
I’m struggeling to figure out how to use the stuff in @umbraco-cms /backoffice api with Lit and TypeScript.
How do I get stuff like editorState.current.contentTypeAlias (from AngularJS in v13)?
Here is some code - please help where this comment are “HERE I WANT TO GET THE CURRENT PAGE CONTENT TYPE ALIAS - HOW??”:
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 { TagsPickerTSService } from './tagspickerts.service';
interface Tag {
Id: number;
Name: string;
ParentName: string;
}
@customElement('tagspickerts-ui')
export default class TagsPickerTSUiElement extends LitElement implements UmbPropertyEditorUiElement {
@property({ type: Object })
public value: { count: number; tags: number[] } = { count: 0, tags: [] };
@state() private tags: Tag[] = [];
@state() private tagHeaders: string[] = [];
#dispatchChangeEvent() {
this.dispatchEvent(new UmbPropertyValueChangeEvent());
}
//HERE I WANT TO GET THE CURRENT PAGE CONTENT TYPE ALIAS - HOW??
async connectedCallback() {
super.connectedCallback();
const service = new TagsPickerTSService();
const tagsData = await service.getTags();
this.tags = tagsData;
this.tagHeaders = [...new Set(this.tags.map(tag => tag.ParentName))];
this.cleanUpInvalidTags();
if(this.value == null){
this.value = { count: 0, tags: [] };
}
}
private cleanUpInvalidTags() {
if(this.value != null){
const validTagIds = this.tags.map(t => t.Id);
this.value = {
...this.value,
tags: this.value.tags.filter(tagId => validTagIds.includes(tagId)),
};
}
}
private toggleTagSelection(tagId: number) {
const isSelected = this.value.tags.includes(tagId);
this.value = {
...this.value,
tags: isSelected
? this.value.tags.filter(id => id !== tagId)
: [...this.value.tags, tagId],
};
this.#dispatchChangeEvent();
}
private handleCountChange(event: Event) {
const input = event.target as HTMLInputElement;
this.value.count = parseInt(input.value) || 0;
this.value = { ...this.value };
this.#dispatchChangeEvent();
}
render() {
return html`
<div class="tags-picker">
<div class="tags-picker-related">
<label>Antal relaterede nyheder vist</label>
<div class="info-message">
OBS!<br />
Hvis du angiver et antal herunder, vil relaterede nyheder blive vist automatisk.
Det betyder også, at det ikke er nødvendigt at indsætte modulet "Relaterede nyheder" på siden.
</div>
<input type="number" .value=${String(this.value.count)} @input=${this.handleCountChange} />
</div>
<div class="propery-block">
<label class="tl">Tags</label>
${this.tagHeaders.map(
parent => html`
<div style="margin-bottom: 1rem;">
<div class="header-label">${parent}</div>
<br />
${this.tags
.filter(tag => tag.ParentName === parent)
.map(
tag => html`
<label
class="tag ${this.value.tags.includes(tag.Id) ? 'selected' : ''}"
@click=${() => this.toggleTagSelection(tag.Id)}
>
<input
type="checkbox"
.checked=${this.value.tags.includes(tag.Id)}
style="margin-right: 0.5rem;"
disabled
/>
${tag.Name}
</label>
`
)}
</div>
`
)}
</div>
</div>
`;
}
static styles = css`
.tag {
margin-right: 1rem;
border: 1px solid #f9dede;
padding: 0.5rem;
border-radius: 6px;
cursor: pointer;
display: inline-block;
}
.tag.selected {
background-color: #f9dede;
}
.info-message {
background-color: #f9dede;
padding: 1rem;
border-radius: 10px;
display: inline-block;
}
.header-label {
display: inline-block;
font-size: 0.8em;
font-weight: bold;
text-transform: uppercase;
padding: 0.1rem 1rem;
background-color: #625A74;
color: #fff;
border-radius: 5px;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
'tagspickerts-ui': TagsPickerTSUiElement;
}
}
1 Like
jacob
(Jacob Overgaard 🚀)
March 24, 2025, 2:05pm
2
Shameless plug, but I think this other issue with Where is the Backoffice UI Documentation? covers your use case as well, where I posted an example:
Unfortunately, it is not well documented. You would have to figure it out through the source code, but this might get you going:
Consume the UMB_PROPERTY_DATASET_CONTEXT, a context class that holds the values of the current entity, so it would work inside property editors, workspace views, and so on:
import { UMB_PROPERTY_DATASET_CONTEXT } from '@umbraco-cms/backoffice/property';
const context = await this.getContext(UMB_PROPERTY_DATASET_CONTEXT);
const allProperties = context?.properties ??…
That will get you the properties on the page, but if you instead want a more generic alias, you could try and consume the UMB_DOCUMENT_WORKSPACE_CONTEXT
which has a property for contentTypeUnique
that is a GUID of your content type.
1 Like
tobi
March 24, 2025, 2:23pm
3
Thanks Jacob
I’ve already seen that answer. When using your code I get this error:
Property ‘getContext’ does not exist on type ‘TagsPickerTSUiElement’.
I really can’t figure out how to make this work?
jacob
(Jacob Overgaard 🚀)
March 24, 2025, 2:30pm
4
Your component should extend from UmbLitElement
to get that method:
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
export class TagsPickerTSUiElement extends UmbLitElement {
}
Does that work for you?
I also tried your example yesterday, and when i tried to get a property from the context I was returned an object e
that had two properties, operator
(a function) and source
.
Not sure what I did wrong. From the constructor I called an async function.
In this async function I did:
const context = await this.getContext(UMB_PROPERTY_DATASET_CONTEXT);
const workspaceName = context?.getName();
console.log('workspaceName', workspaceName);
This worked great, got the workspace name. But continuing with:
const allProperties = context?.properties ?? [];
console.log('allProperties', allProperties);
I get this returned:
My class definition:
export default class TeamAssignerPropertyEditorUIElement
extends UmbElementMixin(LitElement)
implements UmbPropertyEditorUiElement
{ // ...
I’m guessing I am requesting things in the wrong place, maybe, somehow?
jacob
(Jacob Overgaard 🚀)
March 24, 2025, 4:15pm
6
Karl Macklin 🏅:
This worked great, got the workspace name. But continuing with:
const allProperties = context?.properties ?? [];
console.log('allProperties', allProperties);
I get this returned:
This is on me, sorry. The property properties
is an observable. You can get the values continuously by subscribing to it like so:
if (!context) return;
this.observe(context.properties, (props) => {
console.log('props', props);
});
Alternatively, you can get a snapshot of the current values by using the method getProperties()
, which returns a promise:
const allProperties = await context.getProperties();
1 Like
LuukPeters
(Luuk Peters (Proud Nerds))
March 24, 2025, 4:27pm
7
Oh that last remark is good to know. Observables are nice, but sometimes you just need to have a bunch of values.
2 Likes
jacob
(Jacob Overgaard 🚀)
March 24, 2025, 6:29pm
8
I believe that they are based on a BehaviorSubject and thus have a .value method as well. But yeah, the other works nicely.
1 Like
tobi
March 25, 2025, 7:59am
9
@jacob
Thanks for the answers. I’m still dumbfounded though
this is my class definition
export default class TagsPickerTSUiElement extends UmbLitElement implements UmbPropertyEditorUiElement
Then i do this, in connectedCallback()
const context = await this.getContext(UMB_PROPERTY_DATASET_CONTEXT);
this.observe(context.properties, (props) => {
console.log('props', props);
});
but context.properties still doesn’t exist. Wonder what I’m doing wrong?
LuukPeters
(Luuk Peters (Proud Nerds))
March 25, 2025, 8:11am
10
I don’t know if this will help you, because in my situation I needed to check if the current content implements a specific composition (so this is a custom condition). But for that I used the UMB_CONTENT_WORKSPACE_CONTEXT:
import { UMB_CONTENT_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/content';
...
this.consumeContext(UMB_CONTENT_WORKSPACE_CONTEXT, (instance) => {
this.observe(instance.structure.contentTypeAliases, (aliases) => {
if (aliases.includes('SOME_ALIAS')) {
this.permitted = true;
} else {
this.permitted = false;
}
});
});
Like I said, not sure if this is the right direction, but maybe it helps.
Also, don’t you need to consumeContext instead of await it as in your code example?
1 Like
tobi
March 25, 2025, 8:23am
11
I actually did help a lot
Can I ask how you found out to use UMB_CONTENT_WORKSPACE_CONTEXT?
Have you found any useful documentation?
jacob
(Jacob Overgaard 🚀)
March 25, 2025, 8:25am
12
Without having seen your codebase, I would assume that you called it getContext
before it existed. It tries to wait until the context is there, but it may be too early. The best approach is probably to use consumeContext
instead, which has a callback for when the context becomes available, as @LuukPeters suggests in his reply. You would put that code into the constructor instead so that the controller can clean it up for you when the component gets destroyed:
export default class TagsPickerTSUiElement extends UmbLitElement implements UmbPropertyEditorUiElement {
constructor() {
super();
this.consumeContext(UMB_PROPERTY_DATASET_CONTEXT, (dataset) => {
this.observe(context.properties, (props) => {
console.log('props', props);
});
});
}
}
LuukPeters
(Luuk Peters (Proud Nerds))
March 25, 2025, 8:30am
13
From our mutual friend @jacob
As some of you already know, I’m busy updating some packages from Umbraco 13 to 15 and I have arrived at my next challenge; I need to show a workspace view conditionally depending if the page implements a specific composition.
Creating a custom condition wasn’t very hard, but I found it difficult to check if the current page implements the composition. Besides that, I think there is a bug as well, but I wanted to get your opinion before submitting a bug report.
Getting the current content type…
And like Jacob mentioned, I always place the consume contexts in the constructor. It’s probably one of the few things that I put into the constructor instead of the connectCallback.
tobi
March 25, 2025, 9:20am
14
Thanks alot both of you - it really helps a lot
export default class TagsPickerTSUiElement extends UmbLitElement implements UmbPropertyEditorUiElement {
constructor() {
super();
this.consumeContext(UMB_PROPERTY_DATASET_CONTEXT, (dataset) => {
this.observe(dataset.properties, (props) => {
console.log('props', props);
});
});
}
}
still gives this error:
Property 'properties' does not exist on type 'UmbPropertyDatasetContext'
So I guess the properties property really doesn’t exist in UMB_PROPERTY_DATASET_CONTEXT.
Is there anywhere to see some documentation of the properties on the different types of contexts?
I see your code there has: this.observe(dataset.properties...
while Jacobs example leads with this.observe(context.properties...
Could that be a mistake?
tobi
March 25, 2025, 10:27am
16
I believe its correct with “dataset” as it is the return object from consumeContext
jacob
(Jacob Overgaard 🚀)
March 25, 2025, 10:27am
17
You can call the argument whatever you like, so context
is the convention in core, but dataset
works as well.
So I’m not sure what’s going on, because according to the API Docs, it should exist on the interface:
Maybe you get a different instance at build vs runtime? You could cast it to any
in typescript and check the debugger runtime.
tobi
March 25, 2025, 10:40am
18
It helps to update the npm packages…
Thanks
2 Likes
LuukPeters
(Luuk Peters (Proud Nerds))
March 25, 2025, 10:42am
19
This works for me:
import { UMB_PROPERTY_DATASET_CONTEXT } from "@umbraco-cms/backoffice/property"
constructor() {
super();
this.consumeContext(UMB_PROPERTY_DATASET_CONTEXT, (dataset) => {
this.observe(dataset.properties, (props) => {
console.log('props', props);
});
});
}
This is with Umbraco 15.2.3.
1 Like
jacob
(Jacob Overgaard 🚀)
March 25, 2025, 10:43am
20
Glad we got your seemingly first issue (on this forum) resolved. Welcome to the new forum!