Update custom view when settings property change

I have a custom view for section components to indicate chosen background color of sections.

This works well on initial load of page/node:

import { css, html, customElement, LitElement, property, when, state } from '@umbraco-cms/backoffice/external/lit';
import { type PropertyValueMap } from '@umbraco-cms/backoffice/external/lit';
import { UmbElementMixin } from '@umbraco-cms/backoffice/element-api';
import type { UmbBlockEditorCustomViewElement } from '@umbraco-cms/backoffice/block-custom-view';
import type { UmbBlockDataType } from '@umbraco-cms/backoffice/block';
import type { UmbBlockGridTypeModel } from '@umbraco-cms/backoffice/block-grid';
import type { UmbBlockEditorCustomViewConfiguration } from '@umbraco-cms/backoffice/block-custom-view';

//import '@umbraco-cms/backoffice/ufm';

@customElement('mcb-grid-block-custom-view')
export class McbGridBlockCustomView extends UmbElementMixin(LitElement) implements UmbBlockEditorCustomViewElement {

    @state()
    blockType?: UmbBlockGridTypeModel;

    @property({ attribute: false })
    label?: string;

    @property({ type: String, reflect: false })
    icon?: string;

    @property({ type: Number, attribute: false })
    index?: number;

    @property({ attribute: false })
    config?: UmbBlockEditorCustomViewConfiguration;

    @property({ type: Boolean, reflect: true })
    unpublished?: boolean;

    @property({ attribute: false })
    content?: UmbBlockDataType;

    @property({ attribute: false })
    settings?: UmbBlockDataType;

    protected override firstUpdated(changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void {
        super.firstUpdated(changedProperties);

        const bgColor = this.settings?.backgroundColor ? (this.settings?.backgroundColor as any)?.value : '#fff';

        this.style.setProperty('--mcb-block-grid--section-bg-color', bgColor);
        this.style.setProperty('--mcb-block-grid--section-text-color', this.#contrast(bgColor) === 'dark' ? '#fff' : '#000');
    }

    #contrast(hex: string): string {
        const rgb = this.#hexToRgb(hex);
        const o = Math.round(((rgb[0] * 299) + (rgb[1] * 587) + (rgb[2] * 114)) / 1000);

        return (o <= 180) ? 'dark' : 'light';
    };

    #hexToRgb(hex: string): number[] {
        hex = hex.startsWith("#") ? hex.slice(1) : hex;
        if (hex.length === 3) {
            hex = Array.from(hex).reduce((str, x) => str + x + x, ""); // 123 -> 112233
        }

        const bigint = parseInt(hex, 16);
        const r = (bigint >> 16) & 255;
        const g = (bigint >> 8) & 255;
        const b = bigint & 255;

        return [r, g, b];
    };

    override render() {
		const blockValue = { ...this.content, $settings: this.settings, $index: this.index };
		return html`
			<umb-ref-grid-block
				standalone
				href=${(this.config?.showContentEdit ? this.config?.editContentPath : undefined) ?? ''}>
				<umb-icon slot="icon" .name=${this.icon}></umb-icon>
				<umb-ufm-render slot="name" inline .markdown=${this.label} .value=${blockValue}></umb-ufm-render>
				${when(
					this.unpublished,
					() =>
						html`<uui-tag slot="name" look="secondary" title=${this.localize.term('blockEditor_notExposedDescription')}
							><umb-localize key="blockEditor_notExposedLabel"></umb-localize
						></uui-tag>`,
				)}
				<umb-block-grid-areas-container slot="areas" draggable="false"></umb-block-grid-areas-container>
			</umb-ref-grid-block>
		`;
	}

    static override styles = [
        css`
            umb-block-grid-areas-container {
				margin-top: calc(var(--uui-size-2) + 1px);
			}

			umb-block-grid-areas-container::part(area) {
				margin: var(--uui-size-2);
			}

			uui-tag {
				margin-left: 0.5em;
				margin-bottom: -0.3em;
				margin-top: -0.3em;
				vertical-align: text-top;
			}

			:host([unpublished]) umb-icon,
			:host([unpublished]) umb-ufm-render {
				opacity: 0.6;
			}

            :host {
                display: block;
                height: 100%;
                box-sizing: border-box;
                background-color: var(--mcb-block-grid--section-bg-color);
                padding: 12px;
            }

            h5 {
                color: var(--mcb-block-grid--section-text-color);
            }
        `,
    ];
}

export default McbGridBlockCustomView;

How when a background color is changes on the section block in settings property, how can I update/refresh the view to replace to change after submit from the modal?

What I did was to set the background colour within the returned html

I also have an image background for which I used the updated function to capture changes.

(sorry - not TypeScript but hope this may help)

import { html, css, when, LitElement } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UmbImagingRepository } from '@umbraco-cms/backoffice/imaging';

// Ideas from https://forum.umbraco.com/t/custom-block-grid-views-too-complicated/2894/2
// <umb-ref-grid-block> Copied from Umbraco.Web.UI.Client/src/packages/block/block-grid/block-grid-block/block-grid-block.element.ts

export class BlockCustomView extends UmbLitElement {


    // Properties are defined to enable watching for content updates
    static get properties() {
        return {
            imageUrls: { type: Object },
            content: { type: Object },
            settings: { type: Object },
            layout: { type: Object },
            config: { type: Object },
            index: { type: Number },
            label: { type: String }
        };
    }

    constructor() {
        super();

        // Handling of background image
        this.imageUrls = new Map();
        this._imagingRepository = new UmbImagingRepository(this);
    }

    // Watch for content updates
    updated(changedProperties) {
        super.updated?.(changedProperties)
        this._fetchImageUrls()
    }

    async _fetchImageUrls() {
        const imageItems = this.settings.backgroundImage
        if (imageItems) {
            for (const item of imageItems) {
                if (item.mediaKey && !this.imageUrls.has(item.mediaKey)) {
                    const url = await this._resolveImageUrl(item.mediaKey);
                    if (url) {
                        this.imageUrls.set(item.mediaKey, url);
                        this.requestUpdate(); // Trigger re-render
                    }
                }
            }
        }
    }

    async _resolveImageUrl(mediaKey) {
        try {
            const { data } = await this._imagingRepository.requestThumbnailUrls([mediaKey], 1200, 0); // 0 for auto height
            return data?.[0]?.url ?? '';
        } catch (err) {
            console.warn('Failed to load media URL for:', mediaKey, err);
            return '';
        }
    }

    render() {
        const blockValue = { ...this.content, $settings: this.settings, $index: this.index };
        const bgc = this.settings && this.settings.backgroundColour ? `background-color:${this.settings.backgroundColour.value};` : "transparent"
        const label = `<strong>${this.label}</strong>`;
        return html`
            <umb-ref-grid-block
				standalone
				href=${(this.config?.showContentEdit ? this.config?.editContentPath : undefined) ?? ''}>
				<umb-icon slot="icon" .name=${this.icon}></umb-icon>
				<umb-ufm-render slot="name" inline .markdown=${label} .value=${blockValue}></umb-ufm-render>
                ${when( /* I'm not sure what this section does */ this.unpublished, () => html`<uui-tag slot="name" look="secondary" title=${this.localize.term('blockEditor_notExposedDescription')}><umb-localize key="blockEditor_notExposedLabel"></umb-localize></uui-tag>`,)}
                <umb-block-grid-areas-container style="${bgc}${this.settings.backgroundImage?.map(item => {
                    const src = this.imageUrls.get(item.mediaKey) || '';
                    return src
                        ? `background-image:url('${src}');`
                        : ``;
                })}" 
                slot="areas" draggable="false"></umb-block-grid-areas-container>
			</umb-ref-grid-block>`;
    }

    static styles = [
        css`
            :host {
                display:block;
            }
        `,
    ];
}

export default BlockCustomView;

customElements.define('my-custom-block-view', BlockCustomView);

Hi @bjarnef

Good question — for your future learning, it’s worth understanding that this is not Umbraco specific. In the case of you using Lit, it is Lit specific. Using other frameworks or plain Web-Components will mean you should look for a different approach.

First I would like to highlight that you could solve this by turning the settings property into a getter and setter method. Then you can react to when it being set — this would work with any framework or no framework. — Also note this architecturally keeps the logic located together with the property, therefor it is generally my preferred approach.

Another approach is possible, since you are using Lit, and you already learned about the Lit Lifecycle callback of firstUpdated, then you can utilize another lifecycle callback with very little effort, and this should be resolved.
So change the method name to willUpdate, and it will be triggered each time a Property(@property or @state declared) is updated.

To avoid re-setting the CSS-Custom-properties on every property update, I would wrap your logic in this check if (changedProperties.has('settings')) { so it becomes:

    protected override willUpdate(changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void {
        super.willUpdate(changedProperties);
        if (changedProperties.has('settings')) {
            const bgColor = this.settings?.backgroundColor ? (this.settings?.backgroundColor as any)?.value : '#fff';

            this.style.setProperty('--mcb-block-grid--section-bg-color', bgColor);
            this.style.setProperty('--mcb-block-grid--section-text-color', this.#contrast(bgColor) === 'dark' ? '#fff' : '#000');
        }
    }

I hope that works and makes sense, if you like to know more about Lit Lifecycle callbacks you can read about it here:

1 Like

@bjarnef Unrelated to your question: you can set the type of code that’s in your code blocks (in this case typescript). Can you set that? That’ll give your code highlights and color so it’s much easier to read.

@LuukPeters thanks, I have updated the syntax of the code :slight_smile:

@nielslyngsoe it works using willUpdate. I wonder if it is possible to check on specific property change in settings block as it currently react on any change in settings properties.

Besides that it update the background color immediately after selection, which is okay, but I think it ideally may be better to reflect the changes after submit from modal.
However it doesn’t seem “Close” or “Update” from modal make a difference anyway - in both cases property is updated and persisted after “Save & Publish”.

Wouldn’t the setting of Live Editing in your Data-Type affect if the data is live reflected or not? Maybe there is an issue there?

Regarding only reacting to a specific setting, then you can do that by implementing your own custom logic. Retrieve the current value, before setting it, of the specific property and then test against that. But again that is a super optimization that will not do much of change at the end of the day since you are just setting some CSS Custom properties.

:slight_smile:

@nielslyngsoe yes, live editing is enabled and I noticed it was like how it worked in Umbraco 13, so I guess it works as expected :slight_smile:

Yes, it this case it doesn’t matter that much. I may have a closer look at that sometime :grinning_face:

1 Like