Skip to content

Commit

Permalink
Merge pull request #191 from SgLy/feat-event-listener-filter
Browse files Browse the repository at this point in the history
feat(core): improve event listener filter to extend its usage
  • Loading branch information
LastLeaf authored Aug 23, 2024
2 parents cffab3f + 9f01dd4 commit 263ade6
Show file tree
Hide file tree
Showing 5 changed files with 134 additions and 53 deletions.
4 changes: 4 additions & 0 deletions glass-easel-template-compiler/src/proc_gen/tag.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ impl Template {
write!(w, "R.setFnFilter(Q.A,Q.B)")?;
Ok(())
})?;
w.expr_stmt(|w| {
write!(w, "R.setEventListenerWrapper(Q.C)")?;
Ok(())
})?;
}
let mut writer = JsTopScopeWriter::new(String::new());
writer.align(w);
Expand Down
14 changes: 8 additions & 6 deletions glass-easel/src/tmpl/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import { type GeneralBehavior } from '../behavior'
import { type GeneralComponent } from '../component'
import { type DataChange, type DataValue } from '../data_proxy'
import { type ShadowedEvent } from '../event'
import { type ExternalShadowRoot } from '../external_shadow_tree'
import { type NormalizedComponentOptions } from '../global_options'
import { type ShadowRoot } from '../shadow_root'
Expand All @@ -17,7 +16,12 @@ import {
type UpdatePathTreeNode,
} from './proc_gen_wrapper'

export { type TmplDevArgs } from './proc_gen_wrapper'
export {
type TmplDevArgs,
type EventListenerWrapper,
type ChangePropListener,
type ChangePropFilter,
} from './proc_gen_wrapper'

const DEFAULT_PROC_GEN: ProcGen = () => ({
C: (isCreation, defineTextNode, defineElement, defineIfGroup, defineForLoop, defineSlot) => {
Expand All @@ -36,10 +40,11 @@ export type ComponentTemplate = {
content: (name: string) => ProcGen
updateMode?: string
fallbackListenerOnNativeNode?: boolean
eventObjectFilter?: (x: ShadowedEvent<unknown>) => ShadowedEvent<unknown>
procGenWrapperType?: typeof ProcGenWrapper
}

export { GeneralLvaluePathPrefix } from './proc_gen_wrapper'

const enum BindingMapUpdateEnabled {
Disabled,
Enabled,
Expand All @@ -62,7 +67,6 @@ class GlassEaselTemplate implements Template {
genObjectGroupEnv!: ProcGenEnv
updateMode!: string
fallbackListenerOnNativeNode!: boolean
eventObjectFilter?: (x: ShadowedEvent<unknown>) => ShadowedEvent<unknown>

constructor(behavior: GeneralBehavior) {
this.updateTemplate(behavior)
Expand All @@ -89,7 +93,6 @@ class GlassEaselTemplate implements Template {
}
this.updateMode = c.updateMode || ''
this.fallbackListenerOnNativeNode = c.fallbackListenerOnNativeNode || false
this.eventObjectFilter = c.eventObjectFilter
}

createInstance(
Expand Down Expand Up @@ -133,7 +136,6 @@ class GlassEaselTemplateInstance implements TemplateInstance {
this.shadowRoot,
procGen,
template.fallbackListenerOnNativeNode,
template.eventObjectFilter,
)
this.bindingMapGen = undefined
}
Expand Down
98 changes: 62 additions & 36 deletions glass-easel/src/tmpl/proc_gen_wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { Component, type GeneralComponent } from '../component'
import { type DataPath } from '../data_path'
import { type DataValue } from '../data_proxy'
import { Element, StyleSegmentIndex } from '../element'
import { type ShadowedEvent } from '../event'
import { type ShadowedEvent, type EventListener } from '../event'
import { safeCallback } from '../func_arr'
import { ENV } from '../global_options'
import { type NativeNode } from '../native_node'
import { type Node } from '../node'
Expand All @@ -20,26 +21,43 @@ export type UpdatePathTreeNode = true | { [key: string]: UpdatePathTreeNode } |

export type UpdatePathTreeRoot = UpdatePathTreeNode | undefined

type ChangePropListener = (
this: unknown,
newValue: unknown,
oldValue: unknown,
host: unknown,
elem: unknown,
export type ChangePropListener<T> = (
this: GeneralComponent,
newValue: T,
oldValue: T,
host: GeneralComponent,
elem: GeneralComponent,
) => void

export type ChangePropFilter = <T>(
listener: ChangePropListener<T>,
generalLvaluePath?: DataPath | null,
) => ChangePropListener<T>

export interface EventListenerWrapper {
<T>(
elem: GeneralComponent,
event: ShadowedEvent<T>,
listener: EventListener<T>,
generalLvaluePath?: DataPath | null,
): boolean | void
}

const emptyFilter = <T>(x: T) => x

const defaultEventListenerWrapper: EventListenerWrapper = (elem, event, listener) =>
listener.apply(elem, [event])

type TmplArgs = {
key?: number | string
keyList?: RangeListManager
dynEvListeners?: {
[name: string]: (ev: ShadowedEvent<unknown>) => boolean | undefined
[name: string]: EventListener<unknown>
}
dynamicSlotNameMatched?: boolean
changeProp?: {
[name: string]: {
listener: ChangePropListener
listener: ChangePropListener<unknown>
oldValue: unknown
}
}
Expand Down Expand Up @@ -153,22 +171,13 @@ export class ProcGenWrapper {
procGen: ProcGen
fallbackListenerOnNativeNode: boolean
bindingMapDisabled = false
eventObjectFilter: (x: ShadowedEvent<unknown>) => ShadowedEvent<unknown> = emptyFilter
changePropFilter = emptyFilter
eventListenerFilter = emptyFilter
changePropFilter: ChangePropFilter = emptyFilter
eventListenerWrapper: EventListenerWrapper = defaultEventListenerWrapper

constructor(
shadowRoot: ShadowRoot,
procGen: ProcGen,
fallbackListenerOnNativeNode: boolean,
eventObjectFilter?: (x: ShadowedEvent<unknown>) => ShadowedEvent<unknown>,
) {
constructor(shadowRoot: ShadowRoot, procGen: ProcGen, fallbackListenerOnNativeNode: boolean) {
this.shadowRoot = shadowRoot
this.procGen = procGen
this.fallbackListenerOnNativeNode = fallbackListenerOnNativeNode
if (eventObjectFilter) {
this.eventObjectFilter = eventObjectFilter
}
}

create(data: DataValue): { [field: string]: BindingMapGen[] } | undefined {
Expand Down Expand Up @@ -1002,22 +1011,22 @@ export class ProcGenWrapper {
mutated: boolean,
capture: boolean,
isDynamic: boolean,
_generalLvaluePath?: DataPath | null,
generalLvaluePath?: DataPath | null,
) {
const handler = typeof v === 'function' ? this.eventListenerFilter(v) : dataValueToString(v)
const listener = (ev: ShadowedEvent<unknown>) => {
const handler = typeof v === 'function' ? v : dataValueToString(v)
const listener: EventListener<unknown> = (ev) => {
const host = elem.ownerShadowRoot!.getHostNode()
let ret: boolean | undefined
const methodCaller = host.getMethodCaller() as { [key: string]: unknown }
const methodCaller = host.getMethodCaller()
const f = typeof handler === 'function' ? handler : Component.getMethod(host, handler)
if (typeof f === 'function') {
const filteredEv = this.eventObjectFilter(ev)
ret = (f as (ev: ShadowedEvent<unknown>) => boolean | undefined).call(
return this.eventListenerWrapper(
methodCaller,
filteredEv,
ev,
f as EventListener<unknown>,
generalLvaluePath,
)
}
return ret
return undefined
}
if (ENV.DEV) {
Object.defineProperty(listener, 'name', {
Expand Down Expand Up @@ -1072,7 +1081,13 @@ export class ProcGenWrapper {
if (oldValue !== v) {
lv.oldValue = v
const host = elem.ownerShadowRoot!.getHostNode()
lv.listener.call(host.getMethodCaller(), v, oldValue, host, elem)
safeCallback(
'Property Change Observer',
lv.listener,
host.getMethodCaller(),
[v, oldValue, host, elem],
elem,
)
}
}
} else if (elem.hasExternalClass(name)) {
Expand Down Expand Up @@ -1123,25 +1138,36 @@ export class ProcGenWrapper {
}

// add a change property binding
p(elem: Element, name: string, v: ChangePropListener, _generalLvaluePath?: DataPath | null) {
p(
elem: Element,
name: string,
v: ChangePropListener<unknown>,
generalLvaluePath?: DataPath | null,
) {
if (isComponent(elem)) {
if (Component.hasProperty(elem, name)) {
const tmplArgs = getTmplArgs(elem)
if (!tmplArgs.changeProp) {
tmplArgs.changeProp = Object.create(null) as typeof tmplArgs.changeProp
}
tmplArgs.changeProp![name] = {
listener: this.changePropFilter(v),
listener: this.changePropFilter(v, generalLvaluePath),
oldValue: (elem.data as { [k: string]: DataValue })[name],
}
}
}
}

// set filter functions for change properties and event listeners
setFnFilter(changePropFilter: <T>(v: T) => T, eventListenerFilter: <T>(v: T) => T) {
// set change properties filter
setFnFilter(changePropFilter: ChangePropFilter) {
this.changePropFilter = changePropFilter
this.eventListenerFilter = eventListenerFilter
}

// set event listener wrapper
setEventListenerWrapper(eventListenerWrapper?: EventListenerWrapper) {
if (typeof eventListenerWrapper === 'function') {
this.eventListenerWrapper = eventListenerWrapper
}
}

// get dev args object
Expand Down
17 changes: 15 additions & 2 deletions glass-easel/tests/base/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,24 @@ type TemplateOptions = {
fallbackListenerOnNativeNode?: boolean
}

export const tmpl = (src: string, options?: TemplateOptions) => {
type FilterFuncs = {
A?: glassEasel.template.ChangePropFilter
C?: glassEasel.template.EventListenerWrapper
}

export const tmpl = (src: string, options?: TemplateOptions, filterFuncs?: FilterFuncs) => {
const group = TmplGroup.newDev()
group.addTmpl('', src)
const genObjectSrc = `return ${group.getTmplGenObjectGroups()}`
let genObjectSrc = `return ${group.getTmplGenObjectGroups()}`
group.free()
if (filterFuncs !== undefined) {
const A = filterFuncs.A || ((x) => x)
const C = filterFuncs.C || 'undefined'
genObjectSrc = genObjectSrc.replace(
'var Q={A:(x)=>x,B:(x)=>x}',
`var Q={A:${A.toString()},C:${C.toString()}}`,
)
}
// console.info(genObjectSrc)
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const genObjectGroupList = new Function(genObjectSrc)() as { [key: string]: any }
Expand Down
54 changes: 45 additions & 9 deletions glass-easel/tests/tmpl/event.test.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,45 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import { tmpl, domBackend, composedBackend, shadowBackend } from '../base/env'
import * as glassEasel from '../../src'

const testCases = (testBackend: glassEasel.GeneralBackendContext) => {
test('event object', () => {
const template = Object.assign(
tmpl(`
<div id="a" bind:customEv="f"></div>
`),
test('event listener filter', () => {
const customHandler = (a: number, b: number) => a + b
const template = tmpl(
`
<wxs module="w">module.exports = { customHandler: ${customHandler.toString()} }</wxs>
<div
id="a"
bind:customEv="f"
bind:dropEvent="shouldBeDropped"
bind:customHandler="{{ w.customHandler }}"
></div>
`,
{},
{
eventObjectFilter: (x: glassEasel.ShadowedEvent<unknown>) => {
x.target = Object.assign(x.target, { id: 'b' })
return x
C: (elem, event, listener) => {
switch (event.getEventName()) {
case 'customEv': {
event.target = Object.assign(event.target, { id: 'b' })
return listener.apply(elem, [event])
}
case 'dropEvent': {
return undefined
}
case 'customHandler': {
const ret = (listener as any as typeof customHandler)(event.detail as number, 2)
expect(ret).toBe(3)
return undefined
}
default: {
return listener.apply(elem, [event])
}
}
},
},
)

let droppedEventCalled = false
const eventOrder: number[] = []
const def = glassEasel.registerElement({
template,
Expand All @@ -22,11 +48,21 @@ const testCases = (testBackend: glassEasel.GeneralBackendContext) => {
expect(ev.target.id).toBe('b')
eventOrder.push(1)
},
shouldBeDropped() {
droppedEventCalled = true
},
},
})
const elem = glassEasel.Component.createWithContext('root', def.general(), testBackend)
elem.getShadowRoot()!.getElementById('a')!.triggerEvent('customEv')
const div = elem.getShadowRoot()!.getElementById('a')!

div.triggerEvent('customEv')
expect(eventOrder).toStrictEqual([1])

div.triggerEvent('dropEvent')
expect(droppedEventCalled).toBe(false)

div.triggerEvent('customHandler', 1)
})

test('catch bindings', () => {
Expand Down

0 comments on commit 263ade6

Please sign in to comment.