Skip to content

Commit 75b8fd0

Browse files
feat(base): Add initialization, DOM events wrapper to component API. (google#4915)
* Adds an `initialize` constructor hook that is called after the root element is attached, but before `getDefaultFoundation` is called. * Adds a simple way of adding / removing event listeners from a component's root node, without needing access to the root node itself. Needed for google#4475 [#126819221]
1 parent 8d12d74 commit 75b8fd0

File tree

3 files changed

+98
-1
lines changed

3 files changed

+98
-1
lines changed

packages/mdl-base/README.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,9 +156,12 @@ export class MyComponent extends MDLComponent {
156156

157157
| method | description |
158158
| --- | --- |
159+
| `initialize(...args)` | Called after the root element is attached to the component, but _before_ the foundation is instantiated. Any positional arguments passed to the component constructor after the root element, along with the optional foundation 2nd argument, will be provided to this method. This is a good place to do any setup work normally done within a constructor function. |
159160
| `getDefaultFoundation()` | Returns an instance of a foundation class properly configured for the component. Called when no foundation instance is given within the constructor. Subclasses **must** implement this method. |
160161
| `initialSyncWithDOM()` | Called within the constructor. Subclasses may override this method if they wish to perform initial synchronization of state with the host DOM element. For example, a slider may want to check if its host element contains a pre-set value, and adjust its internal state accordingly. Note that the same caveats apply to this method as to foundation class lifecycle methods. Defaults to a no-op. |
161162
| `destroy()` | Subclasses may override this method if they wish to perform any additional cleanup work when a component is destroyed. For example, a component may want to deregister a window resize listener. |
163+
| `listen(type: string, handler: EventListener)` | Adds an event listener to the component's root node for the given `type`. Note that this is simply a proxy to `this.root_.addEventListener`. |
164+
| `unlisten(type: string, handler: EventListener)` | Removes an event listener from the component's root node. Note that this is simply a proxy to `this.root_.removeEventListener`. |
162165
| `emit(type: string, data: Object)` | Dispatches a custom event of type `type` with detail `data` from the component's root node. This is the preferred way of dispatching events within our vanilla components. |
163166

164167
#### Static Methods
@@ -173,6 +176,41 @@ In addition to methods inherited, subclasses should implement the following two
173176

174177
`MDLComponent` calls its foundation's `init()` function within its _constructor_, and its foundation's `destroy()` function within its own _destroy()_ function. Therefore it's important to remember to _always call super() when overriding destroy()_. Not doing so can lead to leaked resources.
175178

179+
#### Initialization and constructor parameters
180+
181+
If you need to pass in additional parameters into a component's constructor, you can make use of the
182+
`initialize` method, as shown above. An example of this is passing in a child component as a
183+
dependency.
184+
185+
```js
186+
class MyComponent extends MDLComponent {
187+
initialize(childComponent = null) {
188+
this.child_ = childComponent ?
189+
childComponent : new ChildComponent(this.root_.querySelector('.child'));
190+
}
191+
192+
getDefaultFoundation() {
193+
return new MyComponentFoundation({
194+
doSomethingWithChildComponent: () => this.child_.doSomething(),
195+
// ...
196+
});
197+
}
198+
}
199+
```
200+
201+
You could call this code like so:
202+
203+
```js
204+
const childComponent = new ChildComponent(document.querySelector('.some-child'));
205+
const myComponent = new MyComponent(
206+
document.querySelector('.my-component'), /* foundation */ undefined, childComponent
207+
);
208+
// use myComponent
209+
```
210+
211+
> NOTE: You could also pass in an initialized foundation if you wish. The example above simply
212+
> showcases how you could pass in initialization arguments without instantiating a foundation.
213+
176214
#### Best Practice: Keep your adapters simple
177215

178216
If you find your adapters getting too complex, you should consider refactoring the complex parts out into their own implementations.

packages/mdl-base/component.js

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,20 @@ export default class MDLComponent {
2525
return new MDLComponent(root, new MDLFoundation());
2626
}
2727

28-
constructor(root, foundation) {
28+
constructor(root, foundation, ...args) {
2929
this.root_ = root;
30+
this.initialize(...args);
3031
this.foundation_ = foundation === undefined ? this.getDefaultFoundation() : foundation;
3132
this.foundation_.init();
3233
this.initialSyncWithDOM();
3334
}
3435

36+
initialize(/* ...args */) {
37+
// Subclasses can override this to do any additional setup work that would be considered part of a
38+
// "constructor". Essentially, it is a hook into the parent constructor before the foundation is
39+
// initialized. Any additional arguments besides root and foundation will be passed in here.
40+
}
41+
3542
getDefaultFoundation() {
3643
// Subclasses must override this method to return a properly configured foundation class for the
3744
// component.
@@ -52,6 +59,18 @@ export default class MDLComponent {
5259
this.foundation_.destroy();
5360
}
5461

62+
// Wrapper method to add an event listener to the component's root element. This is most useful when
63+
// listening for custom events.
64+
listen(evtType, handler) {
65+
this.root_.addEventListener(evtType, handler);
66+
}
67+
68+
// Wrapper method to remove an event listener to the component's root element. This is most useful when
69+
// unlistening for custom events.
70+
unlisten(evtType, handler) {
71+
this.root_.removeEventListener(evtType, handler);
72+
}
73+
5574
// Fires a cross-browser-compatible custom event from the component root of the given type,
5675
// with the given data.
5776
emit(evtType, evtData) {

test/unit/mdl-base/component.test.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515
*/
1616

1717
import test from 'tape';
18+
import domEvents from 'dom-events';
1819
import td from 'testdouble';
20+
1921
import {MDLComponent} from '../../../packages/mdl-base';
2022

2123
class FakeComponent extends MDLComponent {
@@ -34,6 +36,11 @@ class FakeComponent extends MDLComponent {
3436
});
3537
}
3638

39+
initialize(...args) {
40+
this.initializeArgs = args;
41+
this.initializeComesBeforeFoundation = !this.foundation_;
42+
}
43+
3744
initialSyncWithDOM() {
3845
this.synced = true;
3946
}
@@ -105,6 +112,39 @@ test("provides a default destroy() method which calls the foundation's destroy()
105112
t.end();
106113
});
107114

115+
test('#initialize is called within constructor and passed any additional positional component args', t => {
116+
const f = new FakeComponent(document.createElement('div'), /* foundation */ undefined, 'foo', 42);
117+
t.deepEqual(f.initializeArgs, ['foo', 42]);
118+
t.end();
119+
});
120+
121+
test('#initialize is called before getDefaultFoundation()', t => {
122+
const f = new FakeComponent(document.createElement('div'));
123+
t.true(f.initializeComesBeforeFoundation);
124+
t.end();
125+
});
126+
127+
test('#listen adds an event listener to the root element', t => {
128+
const root = document.createElement('div');
129+
const f = new FakeComponent(root);
130+
const handler = td.func('eventHandler');
131+
f.listen('FakeComponent:customEvent', handler);
132+
domEvents.emit(root, 'FakeComponent:customEvent');
133+
t.doesNotThrow(() => td.verify(handler(td.matchers.anything())));
134+
t.end();
135+
});
136+
137+
test('#unlisten removes an event listener from the root element', t => {
138+
const root = document.createElement('div');
139+
const f = new FakeComponent(root);
140+
const handler = td.func('eventHandler');
141+
root.addEventListener('FakeComponent:customEvent', handler);
142+
f.unlisten('FakeComponent:customEvent', handler);
143+
domEvents.emit(root, 'FakeComponent:customEvent');
144+
t.doesNotThrow(() => td.verify(handler(td.matchers.anything()), {times: 0}));
145+
t.end();
146+
});
147+
108148
test('#emit dispatches a custom event with the supplied data', t => {
109149
const root = document.createElement('div');
110150
const f = new FakeComponent(root);

0 commit comments

Comments
 (0)