---
title: Bitovi Academy - Events Part 2
description: Learn about event propagation and event delegation.
---

- ![AI implementation](https://www.bitovi.com/hubfs/AIConsultingIcon.svg)
  
  [AI implementation](https://www.bitovi.com/services/ai-consulting?hsLang=en-us)
- ![Systems engineering](https://www.bitovi.com/hubfs/icon%20-%20backend.svg)
  
  [Systems engineering](https://www.bitovi.com/services/systems-engineering-consulting?hsLang=en-us)
- ![Project management](https://www.bitovi.com/hubfs/icon%20-%20PM.svg)
  
  [Project management](https://www.bitovi.com/services/agile-project-management-consulting?hsLang=en-us)
- ![Product design](https://www.bitovi.com/hubfs/icon%20-%20design.svg)
  
  [Product design](https://www.bitovi.com/services/product-design-consulting?hsLang=en-us)
- ![Frontend development](https://www.bitovi.com/hubfs/icon%20-%20frontend.svg)
  
  [Frontend development](https://www.bitovi.com/services/frontend-development-consulting?hsLang=en-us)
- [View more
  
  →
  
  ](https://www.bitovi.com/digital-consulting-services?hsLang=en-us)

We're Experts in...

- [JavaScript](https://www.bitovi.com/services/frontend/javascript-consulting?hsLang=en-us)
- [AI training](https://www.bitovi.com/ai-training-for-software-engineers?hsLang=en-us)
- [Angular](https://www.bitovi.com/services/frontend/angular-consulting?hsLang=en-us)
- [Design Systems](https://www.bitovi.com/services/axure-figma-migration?hsLang=en-us)
- [React](https://www.bitovi.com/services/frontend/react-consulting?hsLang=en-us)
- [Temporal](https://www.bitovi.com/services/backend/temporal-consulting?hsLang=en-us)
- [React Native](https://www.bitovi.com/services/frontend/react-consulting/react-native?hsLang=en-us)
- [Node.js](https://www.bitovi.com/services/backend/nodejs-consulting-1?hsLang=en-us)

Showcase

![Yum! Brands](https://www.bitovi.com/hubfs/yum-showcase-link-1.png)

[View case study](https://www.bitovi.com/en/bitovi-yum-case-study?hsLang=en-us)

More Projects

- [![Levi's](https://www.bitovi.com/hubfs/levis.svg)](https://www.bitovi.com/web-application-consulting-work/levis-ecommerce-responsive-redesign?hsLang=en-us)
- [![Christie's International Real Estate](https://www.bitovi.com/hubfs/christies.svg)](https://design.bitovi.com/christies)
- [![BAFS](https://www.bitovi.com/hubfs/bafs.svg)](https://www.bitovi.com/ux-design-consulting/ux-case-studies/bafs-ppp?hsLang=en-us)
- [View more
  
  →
  
  ](https://www.bitovi.com/our-software-consulting-work?hsLang=en-us)

Open Source Tools

We build powerful tools and open source them to support the community.

[See what we've built →](https://www.bitovi.com/open-source?hsLang=en-us)

- [![Blog](https://www.bitovi.com/hubfs/icon%20-%20blog.svg)
  
  BlogWe post about perfecting software delivery
  
  ](https://bitovi.com/blog)
- [![Partnerships](https://www.bitovi.com/hubfs/Handshake-1.svg)
  
  PartnershipsLearn about Bitovi's technology partners
  
  ](https://www.bitovi.com/partnerships?hsLang=en-us)
- [![Academy](https://www.bitovi.com/hubfs/limbo/icons/icon-academy.svg)
  
  AcademyFree courses to build delivery skills
  
  ](https://www.bitovi.com/academy/?hsLang=en-us)
- [![Open source tools](https://www.bitovi.com/hubfs/icon%20-%20open%20source.svg)
  
  Open source toolsUse our tools or contribute to our open source mission.
  
  ](https://www.bitovi.com/open-source?hsLang=en-us)

Let's Connect

- [![Discord](https://www.bitovi.com/hubfs/DiscordLogo.svg)
  
  Discord
  
  ](https://discord.gg/J7ejFsZnJ4)
- [![LinkedIn](https://www.bitovi.com/hubfs/LinkedinLogo.svg)
  
  LinkedIn
  
  ](https://www.linkedin.com/company/bitovi/)
- [![GitHub](https://www.bitovi.com/hubfs/GithubLogo.svg)
  
  GitHub
  
  ](https://github.com/bitovi/)

![Eggbot](https://www.bitovi.com/hubfs/build_assets/bitovi-limbo-cms-react/338/js_client_assets/assets/eggbot-LTGhdSGL.png)

Name *

Work Email *

Phone

What's your project?

Send

### Contact Us

(312) 620-0386contact@bitovi.com

- [ Advanced JavaScript ](https://www.bitovi.com/academy/learn-advanced-javascript.html?hsLang=en-us)
- [ DOM and jQuery ](https://www.bitovi.com/academy/learn-dom-jquery.html?hsLang=en-us) 
    - [ Functional Utilities ](https://www.bitovi.com/academy/learn-dom-jquery/functional-utilities.html?hsLang=en-us)
    - [ Finding Elements ](https://www.bitovi.com/academy/learn-dom-jquery/finding-elements.html?hsLang=en-us)
    - [ Traversing Elements ](https://www.bitovi.com/academy/learn-dom-jquery/traversing-elements.html?hsLang=en-us)
    - [ Attributes and Properties ](https://www.bitovi.com/academy/learn-dom-jquery/attributes-and-properties.html?hsLang=en-us)
    - [ Layout ](https://www.bitovi.com/academy/learn-dom-jquery/layout.html?hsLang=en-us)
    - [ Events Part 1 ](https://www.bitovi.com/academy/learn-dom-jquery/events-part-1.html?hsLang=en-us)
    - [ Events Part 2 ](https://www.bitovi.com/academy/learn-dom-jquery/events-part-2.html?hsLang=en-us)
    - [ Fn Plugins ](https://www.bitovi.com/academy/learn-dom-jquery/fn-plugins.html?hsLang=en-us)
    - [ Tabs Widget ](https://www.bitovi.com/academy/learn-dom-jquery/tabs-widget.html?hsLang=en-us)
- [ Debug JavaScript ](https://www.bitovi.com/academy/learn-to-debug-javascript.html?hsLang=en-us)
- [ Learn TypeScript ](https://www.bitovi.com/academy/learn-typescript.html?hsLang=en-us)
- [ Learn React Native ](https://www.bitovi.com/academy/learn-react-native.html?hsLang=en-us)
- [ Learn Web Components ](https://www.bitovi.com/academy/learn-web-components.html?hsLang=en-us)
- [ Agile Program Management With Jira ](https://www.bitovi.com/academy/learn-agile-program-management-with-jira.html?hsLang=en-us)
- [ Learn RxJS ](https://www.bitovi.com/academy/learn-rxjs.html?hsLang=en-us)
- [ Giving a Training ](https://www.bitovi.com/academy/training.html?hsLang=en-us)
- [ Learn Technology Consulting ](https://www.bitovi.com/academy/learn-technology-consulting.html?hsLang=en-us)
- [ Learn React ](https://www.bitovi.com/academy/learn-react.html?hsLang=en-us)
- [ Learn NgRx ](https://www.bitovi.com/academy/learn-ngrx.html?hsLang=en-us)
- [ Learn Graphql ](https://www.bitovi.com/academy/learn-graphql.html?hsLang=en-us)
- [ Learn Docker ](https://www.bitovi.com/academy/learn-docker.html?hsLang=en-us)
- [ Learn CanJS ](https://www.bitovi.com/academy/learn-canjs.html?hsLang=en-us)
- [ Learn Angular ](https://www.bitovi.com/academy/learn-angular.html?hsLang=en-us)
- [ Advanced TypeScript ](https://www.bitovi.com/academy/advanced-typescript.html?hsLang=en-us)

[View More Courses](https://www.bitovi.com/academy/#courses) [ ]

# Events Part 2 `page`

Learn about event propagation and event delegation.

## Overview

In this part, we will:

- Learn about event propagation
- Learn about event delegation

## Slides

## Video

## Propagation Demo

The following listens to `click` on a nested body, div, and anchor element in both the `capture` and `bubble` phase and logs which handler is being dispatched.

See what happens:

- when different propagation methods are called
- when the DOM is modified during event dispatching

```
<style>
    * {font-family: Consolas, Monaco, Menlo, monospace; font-size: 1.2em}
    a {display: block; margin-left: 20px;}
    div {margin-left: 20px;}
</style>
&lt;body&gt;
<div class="well">
    &lt;div&gt;
    <a href="#someHash">&lt;a&gt;&lt;/a&gt;</a>
    &lt;/div&gt;
</div>
&lt;/body&gt;
<script type="module">
/*
What happens if we:
    - stopPropagation() within the body’s capture phase listener?
    - stopImmediatePropagation() within the body’s capture phase listener?
    - remove innerHTML within the body’s capture phase listener?
 */

var body = document.querySelector('body'),
    div = document.querySelector('div'),
    a = document.querySelector('a');

body.addEventListener('click', function(ev) {
    console.log('<- body bubbled');
});

body.addEventListener('click', function(ev) {
    // ev.stopImmediatePropagation();
    // ev.preventDefault();
    // this.innerHTML = "";
    console.log('-> body captured');
}, true);

div.addEventListener('click', function() {
    console.log('<- div bubbled');
});

div.addEventListener('click', function() {
    console.log('-> div captured');
}, true);

a.addEventListener('click', function() {
    console.log('<- a bubble phase');
});

a.addEventListener('click', function() {
    console.log('-> a capture phase');
}, true);
</script>

```

## Delegation Demo

The following shows using event delegation to listen to when any anchor is clicked in the page.

```
<style>
    * {font-family: Consolas, Monaco, Menlo, monospace; font-size: 1.2em}
    a {display: block; margin-left: 20px;}
    div {margin-left: 20px;}
</style>
&lt;body&gt;
<div class="well">
    &lt;div&gt;
    <a href="#someHash">&lt;a&gt;&lt;/a&gt;</a>
    <a href="#thisHash">&lt;a&gt; <span>&lt;span&gt;  &lt;/span&gt;</span> &lt;/a&gt;</a>
    &lt;/div&gt;
</div>
&lt;/body&gt;
<script type="module">
document.body.addEventListener("click", function(ev){
    if( ev.target.matches("a") ) {
        console.log("clicked on an anchor!");
    }
}, false);
</script>

```

Notice that when the `<span>` within the `<a>` is is clicked, nothing is logged. This is because the `event.target` was the `<span>` and not an `<a>`.

```
<style>
    * {font-family: Consolas, Monaco, Menlo, monospace; font-size: 1.2em}
    a {display: block; margin-left: 20px;}
    div {margin-left: 20px;}
</style>
&lt;body&gt;
<div class="well">
    &lt;div&gt;
    <a href="#someHash">&lt;a&gt;&lt;/a&gt;</a>
    <a href="#thisHash">&lt;a&gt; <span>&lt;span&gt;  &lt;/span&gt;</span> &lt;/a&gt;</a>
    &lt;/div&gt;
</div>
&lt;/body&gt;
<script type="module">
document.body.addEventListener("click", function(ev){
    var current = ev.target;
    do {
        if(current.matches("a")) {
            console.log("clicked on an anchor!");
        }
        current = current.parentNode;
    } while ( current && current !== ev.currentTarget )
}, false);
</script>

```

## Setup

Run the following example in CodePen:

```
<div id="qunit"></div>
<div id="qunit-fixture"></div>
<link rel="stylesheet" href="//code.jquery.com/qunit/qunit-1.12.0.css">
<script src="//code.jquery.com/qunit/qunit-1.12.0.js"></script>
<script src="//bitovi.github.io/academy/static/scripts/jquery-test.js"></script>
<link rel="stylesheet" href="//bitovi.github.io/academy/static/scripts/jquery-test.css">
<script type="module">
(function() {
  $ = function(selector) {
    if ( !(this instanceof $) ) {
      return new $(selector);
    }
    var elements;
    if (typeof selector === "string") {
      elements = document.querySelectorAll(selector);
    } else if ($.isArrayLike(selector)) {
      elements = selector;
    }
    [].push.apply(this, elements);
  };

  $.extend = function(target, object) {
    for (var prop in object) {
      if (object.hasOwnProperty(prop)) {
        target[prop] = object[prop];
      }
    }
    return target;
  };

  // Static methods
  $.extend($, {
    isArray: function(obj) {
      return Object.prototype.toString.call(obj) === "[object Array]";
    },
    isArrayLike: function(obj) {
      return obj &&
        typeof obj === "object" &&
        (   obj.length === 0 ||
            typeof obj.length === "number" &&
            obj.length > 0 &&
            obj.length - 1 in obj );

    },
    each: function(collection, cb) {
      if ($.isArrayLike(collection)) {
        for (var i = 0; i < collection.length; i++) {
          if (cb.call(this, i, collection[i]) === false) {
            break;
          }
        }
      } else {
        for (var prop in collection) {
          if (collection.hasOwnProperty(prop)) {
            if (cb.call(this, prop, collection[prop]) === false) {
              break;
            }
          }
        }
      }
      return collection;
    },
    makeArray: function(arr) {
      if ($.isArray(arr)) {
        return arr;
      }
      var array = [];
      $.each(arr, function(i, item) {
        array[i] = item;
      });
      return array;
    },
    proxy: function(fn, context) {
      return function() {
        return fn.apply(context, arguments);
      };
    }
  });

  function makeTraverser(traverser) {
    return function() {
      var elements = [], args = arguments;
      $.each(this, function(i, element) {
        var els = traverser.apply(element, args);
        if ($.isArrayLike(els)) {
          elements.push.apply(elements, els);
        } else if (els) {
          elements.push(els);
        }
      });
      return $(elements);
    };
  }

  $.extend($.prototype, {
    html: function(newHtml) {
      if(arguments.length) {
        return $.each(this, function(i, element) {
          element.innerHTML = newHtml;
        });
      } else {
        return this[0].innerHTML;
      }
    },
    val: function(newVal) {
      if(arguments.length) {
        return $.each(this, function(i, element) {
          element.value = newVal;
        });
      } else {
        return this[0].value;
      }
    },
    text: function(newText) {
      if (arguments.length) {
        return $.each(this, function(i, element) {
          element.textContent = newText;
        });
      } else {
        return this[0].textContent;
      }
    },
    find: makeTraverser(function(selector) {
      return this.querySelectorAll(selector);
    }),
    parent: makeTraverser(function() {
      return this.parentNode;
    }),
    next: makeTraverser(function() {
      return this.nextElementSibling;
    }),
    prev: makeTraverser(function() {
      return this.previousElementSibling;
    }),
    children: makeTraverser(function() {
      return this.children;
    }),
    attr: function(attrName, value) {
      if (arguments.length == 2) {
        return $.each(this, function(i, element) {
          element.setAttribute(attrName, value);
        });
      } else {
        return this[0] && this[0].getAttribute(attrName);
      }
    },
    css: function(cssPropName, value) {
      if (arguments.length == 2) {
        return $.each(this, function(i, element) {
          element.style[cssPropName] = value;
        });
      } else {
        return this[0] &&
          window.getComputedStyle(this[0])
            .getPropertyValue(cssPropName);
      }
    },
    addClass: function(className) {
      return $.each(this, function(i, element) {
        element.classList.add(className);
      });
    },
    removeClass: function(className) {
      return $.each(this, function(i, element) {
        element.classList.remove(className);
      });
    },
    width: function() {
      var paddingLeft = parseInt(this.css("padding-left"), 10),
      paddingRight = parseInt(this.css("padding-right"), 10);
      return this[0].clientWidth - paddingLeft - paddingRight;
    },
    hide: function() {
      return this.css("display", "none");
    },
    show: function() {
      return this.css("display", "");
    },
    offset: function() {
      var offset = this[0].getBoundingClientRect();
      return {
        top: offset.top + window.pageYOffset,
        left: offset.left + window.pageXOffset
      };
    },
    bind: function(eventName, handler) {
      return $.each(this, function(i, element) {
        element.addEventListener(eventName, handler, false);
      });
    },
    unbind: function(eventName, handler) {
      return $.each(this, function(i, element) {
        element.removeEventListener(eventName, handler, false);
      });
    },
    is: function(selector){ },
    data: function(propName, value) { }
    on: function(eventType, selector, handler) { },
    off: function(eventType, selector, handler) { }
  });

})();
</script>

```

Each exercise builds on the previous exercise. There is a completed solution at the end of this page.

## Exercise: `collection.is(selector) -> boolean`

### The problem

[collection.is](https://api.jquery.com/is/) checks the current matched set of elements against a selector.

```
import "https://unpkg.com/jquery@3/dist/jquery.js";

var elements = $([
  document.createElement("div"),
  document.createElement("span"),
]);

console.log(elements.is("div")); //-> true
```

Click to see test code

```
QUnit.test("$.fn.is", function () {
  expect(3);

  var elements = $([
    document.createElement("div"),
    document.createElement("span"),
  ]);

  ok(elements.is("div"), "is div");
  ok(elements.is("span"), "is span");
  ok(!elements.is("a"), "is a");
});
```

### What you need to know

- [matches](https://developer.mozilla.org/en-US/docs/Web/API/Element/matches) returns if an element matches a selector:
  
  ```
  <div id="hello">Hello World</div>
  <script type="module">
    console.log(hello.matches("div")); //-> true
  </script>
  ```

### The solution

Click to see the solution

```
    is: function(selector){
      var matched = false;
      $.each(this, function(i, element){
        if( this.matches( selector) ) {
          matched = true;
        }
      });
      return matched;
    },
```

## Exercise: `collection.data(key [, value])`

### The problem

[collection.data](https://api.jquery.com/data/) stores arbitrary data associated with the matched elements or return the value at the named data store for the first element in the set of matched elements.

```
<div id="hello">Hello World</div>
<script type="module">
  import "https://unpkg.com/jquery@3/dist/jquery.js";

  $("#hello").data("foo", "bar");

  console.log($("#hello").data("foo")); //-> "bar"
</script>
```

Click to see test code

```
QUnit.test("$.fn.data", function () {
  $("#qunit-fixture").html('<div id="el">text</div>');

  $("#el").data("foo", "bar");

  equal($("#el").data("foo"), "bar", "got back bar");
});
```

### What you need to know

- Use [WeakMap](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap) to store data associated with an object in such a way that when the object is removed the data will be also be removed from the `WeakMap` and available for garbage collection.
  
  ```
  var map = new WeakMap();
  
  (function () {
    var key = { name: "key" };
    var value = { name: "value" };
    map.set(key, value);
  })();
  setTimeout(function () {
    console.log(map);
    // In chrome, you can see the contents of the weakmap
    // and it will not have the key and value.
  }, 500);
  ```

### The solution

Click to see the solution

```
    data: (function(){
      var data = new WeakMap();
      return function(propName, value) {
        if (arguments.length == 2) {
          // set the data for every item in the collection
          return $.each(this, function(i, el) {
            var elData = data.get(el);
            if (!elData) {
              elData = {};
              data.set(el, elData);
            }
            elData[propName] = value;
          });
        } else {
          // return the data in the first value
          var el = this[0], elData = data.get(el);
          return elData && elData[propName];
        }
      };
    })(),
```

## Exercise: `collection.on(eventType, selector, handler)`

### The problem

[collection.on](https://api.jquery.com/on/) attaches a delegate event listener.

```
<ul id="root">
  <li>First</li>
  <li>Second</li>
</ul>
<script type="module">
  import "https://unpkg.com/jquery@3/dist/jquery.js";

  $("#root").on("click", "li", function () {
    console.log("clicked an li");
  });
</script>
```

Click to see test code

```
QUnit.test("$.fn.on", function () {
  expect(3);

  var handler = function () {
    equal(this.nodeName.toLowerCase(), "li", "called back with an LI");
  };

  var $ul = $("#qunit-fixture")
    .html(
      `
        <ul>
            <li><span id="one"/></li>
            <li><span id="two"/></li>
        </ul>`
    )
    .children();

  $ul.on("click", "li", handler);

  clickIt($("#one")[0]);
  clickIt($("#two")[0]);

  $ul.html('<li><span id="three"></span></li>');
  clickIt($("#three")[0]);
});
```

### What you need to know

- Instead of binding the `handler`, you’ll need to bind a `delegator` that will conditionally call the `handler`.
- Use `.data` to store the `delegator` and `handler` in an object like `{delegator, handler}`. That object should be stored in a data structure that looks like:
  
  ```
  $([element]).data("events"); //-> {
  //   click: { li: [ {delegator, handler} ] }
  // }
  ```

### The solution

Click to see the solution

```
    on: function(eventType, selector, handler) {
      // Create delegator function
      var delegator = function(ev) {
        var cur = ev.target;
        do {
          if ( $([ cur ]).is(selector) ) {
            handler.call(cur, ev);
          }
          cur = cur.parentNode;
        } while (cur && cur !== ev.currentTarget);
      };

      return $.each(this, function(i, element) {
        // store delegators by event and selector in
        // $.data
        var events = $([ element ]).data("events"), eventTypeEvents;
        if (!events) {
          $([ element ]).data("events", events = {});
        }
        if (!(eventTypeEvents = events[eventType])) {
          eventTypeEvents = events[eventType] = {};
        }
        if (!eventTypeEvents[selector]) {
          eventTypeEvents[selector] = [];
        }
        eventTypeEvents[selector].push({
          handler: handler,
          delegator: delegator
        });
        element.addEventListener(eventType, delegator, false);
      });
    },

```

## Exercise: `collection.off(eventType, selector, handler)`

### The problem

[collection.off](https://api.jquery.com/off/) stops listening for a delegate listener.

Click to see test code

```
QUnit.test("$.fn.off", function () {
  expect(0);

  var handler = function () {
    equal(this.nodeName.toLowerCase(), "li", "called back with an LI");
  };

  var $ul = $("#qunit-fixture")
    .html(
      `
        <ul>
            <li><span id="one"/></li>
            <li><span id="two"/></li>
        </ul>`
    )
    .children();

  $ul.on("click", "li", handler);
  $ul.off("click", "li", handler);

  clickIt($("#three")[0]);
});
```

### What you need to know

- You will need to find the delegate for the handler passed to `.off()` and then call `.removeEventListener`.

### The solution

Click to see the solution

```
    off: function(eventType, selector, handler) {
      return $.each(this, function(i, element) {
        // Find the delegator object for the handler
        // and remove it.
        var events = $([ element ]).data("events");
        if (events[eventType] && events[eventType][selector]) {
          var delegates = events[eventType][selector], i = 0;
          while (i < delegates.length) {
            if (delegates[i].handler === handler) {
              element.removeEventListener(eventType, delegates[i].delegator, false);
              delegates.splice(i, 1);
            } else {
              i++;
            }
          }
        }
      });
    }
```

## Completed solution

Click to see completed solution

```
<div id="qunit"></div>
<div id="qunit-fixture"></div>
<link rel="stylesheet" href="//code.jquery.com/qunit/qunit-1.12.0.css">
<script src="//code.jquery.com/qunit/qunit-1.12.0.js"></script>
<script src="//bitovi.github.io/academy/static/scripts/jquery-test.js"></script>
<link rel="stylesheet" href="//bitovi.github.io/academy/static/scripts/jquery-test.css">
<script type="module">
(function() {
  $ = function(selector) {
    if ( !(this instanceof $) ) {
      return new $(selector);
    }
    var elements;
    if (typeof selector === "string") {
      elements = document.querySelectorAll(selector);
    } else if ($.isArrayLike(selector)) {
      elements = selector;
    }
    [].push.apply(this, elements);
  };

  $.extend = function(target, object) {
    for (var prop in object) {
      if (object.hasOwnProperty(prop)) {
        target[prop] = object[prop];
      }
    }
    return target;
  };

  // Static methods
  $.extend($, {
    isArray: function(obj) {
      return Object.prototype.toString.call(obj) === "[object Array]";
    },
    isArrayLike: function(obj) {
      return obj &&
        typeof obj === "object" &&
        (   obj.length === 0 ||
            typeof obj.length === "number" &&
            obj.length > 0 &&
            obj.length - 1 in obj );

    },
    each: function(collection, cb) {
      if ($.isArrayLike(collection)) {
        for (var i = 0; i < collection.length; i++) {
          if (cb.call(this, i, collection[i]) === false) {
            break;
          }
        }
      } else {
        for (var prop in collection) {
          if (collection.hasOwnProperty(prop)) {
            if (cb.call(this, prop, collection[prop]) === false) {
              break;
            }
          }
        }
      }
      return collection;
    },
    makeArray: function(arr) {
      if ($.isArray(arr)) {
        return arr;
      }
      var array = [];
      $.each(arr, function(i, item) {
        array[i] = item;
      });
      return array;
    },
    proxy: function(fn, context) {
      return function() {
        return fn.apply(context, arguments);
      };
    }
  });

  function makeTraverser(traverser) {
    return function() {
      var elements = [], args = arguments;
      $.each(this, function(i, element) {
        var els = traverser.apply(element, args);
        if ($.isArrayLike(els)) {
          elements.push.apply(elements, els);
        } else if (els) {
          elements.push(els);
        }
      });
      return $(elements);
    };
  }

  $.extend($.prototype, {
    html: function(newHtml) {
      if(arguments.length) {
        return $.each(this, function(i, element) {
          element.innerHTML = newHtml;
        });
      } else {
        return this[0].innerHTML;
      }
    },
    val: function(newVal) {
      if(arguments.length) {
        return $.each(this, function(i, element) {
          element.value = newVal;
        });
      } else {
        return this[0].value;
      }
    },
    text: function(newText) {
      if (arguments.length) {
        return $.each(this, function(i, element) {
          element.textContent = newText;
        });
      } else {
        return this[0].textContent;
      }
    },
    find: makeTraverser(function(selector) {
      return this.querySelectorAll(selector);
    }),
    parent: makeTraverser(function() {
      return this.parentNode;
    }),
    next: makeTraverser(function() {
      return this.nextElementSibling;
    }),
    prev: makeTraverser(function() {
      return this.previousElementSibling;
    }),
    children: makeTraverser(function() {
      return this.children;
    }),
    attr: function(attrName, value) {
      if (arguments.length == 2) {
        return $.each(this, function(i, element) {
          element.setAttribute(attrName, value);
        });
      } else {
        return this[0] && this[0].getAttribute(attrName);
      }
    },
    css: function(cssPropName, value) {
      if (arguments.length == 2) {
        return $.each(this, function(i, element) {
          element.style[cssPropName] = value;
        });
      } else {
        return this[0] &&
          window.getComputedStyle(this[0])
            .getPropertyValue(cssPropName);
      }
    },
    addClass: function(className) {
      return $.each(this, function(i, element) {
        element.classList.add(className);
      });
    },
    removeClass: function(className) {
      return $.each(this, function(i, element) {
        element.classList.remove(className);
      });
    },
    width: function() {
      var paddingLeft = parseInt(this.css("padding-left"), 10),
      paddingRight = parseInt(this.css("padding-right"), 10);
      return this[0].clientWidth - paddingLeft - paddingRight;
    },
    hide: function() {
      return this.css("display", "none");
    },
    show: function() {
      return this.css("display", "");
    },
    offset: function() {
      var offset = this[0].getBoundingClientRect();
      return {
        top: offset.top + window.pageYOffset,
        left: offset.left + window.pageXOffset
      };
    },
    bind: function(eventName, handler) {
      return $.each(this, function(i, element) {
        element.addEventListener(eventName, handler, false);
      });
    },
    unbind: function(eventName, handler) {
      return $.each(this, function(i, element) {
        element.removeEventListener(eventName, handler, false);
      });
    },
    is: function(selector){
      var matched = false;
      $.each(this, function(i, element){
        if( element.matches( selector) ) {
          matched = true;
        }
      });
      return matched;
    },
    data: (function(){
      var data = new WeakMap();
      return function(propName, value) {
        if (arguments.length == 2) {
          return $.each(this, function(i, el) {
            var elData = data.get(el);
            if (!elData) {
              elData = {};
              data.set(el, elData);
            }
            elData[propName] = value;
          });
        } else {
          var el = this[0], elData = data.get(el);
          return elData && elData[propName];
        }
      };
    })(),
    on: function(eventType, selector, handler) {
      // Create delegator function
      var delegator = function(ev) {
        var cur = ev.target;
        do {
          if ( $([ cur ]).is(selector) ) {
            handler.call(cur, ev);
          }
          cur = cur.parentNode;
        } while (cur && cur !== ev.currentTarget);
      };

      return $.each(this, function(i, element) {
        // store delegators by event and selector in
        // $.data
        var events = $([ element ]).data("events"), eventTypeEvents;
        if (!events) {
          $([ element ]).data("events", events = {});
        }
        if (!(eventTypeEvents = events[eventType])) {
          eventTypeEvents = events[eventType] = {};
        }
        if (!eventTypeEvents[selector]) {
          eventTypeEvents[selector] = [];
        }
        eventTypeEvents[selector].push({
          handler: handler,
          delegator: delegator
        });
        element.addEventListener(eventType, delegator, false);
      });
    },
    off: function(eventType, selector, handler) {
      return $.each(this, function(i, element) {
        // Find the delegator object for the handler
        // and remove it.
        var events = $([ element ]).data("events");
        if (events[eventType] && events[eventType][selector]) {
          var delegates = events[eventType][selector], i = 0;
          while (i < delegates.length) {
            if (delegates[i].handler === handler) {
              element.removeEventListener(eventType, delegates[i].delegator, false);
              delegates.splice(i, 1);
            } else {
              i++;
            }
          }
        }
      });
    }
  });

})();
</script>

```

###### On this page...

###### Get help

If you need [help](https://discord.gg/J7ejFsZnJ4) with an exercise, please reach out to us on [Discord](https://discord.gg/J7ejFsZnJ4)

If you find a bug, please [create an issue](https://github.com/bitovi/academy/issues/new) or email [contact@bitovi.com](mailto:contact@bitovi.com?Subject=Training%20Suggestion)

## Join the Bitovi Newsletter

 Do you want to receive updates about new Bitovi Academy courses, Bitovi open source projects, and dev community news? If so, please provide your email. We won’t spam you or give it away for any reason!  
*By providing your email you agree to receive communications from Bitovi*

![](https://bitovi.github.io/academy/static/img/close.svg)

```json
{
  "@context" : "http://schema.org",
  "@type" : "Organization",
  "address" : {
    "@type" : "PostalAddress",
    "addressCountry" : "United States",
    "addressLocality" : "Libertyville",
    "addressRegion" : "IL",
    "postalCode" : "60048",
    "streetAddress" : "1134 Pine Tree Lane "
  },
  "alternateName" : "Bitovi",
  "areaServed" : {
    "@type" : "GeoCircle",
    "geoMidpoint" : {
      "@type" : "GeoCoordinates",
      "latitude" : "41.8781",
      "longitude" : "87.6298"
    },
    "geoRadius" : "5000 km"
  },
  "description" : "Bitovi is a UX, UI design and front-end JavaScript development consulting company",
  "email" : "contact@bitovi.com",
  "image" : "https://www.bitovi.com/hubfs/bitovi-logo-x2.png",
  "logo" : "https://www.bitovi.com/hubfs/bitovi-logo-23-1.svg",
  "mainEntityOfPage" : {
    "@id" : "https://www.bitovi.com/academy/learn-dom-jquery/events-part-2.html",
    "@type" : "WebPage",
    "description" : "Learn about event propagation and event delegation. "
  },
  "naics" : "541511",
  "name" : "Bitovi Web App Consulting",
  "sameAs" : [ "https://www.facebook.com/BitoviLLC/", "https://twitter.com/bitovi", "https://www.linkedin.com/company/bitovi" ],
  "telephone" : "312-620-0386",
  "url" : "http://bitovi.com"
}
```