Elements are custom HTML tags that are used to define and create complex components within layouts. All framework elements are UI components, designed to be highly reusable and customisable.

However, elements cannot be directly integrated into components or displays. They provide specific UI functionality and behavior and are intended to be combined and reused within layouts to create more complex components.

The extensibility of elements makes them powerful and flexible. Developers can create custom elements with specific behaviours and functionality to create highly specialised components tailored to specific use cases.

Elements are a key part of the Unless Component Framework's component architecture, providing a flexible and extensible foundation for creating powerful and reusable UI components that can be easily integrated into layouts and components.

Take a look at the list of elements that are provided by our open-source framework: https://bit.cloud/unless/component-library/element/body.

elements

Example of elements within a component.

Example

import {Element} from '@unless/component-library.element.element'
import {html, TemplateResult} from 'lit'

export class MyElement extends Element {
    static from = () => new MyElement().render()

    public render(): TemplateResult | TemplateResult[] {
        return html`<p>Hello world!</p>`
    }
}

Class methods

Elements are simple components and have only two methods that must be implemented.

ScopeMethodArgumentsDescription
publicrender()noneRenders the element HTML.
staticfrom()noneCreates an instance and renders the element HTML. If the element doesn't use settings, no arguments are needed for this method.

Using props with elements

At times, Elements require certain props to be passed in. To ensure consistency, the interface must conform to ElementProps. Here's an example of an Element that receives some props:

import {
		Element,
		ElementProps,
		DefaultElementSettings
} from '@unless/component-library.element.element'

interface CloseBtnProps extends ElementProps {
		shadowRoot: ShadowRoot
}

export class CloseBtn extends Element {
		private shadowRoot: ShadowRoot

		private handleClose() {
				this
						.shadowRoot
						.querySelector<HTMLDialogElement>('dialog')?
						.close()
		}

		constructor(props: CloseBtnProps) {
				super(DefaultElementSettings)
				this.shadowRoot = props.shadowRoot
		}

		public render() {
				...
		}
}

Constructor implementation with props

When props are included, it is necessary to implement the constructor. In this case, the Element has no settings to be passed to the constructor of the parent class, and therefore we can use DefaultElementSettings.

Consider using settings when possible

It's recommended to use props only for dynamic elements or when the passed value must be by reference, like the ShadowRoot in this example. If possible, settings should be used instead so that the values can be easily edited in the editor.

Using settings with elements

Settings for Elements are typically not provided and are optional because Elements are most often simple. However, for more complex applications, it might be necessary to add settings.

For example, if we want to create a Body Element, settings might include the content text.

import {
		BaseSettings,
		TextSetting
} from '@unless/component-library.utils.types'

export interface BodySettings extends BaseSettings {
    content: TextSetting
}
/* esbuild-ignore */ // <-- See documentation on settings

import {BodySettings} from './settings-model'
import {Subset} from '@unless/component-library.utils.types'
import {deepCopy, getSettings} from '@unless/component-library.utils.helpers'

export const bodyCopyWith =
    (...settingsChanges: Subset<BodySettings>[]) =>
        getSettings(deepCopy(settings), ...settingsChanges)

const settings: BodySettings = {
		content: {
				label: 'content',
				type: 'text',
				value: '',
				placeholder: '',
				helpText: 'Sets the content of the body',
		}
}

export default settings
import {
		Element,
		ElementSettingsProps
} from '@unless/component-library.element.element'
import {BodySettings} from './settings/settings-model'
import {deepCopy} from '@unless/component-library.utils.helpers'
import settings from './settings/settings'
import {BodySettings} from "./settings/settings-model";

interface BodyProps extends ElementSettingsProps<BodySettings> {}

export class Body extends Element<BodySettings> {
    static from = (props: BodyProps) => new Body(props).render()
    static defaultSettings = () => deepCopy(settings)

    constructor(props: BodyProps) {
        super(props.settings)
    }

    public render() {
        return html`<p>${this.settings.content.value}</p>`
    }
}

Class methods with settings

When using settings with elements, it is important to follow a few conventions for consistency.

Props type

To maintain consistency with other layers, an interface must be defined for the constructor props that conforms to ElementWithSettings<TSettings> which contains at least one field: settings. For example, the BodyProps type can be defined as:

import {BodySettings} from "./settings/settings-model";

interface BodyProps extends ElementSettingsProps<BodySettings> {}

Therefore, sub-elements that extend Element must have a constructor implemented as follows:

class Body extends Element<BodySettings> {
    constructor(props: BodyProps) {
        super(props.settings)
    }
}

Public methods

The following public method must be implemented for elements:

MethodArgumentsDescription
public render()noneRenders the element HTML

What’s Next