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
38 changes: 38 additions & 0 deletions .changeset/feat-global-bind-support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
"@lynx-js/web-core": patch
---

Added support for the `global-bind` event handling modifier in the web platform runtime.

This mechanism enables seamless cross-element event communication without requiring a formal DOM tree relationship, allowing decoupled elements to observe and respond to standard events occurring anywhere within the component tree.

### Usage

Global bindings allow an observer element to react to events triggered on another target element.

#### 1. Define the Global Subscription

Attach `global-bindTap` (or any equivalent standard event alias) to your observer element:

```jsx
<view
id='observer'
global-bindTap={(event) => {
// This will trigger whenever 'tap' is caught by a globally bound event.
console.log('Global tap handled!', event);
}}
/>;
```

#### 2. Trigger the Event anywhere

The event will be triggered via normal user interaction (such as `tap`) on any other constituent elements:

```jsx
<view
id='target'
bindTap={(event) => {
// Note: To successfully propagate globally, ensure the event bubbles.
}}
/>;
```
3 changes: 3 additions & 0 deletions .changeset/red-dragons-tickle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
---

---
13 changes: 13 additions & 0 deletions packages/web-platform/web-core-e2e/tests/reactlynx.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,19 @@ test.describe('reactlynx3 tests', () => {
await wait(100);
await expect(await target.getAttribute('style')).toContain('pink');
});
test('basic-global-bind', async ({ page }, { title }) => {
await goto(page, title);
await wait(100);
const target = page.locator('#target');
const observer = page.locator('#observer');

await target.click();
await wait(100);
await expect(await observer.getAttribute('style')).toContain('green');
await target.click();
await wait(100);
await expect(await observer.getAttribute('style')).toContain('pink');
});
test('basic-bindtap-detail', async ({ page }, { title }) => {
await goto(page, title);
await wait(100);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright 2023 The Lynx Authors. All rights reserved.
// Licensed under the Apache License Version 2.0 that can be found in the
// LICENSE file in the root directory of this source tree.
import { useState, root } from '@lynx-js/react';

function App() {
const [color, setColor] = useState('pink');
return (
<view>
{
/*
This is the target element.
When clicking here, the 'tap' event bubbles and should trigger 'global-bindtap'.
*/
}
<view
id='target'
style={{
height: '100px',
width: '100px',
background: 'blue',
}}
/>
{
/*
This is the observer element observing 'global-bindtap'.
*/
}
<view
id='observer'
global-bindTap={() => setColor(color === 'pink' ? 'green' : 'pink')}
style={{
height: '100px',
width: '100px',
background: color,
}}
/>
</view>
);
}

root.render(<App />);
2 changes: 2 additions & 0 deletions packages/web-platform/web-core/binary/client/client.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export class MainThreadWasmContext {
common_event_handler(event: any, bubble_unique_id_path: Uint32Array, event_name: string, is_bubble: boolean): void;
create_element_common(parent_component_unique_id: number, dom: HTMLElement, css_id?: number | null, component_id?: string | null): number;
dispatch_event_by_path(bubble_unique_id_path: Uint32Array, event_name: string, is_capture: boolean, serialized_event: any): boolean;
dispatch_global_bind_event(bubble_unique_id_path: Uint32Array, event_name: string, serialized_event: any): void;
get_component_id(unique_id: number): string | undefined;
get_config(unique_id: number): object;
get_css_id_by_unique_id(unique_id: number): number | undefined;
Expand Down Expand Up @@ -202,6 +203,7 @@ export interface InitOutput {
readonly mainthreadwasmcontext_common_event_handler: (a: number, b: any, c: number, d: number, e: number, f: number, g: number) => void;
readonly mainthreadwasmcontext_create_element_common: (a: number, b: number, c: any, d: number, e: number, f: number) => number;
readonly mainthreadwasmcontext_dispatch_event_by_path: (a: number, b: number, c: number, d: number, e: number, f: number, g: any) => number;
readonly mainthreadwasmcontext_dispatch_global_bind_event: (a: number, b: number, c: number, d: number, e: number, f: any) => void;
readonly mainthreadwasmcontext_get_component_id: (a: number, b: number) => [number, number, number, number];
readonly mainthreadwasmcontext_get_config: (a: number, b: number) => [number, number, number];
readonly mainthreadwasmcontext_get_css_id_by_unique_id: (a: number, b: number) => number;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export const mainthreadwasmcontext_add_run_worklet_event: (a: number, b: number,
export const mainthreadwasmcontext_common_event_handler: (a: number, b: any, c: number, d: number, e: number, f: number, g: number) => void;
export const mainthreadwasmcontext_create_element_common: (a: number, b: number, c: any, d: number, e: number, f: number) => number;
export const mainthreadwasmcontext_dispatch_event_by_path: (a: number, b: number, c: number, d: number, e: number, f: number, g: any) => number;
export const mainthreadwasmcontext_dispatch_global_bind_event: (a: number, b: number, c: number, d: number, e: number, f: any) => void;
export const mainthreadwasmcontext_get_component_id: (a: number, b: number) => [number, number, number, number];
export const mainthreadwasmcontext_get_config: (a: number, b: number) => [number, number, number];
export const mainthreadwasmcontext_get_css_id_by_unique_id: (a: number, b: number) => number;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export class MainThreadWasmContext {
common_event_handler(event: any, bubble_unique_id_path: Uint32Array, event_name: string, is_bubble: boolean): void;
create_element_common(parent_component_unique_id: number, dom: HTMLElement, css_id?: number | null, component_id?: string | null): number;
dispatch_event_by_path(bubble_unique_id_path: Uint32Array, event_name: string, is_capture: boolean, serialized_event: any): boolean;
dispatch_global_bind_event(bubble_unique_id_path: Uint32Array, event_name: string, serialized_event: any): void;
get_component_id(unique_id: number): string | undefined;
get_config(unique_id: number): object;
get_css_id_by_unique_id(unique_id: number): number | undefined;
Expand Down Expand Up @@ -202,6 +203,7 @@ export interface InitOutput {
readonly mainthreadwasmcontext_common_event_handler: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => void;
readonly mainthreadwasmcontext_create_element_common: (a: number, b: number, c: number, d: number, e: number, f: number) => number;
readonly mainthreadwasmcontext_dispatch_event_by_path: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => number;
readonly mainthreadwasmcontext_dispatch_global_bind_event: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
readonly mainthreadwasmcontext_get_component_id: (a: number, b: number, c: number) => void;
readonly mainthreadwasmcontext_get_config: (a: number, b: number, c: number) => void;
readonly mainthreadwasmcontext_get_css_id_by_unique_id: (a: number, b: number) => number;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export const mainthreadwasmcontext_add_run_worklet_event: (a: number, b: number,
export const mainthreadwasmcontext_common_event_handler: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => void;
export const mainthreadwasmcontext_create_element_common: (a: number, b: number, c: number, d: number, e: number, f: number) => number;
export const mainthreadwasmcontext_dispatch_event_by_path: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => number;
export const mainthreadwasmcontext_dispatch_global_bind_event: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
export const mainthreadwasmcontext_get_component_id: (a: number, b: number, c: number) => void;
export const mainthreadwasmcontext_get_config: (a: number, b: number, c: number) => void;
export const mainthreadwasmcontext_get_css_id_by_unique_id: (a: number, b: number) => number;
Expand Down
8 changes: 8 additions & 0 deletions packages/web-platform/web-core/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,12 @@ lazy_static::lazy_static! {
"scrolltoupperedge",
"scrolltoloweredge"
]);

pub static ref EVENT_TYPES: Vec<&'static str> = vec![
"bindevent",
"capture-bind",
"catchevent",
"capture-catch",
"global-bindevent",
];
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ impl MainThreadWasmContext {
let event_type = event_type.to_ascii_lowercase();
self.enable_event(&event_name);

let has_handler = event_handler_identifier.is_some();

let is_allowlisted = constants::ELEMENT_REACTIVE_EVENTS.contains(event_name_str);
let mut should_enable = false;
let mut should_disable = false;
Expand All @@ -53,10 +55,15 @@ impl MainThreadWasmContext {

element_data.replace_framework_cross_thread_event_handler(
event_name.clone(),
event_type,
event_type.clone(),
event_handler_identifier,
);
}

if event_type == "global-bindevent" {
self.update_global_bind_events(unique_id, &event_name, has_handler);
}

if should_enable {
if let Some(element) = self.unique_id_to_dom_map.get(&unique_id) {
self
Expand Down Expand Up @@ -84,6 +91,8 @@ impl MainThreadWasmContext {
let event_type = event_type.to_ascii_lowercase();
self.enable_event(&event_name);

let has_handler = event_handler_identifier.is_some();

let is_allowlisted = constants::ELEMENT_REACTIVE_EVENTS.contains(event_name_str);
let mut should_enable = false;
let mut should_disable = false;
Expand All @@ -102,10 +111,15 @@ impl MainThreadWasmContext {

element_data.replace_framework_run_worklet_event_handler(
event_name.clone(),
event_type,
event_type.clone(),
event_handler_identifier,
);
}

if event_type == "global-bindevent" {
self.update_global_bind_events(unique_id, &event_name, has_handler);
}

if should_enable {
if let Some(element) = self.unique_id_to_dom_map.get(&unique_id) {
self
Expand All @@ -121,6 +135,29 @@ impl MainThreadWasmContext {
}
}

fn update_global_bind_events(&mut self, unique_id: usize, event_name: &str, has_handler: bool) {
if has_handler {
self
.global_bind_events
.entry(event_name.to_string())
.or_default()
.insert(unique_id);
} else if let Some(binding) = self.get_element_data_by_unique_id(unique_id) {
let element_data = binding.borrow();
if element_data
.get_framework_cross_thread_event_handler(event_name, "global-bindevent")
.is_none()
&& element_data
.get_framework_run_worklet_event_handler(event_name, "global-bindevent")
.is_none()
{
if let Some(ids) = self.global_bind_events.get_mut(event_name) {
ids.remove(&unique_id);
}
}
}
}

pub fn get_event(
&self,
unique_id: usize,
Expand All @@ -138,10 +175,9 @@ impl MainThreadWasmContext {

pub fn get_events(&self, unique_id: usize) -> Vec<EventInfo> {
let mut event_infos: Vec<EventInfo> = vec![];
let event_types = vec!["bindevent", "capture-bind", "catchevent", "capture-catch"];
let binding = self.get_element_data_by_unique_id(unique_id).unwrap();
let element_data = binding.borrow();
for event_type in event_types {
for event_type in constants::EVENT_TYPES.iter() {
for event_name in self.enabled_events.iter() {
if let Some(event_handlers) =
element_data.get_framework_cross_thread_event_handler(event_name, event_type)
Expand Down Expand Up @@ -289,6 +325,79 @@ impl MainThreadWasmContext {
self.dispatch_event_by_path(&[*target_id], event_name, false, &event);
}
}

if is_bubble {
self.dispatch_global_bind_event(&bubble_unique_id_path, event_name, &event);
}
}

pub fn dispatch_global_bind_event(
&self,
bubble_unique_id_path: &[usize],
event_name: &str,
serialized_event: &JsValue,
) {
let event_name_lowercase = event_name.to_ascii_lowercase();
let target_unique_id = bubble_unique_id_path.first().cloned().unwrap_or_default();

let target_element_dataset =
if let Some(binding) = self.get_element_data_by_unique_id(target_unique_id) {
binding.borrow().dataset.clone()
} else {
None
};

let global_bind_ids: Vec<usize> = self
.global_bind_events
.get(&event_name_lowercase)
.map(|ids| ids.iter().copied().collect())
.unwrap_or_default();

for unique_id in global_bind_ids {
let binding = match self.get_element_data_by_unique_id(unique_id) {
Some(b) => b,
None => continue,
};
let current_target_element_data = binding.borrow();

let bind_handler = current_target_element_data
.get_framework_cross_thread_event_handler(&event_name_lowercase, "global-bindevent");

if let Some(handler) = bind_handler {
let current_target_parent_component_id = {
let parent_component_unique_id = current_target_element_data.parent_component_unique_id;
if self.page_element_unique_id == Some(parent_component_unique_id) {
None
} else {
self
.get_element_data_by_unique_id(parent_component_unique_id)
.and_then(|binding| binding.borrow().component_id.clone())
}
};
self.mts_binding.publish_event(
&handler,
current_target_parent_component_id.as_deref(),
serialized_event,
target_unique_id,
&target_element_dataset.clone().into(),
unique_id,
&current_target_element_data.dataset.clone().into(),
);
}

let run_worklet_handler = current_target_element_data
.get_framework_run_worklet_event_handler(&event_name_lowercase, "global-bindevent");
if let Some(handler) = run_worklet_handler {
self.mts_binding.publish_mts_event(
&handler,
serialized_event,
target_unique_id,
&target_element_dataset.clone().into(),
unique_id,
&current_target_element_data.dataset.clone().into(),
);
}
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ pub struct MainThreadWasmContext {
pub(super) mts_binding: RustMainthreadContextBinding,
pub(super) config_enable_css_selector: bool,
pub(super) style_manager: StyleManager,
pub(super) global_bind_events: FnvHashMap<String, FnvHashSet<usize>>,
}

impl MainThreadWasmContext {
Expand Down Expand Up @@ -57,6 +58,7 @@ impl MainThreadWasmContext {
page_element_unique_id: None,
config_enable_css_selector,
style_manager,
global_bind_events: FnvHashMap::default(),
}
}

Expand Down
Loading
Loading