Skip to content

Commit 126670b

Browse files
authored
Merge pull request #75 from github/auto-call-listenforbind
Auto call listenForBind
2 parents 7b1e56f + bc0d932 commit 126670b

3 files changed

Lines changed: 43 additions & 5 deletions

File tree

docs/_guide/actions.md

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -160,14 +160,21 @@ class HelloWorldElement extends HTMLElement {
160160

161161
### Binding dynamically added actions
162162

163-
Catalyst doesn't automatically bind actions to elements that are dynamically injected into the DOM. If you need to dynamically inject actions (for example you're injecting HTML via AJAX) you can call the `listenForBind` function to set up a observer that will bind actions when they are added to a controller.
164-
165-
You can provide the element you'd like to observe as a first argument which will default to `document`.
163+
Catalyst automatically listens for elements that are dynamically injected into the DOM, and will bind any element's `data-action` attributes. It does this by calling `listenForBind(controller.ownerDocument)`. If for some reason you need to observe other documents (such as mutations within an iframe), then you can call the `listenForBind` manually, passing a `Node` to listen to DOM mutations on.
166164

167165
Batch processing binds events in small batches to maintain UI stability (using `requestAnimationFrame` behind the scenes).
168166

169167
```js
170168
import {listenForBind} from '@github/catalyst'
171169

172-
listenForBind(document)
170+
@controller
171+
class HelloWorldElement extends HTMLElement {
172+
@target iframe: HTMLIFrameElement
173+
174+
connectedCallback() {
175+
// listenForBind(this.ownerDocument) is automatically called.
176+
177+
listenForBind(this.iframe.document.body)
178+
}
179+
}
173180
```

src/bind.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ export function bind(controller: HTMLElement): void {
1111
listenForBind(controller.shadowRoot)
1212
}
1313
bindElements(controller)
14+
listenForBind(controller.ownerDocument)
1415
}
1516

17+
const observers = new WeakMap<Node, Subscription>()
1618
/**
1719
* Set up observer that will make sure any actions that are dynamically
1820
* injected into `el` will be bound to it's controller.
@@ -21,6 +23,7 @@ export function bind(controller: HTMLElement): void {
2123
* stop further live updates.
2224
*/
2325
export function listenForBind(el: Node = document): Subscription {
26+
if (observers.has(el)) return observers.get(el)!
2427
let closed = false
2528
const observer = new MutationObserver(mutations => {
2629
for (const mutation of mutations) {
@@ -36,15 +39,18 @@ export function listenForBind(el: Node = document): Subscription {
3639
}
3740
})
3841
observer.observe(el, {childList: true, subtree: true, attributes: true, attributeFilter: ['data-action']})
39-
return {
42+
const subscription = {
4043
get closed() {
4144
return closed
4245
},
4346
unsubscribe() {
4447
closed = true
48+
observers.delete(el)
4549
observer.disconnect()
4650
}
4751
}
52+
observers.set(el, subscription)
53+
return subscription
4854
}
4955

5056
interface Subscription {

test/bind.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,30 @@ describe('bind', () => {
138138
expect(instance.foo).to.have.been.called.exactly(2)
139139
})
140140

141+
it('binds elements added to elements subtree', async () => {
142+
const instance = document.createElement('bind-test-element')
143+
chai.spy.on(instance, 'foo')
144+
const el1 = document.createElement('div')
145+
const el2 = document.createElement('div')
146+
el1.setAttribute('data-action', 'click:bind-test-element#foo')
147+
el2.setAttribute('data-action', 'submit:bind-test-element#foo')
148+
document.body.appendChild(instance)
149+
150+
bind(instance)
151+
152+
instance.append(el1, el2)
153+
// We need to wait for a couple of frames after injecting the HTML into to
154+
// controller so that the actions have been bound to the controller.
155+
await waitForNextAnimationFrame()
156+
document.body.removeChild(instance)
157+
158+
expect(instance.foo).to.have.not.been.called()
159+
el1.click()
160+
expect(instance.foo).to.have.been.called.exactly(1)
161+
el2.dispatchEvent(new CustomEvent('submit'))
162+
expect(instance.foo).to.have.been.called.exactly(2)
163+
})
164+
141165
it('can bind elements within the shadowDOM', () => {
142166
const instance = document.createElement('bind-test-element')
143167
chai.spy.on(instance, 'foo')
@@ -199,6 +223,7 @@ describe('bind', () => {
199223
chai.spy.on(instance, 'foo')
200224
root.appendChild(instance)
201225
listenForBind(root).unsubscribe()
226+
listenForBind(document).unsubscribe()
202227
const button = document.createElement('button')
203228
button.setAttribute('data-action', 'click:bind-test-element#foo')
204229
instance.appendChild(button)

0 commit comments

Comments
 (0)