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
15 changes: 11 additions & 4 deletions docs/_guide/actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,14 +160,21 @@ class HelloWorldElement extends HTMLElement {

### Binding dynamically added actions

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.

You can provide the element you'd like to observe as a first argument which will default to `document`.
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.

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

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

listenForBind(document)
@controller
class HelloWorldElement extends HTMLElement {
@target iframe: HTMLIFrameElement

connectedCallback() {
// listenForBind(this.ownerDocument) is automatically called.

listenForBind(this.iframe.document.body)
}
}
```
8 changes: 7 additions & 1 deletion src/bind.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ export function bind(controller: HTMLElement): void {
listenForBind(controller.shadowRoot)
}
bindElements(controller)
listenForBind(controller.ownerDocument)
}

const observers = new WeakMap<Node, Subscription>()
/**
* Set up observer that will make sure any actions that are dynamically
* injected into `el` will be bound to it's controller.
Expand All @@ -21,6 +23,7 @@ export function bind(controller: HTMLElement): void {
* stop further live updates.
*/
export function listenForBind(el: Node = document): Subscription {
if (observers.has(el)) return observers.get(el)!
let closed = false
const observer = new MutationObserver(mutations => {
for (const mutation of mutations) {
Expand All @@ -36,15 +39,18 @@ export function listenForBind(el: Node = document): Subscription {
}
})
observer.observe(el, {childList: true, subtree: true, attributes: true, attributeFilter: ['data-action']})
return {
const subscription = {
get closed() {
return closed
},
unsubscribe() {
closed = true
observers.delete(el)
observer.disconnect()
}
}
observers.set(el, subscription)
return subscription
}

interface Subscription {
Expand Down
25 changes: 25 additions & 0 deletions test/bind.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,30 @@ describe('bind', () => {
expect(instance.foo).to.have.been.called.exactly(2)
})

it('binds elements added to elements subtree', async () => {
const instance = document.createElement('bind-test-element')
chai.spy.on(instance, 'foo')
const el1 = document.createElement('div')
const el2 = document.createElement('div')
el1.setAttribute('data-action', 'click:bind-test-element#foo')
el2.setAttribute('data-action', 'submit:bind-test-element#foo')
document.body.appendChild(instance)

bind(instance)

instance.append(el1, el2)
// We need to wait for a couple of frames after injecting the HTML into to
// controller so that the actions have been bound to the controller.
await waitForNextAnimationFrame()
document.body.removeChild(instance)

expect(instance.foo).to.have.not.been.called()
el1.click()
expect(instance.foo).to.have.been.called.exactly(1)
el2.dispatchEvent(new CustomEvent('submit'))
expect(instance.foo).to.have.been.called.exactly(2)
})

it('can bind elements within the shadowDOM', () => {
const instance = document.createElement('bind-test-element')
chai.spy.on(instance, 'foo')
Expand Down Expand Up @@ -199,6 +223,7 @@ describe('bind', () => {
chai.spy.on(instance, 'foo')
root.appendChild(instance)
listenForBind(root).unsubscribe()
listenForBind(document).unsubscribe()
const button = document.createElement('button')
button.setAttribute('data-action', 'click:bind-test-element#foo')
instance.appendChild(button)
Expand Down