Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 16 additions & 12 deletions docs/4-development/04-slots.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,15 @@ Defining a slot with the `slot` decorator means that this slot will be managed b
- The library will invalidate this UI5 Web Component whenever its children are added, removed, or rearranged (and additionally when invalidated, if `invalidateOnChildChange` is set).

```ts
import type { Slot} from "@ui5/webcomponents-base/dist/UI5Element.js"
import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js";
import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js";
import slot from "@ui5/webcomponents-base/dist/decorators/slot.js";
import slot from "@ui5/webcomponents-base/dist/decorators/slot-strict.js";

@customElement("my-demo-component")
class MyDemoComponent extends UI5Element {
@slot()
mySlot!: Array<HTMLElement>;
mySlot!: Slot<HTMLElement>;
}
```

Expand All @@ -63,12 +64,12 @@ The `type` option accepts a type constructor (e.g., `HTMLElement`, `Node`) and i
```ts
import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js";
import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js";
import slot from "@ui5/webcomponents-base/dist/decorators/slot.js";
import slot from "@ui5/webcomponents-base/dist/decorators/slot-strict.js";

@customElement("my-demo-component")
class MyDemoComponent extends UI5Element {
@slot({ type: HTMLElement })
mySlot!: Array<HTMLElement>;
mySlot!: Slot<HTMLElement>;;
}
```

Expand All @@ -90,28 +91,31 @@ The `"default"` option accepts a boolean value and is used to define whether thi
```ts
import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js";
import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js";
import slot from "@ui5/webcomponents-base/dist/decorators/slot.js";
import slot from "@ui5/webcomponents-base/dist/decorators/slot-strict.js";

@customElement("my-demo-component")
class MyDemoComponent extends UI5Element {
@slot({ type: HTMLElement, "default": true })
mySlot!: Array<HTMLElement>;
mySlot!: Slot<HTMLElement>;;
}
```

**Note:** The `Slot<T>` and `DefaultSlot<T>` marker types were introduced in version 2.20. In previous versions, slots were typed as `Array<T>` (e.g., `mySlot!: Array<HTMLElement>`). The new types enable better slot discoverability in TypeScript environments while the array is now part of the type itself.


### Individual Slots

The `individualSlots` option accepts a boolean value and determines whether each child will have its own slot, allowing you to arrange or wrap the children arbitrarily. This means that you need to handle the rendering on your own.

```ts
import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js";
import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js";
import slot from "@ui5/webcomponents-base/dist/decorators/slot.js";
import slot from "@ui5/webcomponents-base/dist/decorators/slot-strict.js";

@customElement("my-demo-component")
class MyDemoComponent extends UI5Element {
@slot({ type: HTMLElement, individualSlots: true })
mySlot!: Array<HTMLElement>;
mySlot!: Slot<HTMLElement>;;
}
```

Expand All @@ -138,17 +142,17 @@ The `invalidateOnChildChange` option accepts a boolean value or an object litera
```ts
import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js";
import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js";
import slot from "@ui5/webcomponents-base/dist/decorators/slot.js";
import slot from "@ui5/webcomponents-base/dist/decorators/slot-strict.js";

@customElement("my-demo-component")
class MyDemoComponent extends UI5Element {
@slot({ type: HTMLElement, invalidateOnChildChange: true })
mySlot!: Array<HTMLElement>;
mySlot!: Slot<HTMLElement>;;

@slot({ type: HTMLElement, invalidateOnChildChange: { properties: true, slots: false }})
mySlot2!: Array<HTMLElement>;
mySlot2!: Slot<HTMLElement>;;

@slot({ type: HTMLElement, invalidateOnChildChange: { properties: ["myProp"], slots: ["anotherSlot"] }})
mySlot3!: Array<HTMLElement>;
mySlot3!: Slot<HTMLElement>;;
}
```
21 changes: 11 additions & 10 deletions docs/4-development/11-deep-dive-and-best-practices.md
Original file line number Diff line number Diff line change
Expand Up @@ -436,12 +436,13 @@ class MyComponent extends UI5Element {}

We can define our slots as class members via the `@slot` decorator as follows:
```ts
import slot from "@ui5/webcomponents-base/dist/decorators/slot.js";
import type { Slot} from "@ui5/webcomponents-base/dist/UI5Element.js"
import slot from "@ui5/webcomponents-base/dist/decorators/slot-strict.js";

@customElement("my-component")
class MyComponent extends UI5Element {
@slot()
items!: Array<HTMLElement>;
items!: Slot<HTMLElement>;;
}
```

Expand Down Expand Up @@ -540,7 +541,7 @@ export default function () {
All slots are named if you simply use the `@slot` decorator without any settings, while the default slots must be explicitly marked as such with the `"default"` setting:

```ts
import slot from "@ui5/webcomponents-base/dist/decorators/slot.js";
import slot from "@ui5/webcomponents-base/dist/decorators/slot-strict.js";

@customElement("my-component")
class MyComponent extends UI5Element {
Expand All @@ -554,7 +555,7 @@ class MyComponent extends UI5Element {
Simply use the `@slot` decorator without any settings:

```ts
import slot from "@ui5/webcomponents-base/dist/decorators/slot.js";
import slot from "@ui5/webcomponents-base/dist/decorators/slot-strict.js";

@customElement("my-component")
class MyComponent extends UI5Element {
Expand Down Expand Up @@ -592,7 +593,7 @@ export default function MyComponentTemplate() {
```

```ts
import slot from "@ui5/webcomponents-base/dist/decorators/slot.js";
import slot from "@ui5/webcomponents-base/dist/decorators/slot-strict.js";

@customElement("my-component")
class MyComponent extends UI5Element {
Expand All @@ -614,7 +615,7 @@ The `@slot` decorator provides an option called `individualSlots`, which is of b

First, enable `individualSlots` by setting it to `true`:
```ts
import slot from "@ui5/webcomponents-base/dist/decorators/slot.js";
import slot from "@ui5/webcomponents-base/dist/decorators/slot-strict.js";

@customElement("my-component")
class MyComponent extends UI5Element {
Expand All @@ -637,7 +638,7 @@ export default function MyComponentTemplate() {

Here is an example using the `Carousel` web component, which leverages `individualSlots` to wrap each slotted child within the content slot to achieve a specific design:
```ts
import slot from "@ui5/webcomponents-base/dist/decorators/slot.js";
import slot from "@ui5/webcomponents-base/dist/decorators/slot-strict.js";

@customElement("ui5-carousel")
class Carousel extends UI5Element {
Expand Down Expand Up @@ -680,7 +681,7 @@ The simplest way to use this option is to set `invalidateOnChildChange` to `"tru


```ts
import slot from "@ui5/webcomponents-base/dist/decorators/slot.js";
import slot from "@ui5/webcomponents-base/dist/decorators/slot-strict.js";

@customElement("my-component")
class MyComponent extends UI5Element {
Expand All @@ -692,7 +693,7 @@ class MyComponent extends UI5Element {
For more specific scenarios, you can use a more detailed configuration. The following example demonstrates how to invalidate the `"my-component"` web component only when certain properties or slots of the slotted UI5Element instances change. In this case, the component will be invalidated if the "myProp" property or the "mySlot" slot of the child elements are modified.

```ts
import slot from "@ui5/webcomponents-base/dist/decorators/slot.js";
import slot from "@ui5/webcomponents-base/dist/decorators/slot-strict.js";

@customElement("my-component")
class MyComponent extends UI5Element {
Expand Down Expand Up @@ -920,7 +921,7 @@ import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js";
import ResizeHandler from "@ui5/webcomponents-base/dist/delegate/ResizeHandler.js";
import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js";
import property from "@ui5/webcomponents-base/dist/decorators/property.js";
import slot from "@ui5/webcomponents-base/dist/decorators/slot.js";
import slot from "@ui5/webcomponents-base/dist/decorators/slot-strict.js";

@customElement({
tag: "my-component",
Expand Down
5 changes: 3 additions & 2 deletions packages/ai/src/Button.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js";
import type { DefaultSlot } from "@ui5/webcomponents-base/dist/UI5Element.js";
import { renderFinished } from "@ui5/webcomponents-base/dist/Render.js";
import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js";
import property from "@ui5/webcomponents-base/dist/decorators/property.js";
import { i18n } from "@ui5/webcomponents-base/dist/decorators.js";
import event from "@ui5/webcomponents-base/dist/decorators/event-strict.js";
import slot from "@ui5/webcomponents-base/dist/decorators/slot.js";
import slot from "@ui5/webcomponents-base/dist/decorators/slot-strict.js";
import query from "@ui5/webcomponents-base/dist/decorators/query.js";
import jsxRenderer from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js";
import type SplitButton from "@ui5/webcomponents/dist/SplitButton.js";
Expand Down Expand Up @@ -183,7 +184,7 @@ class Button extends UI5Element {
* @public
*/
@slot({ type: HTMLElement, "default": true })
states!: Array<ButtonState>;
states!: DefaultSlot<ButtonState>;

@query("[ui5-split-button]")
_splitButton?: SplitButton;
Expand Down
5 changes: 3 additions & 2 deletions packages/ai/src/Input.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js";
import property from "@ui5/webcomponents-base/dist/decorators/property.js";
import event from "@ui5/webcomponents-base/dist/decorators/event-strict.js";
import slot from "@ui5/webcomponents-base/dist/decorators/slot.js";
import slot from "@ui5/webcomponents-base/dist/decorators/slot-strict.js";
import jsxRenderer from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js";
import {
isEscape,
Expand All @@ -26,6 +26,7 @@ import {
INPUT_WRITING_ASSISTANT_BUTTON_TOOLTIP,
WRITING_ASSISTANT_GENERATING_ANNOUNCEMENT,
} from "./generated/i18n/i18n-defaults.js";
import type { Slot } from "@ui5/webcomponents-base/dist/UI5Element.js";

type InputVersionChangeEventDetail = {
backwards: boolean,
Expand Down Expand Up @@ -164,7 +165,7 @@ class Input extends BaseInput {
type: HTMLElement,
invalidateOnChildChange: true,
})
actions!: Array<HTMLElement>;
actions!: Slot<HTMLElement>;

_previousCurrentStep = 0;
_previousTotalSteps = 0;
Expand Down
7 changes: 4 additions & 3 deletions packages/ai/src/PromptInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js";
import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js";
import property from "@ui5/webcomponents-base/dist/decorators/property.js";
import event from "@ui5/webcomponents-base/dist/decorators/event-strict.js";
import slot from "@ui5/webcomponents-base/dist/decorators/slot.js";
import slot from "@ui5/webcomponents-base/dist/decorators/slot-strict.js";
import i18n from "@ui5/webcomponents-base/dist/decorators/i18n.js";
import jsxRenderer from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js";
import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js";
Expand All @@ -22,6 +22,7 @@ import PromptInputTemplate from "./PromptInputTemplate.js";
// Styles
import PromptInputCss from "./generated/themes/PromptInput.css.js";
import type { UI5CustomEvent } from "@ui5/webcomponents-base/dist/index.js";
import type { Slot, DefaultSlot } from "@ui5/webcomponents-base/dist/UI5Element.js";

/**
* @class
Expand Down Expand Up @@ -202,7 +203,7 @@ class PromptInput extends UI5Element {
* @public
*/
@slot({ type: HTMLElement, "default": true })
suggestionItems!: Array<IInputSuggestionItem>;
suggestionItems!: DefaultSlot<IInputSuggestionItem>;

/**
* Defines the value state message that will be displayed as pop up under the component.
Expand All @@ -220,7 +221,7 @@ class PromptInput extends UI5Element {
type: HTMLElement,
invalidateOnChildChange: true,
})
valueStateMessage!: Array<HTMLElement>;
valueStateMessage!: Slot<HTMLElement>;

@i18n("@ui5/webcomponents-ai")
static i18nBundle: I18nBundle;
Expand Down
5 changes: 3 additions & 2 deletions packages/ai/src/TextArea.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js";
import property from "@ui5/webcomponents-base/dist/decorators/property.js";
import slot from "@ui5/webcomponents-base/dist/decorators/slot.js";
import slot from "@ui5/webcomponents-base/dist/decorators/slot-strict.js";
import event from "@ui5/webcomponents-base/dist/decorators/event-strict.js";
import jsxRenderer from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js";

Expand All @@ -17,6 +17,7 @@ import valueStateMessageStyles from "@ui5/webcomponents/dist/generated/themes/Va

// Templates
import TextAreaTemplate from "./TextAreaTemplate.js";
import type { Slot } from "@ui5/webcomponents-base/dist/UI5Element.js";

type TextAreaVersionChangeEventDetail = {
backwards: boolean,
Expand Down Expand Up @@ -131,7 +132,7 @@ class TextArea extends BaseTextArea {
focused = false;

@slot({ type: HTMLElement })
menu!: Array<HTMLElement>;
menu!: Slot<HTMLElement>;

static i18nBundle: I18nBundle;

Expand Down
15 changes: 13 additions & 2 deletions packages/base/src/UI5Element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import merge from "./thirdparty/merge.js";
import { boot } from "./Boot.js";
import UI5ElementMetadata from "./UI5ElementMetadata.js";
import type {
Slot,
Slot as SlotMetadata,
SlotValue,
State,
PropertyValue,
Expand Down Expand Up @@ -170,6 +170,16 @@ type TargetedEventHandler<D, T> = {
}["asMethod"];
type Convert<T, K extends UI5Element> = { [Property in keyof T as `on${KebabToPascal<string & Property>}`]: IsAny<T[Property], any, TargetedEventHandler<T[Property], K>> }

// Create a unique symbol as a marker
declare const SlotMarker: unique symbol;
declare const DefaultSlotMarker: unique symbol;

export type Slot<T> = T[] & { [SlotMarker]: true };
export type DefaultSlot<T> = T[] & { [DefaultSlotMarker]: true };

export type IsSlot<T> = T extends { [SlotMarker]: true } ? true : T extends { [DefaultSlotMarker]: true } ? true : false;
export type IsDefaultSlot<T> = T extends { [DefaultSlotMarker]: true } ? true : false;

/**
* @class
* Base class for all UI5 Web Components
Expand All @@ -183,6 +193,7 @@ abstract class UI5Element extends HTMLElement {
};
_jsxEvents!: Omit<JSX.DOMAttributes<this>, keyof Convert<this["eventDetails"], this> | "onClose" | "onToggle" | "onChange" | "onSelect" | "onInput"> & Convert<this["eventDetails"], this>;
_jsxProps!: Pick<JSX.AllHTMLAttributes<HTMLElement>, GlobalHTMLAttributeNames> & ElementProps<this> & Partial<this["_jsxEvents"]> & { key?: any };

__id?: string;
_suppressInvalidation: boolean;
_changedState: Array<ChangeInfo>;
Expand Down Expand Up @@ -577,7 +588,7 @@ abstract class UI5Element extends HTMLElement {
* Removes all children from the slot and detaches listeners, if any
* @private
*/
_clearSlot(slotName: string, slotData: Slot) {
_clearSlot(slotName: string, slotData: SlotMetadata) {
const propertyName = slotData.propertyName || slotName;
const children = this._state[propertyName] as Array<SlotValue>;

Expand Down
2 changes: 2 additions & 0 deletions packages/base/src/decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import event from "./decorators/event.js";
import eventStrict from "./decorators/event-strict.js";
import property from "./decorators/property.js";
import slot from "./decorators/slot.js";
import slotStrict from "./decorators/slot-strict.js";
import bound from "./decorators/bound.js";
import i18n from "./decorators/i18n.js";

Expand All @@ -12,6 +13,7 @@ export {
eventStrict,
property,
slot,
slotStrict,
bound,
i18n,
};
47 changes: 47 additions & 0 deletions packages/base/src/decorators/slot-strict.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import type UI5Element from "../UI5Element.js";
import type { Slot, DefaultSlot } from "../UI5Element.js";
import type { Slot as SlotMetadata } from "../UI5ElementMetadata.js";

function slot<
T extends Record<K, Slot<unknown> | DefaultSlot<unknown>>,
K extends string
>(slotData?: SlotMetadata): (target: T, prop: K) => void {
return (target: any, slotKey: string | symbol) => {
const ctor = target.constructor as typeof UI5Element;

if (!Object.prototype.hasOwnProperty.call(ctor, "metadata")) {
ctor.metadata = {};
}

const metadata = ctor.metadata;
if (!metadata.slots) {
metadata.slots = {};
}

const slotMetadata = metadata.slots;

if (slotData && slotData.default && slotMetadata.default) {
throw new Error("Only one slot can be the default slot.");
}

const key = slotData && slotData.default ? "default" : (slotKey as string);
slotData = slotData || { type: HTMLElement };

if (!slotData.type) {
slotData.type = HTMLElement;
}

if (!slotMetadata[key]) {
slotMetadata[key] = slotData;
}

if (slotData.default) {
delete slotMetadata.default.default;
slotMetadata.default.propertyName = slotKey as string;
}

ctor.metadata.managedSlots = true;
};
}

export default slot;
Loading
Loading