Binding Events on Class Name - ldco2016/microurb_web_framework GitHub Wiki

import { User } from "../models/User";

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

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

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

  template(): string {
    return `<div>
      <h1>User Form</h1>
      <div>User name: ${this.model.get("name")}</div>
      <div>User age: ${this.model.get("age")}</div>
      <input />
      <button>Click Me</button>
    </div>`;
  }

I now have the ability to print up information from a model and show it inside my template, but now I need to figure out how to do the opposite of that. I want to ensure I can take some user input like a button click or user typing and use that information to somehow update my model.

I will set my sites low by or something easy to get started. I want to add a button to my template called Set Random Age and anytime I click on that button, I want to try to set a random age on my user model and of course, once I set the random age, I would also expect the HTML on the screen to be automatically updated as well.

Screen Shot 2021-08-31 at 12 30 27 PM

The first thing I will do is add in a new button to my template:

import { User } from "../models/User";

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

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

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

  template(): string {
    return `<div>
      <h1>User Form</h1>
      <div>User name: ${this.model.get("name")}</div>
      <div>User age: ${this.model.get("age")}</div>
      <input />
      <button>Click Me</button>
      <button>Set Random Age</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);
  }
}

Now I can go back up to my event handler and add in an event handler for anytime my button gets clicked on, but I already have a click event handler that will bind to all the different buttons inside my template and that's an issue. If I always bind to events based upon the tag name, I might accidentally end up binding an event handler to multiple different elements. So I need to figure out which button to bind to and for that all I have to do is add a class name or id to any elements in my template like so:

import { User } from "../models/User";

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

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

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

  template(): string {
    return `<div>
      <h1>User Form</h1>
      <div>User name: ${this.model.get("name")}</div>
      <div>User age: ${this.model.get("age")}</div>
      <input />
      <button>Click Me</button>
      <button class="set-age">Set Random Age</button>
    </div>`;
  }

The . means try to find an element inside my template with a class name of set-age. This will work naturally out of the box and the reason for that is that when I put together my bindEvents() method, I am trying to find an element inside of my template using querySelectorAll() and so effectively what I am passing into the selector is the equivalent to .set-age.

The one downside is the event handler of this.onButtonClick is still going to bind to both buttons. So I will delete that and define onSetAgeClick instead like so:

  eventsMap(): { [key: string]: () => void } {
    return {
      "click:.set-age": this.onSetAgeClick,
    };
  }

  onSetAgeClick(): void {
    console.log("button was clicked");
  }

  template(): string {
    return `<div>
      <h1>User Form</h1>
      <div>User name: ${this.model.get("name")}</div>
      <div>User age: ${this.model.get("age")}</div>
      <input />
      <button>Click Me</button>
      <button class="set-age">Set Random Age</button>
    </div>`;
  }

So now whenever I render this to the browser, this class will try to find some element with a class name of set-age, it will watch for a click event and anytime someone clicks on that it will run this.onSetAgeClick.

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