Go through the basic JavaScript skills before interview --- Event in JS

Question

a. Implement a general event listener function

b. What's the event bubbling?

c. How to listen every image click in an unlimited drop-down picture list?

Skills

1. Event binding

// general event binding function
const bindEvent = (elem, type, fn) => {
    elem.addEventListener(type, fn)
}

const btn = document.getElementById('btn')
bindEvent(btn, 'click', event => {
    event.preventDefault() // prevent default action
    alert('click')
})

2. Event bubbling

<body>
    <div id="div1">
        <p id="p1">active</p>
        <p id="p2">cancel</p>
        <p id="p3">cancel</p>
        <p id="p4">cancel</p>
    </div>
    <div id="div2">
        <p id="p5">cancel</p>
        <p id="p6">cancel</p>
    </div>
</body>
// bind event
const body = document.body
bindEvent(body, 'click', event => {
    console.log('body clicked')
    console.log(event.target) // the element which triggers the event
})

// when click p1, the output is 'body clicked' + '<p id="p1">active</p>'

const div1 = document.getElementById('div1')
bindEvent(div1, 'click', event => {
    console.log('div1 clicked')
})

// click p1 output: 
// div1 clicked
// body clicked
// <p id="p1">active</p>

// click p5 output: 
// body clicked
// <p id="p5">cancel</p>

Event will propagate from bottom to top following the DOM tree

3. Event delegate

Right now, we have a new demand: when you click p1, the output should be "active", and when you click p2, p3, p4, p5, output should be "cancel".

The question is: do we have to bind event for all p2, p3, p4 and p5?

// the solution is to take advantage of event bubbling
// 1. bind active event to p1
const p1 = document.getElementById('p1')
bindEvent(p1, 'click', event => {
    // 3. add stopPropagation to prevent event bubbling to body
    event.stopPropagation()
    console.log('active')
})

// 2. bind cancel event to body
const body = document.body
bindEvent(body, 'click', event => {
    console.log('cancel')
})

Let's have a look at another example:

<div id="div1">
    <a href="#">a1</a>
    <a href="#">a2</a>
    <a href="#">a3</a>
    <a href="#">a4</a>
</div>
<button>
    add one a tag
</button>

We need to add click event for all a tags, but we can't know how many a tags exactly in div1, as the button can add a new a tag. Use event delegate to handle this case:

const div1 = document.getElementById('div1')
div1.addEventListener('click', e => {
    event.preventDefault() // prevent the a tag function
    const target = e.target
    if(e.nodeName === 'A'){
        console.log(target.innerHTML) // this will output the a tag inner html
    }
})

The advantages of event delegate:

  1. Make simpler code
  2. Save memory as we don't have to bind / unbind too many elements
  3. Don't abuse!

Let's refactor the general event binding function right now, as it was too simple!

const bindEvent(elem, type, selector, fn) {
    // check if it's normal binding or delegate binding
    if(fn==null){
        fn = selector
        selector = null
    }
    elem.addEventListener(type, event => {
        const target = event.target
        if(selectror){
            // delegate bind
            if(target.matches(selector)){
                fn.call(target, event)
            }
        }else{
            // normal bind
            fn.call(target, event)
        }
    })
}

// when use it:
// normal binding
const p1 = document.getElementById('p1')
bindEvent(p1, 'click', function(event) { // Don't use arrow function here!
    event.stopPropagation()
    console.log('active')
})
// delegate binding
const div1 = document.getElementById('div1')
bindEvent(div3, 'click', 'a', function(event) {
    event.preventDefault()
    console.log(this.innerHTML)
})