1 | Composi
|
2 | =======
|
3 |
|
4 | Contents
|
5 | --------
|
6 | - [Components](./components.md)
|
7 | - [JSX](./jsx.md)
|
8 | - [Hyperx](./hyperx.md)
|
9 | - [Hyperscript](./hyperscript.md)
|
10 | - [Mount and Render](./render.md)
|
11 | - [State](./state.md)
|
12 | - [Lifecycle Methods](./lifecycle.md)
|
13 | - Events
|
14 | - [Styles](./styles.md)
|
15 | - [Unmount](./unmount.md)
|
16 | - [Installation](../README.md)
|
17 | - [Third Party Libraries](./third-party.md)
|
18 | - [Functional Components](./functional-components.md)
|
19 | - [Deployment](./deployment.md)
|
20 |
|
21 | You might have the best looking components on the planet, but if there wasn't a way for users to interact with them, they would be essentially useless. There are two ways to implement events:
|
22 |
|
23 | 1. inline events
|
24 | 2. handleEvent interface
|
25 |
|
26 | Inline events are the primary way developers implement events in React, Angular, Vue and other libraries and frameworks. The second way is to create a `handleEvent` object or method that gets passed to an `addEventListener` event on the component. Instead of passing the event listener a callback, you pass it an object with a method called `handleEvent`. This will get called when the event fires. You will usually use the `componentWasCreated` lifecycle method to set up the event listener.
|
27 |
|
28 | ### Which to Use
|
29 | Although inline events are in vogue with libraries and frameworks, they can lead to substantial memory usage when you use them on the items of a list of 1000s of items. This can lead to sluggish performance. With the `handleEvent` interface, you can implement event delegation, reducing memory usage, and there is no callback scope, avoiding the problem of memory leaks. This approach is also safer, reducing the number of attack points for script injection. Event removal is also dead simple.
|
30 |
|
31 | If you just want to get things done fast, use inline events. Otherwise use the `handleEvent` interface. We strongly recommend you give `handleEvent` a try. If components will be created and destroyed, use the `handleEvent` interface. It's safer.
|
32 |
|
33 | Inline Events
|
34 | -------------
|
35 |
|
36 | Inline events (DOM Level 0) have been around since 1996. They are the original event system for JavaScript. In versions of Internet Export upto 8, inline events were a source of serious memory leaks that could crash the browser. Composi's support for Internet Explorer starts at version 9, so this is not a concern. You can use inline events with a Component instance or when extending Component. How you do so in each case differs quite a bite. This is due to the way Component instantiation happens. When you create an instance of the Component class, you pass it an object literal of properties. That object literal will not have access to the Component instance, so no `this`. You'll need to define your callbacks and other custom properties separate from the component initialization.
|
37 |
|
38 | ```javascript
|
39 | import {h, Component} from 'composi'
|
40 |
|
41 | const increase = () => {
|
42 | counter.setState({disabled: false, number: counter.state.number + 1})
|
43 | }
|
44 |
|
45 | const decrease = () => {
|
46 | if (counter.state.number < 2) {
|
47 | counter.setState({disabled: true, number: counter.state.number - 1})
|
48 | } else {
|
49 | counter.setState({disabled: false, number: counter.state.number - 1})
|
50 | }
|
51 | }
|
52 | export const counter = new Component({
|
53 | container: '#counter',
|
54 | state: {disabled: false, number: 1},
|
55 | render: (data) => {
|
56 | const {disabled, number} = data
|
57 | return (
|
58 | <div class='counter' id={uuid()}>
|
59 | <button key='beezle' disabled={disabled} onclick={decrease} id="decrease">-</button>
|
60 | <span>{number}</span>
|
61 | <button onclick={increase} id="increase">+</button>
|
62 | </div>
|
63 | )
|
64 | }
|
65 | })
|
66 | ```
|
67 |
|
68 | Notice how in the above example, `increase` and `decrease` are separate functions. This may not be a big deal for you, or it may drive you up the wall.
|
69 |
|
70 | Inline Events on Extended Component
|
71 | -----------------------------------
|
72 | When we extend the Component class, we can avoid all the above issues of inline event callbacks. This gives us direct access to the component instance through the `this` keyword and results in code that is more readable and maintainable. To preserve the scope of the component, you do need to bind the inline event. Notice how we do this below:
|
73 |
|
74 | ```javascript
|
75 | import {h, Component} from 'composi'
|
76 |
|
77 | // Define Counter class by extending Component:
|
78 | export class Counter extends Component {
|
79 | constructor (opts) {
|
80 | super(opts)
|
81 | }
|
82 |
|
83 | render(data) {
|
84 | const {disabled, number} = data
|
85 | // Use bind on the inline events:
|
86 | return (
|
87 | <div class='counter' id={uuid()}>
|
88 | <button key='beezle' disabled={disabled} onclick={this.decrease.bind(this)} id="decrease">-</button>
|
89 | <span>{number}</span>
|
90 | <button onclick={this.increase.bind(this)} id="increase">+</button>
|
91 | </div>
|
92 | )
|
93 | }
|
94 |
|
95 | // Because these methods are part of the class,
|
96 | // we can accesss the component directly to set state:
|
97 | increase() {
|
98 | this.setState({disabled: false, number: this.state.number + 1})
|
99 | }
|
100 |
|
101 | decrease() {
|
102 | if (this.state.number < 2) {
|
103 | this.setState({disabled: true, number: this.state.number - 1})
|
104 | } else {
|
105 | this.setState({disabled: false, number: this.state.number - 1})
|
106 | }
|
107 | }
|
108 | }
|
109 | ```
|
110 |
|
111 | One way to avoid the necessity of binding the inline event is to move that to the constructor:
|
112 |
|
113 |
|
114 | ```javascript
|
115 | import {h, Component} from 'composi'
|
116 |
|
117 | // Define Counter class by extending Component:
|
118 | export class Counter extends Component {
|
119 | constructor (opts) {
|
120 | super(opts)
|
121 | // Bind events to "this":
|
122 | this.increase = this.increase.bind(this)
|
123 | this.decrease = this.decrease.bind(this)
|
124 | }
|
125 |
|
126 | render(data) {
|
127 | const {disabled, number} = data
|
128 | // Use bind on the inline events:
|
129 | return (
|
130 | <div class='counter' id={uuid()}>
|
131 | <button key='beezle' disabled={disabled} onclick={this.decrease} id="decrease">-</button>
|
132 | <span>{number}</span>
|
133 | <button onclick={this.increase} id="increase">+</button>
|
134 | </div>
|
135 | )
|
136 | }
|
137 |
|
138 | // Because these methods are part of the class,
|
139 | // we can accesss the component directly to set state:
|
140 | increase() {
|
141 | this.setState({disabled: false, number: this.state.number + 1})
|
142 | }
|
143 |
|
144 | decrease() {
|
145 | if (this.state.number < 2) {
|
146 | this.setState({disabled: true, number: this.state.number - 1})
|
147 | } else {
|
148 | this.setState({disabled: false, number: this.state.number - 1})
|
149 | }
|
150 | }
|
151 | }
|
152 | ```
|
153 |
|
154 | Arrow Functions for Inline Events
|
155 | ---------------------------------
|
156 | Another way to get around having to use `bind(this)` on your inline events by using arrows functions. To do this, the value of the inline event needs to be an arrow function that returns the component method. Refactoring the `render` method from above, we get this:
|
157 |
|
158 | ```javascript
|
159 | render(data) {
|
160 | const {disabled, number} = data
|
161 | // Use bind on the inline events:
|
162 | return (
|
163 | <div class='counter' id={uuid()}>
|
164 | <button key='beezle' disabled={disabled} onclick={() => this.decrease()} id="decrease">-</button>
|
165 | <span>{number}</span>
|
166 | <button onclick={() => this.increase()} id="increase">+</button>
|
167 | </div>
|
168 | )
|
169 | }
|
170 | ```
|
171 |
|
172 | Another option is to use the `handleEvent` interface, as explained next.
|
173 |
|
174 | Using handleEvent
|
175 | -----------------
|
176 | Perhaps the least used and understood method of handling events has been around since 2000. We're talking about `handldeEvent`. It's supported in browsers all the way back to IE6. There are two ways you can use the `handleEvent` interface: as an object or as a class method. The interface might appear a little peculiar at first. This is offset by the benefits it provides over other types of event registration. The biggest benefit of `handleEvent` is that it reduces memory usage and helps avoid memory leaks.
|
177 |
|
178 | ### Usage
|
179 | The `handleEvent` interface cannot be used with inline events. It can only be used with `addEventListener`. It will be the second argument instead of a callback. The `handleEvent` will be a function defined on that object. There are two ways to do this: as the method of an object literal or as a class method.
|
180 |
|
181 | handleEvent Object
|
182 | ------------------
|
183 | To use the `handleEvent` interface as an object, you just create an object literal that at minumum has `handleEvent` as a function:
|
184 |
|
185 | ```javascript
|
186 | const handler = {
|
187 | handleEvent: (e) => {
|
188 | // Do stuff here
|
189 | }
|
190 | }
|
191 | ```
|
192 |
|
193 | Let's take a look at how to use a `handleEvent` object with a Component instance.
|
194 |
|
195 | ### Component Instance
|
196 | Here is an example of a component instance using the `handleEvent` interface. In this case we define a separate object with properties, including the `handleEvent` method. Notice that the handler object has its own private state that we can access easily from the `handleEvent` method. To set up the event listener for `handleEvent` we use the `componentWasCreated` lifecycle method:
|
197 |
|
198 | ```javascript
|
199 | import {h, Component} from 'composi'
|
200 |
|
201 | const title = new Component({
|
202 | container: 'body',
|
203 | state: 'World',
|
204 | render: (message) => (
|
205 | <h1>Hello, {message}!</h1>
|
206 | ),
|
207 | // Bind event with handleEvent object after component is injected in DOM:
|
208 | componentWasCreated: () => {
|
209 | document.querySelector('h1').addEventListener('click', handler)
|
210 | }
|
211 | })
|
212 |
|
213 | // Define handleEvent object:
|
214 | const handler = {
|
215 | state: 0,
|
216 | handleEvent: function(e) {
|
217 | alert(e.target.textContent)
|
218 | // Increase this object's state:
|
219 | this.state++
|
220 | // Log the new state:
|
221 | console.log(this.state)
|
222 | }
|
223 | }
|
224 |
|
225 | // Render the title:
|
226 | const title.update()
|
227 | ```
|
228 |
|
229 | Notice that the `handleEvent` function in the `handler` object above has access to other object properties through the `this` keyword.
|
230 |
|
231 | ### Component Class Extension
|
232 | Using a `handleEvent` object when extending the Component class is the same. Like the previous example, we use the `componentWasCreated` lifecycle method to add an event listener and pass it a `handleEvent` object:
|
233 |
|
234 | ```javascript
|
235 | import {h, Component} from 'composi'
|
236 |
|
237 | class Title extends Component {
|
238 | constructor(props) {
|
239 | super(props)
|
240 | this.container = 'header'
|
241 | this.state = 'World'
|
242 | }
|
243 | render(message) {
|
244 | return (
|
245 | <h1>Hello, {message}!</h1>
|
246 | )
|
247 | }
|
248 |
|
249 | // Bind event with handleEvent object after component is injected in DOM:
|
250 | componentWasCreated() {
|
251 | this.element.addEventListener('click', handler)
|
252 | }
|
253 | }
|
254 |
|
255 | // Define handleEvent object:
|
256 | const handler = {
|
257 | state: 0,
|
258 | handleEvent: function(e) {
|
259 | alert(e.target.textContent)
|
260 | // Increase this object's state:
|
261 | this.state++
|
262 | // Log the new state:
|
263 | console.log(this.state)
|
264 | }
|
265 | }
|
266 | // Create new title component:
|
267 | const title = new Title()
|
268 |
|
269 | // Render the title:
|
270 | title.update()
|
271 | ```
|
272 |
|
273 | handleEvent Method in Components
|
274 | --------------------------------
|
275 | The other way to use the `handleEvent` interface is as a method of a class. When we extend the Component class, we can make `handleEvent` one of its methods. Because the `handleEvent` method is defined directly on the class, we pass the class itself directly to the event listener by means of the `this` keyword. This means that the `handleEvent` method will have access to all the properties and methods of the class through normal use of `this`. Notice how we can directly access the component's state property from within the `handleEvent` method. Because we are putting this directly on the component class, we this method `handleEvent`:
|
276 |
|
277 | ```javascript
|
278 | class Title extends Component {
|
279 | constructor(props) {
|
280 | super(props)
|
281 | this.container = 'header'
|
282 | this.state = 'World',
|
283 | }
|
284 | render(message) {
|
285 | return (
|
286 | <h1>Hello, {message}!</h1>
|
287 | )
|
288 | }
|
289 | handleEvent(e) {
|
290 | // Log the event target:
|
291 | console.log(e.target.nodeName)
|
292 | // Alert the state.
|
293 | // We can access it from the 'this' keyword:
|
294 | alert(this.state)
|
295 | }
|
296 |
|
297 | // Bind event with handleEvent object after component is injected in DOM:
|
298 | componentWasCreated() {
|
299 | this.element.addEventListener('click', this)
|
300 | }
|
301 | }
|
302 | // Create new title component:
|
303 | const title = new Title()
|
304 | ```
|
305 |
|
306 | When we pass the component to the `addEventListener` as `this`, the browser see that this is an object and looks for a method called `handleEvent`. That's why we need to name our method `handleEvent`.
|
307 |
|
308 | Event Delegation with handleEvent
|
309 | ---------------------------------
|
310 | In the previous examples, we only registered one event on an element. Inline events allow us to register events that are captured on multiple items. This is useful when you have a list of interactive items. We can implement event delegation with the `handleEvent` interface as well. It just requires a little extra code on our part.
|
311 |
|
312 | Remember that the only argument `handleEvent` receives is the event. From this we can check the event target to see what the user interacted with. Below is an example doing this:
|
313 |
|
314 | ```javascript
|
315 | import {h, Component} from 'composi'
|
316 |
|
317 | const fruits = [
|
318 | {
|
319 | key: 101,
|
320 | name: 'Apples'
|
321 | },
|
322 | {
|
323 | key: 102,
|
324 | name: 'Oranges'
|
325 | },
|
326 | {
|
327 | key: 103,
|
328 | name: 'Bananas'
|
329 | }
|
330 | ]
|
331 |
|
332 | class List extends Component {
|
333 | constructor(opts) {
|
334 | super(opts)
|
335 | this.state = fruits
|
336 | // key to use for adding new items:
|
337 | this.key = 1000
|
338 | }
|
339 | render() {
|
340 | let state = this.state
|
341 | return (
|
342 | <div>
|
343 | <p>
|
344 | <input id='nameInput' type='text' />
|
345 | <button id='addItem'>Add</button>
|
346 | </p>
|
347 | <ul id='fruitList' class='list'>
|
348 | {
|
349 | this.state.map(fruit => <li key={fruit.key}>{fruit.name}</li>)
|
350 | }
|
351 | </ul>
|
352 | </div>
|
353 | )
|
354 | }
|
355 | handleEvent(e) {
|
356 | // Handle button click:
|
357 | if (e.target.id === 'buttonAdd') {
|
358 | const nameInput = this.element.querySelector('#nameInput')
|
359 | const name = nameInput.value
|
360 | if (!name) {
|
361 | alert('Please provide a name!')
|
362 | return
|
363 | }
|
364 | this.setState({name, key: this.key++}, this.state.length)
|
365 | nameInput.value = ''
|
366 | nameInput.focus()
|
367 | // Handle list item click:
|
368 | } else if (e.target.nodeName === 'LI') {
|
369 | alert(e.target.textContent.trim())
|
370 | }
|
371 | }
|
372 | componentWasCreated() {
|
373 | this.element.addEventListener('click', this)
|
374 | }
|
375 | }
|
376 | const list = new List({
|
377 | container: 'section'
|
378 | })
|
379 | list.update()
|
380 | ```
|
381 |
|
382 | We could refactor the `handleEvent` method to make it a bit cleaner. We'll check the `e.target` value and use the `&&` operator to execute a function:
|
383 |
|
384 | ```javascript
|
385 | handleEvent(e) {
|
386 | // Define function for addItem:
|
387 | function addItem(e) {
|
388 | const nameInput = this.element.querySelector('#nameInput')
|
389 | const name = nameInput.value
|
390 | if (!name) {
|
391 | alert('Please provide a name!')
|
392 | return
|
393 | }
|
394 | this.setState({name, key: this.key++}, this.state.length)
|
395 | nameInput.value = ''
|
396 | nameInput.focus()
|
397 | }
|
398 | // Handle button click:
|
399 | e.target.id === 'buttonAdd' && addItem(e)
|
400 |
|
401 | // Handle list item click:
|
402 | e.target.nodeName === 'LI' && alert(e.target.textContent.trim())
|
403 | }
|
404 | ```
|
405 |
|
406 | As you can see in the above example, `handleEvent` allows us to implement events in a very efficient manner without any drawbacks. No callback hell with scope issues. If you have a lot of events of the same type on different elements, you can use a switch statement to simplify things. To make your guards simpler, you might resort to using classes on all interactive elements. We've redone the above example to show this approach:
|
407 |
|
408 | ```javascript
|
409 | import {h, Component} from 'composi'
|
410 |
|
411 | const fruits = [
|
412 | {
|
413 | key: 101,
|
414 | name: 'Apples'
|
415 | },
|
416 | {
|
417 | key: 102,
|
418 | name: 'Oranges'
|
419 | },
|
420 | {
|
421 | key: 103,
|
422 | name: 'Bananas'
|
423 | }
|
424 | ]
|
425 |
|
426 | class List extends Component {
|
427 | constructor(opts) {
|
428 | super(opts)
|
429 | this.state = fruits
|
430 | this.key = 1000
|
431 | }
|
432 | render() {
|
433 | let state = this.state
|
434 | return (
|
435 | <div>
|
436 | <p>
|
437 | <input id='nameInput' type='text' />
|
438 | <button class='addItem'>Add</button>
|
439 | </p>
|
440 | <ul id='fruitList' class='list'>
|
441 | {
|
442 | this.state.map(fruit => <li class='listItem' key={fruit.key}>{fruit.name}</li>)
|
443 | }
|
444 | </ul>
|
445 | </div>
|
446 | )
|
447 | }
|
448 | handleEvent(e) {
|
449 | switch(e.target.className) {
|
450 | // Handle button click:
|
451 | case 'addItem':
|
452 | const nameInput = this.element.querySelector('#nameInput')
|
453 | const name = nameInput.value
|
454 | if (!name) {
|
455 | alert('Please provide a name!')
|
456 | return
|
457 | }
|
458 | this.setState({name, key: this.key++}, this.state.length)
|
459 | nameInput.value = ''
|
460 | nameInput.focus()
|
461 | break
|
462 | // Handle list item clic:
|
463 | case 'listItem':
|
464 | alert(e.target.textContent.trim())
|
465 | break
|
466 | // Other checks here:
|
467 | }
|
468 | }
|
469 | componentWasCreated() {
|
470 | this.element.addEventListener('click', this)
|
471 | }
|
472 | }
|
473 | const list = new List({
|
474 | container: 'section'
|
475 | })
|
476 | list.update()
|
477 | ```
|
478 |
|
479 | Removing Event with handleEvent
|
480 | -------------------------------
|
481 |
|
482 | Event removal with `handleEvent` interface couldn't be simpler. Just use the event and `this`:
|
483 |
|
484 | ```javascript
|
485 | // Example of simple event target:
|
486 | class List extends Component {
|
487 | render(data) {
|
488 | return (
|
489 | <div>
|
490 | <p>
|
491 | <button id='remove-event'>Remove Event</button>
|
492 | </p>
|
493 | <ul>
|
494 | {
|
495 | data.map(item => <li>{item}</li>)
|
496 | }
|
497 | </ul>
|
498 | </div>
|
499 | )
|
500 | }
|
501 | // Handle click on list item and button.
|
502 | // Remove event by passing "this" with event.
|
503 | handleEvent(e) {
|
504 | if (e.target.nodeName === 'LI') {
|
505 | alert(e.target.textContent)
|
506 | } else if (e.target.id === 'remove-event') {
|
507 | this.element.removeEventListener('click', this)
|
508 | }
|
509 | }
|
510 |
|
511 | // Bind event with handleEvent object after component is injected in DOM:
|
512 | componentWasCreated() {
|
513 | // Add event listener to component base (div):
|
514 | this.element.addEventListener('click', this)
|
515 | }
|
516 | }
|
517 | ```
|
518 |
|
519 | Dynamically Changing handleEvent
|
520 | --------------------------------
|
521 | One thing you can easily do with `handleEvents` that you cnnot do with inline events or ordinary events listeners is change the code for events on the fly. If you've ever tried to do something like this in the past, you probably wound up with callbacks litered with conditional guards. When you use `handleEvent` to control how an event listener works, this becomes quite simple. It's just a matter of assigning a new value.
|
522 |
|
523 | For example, let's say you have a `handleEvent` method on a class and under certain circumstances you want to change how it functions. Instead of hanving the handler full of conditional checks, you can jsut toggle it for a completely different `handleEvent`. Let's take a look at how to do this. Below we have a simple list. To switch out the behavior of the `handleEvent` method, we'll give the class a new method: `newHandleEvent`. And when we want to switch from the current, default version, we'll just assign it to `oldHandleEvent`. It's that simple:
|
524 |
|
525 | ```javascript
|
526 | import {h, Component} from 'composi'
|
527 |
|
528 | export class List extends Component {
|
529 | constructor(opts) {
|
530 | super(opts)
|
531 | const beezle = this
|
532 | // Keep track of toggle handleEvent behavior:
|
533 | this.toggle = false
|
534 | this.render = (fruits) => (
|
535 | <div>
|
536 | <p>
|
537 | <br/>
|
538 | <br/>
|
539 | <button id='change-behavior'>Change List Behavior</button>
|
540 | </p>
|
541 | <ul class='list'>
|
542 | {
|
543 | fruits.map((fruit, idx) => <li data-foo={fruit.name} key={fruit.key}><h3>{idx +1}: {fruit.name}</h3></li>)
|
544 | }
|
545 | </ul>
|
546 | </div>
|
547 | )
|
548 | }
|
549 | handleEvent(e) {
|
550 | const li = e.target.nodeName === 'LI' ? e.target : e.target.closest('li')
|
551 | if (li && li.nodeName && li.nodeName === 'LI') {
|
552 | alert(li.dataset.foo)
|
553 | }
|
554 | if (e.target.id === 'change-behavior' && !this.toggle) {
|
555 | // Save original handleEvent so we can get it back on next toggle:
|
556 | this.oldHandleEvent = this.handleEvent
|
557 | // Use new handleEvent:
|
558 | this.handleEvent = this.newHandleEvent
|
559 | // Update toggle value for next check:
|
560 | this.toggle = !this.toggle
|
561 | }
|
562 |
|
563 | }
|
564 | // We'll use this one to switch with default one defined above.
|
565 | newHandleEvent(e) {
|
566 | const li = e.target.nodeName === 'LI' ? e.target : e.target.closest('li')
|
567 | if (li && li.nodeName && li.nodeName === 'LI') {
|
568 | console.log('This is the new behavior.')
|
569 | console.log(li.dataset.foo)
|
570 | }
|
571 | if (e.target.id === 'change-behavior' && this.toggle) {
|
572 | // Get back orginal handleEvent:
|
573 | this.handleEvent = this.oldHandleEvent
|
574 | // Update toggle value for next check:
|
575 | this.toggle = !this.toggle
|
576 | }
|
577 | }
|
578 | // Attach event to capture handleEvent:
|
579 | componentWasCreated() {
|
580 | this.element.addEventListener('click', this)
|
581 | }
|
582 | }
|
583 | ```
|
584 | Because this approach does not involve callbacks, there are no scope issues, DOM memory leaks, etc.
|
585 |
|
586 | Event Target Gotchas
|
587 | --------------------
|
588 |
|
589 | Regardless whether you are using inline events or the `handleEvent` interface, you need to be aware about what the event target could be. In the case of simple markup, there is little to worry about. Suppose you have a simple list of items:
|
590 |
|
591 | ```html
|
592 | <ul>
|
593 | <li>Apples</li>
|
594 | <li>Oranges</li>
|
595 | <li>Events</li>
|
596 | </ul>
|
597 | ```
|
598 |
|
599 | Assuming that an event listener is registered on the list, when the user clicks on a list item, the event target will be the list item. Clicking on the first item:
|
600 |
|
601 | ```javascript
|
602 | event.target // <li>Apples</li>
|
603 | ```
|
604 | However, if the item being interacted with has child elements, then the target may not be what you are expecting. Let's look at a more complex list:
|
605 |
|
606 | ```html
|
607 | <ul>
|
608 | <li>
|
609 | <h3>Name: Apples</h3>
|
610 | <h4>Quantity: 4</h4>
|
611 | </li>
|
612 | <li>
|
613 | <h3>Oranges</h3>
|
614 | <h4>Quantity: 6</h4>
|
615 | </li>
|
616 | <li>
|
617 | <h3>Bananas</h3>
|
618 | <h4>Quantity: 2</h4>
|
619 | </li>
|
620 | </ul>
|
621 | ```
|
622 |
|
623 | With an event listener registered on the list, when the user clicks, the event target might be the list item, or the H3 or the H4. In cases like this, you'll need to check what the event target is before using it.
|
624 |
|
625 | Here is an example of an event target that will always be predictable, in this case, the list item itself:
|
626 |
|
627 | ```javascript
|
628 | // Example of simple event target:
|
629 | class List extends Component {
|
630 | // Use arrow function in inline event:
|
631 | render(data) {
|
632 | return (
|
633 | <ul>
|
634 | {
|
635 | data.map(item => <li onclick={(e) => this.announceItem(e)}>{item}</li>})
|
636 | }
|
637 | </ul>
|
638 | )
|
639 | }
|
640 | announceItem(e) {
|
641 | // If user clicked directly on list item:
|
642 | e.target.nodeName === 'LI' && alert(e.target.textContent)
|
643 | }
|
644 | }
|
645 | ```
|
646 |
|
647 | Here is a list target that will not be predictable:
|
648 |
|
649 | ```javascript
|
650 | // Example of simple event target:
|
651 | class List extends Component {
|
652 | render(data) {
|
653 | // Use arrow function in inline event:
|
654 | return (
|
655 | <ul>
|
656 | {
|
657 | data.map(item => (
|
658 | <li onclick={(e) => this.announceItem(e)}>
|
659 | <h3>{item.name}</h3>
|
660 | <h4>{item.value}</h4>
|
661 | </li>))
|
662 | }
|
663 | </ul>
|
664 | )
|
665 | }
|
666 | announceItem(e) {
|
667 | // Here e.target might be the list item,
|
668 | // or the h3, or the h4:
|
669 | alert(e.target.textContent)
|
670 | }
|
671 | }
|
672 | ```
|
673 |
|
674 | To get around the uncertainty of what the event target might be, you'll need to use guards in the callback. In the example below we're using a ternary operator(condition ? result : alternativeResult) to do so:
|
675 |
|
676 | ```javascript
|
677 | // Example of simple event target:
|
678 | class List extends Component {
|
679 | render(data) => {
|
680 | return (
|
681 | <ul>
|
682 | {
|
683 | data.map(item => <li onclick={(e) => this.announceItem(e)}>
|
684 | <h3>{item.name}</h3>
|
685 | <h4>{item.value}</h4>
|
686 | </li>)
|
687 | }
|
688 | </ul>
|
689 | )
|
690 | }
|
691 | announceItem(e) {
|
692 | // Here e.target might be the list item,
|
693 | // or the h3, or the h4.
|
694 | // Therefore we need to test whether the target.nodeName
|
695 | // is "LI". If not, we get its parent node:
|
696 | const target = e.target.nodeName === 'LI' ? e.target : e.target.parentNode
|
697 | // Alert the complete list item content:
|
698 | alert(target.textContent)
|
699 | }
|
700 | }
|
701 | ```
|
702 |
|
703 | ### Element.closest
|
704 |
|
705 | In the above example the solution works and it's not hard to implement. However, you may have an interactive element with even more deeply nested children that could be event targets. In such a case, adding more parentNode tree climbing becomes unmanagable. To solve this you can use the `Element.closest` method. This is available in modern browsers. Composi includes a polyfill for older browsers, so you can use it now to handle these types of situations where you need event delegation. Here's the previous example redone with `closest`. No matter how complex the list item's children become, we'll always be able to capture the event on the list item itself:
|
706 |
|
707 | ```javascript
|
708 | // Example of simple event target:
|
709 | class List extends Component {
|
710 | render(data) {
|
711 | return (
|
712 | <ul>
|
713 | {
|
714 | data.map(item => (
|
715 | <li onclick={(e) => this.announceItem(e)}>
|
716 | <h3>
|
717 | <em>{item.name}</em>
|
718 | </h3>
|
719 | <h4>
|
720 | <span>{item.value}</span>
|
721 | </h4>
|
722 | </li>)
|
723 | )
|
724 | }
|
725 | </ul>
|
726 | )
|
727 | }
|
728 | announceItem(e) {
|
729 | // Use "closest" to test for list item:
|
730 | const target = e.target.nodeName === 'LI' ? e.target : e.target.closest('li')
|
731 | // Alert the complete list item content:
|
732 | alert(target.textContent)
|
733 | }
|
734 | }
|
735 | ```
|
736 |
|
737 | Do Not Mix!
|
738 | -----------
|
739 |
|
740 | It's not a good idea to mix inline events and `handleEvent` in the same component. If the inline event has the same target as the target used by `handleEvent` this can lead to weird situations where neither or both may execute. This can lead to situations that are very hard to troubleshoot. So, in a component choose the way you want to handle events and stick to it. |
\ | No newline at end of file |