UNPKG

25.6 kBMarkdownView Raw
1Composi
2=======
3
4Contents
5--------
6- [Installation](../README.md)
7- [JSX](./jsx.md)
8- [Hyperx](./hyperx.md)
9- [Hyperscript](./hyperscript.md)
10- [Functional Components](./functional-components.md)
11- [Mount and Render](./render.md)
12- [Components](./components.md)
13- [State](./state.md)
14- [Lifecycle Methods](./lifecycle.md)
15- Events
16- [Styles](./styles.md)
17- [Unmount](./unmount.md)
18- [Third Party Libraries](./third-party.md)
19- [Deployment](./deployment.md)
20
21You 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
231. inline events
242. 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
29Although 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
31If 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
33Inline Events
34-------------
35
36Inline 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
39import {h, Component} from 'composi'
40
41const increase = () => {
42 counter.setState({disabled: false, number: counter.state.number + 1})
43}
44
45const 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}
52export 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
68Notice 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
70Inline Events on Extended Component
71-----------------------------------
72When 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
75import {h, Component} from 'composi'
76
77// Define Counter class by extending Component:
78export 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
111One way to avoid the necessity of binding the inline event is to move that to the constructor:
112
113
114```javascript
115import {h, Component} from 'composi'
116
117// Define Counter class by extending Component:
118export 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
154Arrow Functions for Inline Events
155---------------------------------
156Another 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
159render(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
172Another option is to use the `handleEvent` interface, as explained next.
173
174Using handleEvent
175-----------------
176Perhaps 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
179The `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
181handleEvent Object
182------------------
183To use the `handleEvent` interface as an object, you just create an object literal that at minumum has `handleEvent` as a function:
184
185```javascript
186const handler = {
187 handleEvent: (e) => {
188 // Do stuff here
189 }
190}
191```
192
193Let's take a look at how to use a `handleEvent` object with a Component instance.
194
195### Component Instance
196Here 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
199import {h, Component} from 'composi'
200
201const 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:
214const 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:
226const title.update()
227```
228
229Notice that the `handleEvent` function in the `handler` object above has access to other object properties through the `this` keyword.
230
231### Component Class Extension
232Using 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
235import {h, Component} from 'composi'
236
237class 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:
256const 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:
267const title = new Title()
268
269// Render the title:
270title.update()
271```
272
273handleEvent Method in Components
274--------------------------------
275The 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
278class 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:
303const title = new Title()
304```
305
306When 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
308Event Delegation with handleEvent
309---------------------------------
310In 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
312Remember 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
315import {h, Component} from 'composi'
316
317const 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
332class 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}
376const list = new List({
377 container: 'section'
378})
379list.update()
380```
381
382We 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
385handleEvent(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
406As 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
409import {h, Component} from 'composi'
410
411const 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
426class 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}
473const list = new List({
474 container: 'section'
475})
476list.update()
477```
478
479Removing Event with handleEvent
480-------------------------------
481
482Event removal with `handleEvent` interface couldn't be simpler. Just use the event and `this`:
483
484```javascript
485// Example of simple event target:
486class 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
519Dynamically Changing handleEvent
520--------------------------------
521One 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
523For 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
526import {h, Component} from 'composi'
527
528export 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```
584Because this approach does not involve callbacks, there are no scope issues, DOM memory leaks, etc.
585
586Event Target Gotchas
587--------------------
588
589Regardless 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
599Assuming 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
602event.target // <li>Apples</li>
603```
604However, 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
623With 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
625Here 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:
629class 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
647Here is a list target that will not be predictable:
648
649```javascript
650// Example of simple event target:
651class 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
674To 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:
678class 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
705In 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:
709class 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
737If you want to use `closest` but need to support IE, you can include this [polyfill](https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill).
738
739Do Not Mix!
740-----------
741
742It'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