1- const bound = new Set < string > ( )
1+ const controllers = new Set < string > ( )
2+
23/*
34 * Bind `[data-action]` elements from the DOM to their actions.
45 *
56 */
67export function bind ( controller : HTMLElement ) : void {
7- const tag = controller . tagName . toLowerCase ( )
8- bound . add ( tag )
9- const actionAttributeMatcher = `[data-action*=":${ tag } #"]`
10-
11- for ( const el of controller . querySelectorAll ( actionAttributeMatcher ) ) {
12- // Ignore nested elements
13- if ( el . closest ( tag ) !== controller ) continue
14- bindActionsToController ( controller , el )
15- }
16-
17- // Also bind the controller to itself
18- if ( controller . matches ( actionAttributeMatcher ) ) {
19- bindActionsToController ( controller , controller )
20- }
21- }
22-
23- // Match the pattern of `eventName:constructor#method`.
24- function * getActions ( el : Element ) : Generator < [ string , string , string ] > {
25- for ( const binding of ( el . getAttribute ( 'data-action' ) || '' ) . split ( ' ' ) ) {
26- const [ rest , methodName ] = binding . split ( '#' )
27- if ( ! methodName ) continue
28-
29- // eventName may contain `:` so account for that
30- // by splitting by the last instance of `:`
31- const colonIndex = rest . lastIndexOf ( ':' )
32- if ( colonIndex < 0 ) continue
33-
34- yield [ rest . slice ( 0 , colonIndex ) , rest . slice ( colonIndex + 1 ) , methodName ]
35- }
36- }
37-
38- function bindActionToController ( controller : HTMLElement , el : Element , methodName : string , eventName : string ) {
39- // Check the `method` is present on the prototype
40- const methodDescriptor =
41- Object . getOwnPropertyDescriptor ( controller , methodName ) ||
42- Object . getOwnPropertyDescriptor ( Object . getPrototypeOf ( controller ) , methodName )
43- if ( methodDescriptor && typeof methodDescriptor . value == 'function' ) {
44- el . addEventListener ( eventName , ( event : Event ) => {
45- methodDescriptor . value . call ( controller , event )
46- } )
47- }
48- }
49-
50- // Bind the data-action attribute of a single element to the controller
51- function bindActionsToController ( controller : HTMLElement , el : Element ) {
52- const tag = controller . tagName . toLowerCase ( )
53-
54- for ( const [ eventName , tagName , methodName ] of getActions ( el ) ) {
55- if ( tagName === tag ) {
56- bindActionToController ( controller , el , methodName , eventName )
57- }
58- }
59- }
60-
61- interface Subscription {
62- closed : boolean
63- unsubscribe ( ) : void
8+ controllers . add ( controller . tagName . toLowerCase ( ) )
9+ bindElements ( controller )
6410}
6511
6612/**
@@ -70,26 +16,22 @@ interface Subscription {
7016 * This returns a Subscription object which you can call `unsubscribe()` on to
7117 * stop further live updates.
7218 */
73- export function listenForBind ( el : Node = document , batchSize = 30 ) : Subscription {
19+ export function listenForBind ( el : Node = document ) : Subscription {
7420 let closed = false
75-
7621 const observer = new MutationObserver ( mutations => {
77- const queue = new Set < Element > ( )
7822 for ( const mutation of mutations ) {
79- if ( mutation . type === 'childList' && mutation . addedNodes . length ) {
23+ if ( mutation . type === 'attributes' && mutation . target instanceof Element ) {
24+ bindActions ( mutation . target )
25+ } else if ( mutation . type === 'childList' && mutation . addedNodes . length ) {
8026 for ( const node of mutation . addedNodes ) {
81- if ( ! ( node instanceof Element ) ) continue
82- if ( node . hasAttribute ( 'data-action' ) ) {
83- queue . add ( node )
27+ if ( node instanceof Element ) {
28+ bindElements ( node )
8429 }
8530 }
8631 }
8732 }
88- if ( queue . size ) requestAnimationFrame ( ( ) => processQueue ( queue , batchSize ) )
8933 } )
90-
91- observer . observe ( el , { childList : true , subtree : true } )
92-
34+ observer . observe ( el , { childList : true , subtree : true , attributes : true , attributeFilter : [ 'data-action' ] } )
9335 return {
9436 get closed ( ) {
9537 return closed
@@ -101,22 +43,48 @@ export function listenForBind(el: Node = document, batchSize = 30): Subscription
10143 }
10244}
10345
104- function processQueue ( queue : Set < Element > , batchSize : number ) {
105- let counter = batchSize
106- for ( const el of queue ) {
107- for ( const [ eventName , controllerTag , methodName ] of getActions ( el ) ) {
108- if ( ! bound . has ( controllerTag ) ) continue
109- const controller = el . closest ( controllerTag )
110- if ( ! ( controller instanceof HTMLElement ) ) continue
46+ interface Subscription {
47+ closed : boolean
48+ unsubscribe ( ) : void
49+ }
50+
51+ function bindElements ( root : Element ) {
52+ for ( const el of root . querySelectorAll ( '[data-action]' ) ) {
53+ bindActions ( el )
54+ }
55+ // Also bind the controller to itself
56+ if ( root . hasAttribute ( 'data-action' ) ) {
57+ bindActions ( root )
58+ }
59+ }
11160
112- bindActionToController ( controller , el , methodName , eventName )
61+ // Bind a single function to all events to avoid anonymous closure performance penalty.
62+ function handleEvent ( event : Event ) {
63+ const el = event . currentTarget as Element
64+ for ( const binding of bindings ( el ) ) {
65+ if ( event . type === binding . type && controllers . has ( binding . tag ) ) {
66+ const controller = el . closest ( binding . tag ) as Element & Record < string , ( ev : Event ) => unknown >
67+ if ( controller && typeof controller [ binding . method ] === 'function' ) {
68+ controller [ binding . method ] ( event )
69+ }
11370 }
114- queue . delete ( el )
71+ }
72+ }
11573
116- counter -= 1
117- if ( counter === 0 ) break
74+ type Binding = { type : string ; tag : string ; method : string }
75+ function * bindings ( el : Element ) : Iterable < Binding > {
76+ for ( const action of ( el . getAttribute ( 'data-action' ) || '' ) . split ( ' ' ) ) {
77+ const eventSep = action . lastIndexOf ( ':' )
78+ const methodSep = action . lastIndexOf ( '#' )
79+ const type = action . slice ( 0 , eventSep )
80+ const tag = action . slice ( eventSep + 1 , methodSep )
81+ const method = action . slice ( methodSep + 1 )
82+ yield { type, tag, method}
11883 }
119- if ( queue . size !== 0 ) {
120- requestAnimationFrame ( ( ) => processQueue ( queue , batchSize ) )
84+ }
85+
86+ function bindActions ( el : Element ) {
87+ for ( const binding of bindings ( el ) ) {
88+ el . addEventListener ( binding . type , handleEvent )
12189 }
12290}
0 commit comments