Binding Events Handlers - ldco2016/microurb_web_framework GitHub Wiki

export class UserForm {
  constructor(public parent: Element) {}

  eventsMap(): { [key: string]: () => void } {
    return {
      "click:button": this.onButtonClick,
    };
  }

  onButtonClick() {
    console.log("Howdy");
  }

  template(): string {
    return `<div>
      <h1>User Form</h1>
      <input />
      <button>Click Me</button>
    </div>`;
  }

  render(): void {
    const templateElement = document.createElement("template");
    templateElement.innerHTML = this.template();
    console.log(this.parent);

    this.parent.append(templateElement.content);
  }
}

I am going to go down to my render method and add additional logic that is going to call that eventsMap method, take the object I get back, iterate over all the key/value pairs inside there and set up all these different event handlers.

Screen Shot 2021-09-07 at 11 21 17 AM

I am not going to set up all this logic inside the render() method, I think it would be really nice to set up a helper method so I don't clutter up the render() method in general.

The Template Element I am making use of has a content property inside of render right here: this.parent.append(templateElement.content);. So the content property is the actual reference to the HTML inside that Template Element.

Screen Shot 2021-09-08 at 11 15 58 AM

The content property is of type DocumentFragment. That is an object that is included inside the browser by default, a DocumentFragment is essentially an object that can contain a reference to some HTML. A DocumentFragment's purpose is to kind of hold some HTML inside of memory before it gets attached to the DOM. So essentially you are supposed to use a DocumentFragment for what I am using it right now, which is prepping some HTML to be inserted into the DOM.

So I will make a helper method that takes in a reference to a DocumentFragment and then iterate through the eventsMap and try to bind all the events to the HTML inside that DocumentFragment. So I am going to create a helper method called bindEvents like so:

export class UserForm {
  constructor(public parent: Element) {}

  eventsMap(): { [key: string]: () => void } {
    return {
      "click:button": this.onButtonClick,
    };
  }

  onButtonClick() {
    console.log("Howdy");
  }

  template(): string {
    return `<div>
      <h1>User Form</h1>
      <input />
      <button>Click Me</button>
    </div>`;
  }

  bindEvents(fragment: DocumentFragment): void {}

  render(): void {
    const templateElement = document.createElement("template");
    templateElement.innerHTML = this.template();
    console.log(this.parent);

    this.parent.append(templateElement.content);
  }
}

So the first thing I need to do is get a reference to my eventsMap, so I can iterate over its object and set up all these different event handlers like so:

export class UserForm {
  constructor(public parent: Element) {}

  eventsMap(): { [key: string]: () => void } {
    return {
      "click:button": this.onButtonClick,
    };
  }

  onButtonClick() {
    console.log("Howdy");
  }

  template(): string {
    return `<div>
      <h1>User Form</h1>
      <input />
      <button>Click Me</button>
    </div>`;
  }

  bindEvents(fragment: DocumentFragment): void {
    const eventsMap = this.eventsMap();
  }

  render(): void {
    const templateElement = document.createElement("template");
    templateElement.innerHTML = this.template();
    console.log(this.parent);

    this.parent.append(templateElement.content);
  }
}

Now I can set up a for...in loop to iterate over all the key/value pairs inside there like so:

export class UserForm {
  constructor(public parent: Element) {}

  eventsMap(): { [key: string]: () => void } {
    return {
      "click:button": this.onButtonClick,
    };
  }

  onButtonClick() {
    console.log("Howdy");
  }

  template(): string {
    return `<div>
      <h1>User Form</h1>
      <input />
      <button>Click Me</button>
    </div>`;
  }

  bindEvents(fragment: DocumentFragment): void {
    const eventsMap = this.eventsMap();

    for (let eventKey in eventsMap) {
      
    }
  }

  render(): void {
    const templateElement = document.createElement("template");
    templateElement.innerHTML = this.template();
    console.log(this.parent);

    this.parent.append(templateElement.content);
  }
}

So, eventKey is going to be the strings of 'click:button' and whatever else.

So I need to split the eventKey in half, based on that colon to get the event name and the selector for the element I want to attach my event handler to like so:

export class UserForm {
  constructor(public parent: Element) {}

  eventsMap(): { [key: string]: () => void } {
    return {
      "click:button": this.onButtonClick,
    };
  }

  onButtonClick() {
    console.log("Howdy");
  }

  template(): string {
    return `<div>
      <h1>User Form</h1>
      <input />
      <button>Click Me</button>
    </div>`;
  }

  bindEvents(fragment: DocumentFragment): void {
    const eventsMap = this.eventsMap();

    for (let eventKey in eventsMap) {
      const [eventName, selector] = eventKey.split(":");
    }
  }

  render(): void {
    const templateElement = document.createElement("template");
    templateElement.innerHTML = this.template();
    console.log(this.parent);

    this.parent.append(templateElement.content);
  }
}

Now this is some ES2015 syntax here, when I call eventKey I expect to get back an array with two elements inside it. So I am using destructuring to get a reference to the first element inside there, eventName and a reference to the second element inside there I am calling selector.

In this case if I did a console log of eventName I would expect back // click and if I did a console log of selector I would expect to get back // button.

So now I can set up the actual event handler by looking at the fragment I passed in which is a reference to the HTML I am about to inject into the DOM and I am looking to find every element inside there that matches the selector like so:

export class UserForm {
  constructor(public parent: Element) {}

  eventsMap(): { [key: string]: () => void } {
    return {
      "click:button": this.onButtonClick,
    };
  }

  onButtonClick() {
    console.log("Howdy");
  }

  template(): string {
    return `<div>
      <h1>User Form</h1>
      <input />
      <button>Click Me</button>
    </div>`;
  }

  bindEvents(fragment: DocumentFragment): void {
    const eventsMap = this.eventsMap();

    for (let eventKey in eventsMap) {
      const [eventName, selector] = eventKey.split(":");

      fragment.querySelectorAll(selector);
    }
  }

  render(): void {
    const templateElement = document.createElement("template");
    templateElement.innerHTML = this.template();
    console.log(this.parent);

    this.parent.append(templateElement.content);
  }
}

That will give me back an array of elements that match that selector.

I can then iterate over that array and for every element that I match I can attach whatever event handler I had referenced this.onButtonClick like so:

export class UserForm {
  constructor(public parent: Element) {}

  eventsMap(): { [key: string]: () => void } {
    return {
      "click:button": this.onButtonClick,
    };
  }

  onButtonClick() {
    console.log("Howdy");
  }

  template(): string {
    return `<div>
      <h1>User Form</h1>
      <input />
      <button>Click Me</button>
    </div>`;
  }

  bindEvents(fragment: DocumentFragment): void {
    const eventsMap = this.eventsMap();

    for (let eventKey in eventsMap) {
      const [eventName, selector] = eventKey.split(":");

      fragment.querySelectorAll(selector).forEach(element => {
        element.addEventListener(eventName, )
      });
    }
  }

  render(): void {
    const templateElement = document.createElement("template");
    templateElement.innerHTML = this.template();
    console.log(this.parent);

    this.parent.append(templateElement.content);
  }
}

I am watching for the eventName which in this case its simply click and for the second argument to that addEventListener(), I specify the callback function which is the value at eventsMap like so:

export class UserForm {
  constructor(public parent: Element) {}

  eventsMap(): { [key: string]: () => void } {
    return {
      "click:button": this.onButtonClick,
    };
  }

  onButtonClick() {
    console.log("Howdy");
  }

  template(): string {
    return `<div>
      <h1>User Form</h1>
      <input />
      <button>Click Me</button>
    </div>`;
  }

  bindEvents(fragment: DocumentFragment): void {
    const eventsMap = this.eventsMap();

    for (let eventKey in eventsMap) {
      const [eventName, selector] = eventKey.split(":");

      fragment.querySelectorAll(selector).forEach((element) => {
        element.addEventListener(eventName, eventsMap[eventKey]);
      });
    }
  }

  render(): void {
    const templateElement = document.createElement("template");
    templateElement.innerHTML = this.template();
    console.log(this.parent);

    this.parent.append(templateElement.content);
  }
}

I can now call bind events from my render() method after I set up the templateElement like so:

export class UserForm {
  constructor(public parent: Element) {}

  eventsMap(): { [key: string]: () => void } {
    return {
      "click:button": this.onButtonClick,
    };
  }

  onButtonClick() {
    console.log("Howdy");
  }

  template(): string {
    return `<div>
      <h1>User Form</h1>
      <input />
      <button>Click Me</button>
    </div>`;
  }

  bindEvents(fragment: DocumentFragment): void {
    const eventsMap = this.eventsMap();

    for (let eventKey in eventsMap) {
      const [eventName, selector] = eventKey.split(":");

      fragment.querySelectorAll(selector).forEach((element) => {
        element.addEventListener(eventName, eventsMap[eventKey]);
      });
    }
  }

  render(): void {
    const templateElement = document.createElement("template");
    templateElement.innerHTML = this.template();
    
    this.bindEvents()

    this.parent.append(templateElement.content);
  }
}

I need to pass in the DocumentFragment from the templateElement I just created. The DocumentFragment is a reference to all the HTML that I am trying to pass into the DOM.

export class UserForm {
  constructor(public parent: Element) {}

  eventsMap(): { [key: string]: () => void } {
    return {
      "click:button": this.onButtonClick,
    };
  }

  onButtonClick() {
    console.log("Howdy");
  }

  template(): string {
    return `<div>
      <h1>User Form</h1>
      <input />
      <button>Click Me</button>
    </div>`;
  }

  bindEvents(fragment: DocumentFragment): void {
    const eventsMap = this.eventsMap();

    for (let eventKey in eventsMap) {
      const [eventName, selector] = eventKey.split(":");

      fragment.querySelectorAll(selector).forEach((element) => {
        element.addEventListener(eventName, eventsMap[eventKey]);
      });
    }
  }

  render(): void {
    const templateElement = document.createElement("template");
    templateElement.innerHTML = this.template();

    this.bindEvents(templateElement.content);

    this.parent.append(templateElement.content);
  }
}

This is a clever way to handle events if you don't have something like JSX to find all your events for you.

Now I should be able to click on my button and see howdy appear:

Screen Shot 2021-09-08 at 11 40 41 AM

I now have an eventing system built into my View.

⚠️ **GitHub.com Fallback** ⚠️