UNPKG

26.3 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, Render and Unmount](./render.md)
12- [Components](./components.md)
13- [State](./state.md)
14- [Lifecycle Methods](./lifecycle.md)
15- Events
16 - [Types of Events](#Types-of-Events)
17 - [Which to Use](#Which-to-Use)
18 - [Inline Events](#Inline-Events)
19 - [Inline Events on Extended Component](#Inline-Events-on-Extended-Component)
20 - [Arrow Functions for Inline Events](#Arrow-Functions-for-Inline-Events)
21 - [Using handleEvent](#Using-handleEvent)
22 - [handleEvent Object](#handleEvent-Object)
23 - [Component Instance](#Component-Instance)
24 - [handleEvent Method in Components](#handleEvent-Method-in-Components)
25 - [Event Delegation with handleEvent](#Event-Delegation-with-handleEvent)
26 - [Removing Event with handleEvent](#Removing-Event-with-handleEvent)
27 - [Dynamically Changing handleEvent](#Dynamically-Changing-handleEvent)
28 - [Event Target Gotchas](#gotchas)
29 - [Using Element.closest for Event Target](#Using-Element.closest-for-Event-Target)
30 - [Do Not Mix!](#Do-Not-Mix!)
31- [Styles](./styles.md)
32- [Unmount](./unmount.md)
33- [State Management with DataStore](./data-store.md)
34- [Third Party Libraries](./third-party.md)
35- [Deployment](./deployment.md)
36- [Differrences with React](./composi-react.md)
37
38You 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:
39
40## Types of Events
41
421. inline events
432. handleEvent interface
44
45 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 `componentDidMount` lifecycle method to set up the event listener.
46
47## Which to Use
48Although 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.
49
50If 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.
51
52## Inline Events
53
54Inline 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.
55
56```javascript
57import {h, Component} from 'composi'
58
59const increase = () => {
60 counter.setState({disabled: false, number: counter.state.number + 1})
61}
62
63const decrease = () => {
64 if (counter.state.number < 2) {
65 counter.setState({disabled: true, number: counter.state.number - 1})
66 } else {
67 counter.setState({disabled: false, number: counter.state.number - 1})
68 }
69}
70export const counter = new Component({
71 container: '#counter',
72 state: {disabled: false, number: 1},
73 render: (data) => {
74 const {disabled, number} = data
75 return (
76 <div class='counter' id={uuid()}>
77 <button key='beezle' disabled={disabled} onclick={decrease} id="decrease">-</button>
78 <span>{number}</span>
79 <button onclick={increase} id="increase">+</button>
80 </div>
81 )
82 }
83})
84```
85
86Notice 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.
87
88## Inline Events on Extended Component
89
90When 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:
91
92```javascript
93import {h, Component} from 'composi'
94
95// Define Counter class by extending Component:
96export class Counter extends Component {
97 constructor (opts) {
98 super(opts)
99 }
100
101 render(data) {
102 const {disabled, number} = data
103 // Use bind on the inline events:
104 return (
105 <div class='counter' id={uuid()}>
106 <button key='beezle' disabled={disabled} onclick={this.decrease.bind(this)} id="decrease">-</button>
107 <span>{number}</span>
108 <button onclick={this.increase.bind(this)} id="increase">+</button>
109 </div>
110 )
111 }
112
113 // Because these methods are part of the class,
114 // we can accesss the component directly to set state:
115 increase() {
116 this.setState({disabled: false, number: this.state.number + 1})
117 }
118
119 decrease() {
120 if (this.state.number < 2) {
121 this.setState({disabled: true, number: this.state.number - 1})
122 } else {
123 this.setState({disabled: false, number: this.state.number - 1})
124 }
125 }
126}
127```
128
129One way to avoid the necessity of binding the inline event is to move that to the constructor:
130
131
132```javascript
133import {h, Component} from 'composi'
134
135// Define Counter class by extending Component:
136export class Counter extends Component {
137 constructor (opts) {
138 super(opts)
139 // Bind events to "this":
140 this.increase = this.increase.bind(this)
141 this.decrease = this.decrease.bind(this)
142 }
143
144 render(data) {
145 const {disabled, number} = data
146 // Use bind on the inline events:
147 return (
148 <div class='counter' id={uuid()}>
149 <button key='beezle' disabled={disabled} onclick={this.decrease} id="decrease">-</button>
150 <span>{number}</span>
151 <button onclick={this.increase} id="increase">+</button>
152 </div>
153 )
154 }
155
156 // Because these methods are part of the class,
157 // we can accesss the component directly to set state:
158 increase() {
159 this.setState({disabled: false, number: this.state.number + 1})
160 }
161
162 decrease() {
163 if (this.state.number < 2) {
164 this.setState({disabled: true, number: this.state.number - 1})
165 } else {
166 this.setState({disabled: false, number: this.state.number - 1})
167 }
168 }
169}
170```
171
172## Arrow Functions for Inline Events
173
174Another 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:
175
176```javascript
177render(data) {
178 const {disabled, number} = data
179 // Use bind on the inline events:
180 return (
181 <div class='counter' id={uuid()}>
182 <button key='beezle' disabled={disabled} onclick={() => this.decrease()} id="decrease">-</button>
183 <span>{number}</span>
184 <button onclick={() => this.increase()} id="increase">+</button>
185 </div>
186 )
187}
188```
189
190Another option is to use the `handleEvent` interface, as explained next.
191
192## Using handleEvent
193
194Perhaps 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.
195
196### Usage
197The `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.
198
199## handleEvent Object
200
201To use the `handleEvent` interface as an object, you just create an object literal that at minumum has `handleEvent` as a function:
202
203```javascript
204const handler = {
205 handleEvent: (e) => {
206 // Do stuff here
207 }
208}
209```
210
211Let's take a look at how to use a `handleEvent` object with a Component instance.
212
213## Component Instance
214
215Here 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 `componentDidMount` lifecycle method:
216
217```javascript
218import {h, Component} from 'composi'
219
220const title = new Component({
221 container: 'body',
222 state: 'World',
223 render: (message) => (
224 <h1>Hello, {message}!</h1>
225 ),
226 // Bind event with handleEvent object after component is injected in DOM:
227 componentDidMount: () => {
228 document.querySelector('h1').addEventListener('click', handler)
229 }
230})
231
232// Define handleEvent object:
233const handler = {
234 state: 0,
235 handleEvent: function(e) {
236 alert(e.target.textContent)
237 // Increase this object's state:
238 this.state++
239 // Log the new state:
240 console.log(this.state)
241 }
242}
243
244// Render the title:
245const title.update()
246```
247
248Notice that the `handleEvent` function in the `handler` object above has access to other object properties through the `this` keyword.
249
250### Component Class Extension
251Using a `handleEvent` object when extending the Component class is the same. Like the previous example, we use the `componentDidMount` lifecycle method to add an event listener and pass it a `handleEvent` object:
252
253```javascript
254import {h, Component} from 'composi'
255
256class Title extends Component {
257 constructor(props) {
258 super(props)
259 this.container = 'header'
260 this.state = 'World'
261 }
262 render(message) {
263 return (
264 <h1>Hello, {message}!</h1>
265 )
266 }
267
268 // Bind event with handleEvent object after component is injected in DOM:
269 componentDidMount() {
270 this.element.addEventListener('click', handler)
271 }
272}
273
274// Define handleEvent object:
275const handler = {
276 state: 0,
277 handleEvent: function(e) {
278 alert(e.target.textContent)
279 // Increase this object's state:
280 this.state++
281 // Log the new state:
282 console.log(this.state)
283 }
284}
285// Create new title component:
286const title = new Title()
287
288// Render the title:
289title.update()
290```
291
292## handleEvent Method in Components
293
294The 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`:
295
296```javascript
297class Title extends Component {
298 constructor(props) {
299 super(props)
300 this.container = 'header'
301 this.state = 'World',
302 }
303 render(message) {
304 return (
305 <h1>Hello, {message}!</h1>
306 )
307 }
308 handleEvent(e) {
309 // Log the event target:
310 console.log(e.target.nodeName)
311 // Alert the state.
312 // We can access it from the 'this' keyword:
313 alert(this.state)
314 }
315
316 // Bind event with handleEvent object after component is injected in DOM:
317 componentDidMount() {
318 this.element.addEventListener('click', this)
319 }
320}
321// Create new title component:
322const title = new Title()
323```
324
325When 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`.
326
327## Event Delegation with handleEvent
328
329In 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.
330
331Remember 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:
332
333```javascript
334import {h, Component} from 'composi'
335
336const fruits = [
337 {
338 key: 101,
339 name: 'Apples'
340 },
341 {
342 key: 102,
343 name: 'Oranges'
344 },
345 {
346 key: 103,
347 name: 'Bananas'
348 }
349]
350
351class List extends Component {
352 constructor(opts) {
353 super(opts)
354 this.state = fruits
355 // key to use for adding new items:
356 this.key = 1000
357 }
358 render() {
359 let state = this.state
360 return (
361 <div>
362 <p>
363 <input id='nameInput' type='text' />
364 <button id='addItem'>Add</button>
365 </p>
366 <ul id='fruitList' class='list'>
367 {
368 this.state.map(fruit => <li key={fruit.key}>{fruit.name}</li>)
369 }
370 </ul>
371 </div>
372 )
373 }
374 handleEvent(e) {
375 // Handle button click:
376 if (e.target.id === 'buttonAdd') {
377 const nameInput = this.element.querySelector('#nameInput')
378 const name = nameInput.value
379 if (!name) {
380 alert('Please provide a name!')
381 return
382 }
383 this.setState({name, key: this.key++}, this.state.length)
384 nameInput.value = ''
385 nameInput.focus()
386 // Handle list item click:
387 } else if (e.target.nodeName === 'LI') {
388 alert(e.target.textContent.trim())
389 }
390 }
391 componentDidMount() {
392 this.element.addEventListener('click', this)
393 }
394}
395const list = new List({
396 container: 'section'
397})
398list.update()
399```
400
401We 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:
402
403```javascript
404handleEvent(e) {
405 // Define function for addItem:
406 function addItem(e) {
407 const nameInput = this.element.querySelector('#nameInput')
408 const name = nameInput.value
409 if (!name) {
410 alert('Please provide a name!')
411 return
412 }
413 this.setState({name, key: this.key++}, this.state.length)
414 nameInput.value = ''
415 nameInput.focus()
416 }
417 // Handle button click:
418 e.target.id === 'buttonAdd' && addItem(e)
419
420 // Handle list item click:
421 e.target.nodeName === 'LI' && alert(e.target.textContent.trim())
422}
423```
424
425As 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:
426
427```javascript
428import {h, Component} from 'composi'
429
430const fruits = [
431 {
432 key: 101,
433 name: 'Apples'
434 },
435 {
436 key: 102,
437 name: 'Oranges'
438 },
439 {
440 key: 103,
441 name: 'Bananas'
442 }
443]
444
445class List extends Component {
446 constructor(opts) {
447 super(opts)
448 this.state = fruits
449 this.key = 1000
450 }
451 render() {
452 let state = this.state
453 return (
454 <div>
455 <p>
456 <input id='nameInput' type='text' />
457 <button class='addItem'>Add</button>
458 </p>
459 <ul id='fruitList' class='list'>
460 {
461 this.state.map(fruit => <li class='listItem' key={fruit.key}>{fruit.name}</li>)
462 }
463 </ul>
464 </div>
465 )
466 }
467 handleEvent(e) {
468 switch(e.target.className) {
469 // Handle button click:
470 case 'addItem':
471 const nameInput = this.element.querySelector('#nameInput')
472 const name = nameInput.value
473 if (!name) {
474 alert('Please provide a name!')
475 return
476 }
477 this.setState({name, key: this.key++}, this.state.length)
478 nameInput.value = ''
479 nameInput.focus()
480 break
481 // Handle list item clic:
482 case 'listItem':
483 alert(e.target.textContent.trim())
484 break
485 // Other checks here:
486 }
487 }
488 componentDidMount() {
489 this.element.addEventListener('click', this)
490 }
491}
492const list = new List({
493 container: 'section'
494})
495list.update()
496```
497
498## Removing Event with handleEvent
499
500
501Event removal with `handleEvent` interface couldn't be simpler. Just use the event and `this`:
502
503```javascript
504// Example of simple event target:
505class List extends Component {
506 render(data) {
507 return (
508 <div>
509 <p>
510 <button id='remove-event'>Remove Event</button>
511 </p>
512 <ul>
513 {
514 data.map(item => <li>{item}</li>)
515 }
516 </ul>
517 </div>
518 )
519 }
520 // Handle click on list item and button.
521 // Remove event by passing "this" with event.
522 handleEvent(e) {
523 if (e.target.nodeName === 'LI') {
524 alert(e.target.textContent)
525 } else if (e.target.id === 'remove-event') {
526 this.element.removeEventListener('click', this)
527 }
528 }
529
530 // Bind event with handleEvent object after component is injected in DOM:
531 componentDidMount() {
532 // Add event listener to component base (div):
533 this.element.addEventListener('click', this)
534 }
535}
536```
537
538## Dynamically Changing handleEvent
539
540One 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.
541
542For 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:
543
544```javascript
545import {h, Component} from 'composi'
546
547export class List extends Component {
548 constructor(opts) {
549 super(opts)
550 const beezle = this
551 // Keep track of toggle handleEvent behavior:
552 this.toggle = false
553 this.render = (fruits) => (
554 <div>
555 <p>
556 <br/>
557 <br/>
558 <button id='change-behavior'>Change List Behavior</button>
559 </p>
560 <ul class='list'>
561 {
562 fruits.map((fruit, idx) => <li data-foo={fruit.name} key={fruit.key}><h3>{idx +1}: {fruit.name}</h3></li>)
563 }
564 </ul>
565 </div>
566 )
567 }
568 handleEvent(e) {
569 const li = e.target.nodeName === 'LI' ? e.target : e.target.closest('li')
570 if (li && li.nodeName && li.nodeName === 'LI') {
571 alert(li.dataset.foo)
572 }
573 if (e.target.id === 'change-behavior' && !this.toggle) {
574 // Save original handleEvent so we can get it back on next toggle:
575 this.oldHandleEvent = this.handleEvent
576 // Use new handleEvent:
577 this.handleEvent = this.newHandleEvent
578 // Update toggle value for next check:
579 this.toggle = !this.toggle
580 }
581
582 }
583 // We'll use this one to switch with default one defined above.
584 newHandleEvent(e) {
585 const li = e.target.nodeName === 'LI' ? e.target : e.target.closest('li')
586 if (li && li.nodeName && li.nodeName === 'LI') {
587 console.log('This is the new behavior.')
588 console.log(li.dataset.foo)
589 }
590 if (e.target.id === 'change-behavior' && this.toggle) {
591 // Get back orginal handleEvent:
592 this.handleEvent = this.oldHandleEvent
593 // Update toggle value for next check:
594 this.toggle = !this.toggle
595 }
596 }
597 // Attach event to capture handleEvent:
598 componentDidMount() {
599 this.element.addEventListener('click', this)
600 }
601}
602```
603Because this approach does not involve callbacks, there are no scope issues, DOM memory leaks, etc.
604
605<a id='gotchas'></a>
606## Event Target Gotchas
607
608Regardless 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:
609
610```html
611<ul>
612 <li>Apples</li>
613 <li>Oranges</li>
614 <li>Events</li>
615</ul>
616```
617
618Assuming 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:
619
620```javascript
621event.target // <li>Apples</li>
622```
623However, 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:
624
625```html
626<ul>
627 <li>
628 <h3>Name: Apples</h3>
629 <h4>Quantity: 4</h4>
630 </li>
631 <li>
632 <h3>Oranges</h3>
633 <h4>Quantity: 6</h4>
634 </li>
635 <li>
636 <h3>Bananas</h3>
637 <h4>Quantity: 2</h4>
638 </li>
639</ul>
640```
641
642With 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.
643
644Here is an example of an event target that will always be predictable, in this case, the list item itself:
645
646```javascript
647// Example of simple event target:
648class List extends Component {
649 // Use arrow function in inline event:
650 render(data) {
651 return (
652 <ul>
653 {
654 data.map(item => <li onclick={(e) => this.announceItem(e)}>{item}</li>})
655 }
656 </ul>
657 )
658 }
659 announceItem(e) {
660 // If user clicked directly on list item:
661 e.target.nodeName === 'LI' && alert(e.target.textContent)
662 }
663}
664```
665
666Here is a list target that will not be predictable:
667
668```javascript
669// Example of simple event target:
670class List extends Component {
671 render(data) {
672 // Use arrow function in inline event:
673 return (
674 <ul>
675 {
676 data.map(item => (
677 <li onclick={(e) => this.announceItem(e)}>
678 <h3>{item.name}</h3>
679 <h4>{item.value}</h4>
680 </li>))
681 }
682 </ul>
683 )
684 }
685 announceItem(e) {
686 // Here e.target might be the list item,
687 // or the h3, or the h4:
688 alert(e.target.textContent)
689 }
690}
691```
692
693To 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:
694
695```javascript
696// Example of simple event target:
697class List extends Component {
698 render(data) => {
699 return (
700 <ul>
701 {
702 data.map(item => <li onclick={(e) => this.announceItem(e)}>
703 <h3>{item.name}</h3>
704 <h4>{item.value}</h4>
705 </li>)
706 }
707 </ul>
708 )
709 }
710 announceItem(e) {
711 // Here e.target might be the list item,
712 // or the h3, or the h4.
713 // Therefore we need to test whether the target.nodeName
714 // is "LI". If not, we get its parent node:
715 const target = e.target.nodeName === 'LI' ? e.target : e.target.parentNode
716 // Alert the complete list item content:
717 alert(target.textContent)
718 }
719}
720```
721
722## Using Element.closest for Event Target
723
724In 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:
725
726```javascript
727// Example of simple event target:
728class List extends Component {
729 render(data) {
730 return (
731 <ul>
732 {
733 data.map(item => (
734 <li onclick={(e) => this.announceItem(e)}>
735 <h3>
736 <em>{item.name}</em>
737 </h3>
738 <h4>
739 <span>{item.value}</span>
740 </h4>
741 </li>)
742 )
743 }
744 </ul>
745 )
746 }
747 announceItem(e) {
748 // Use "closest" to test for list item:
749 const target = e.target.nodeName === 'LI' ? e.target : e.target.closest('li')
750 // Alert the complete list item content:
751 alert(target.textContent)
752 }
753}
754```
755
756If 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).
757
758
759## Do Not Mix!
760
761It'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