-
-
Notifications
You must be signed in to change notification settings - Fork 333
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(addon/modifiers): adds
mutation-observer
modifier for reportin…
…g on DOM mutations.
- Loading branch information
1 parent
2fb155c
commit ef17137
Showing
3 changed files
with
174 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
import Modifier from 'ember-modifier'; | ||
import { assert } from '@ember/debug'; | ||
import { registerDestructor } from '@ember/destroyable'; | ||
|
||
/** | ||
* @modifier mutation-observer | ||
* | ||
* This Ember modifier uses the MutationObserver API to observe changes in the | ||
* DOM of a given element. It initializes a MutationObserver, attaches it to | ||
* the provided DOM element, and invokes a callback whenever a mutation is detected. | ||
* The modifier also automatically cleans up the observer when the element is destroyed. | ||
* | ||
* | ||
* @param {Element} element - The DOM element to observe. | ||
* @param {Function} callback - The callback function to be called when mutations are observed. | ||
* @param {Object} config - Configuration options for MutationObserver, such as `{ childList: true, subtree: true }`. | ||
* | ||
* This modifier allows you to specify the DOM element you want to observe, a callback | ||
* function that gets executed whenever a mutation occurs on that element, and a configuration | ||
* object that defines what types of mutations to observe. | ||
* | ||
* The `config` parameter should be a JSON object that matches the options for | ||
* `MutationObserver.observe`, such as `{ childList: true, attributes: true, subtree: true }`. | ||
* | ||
* @example | ||
* ```hbs | ||
* <div {{mutation-observer this.handleMutation config=(hash childList=true subtree=true)}}> | ||
* <!-- Content that might change --> | ||
* </div> | ||
* ``` | ||
* | ||
* @see https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver | ||
*/ | ||
export default class MutationObserverModifier extends Modifier { | ||
observer; | ||
|
||
constructor(owner, args) { | ||
super(owner, args); | ||
// Register cleanup logic to disconnect the observer when destroyed | ||
registerDestructor(this, cleanup); | ||
} | ||
|
||
modify(element, [callback], { config = { childList: true } }) { | ||
assert( | ||
'{{mutation-observer}} requires a callback as the first parameter', | ||
typeof callback === 'function' | ||
); | ||
|
||
this.observer = new MutationObserver(callback); | ||
this.observer.observe(element, config); | ||
} | ||
} | ||
|
||
function cleanup(instance) { | ||
instance.observer?.disconnect(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { default } from 'ember-paper/modifiers/mutation-observer'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
import { module, test } from 'qunit'; | ||
import { setupRenderingTest } from 'ember-qunit'; | ||
import { render, settled } from '@ember/test-helpers'; | ||
import { hbs } from 'ember-cli-htmlbars'; | ||
import { A } from '@ember/array'; | ||
|
||
function items() { | ||
return A(['ONE', 'TWO', 'THREE']); | ||
} | ||
|
||
module('Integration | Modifier | mutation-observer', function (hooks) { | ||
setupRenderingTest(hooks); | ||
|
||
test('it does not respond if DOM elements do not change', async function (assert) { | ||
assert.expect(1); | ||
|
||
const callMeMaybe = () => { | ||
assert.ok(false, 'No DOM change, callback should not be called'); | ||
}; | ||
this.set('callMeMaybe', callMeMaybe); | ||
|
||
await render(hbs` | ||
<div {{mutation-observer this.callMeMaybe}}></div> | ||
`); | ||
|
||
assert.ok(true); | ||
}); | ||
|
||
test('it responds by default when DOM elements are removed', async function (assert) { | ||
assert.expect(4); | ||
|
||
const callMeMaybe = () => { | ||
assert.ok(true, 'Callback has been fired due to DOM removal'); | ||
}; | ||
this.set('callMeMaybe', callMeMaybe); | ||
this.set('items', items()); | ||
|
||
await render(hbs` | ||
<div {{mutation-observer this.callMeMaybe}}> | ||
{{#each items as |item|}} | ||
<div class={{item}}>{{item}}</div> | ||
{{/each}} | ||
</div> | ||
`); | ||
|
||
this.items.removeAt(1); | ||
await settled(); | ||
|
||
assert.dom('.ONE').exists(); | ||
assert.dom('.TWO').doesNotExist(); | ||
assert.dom('.THREE').exists(); | ||
}); | ||
|
||
test('it responds by default when DOM elements are added', async function (assert) { | ||
assert.expect(9); | ||
|
||
const callMeMaybe = () => { | ||
assert.ok(true, 'Callback has been fired due to DOM addition'); | ||
}; | ||
this.set('callMeMaybe', callMeMaybe); | ||
this.set('items', items()); | ||
|
||
await render(hbs` | ||
<div {{mutation-observer this.callMeMaybe}}> | ||
{{#each this.items as |item|}} | ||
<div class={{item}}>{{item}}</div> | ||
{{/each}} | ||
</div> | ||
`); | ||
|
||
assert.dom('.ONE').exists(); | ||
assert.dom('.TWO').exists(); | ||
assert.dom('.THREE').exists(); | ||
assert.dom('.FOUR').doesNotExist(); | ||
|
||
this.items.addObject('FOUR'); | ||
await settled(); | ||
|
||
assert.dom('.ONE').exists(); | ||
assert.dom('.TWO').exists(); | ||
assert.dom('.THREE').exists(); | ||
assert.dom('.FOUR').exists(); | ||
}); | ||
|
||
test('it responds by default when DOM elements are reordered', async function (assert) { | ||
assert.expect(8); | ||
|
||
const callMeMaybe = () => { | ||
assert.ok(true, 'Callback has been fired due to DOM mutation'); | ||
}; | ||
this.set('callMeMaybe', callMeMaybe); | ||
this.set('items', items()); | ||
|
||
await render(hbs` | ||
<div id="outer" {{mutation-observer this.callMeMaybe}}> | ||
{{#each this.items as |item|}} | ||
<div class={{item}}>{{item}}</div> | ||
{{/each}} | ||
</div> | ||
`); | ||
|
||
assert.dom('.ONE').exists(); | ||
assert.dom('.TWO').exists(); | ||
assert.dom('.THREE').exists(); | ||
|
||
this.items.reverseObjects(); | ||
await settled(); | ||
|
||
assert.dom('.ONE').exists(); | ||
assert.dom('.TWO').exists(); | ||
assert.dom('.THREE').exists(); | ||
assert.equal( | ||
this.element.textContent.trim(), | ||
'THREE\n TWO\n ONE' | ||
); | ||
}); | ||
}); |