Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: enhance @event decorator #9944

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
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
110 changes: 96 additions & 14 deletions docs/4-development/05-events.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ In this article, we will discuss events in the context of UI5 Web Components.

Components use `CustomEvent` to inform developers of important state changes in the components. For example, the `change` event is fired whenever the value of a `ui5-input` is changed.

## `@event` Decorator
## The `@event` Decorator

To define your own custom event, you need to use the `@event` decorator.

Expand All @@ -28,9 +28,9 @@ class MyDemoComponent extends UI5Element {}

**Note:** This decorator is used only to describe the events of the component and is not meant to create emitters.

## How to use events
## Usage

As mentioned earlier, the `@event` decorator doesn't create event emitters. To notify developers of component changes, we have to fire events ourselves. This can be done using the `fireEvent` method that comes from the `UI5Element` class.
As mentioned earlier, the `@event` decorator doesn't create event emitters. To notify developers of component changes, we have to fire events ourselves. This can be done using the `fireEvent` and the newer `fireDecoratorEvent` methods that comes from the `UI5Element` class. The difference between the methods is explained below.

```ts
import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js";
Expand All @@ -46,21 +46,16 @@ class MyDemoComponent extends UI5Element {

onNativeInputChange(e) {
this.value = e.target.value;
this.fireEvent("change");
this.fireDecoratorEvent("change"); // or this.fireEvent("change");
}
}
```

Events fired by the `fireEvent` method can be configurable, meaning you can decide whether the event should be cancelable or able to bubble. This can be done by setting the third and fourth parameters of the function to true, respectively.
**Note:** By default, the `fireDecoratorEvent` (and `fireEvent`) method returns a boolean value that helps you understand whether the event was canceled (i.e., if the `preventDefault` method was called).

```ts
this.fireEvent("change", {}, cancelable, bubbles);
```
## Event Detail

**Note:** By default, the `fireEvent` method returns a boolean value that helps you understand whether the event was canceled (i.e., if the `preventDefault` method was called).

## Types
The `@event` decorator is generic and accepts a TypeScript type that describes its detail. This type is crucial for preventing incorrect detail data when the event is fired using `fireEvent` (which is also generic) and for ensuring type safety when listening for the event, so you know what kind of detail data to expect.
The `@event` decorator is generic and accepts a TypeScript type that describes its detail. This type is crucial for preventing incorrect detail data when the event is fired using `fireDecoratorEvent` and `fireEvent` methods (both generic) and for ensuring type safety when listening for the event, so you know what kind of detail data to expect.

**Note:** It's required to export all types that describe specific event details for all public events.

Expand Down Expand Up @@ -88,7 +83,7 @@ class MyDemoComponent extends UI5Element {
value = "";

onNativeInputChange(e: Event) {
this.fireEvent<MyDemoComponentChangeEventDetail>("change", {
this.fireDecoratorEvent<MyDemoComponentChangeEventDetail>("change", {
valid: true,
});
}
Expand All @@ -97,7 +92,94 @@ class MyDemoComponent extends UI5Element {
export { MyDemoComponent };
```

## noConflict mode
## Event Configuration

### Bubbling and Preventing

Whether the events should be cancelable or able to bubble is configurable.
by setting `cancelable` and `bubbles` in the `@event` decorator.

- `cancelable: true` means the event can be prevented by calling the native `preventDefault()` method in the event handler- by default it's `false`.

- `bubbles: true` means the event will bubble - by default it's `false`.

Since `v2.4.0` this can be configured in the `@event` decorator:

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

@customElement("my-demo-component")
@event("change", {
bubbles: true // false by default
cancelable: true // false by default
})
class MyDemoComponent extends UI5Element {

onSomeAction() {
this.fireDecoratorEvent("change")
}
}
```

### The `fireDecoratorEvent` method

The method is available since version `v2.4.0` and it fires a custom event and gets the configuration for the event from the `@event` decorator. In case you rely on the decorator settings, you must use the `fireDecoratorEvent` method.

Keep in mind that `cancelable` and `bubbles` are `false` by default and you must explicitly enable them in the `@event` decorator if required.

- Fire event with default configuration

```ts
@event("change")
```

```ts
// Fires the event as NOT preventable and NOT bubbling
this.fireDecoratorEvent("change");
```

- Fire event with non-default configuration

```ts
@event("change", {
bubbles: true // false by default
cancelable: true // false by default
})
```

```ts
// Fires the event as preventable and bubbling
this.fireDecoratorEvent("change");
```

**Note:** since `v2.4.0` it's recommended to describe the event in the `@event` decorator and use the `fireDecoratorEvent` method.

### The `fireEvent` method

The method is available since the very beginning of the project and like `fireDecoratorEvent` fires a custom event, but does not consider the settings in the `@event` decorator. So, if you set `cancelable` and `bubbles` in the `@event` decorator, but fire the component events via `fireEvent`, the configured values won't be considered.

Another difference is the default values of the event settings. When using `fireEvent` by default it assumes the event is bubbling (bubbles: true) and not preventable (cancelable: false).

- Fire event with default configuration

```ts
// Fires the event as NOT preventable and bubbling
this.fireEvent("change");
```

- Fire event with non-default configuration

The method allows configuring the `cancelable` and `bubbles` fields via function arguments - the third and fourth parameters respectively.

```ts
// Fires the event as preventable and non-bubbling
this.fireEvent("change", {}, true, false);
```

### noConflict mode

By default, UI5 Web Components fire all custom events twice: once with their name (e.g., `change`) and once more with a `ui5-` prefix (e.g., `ui5-change`). For example, when the `ui5-switch` is toggled, it fires a `change` event and a `ui5-change` event.

This `noConflict` setting allows us to prevent clashes between native and custom events.
Expand Down
74 changes: 68 additions & 6 deletions docs/4-development/11-deep-dive-and-best-practices.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,9 @@ class MyDemoComponent extends UI5Element {

### Firing the Event


#### The `fireEvent` method

Use the `UI5Element#fireEvent` method to trigger the event:

```ts
Expand All @@ -257,9 +260,63 @@ class MyDemoComponent extends UI5Element {
}
```

By defualt when using `fireEvent` it assumes the event is bubbling (bubbles: true) and not preventable (cancelable: false).

- Fire event with default configuration

```ts
// Fires the event as NOT preventable and bubbling
this.fireEvent("change");
```

- Fire event with non-default configuration

The method allows configuring the `cancelable` and `bubbles` fields via function arguments - the third and fourth parameters respectively.

```ts
// Fires the event as preventable and non-bubbling
this.fireEvent("change", {}, true, false);
```

#### The `fireDecoratorEvent` method

Use the `UI5Element#fireDecoratorEvent` method to trigger the event.

The method is available since version `v2.4.0` and it is similar to `fireEvent`. It fires a custom event, but gets the configuration for the event from the `@event` decorator. In case you rely on the decorator settings, you must use the `fireDecoratorEvent` method.

Keep in mind that `cancelable` and `bubbles` are `false` by default and you must explicitly enable them in the `@event` decorator if required.

- Fire event with default configuration

```ts
@event("change")
```

```ts
// Fires the event as NOT preventable and NOT bubbling
this.fireDecoratorEvent("change");
```

- Fire event with non-default configuration

```ts
@event("change", {
bubbles: true // false by default
cancelable: true // false by default
})
```

```ts
// Fires the event as preventable and bubbling
this.fireDecoratorEvent("change");
```

**Note:** since `v2.4.0` it's recommended to describe the event in the `@event` decorator and use the `fireDecoratorEvent` method.


### Describing the Event Detail

When an event includes a detail it's recommended to create a TypeScript type that describes the event detail and use it in the `fireEvent` (as it's a generic method) to force static checks ensuring that proper event detail is passed.
When an event includes a detail it's recommended to create a TypeScript type that describes the event detail and use it in the `fireEvent` or `fireDecoratorEvent` (as generic methods) to force static checks ensuring that proper event detail is passed.
The naming convention for the type is a combination of the component class name ("MyDemoComponent"), the event name ("SelectionChange"), followed by "EventDetail", written in PascalCase, e.g "MyDemoComponentSelectionChangeEventDetail":


Expand All @@ -277,7 +334,7 @@ export type MyDemoComponentSelectionChangeEventDetail = {
class MyDemoComponent extends UI5Element {

onItemSelected(e: Event) {
this.fireEvent<MyDemoComponentSelectionChangeEventDetail>("selection-change", {
this.fireDecoratorEvent<MyDemoComponentSelectionChangeEventDetail>("selection-change", {
valid: true,
});
}
Expand All @@ -302,19 +359,24 @@ By default, events are fired in pairs: one with the standard name and another pr

### Preventable Events

It's common to prevent certain events in an application. You must enable the `cancelable` flag to make the event preventable.
It's common to prevent certain events in an application. You must configure the `cancelable` setting in the `@event` decorator to make the event preventable.

```ts
this.fireEvent("change", null, true /* cancelable */);
@event("change", {
cancelable: true // false by default
})
```

Sometimes, you may also need to update (or revert) the component's state when an event is prevented by the consuming side. To determine if an event was prevented, check the return value of the `fireEvent` method. It returns false if the event was cancelled (`preventDefault` was called) and true otherwise:
You most likely will need to update (or revert) the component's state when an event is prevented by the consuming side. To determine if an event was prevented, check the return value of the `fireDecoratorEvent` method. It returns false if the event was cancelled (`preventDefault` was called) and true otherwise:

```ts
@event("change", {
cancelable: true // false by default
})
class Switch extends UI5Element {
toggle() {
this.checked = !this.checked;
const changePrevented = !this.fireEvent("change", null, true /* cancelable */);
const changePrevented = !this.fireDecoratorEvent("change");

if (changePrevented) {
this.checked = !this.checked;
Expand Down
2 changes: 1 addition & 1 deletion docs/5-contributing/03-DoD.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Consider what makes the most sense in your particular use case.
- Did I **describe my event's details**?

If an event that your component fires has event details, you should create a Typescript type, use it in the `@event` decorator,
use it with `fireEvent`, and finally, export it as a named type export.
use it with `fireEvent` or `fireDecoratorEvent`, and finally, export it as a named type export.

## CSS

Expand Down
6 changes: 4 additions & 2 deletions packages/ai/src/Button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,9 @@ import ButtonCss from "./generated/themes/Button.css.js";
* mouse/tap or by using the Enter or Space key.
* @public
*/
@event("click")
@event("click", {
bubbles: true,
})
class Button extends UI5Element {
/**
* Defines the component design.
Expand Down Expand Up @@ -210,7 +212,7 @@ class Button extends UI5Element {
*/
_onclick(e: MouseEvent): void {
e.stopImmediatePropagation();
this.fireEvent("click");
this.fireDecoratorEvent("click");
}

get _effectiveState() {
Expand Down
20 changes: 13 additions & 7 deletions packages/ai/src/PromptInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ import PromptInputCss from "./generated/themes/PromptInput.css.js";
* @since 2.0.0
* @public
*/
@event("submit")
@event("submit", {
bubbles: true,
})

/**
* Fired when the value of the component changes at each keystroke,
Expand All @@ -70,7 +72,9 @@ import PromptInputCss from "./generated/themes/PromptInput.css.js";
* @since 2.0.0
* @public
*/
@event("input")
@event("input", {
bubbles: true,
})

/**
* Fired when the input operation has finished by pressing Enter
Expand All @@ -79,7 +83,9 @@ import PromptInputCss from "./generated/themes/PromptInput.css.js";
* @since 2.0.0
* @public
*/
@event("change")
@event("change", {
bubbles: true,
})
class PromptInput extends UI5Element {
/**
* Defines the value of the component.
Expand Down Expand Up @@ -229,22 +235,22 @@ class PromptInput extends UI5Element {

_onkeydown(e: KeyboardEvent) {
if (isEnter(e)) {
this.fireEvent("submit");
this.fireDecoratorEvent("submit");
}
}

_onInnerInput(e: CustomEvent<InputEventDetail>) {
this.value = (e.target as Input).value;

this.fireEvent("input");
this.fireDecoratorEvent("input");
}

_onInnerChange() {
this.fireEvent("change");
this.fireDecoratorEvent("change");
}

_onButtonClick() {
this.fireEvent("submit");
this.fireDecoratorEvent("submit");
}

_onTypeAhead(e: CustomEvent): void {
Expand Down
Loading
Loading