Check if a node implements a composition (custom condition); easier way and bug?

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 is easy:

this.consumeContext(UMB_DOCUMENT_WORKSPACE_CONTEXT, (instance) => {
	const contentTypeKey = instance.getContentTypeUnique();
	if (!contentTypeKey) {
		this.permitted = false;
		return;
	}
}

Next I get the details of the current content type:

this.consumeContext(UMB_DOCUMENT_TYPE_DETAIL_STORE_CONTEXT, 
	(storeInstance) => {
	const contentType = storeInstance.getItems([contentTypeKey])[0];
}

This content Type has a list of compositions keys, but no more details.

const compositionUniques = contentType?.compositions
	.map(composition => composition.contentType.unique);

It’s too bad I only get keys, because I need to check the alias of the composition. So I need to perform additional logic to get the composition itself before I can get to check the alias. This feels a bit much work for something that should be so simple.

Also, this is where the bug comes in. When dealing with content types, there are two functions on the document type details context you can use:

//This gets an array UmbDocumentTypeDetailsModel
const contentType = storeInstance.getItems([contentTypeKey]);

//This gets an observable that ultimately gets a
// UmbDocumentTypeDetailsModel as well
const contentType= storeInstance.byUnique(contentTypeKey)

HOWEVER, when I plug in my unique key of my composition, the getItems() function always returns an empty array, but the byUnique function does work as expected.

//This list contains in my case two keys
const compositionUniques = contentType?.compositions.map(composition => composition.contentType.unique);

//This list is always empty!
const compositions = storeInstance.getItems(compositionUniques);

//However, this works!
const expirationContentType = storeInstance.byUnique(compositionUniques [0]);
expirationContentType.subscribe((documentType) => {
	console.log(documentType?.alias);
});

I would expect the getItems to also work on compositions, because I now have to use the observable to make my custom condition work, which is not ideal.

So:

  1. Is there a easier way to check if the current page implements a composition
  2. So you think it’s a bug that getItems() doesn’t seem to work with compositions?

I would look to see what endpoints of the Management API return as it may be that the server never returns enough info for you to use in the client side code.

Browse to Umbraco/swagger and experiment with the APIs themselves first.

Well, if getItems just worked, I would have been done already. I did solve it, albeit not the nicest way, by simply subscribing to all instances of the observables I get from using byUnique():

export class CustomCondition extends UmbConditionBase<CustomConditionConfig> implements UmbExtensionCondition {
	constructor(host: UmbControllerHost, args: UmbConditionControllerArguments<CustomConditionConfig>) {
	super(host, args);

	this.consumeContext(UMB_DOCUMENT_WORKSPACE_CONTEXT, (instance) => {
		const contentTypeKey = instance.getContentTypeUnique();

		if (!contentTypeKey) {
			this.permitted = false;
			return;
		}

		this.consumeContext(UMB_DOCUMENT_TYPE_DETAIL_STORE_CONTEXT, async (storeInstance) => {
			const contentType = storeInstance.getItems([contentTypeKey])[0];

			const compositionUniques = contentType?.compositions.map(composition => composition.contentType.unique);
			if (!compositionUniques) {
				this.permitted = false;
				return;
			}

			compositionUniques.map(unique => storeInstance.byUnique(unique).subscribe((compositionType) => {
				if (compositionType?.alias === "SOME_ALIAS") { //TODO:this should be a constant
					this.permitted = true;
				}
			}));
		})
	});
}

Hi Luuk, fantastically interesting case.

Especially because this is regarding one of the things that are way different than the earlier version. The facts that its not the server handling the composing of Content Types, but the client. The server just stores the Content Type Model and knows nothing about how they merge.

This means the Client loads all Content Types needed for your Document.
All that is happening through the UmbContentTypeStructureManager, which is the one you need to talk to, no need to ask the server.

This one holds all the data of the current ContentTypes (The owner and the compositions).

The following example outputs all the Aliases and Uniques(key/GUIDs) of the Content Types for the current Content(Document/Media/member). Notice I made sure to make this generic enough to work for Documents, Media and Members, by using the CONTENT_WORKSPACE context token.

this.consumeContext(UMB_CONTENT_WORKSPACE_CONTEXT, (instance) => {
			console.log(instance.structure.getContentTypeUniques(), instance.structure.getContentTypeAliases());
		});

Because the composing is happening in the Client, it means that the Composition can happen on the fly, meaning it can also change reactively. So it is not ideal for your Condition to assume the Composition is static(Like asking the server or getting the Aliases initially). Instead we will observe the content type aliases, so your conditions approves once it is in place. And as well disapproves if it goes away again.

	this.consumeContext(UMB_CONTENT_WORKSPACE_CONTEXT, (instance) => {
		this.observe(instance.structure.contentTypeAliases, (aliases) => {
			if(aliases.includes('my-content-type-alias')) {
				this.permitted = true;
			} else {
				this.permitted = false;
			}
		});
	});

It may seem like overdoing things, but one reason that exists today is that you can edit Content Types while having a Document open. And in a not-so-far future we will implement Signal-R. Meaning Content Type Compositions could change across Tabs and Devices — So I will recommend always making reactive logic and assume things can change.

I hope this works for you and then I’m really looking forward to hear how it goes.

And maybe you will see new ways to go about things, cause these abilities opens op for a whole new way of building things. So please go read more about the Structure Manager here: UmbContentTypeStructureManager | @umbraco-cms/backoffice

Cause it could be that you rather wanted your Workspace View to appear when a Content Type contains a Property of a certain Data-Type. Cause that would also be right at hand.

Good luck.

1 Like

Thanks Niels,

This works really well. I think the most challenging thing for me is to decide and find out what contexts to use. So far I’m missing just a list with all workspaces with what you would use it for in combination with an example or two. I understand that https://apidocs.umbraco.com is meant as that documentation, but I’m struggling to find what I need.

Having said that, I think I should elaborate a little why I need to check the composition. We thought long and hard how to add a certain semantic value to a content type while maintaining the flexbility to go completely custom for a customer. So we use compositions to give this semantic value to a content type. Most of these compositions don’t even have any fields. A few examples:

  • Hospitals need to keep certain medical information up to date on their site, but not for every page type; it’s usually medical information pages. So we build the site as usual with content types. We install our own content expiration package and that will add a ‘content expiration’ composition to Umbraco. By simply adding this composition to a content type, you now have a page where the content expires. And it’s exactly this case that prompted my question :wink:

  • But we also have a ‘core’ package with essentials and that adds a number of compositions, like ‘404’, ‘homepage’, ‘settings’ etc. This core package also has endpoints and services like: get all homepages, get all 404 pages. This API will get all pages that implement the relevant composition. Why would we do this? Well on Umbraco instances with multiple sites, the homepage content type for each site can be completely different for each site. So by adding a ‘homepage’ composition, we provide semantic value to the content type or node, that it is in fact a homepage and in that case it doesn’t matter anymore what the actual content type is.

This topic was automatically closed 5 days after the last reply. New replies are no longer allowed.