Skip to content

Commit

Permalink
fix(core/renderer): improve event handling and add event listener sup…
Browse files Browse the repository at this point in the history
…port

- Refactor EventEmitter and Event decorators for better event management
- Add event support for onCamelEventName or on-kebab-case-name
- Add tests for event handling and listener behavior
  • Loading branch information
star-ll committed Nov 10, 2024
1 parent 0ddfe92 commit 984347d
Show file tree
Hide file tree
Showing 7 changed files with 163 additions and 18 deletions.
6 changes: 4 additions & 2 deletions packages/core/src/decorators/Component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { doWatch } from './Watch';
import { DecoPlugin } from '../api/plugin';
import { isObjectAttribute } from '../utils/is';
import { warn } from '../utils/error';
import { EventEmitter } from './Event';

export interface DecoWebComponent {
[K: string | symbol]: any;
Expand Down Expand Up @@ -250,8 +251,9 @@ function getCustomElementWrapper(target: any, { tag, style, observedAttributes }

if (events) {
for (const eventName of events.keys()) {
const eventEmit = events.get(eventName);
eventEmit.setEventTarget(this);
const eventInit = events.get(eventName);
const eventEmit = new EventEmitter({ ...eventInit });
eventEmit.setEventTarget(this as any);
this[eventName] = eventEmit;
}
}
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/decorators/Event.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { isFunction } from '../utils/is';

export function Event(eventInit: EventInit = { composed: true }) {
export function Event(eventInit: EventInit = { composed: true, bubbles: true, cancelable: true }) {
return function eventEmit(target: any, eventName: string) {
const events = Reflect.getMetadata('events', target) || new Map();
events.set(eventName, new EventEmitter({ ...eventInit }));
events.set(eventName, eventInit);
Reflect.defineMetadata('events', events, target);
};
}
Expand Down
7 changes: 5 additions & 2 deletions packages/examples/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@ function handlerRouter(route: Route) {
}

route.component().then(() => {
const component = document.createElement(route.name);
document.body.innerHTML = '';
document.body.appendChild(component);

if (route.name) {
const component = document.createElement(route.name);
document.body.appendChild(component);
}
});
}
window.addEventListener('hashchange', function (e) {
Expand Down
46 changes: 46 additions & 0 deletions packages/examples/src/renderer/event.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Component, DecoElement, EventEmitter, Event, State, Ref, RefType } from '@decoco/core';

@Component('emit-event')
export class EmitEvent extends DecoElement {
@Event() emitter: EventEmitter;

triggerEvent(eventName: string, successTip: string) {
this.emitter!.emit(eventName, successTip);
}
}

@Component('emit-event-test')
export class EmitEventTest extends DecoElement {
@Ref() emitRef!: RefType<EmitEvent>;
@Ref() emitRef2!: RefType<EmitEvent>;
@Ref() emitRef3!: RefType<EmitEvent>;
@Ref() emitRef4!: RefType<EmitEvent>;

@State() text = '';
render() {
const onEvent = (e: CustomEvent) => {
this.text = e.detail;
};
return (
<div>
<div className="result">{this.text}</div>
<emit-event ref={this.emitRef} onTestEvent={onEvent}></emit-event>
<emit-event ref={this.emitRef2} onTestEventCapture={onEvent}></emit-event>
<emit-event ref={this.emitRef3} on-test-event={onEvent}></emit-event>
<emit-event ref={this.emitRef4} on-test-event-capture={onEvent}></emit-event>
<button onClick={() => this.emitRef.current!.triggerEvent('testevent', 'onTestEvent ok')}>
onTestEvent
</button>
<button onClick={() => this.emitRef2.current!.triggerEvent('testevent', 'onTestEventCapture ok')}>
onTestEventCapture
</button>
<button onClick={() => this.emitRef3.current!.triggerEvent('test-event', 'test-event ok')}>
on-test-event
</button>
<button onClick={() => this.emitRef4.current!.triggerEvent('test-event', 'test-event-capture ok')}>
on-test-event-capture
</button>
</div>
);
}
}
7 changes: 5 additions & 2 deletions packages/examples/src/router.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export interface Route {
name: string;
name?: string;
component: () => Promise<any>;
}

Expand All @@ -12,8 +12,11 @@ const routes: { [name: string]: Route } = {
name: 'test-diff',
component: () => import('./renderer/test-diff'),
},
'/renderer/event': {
name: 'emit-event-test',
component: () => import('./renderer/event'),
},
'/renderer/jsx': {
name: 'test-jsx',
component: () => import('./renderer/jsx'),
},
'/lifecycycle': {
Expand Down
34 changes: 24 additions & 10 deletions packages/renderer/src/patch/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,33 @@ function handleOtherProps(element: HTMLElement, propName: string, value: any) {
}
}

function parseEventName(eventName: string) {
if (eventName.includes('-')) {
// on-lowerCamelCase-Capture or on-UpperCamelCase or on-kebab-case-capture
const parseEventName = eventName.split('-');
const useCapture = parseEventName[parseEventName.length - 1].toLowerCase() === 'capture';
return {
useCapture,
eventName: parseEventName
.slice(1, useCapture ? parseEventName.length - 1 : parseEventName.length)
.join('-'),
};
} else {
// onUpperCamelCase
const useCapture = eventName.endsWith('Capture');
const parseEventName = eventName.slice(2, useCapture ? eventName.length - 7 : eventName.length);
return {
useCapture,
eventName: parseEventName.toLowerCase(),
};
}
}

function patchEvents(element: HTMLElement, props: Props, oldProps: Props) {
// unmount old events
for (const key of Object.keys(oldProps)) {
if (isElementEventListener(key) && typeof oldProps[key] === 'function') {
const useCapture = key.endsWith('Capture');
let eventName = key.toLowerCase().slice(2);
if (useCapture) {
eventName = key.slice(0, -7).toLowerCase();
}
const { eventName, useCapture } = parseEventName(key);
element.removeEventListener(eventName, oldProps[key] as EventListener, useCapture);
}
}
Expand All @@ -73,11 +91,7 @@ function patchEvents(element: HTMLElement, props: Props, oldProps: Props) {
console.error(new Error(`${element.tagName} ${key} must be a function!`));
}

const useCapture = key.endsWith('Capture');
let eventName = key.toLowerCase().slice(2);
if (useCapture) {
eventName = key.slice(0, -7).toLowerCase();
}
const { eventName, useCapture } = parseEventName(key);
element.addEventListener(eventName, props[key] as EventListener, useCapture);
}
}
Expand Down
77 changes: 77 additions & 0 deletions tests/event.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { test, expect } from '@playwright/test';

test('button click triggers event and updates the text', async ({ page }) => {
// 1. 访问页面,假设页面已经包含了 EmitEventTest 组件
await page.goto('http://localhost:5173/#/renderer/event'); // 替换为你的实际 URL

// 2. 获取按钮和结果元素
const button1 = await page.locator('button:has-text("onTestEvent")');
const button2 = await page.locator('button:has-text("onTestEventCapture")');
const button3 = await page.locator('button:has-text("on-test-event")');
const button4 = await page.locator('button:has-text("on-test-event-capture")');
const result = await page.locator('.result');

// 3. 点击 "onTestEvent" 按钮,并验证文本更新
await button1.click();
await expect(result).toHaveText('onTestEvent ok'); // 确保文本更新为 "onTestEvent ok"

// 4. 点击 "onTestEventCapture" 按钮,并验证文本更新
await button2.click();
await expect(result).toHaveText('onTestEventCapture ok'); // 确保文本更新为 "onTestEventCapture ok"

// 5. 点击 "on-test-event" 按钮,并验证文本更新
await button3.click();
await expect(result).toHaveText('test-event ok'); // 确保文本更新为 "test-event ok"

// 6. 点击 "on-test-event-capture" 按钮,并验证文本更新
await button4.click();
await expect(result).toHaveText('test-event-capture ok'); // 确保文本更新为 "test-event-capture ok"
});

test('event listener should capture event in capture phase', async ({ page }) => {
// 1. 访问页面
await page.goto('http://localhost:3000'); // 替换为你的实际 URL

// 2. 获取按钮和结果元素
const button2 = await page.locator('button:has-text("onTestEventCapture")');
const result = await page.locator('.result');

// 3. 点击 "onTestEventCapture" 按钮,并验证文本更新
await button2.click();
await expect(result).toHaveText('onTestEventCapture ok'); // 验证在捕获阶段触发的事件更新文本
});

test('event listener should trigger the correct event', async ({ page }) => {
// 1. 访问页面
await page.goto('http://localhost:3000'); // 替换为你的实际 URL

// 2. 获取按钮和结果元素
const button1 = await page.locator('button:has-text("onTestEvent")');
const result = await page.locator('.result');

// 3. 点击按钮,触发事件,并验证文本更新
await button1.click();
await expect(result).toHaveText('onTestEvent ok'); // 验证文本更新为 "onTestEvent ok"
});

test('multiple events can be triggered and update text', async ({ page }) => {
// 1. 访问页面
await page.goto('http://localhost:3000'); // 替换为你的实际 URL

// 2. 获取按钮和结果元素
const button1 = await page.locator('button:has-text("onTestEvent")');
const button2 = await page.locator('button:has-text("onTestEventCapture")');
const button3 = await page.locator('button:has-text("on-test-event")');
const button4 = await page.locator('button:has-text("on-test-event-capture")');
const result = await page.locator('.result');

// 3. 顺序点击按钮,并验证文本更新
await button1.click();
await expect(result).toHaveText('onTestEvent ok'); // 第一个事件的文本
await button2.click();
await expect(result).toHaveText('onTestEventCapture ok'); // 第二个事件的文本
await button3.click();
await expect(result).toHaveText('test-event ok'); // 第三个事件的文本
await button4.click();
await expect(result).toHaveText('test-event-capture ok'); // 第四个事件的文本
});

0 comments on commit 984347d

Please sign in to comment.