Adding Methods to the User - ldco2016/microurb_web_framework GitHub Wiki

So I want to modify a User Model in a very specific way, there might be other applications or other classes inside my app where I want to set a random age as well.

Usually, anytime we try to write out some logic that directly modifies a Model, we usually tuck that method into the Model Class itself as a helper method. So I need to open up my User Model file and add a method to it to set a random age, then I can have some reusable random age code tied to the Model instead of being tucked away in my View class.

import { Model } from "./Model";
import { Attributes } from "./Attributes";
import { ApiSync } from "./ApiSync";
import { Eventing } from "./Eventing";
import { Collection } from "./Collection";

export interface UserProps {
  id?: number;
  name?: string;
  age?: number;
}

const rootUrl = "http://localhost:3000/users";

export class User extends Model<UserProps> {
  static buildUser(attrs: UserProps): User {
    return new User(
      new Attributes<UserProps>(attrs),
      new Eventing(),
      new ApiSync<UserProps>(rootUrl)
    );
  }

  static buildUserCollection(): Collection<User, UserProps> {
    return new Collection<User, UserProps>(rootUrl, (json: UserProps) =>
      User.buildUser(json)
    );
  }

  setRandomAge(): void {
    const age = Math.round(Math.random() * 100);
    this.set({ age });
  }
}

I computed some random age and set it on my Model. This gives me a random age as a positive number, const age = Math.round(Math.random() * 100);. I can set that age on the Model like so: this.set({ age });.

Now I have an additional method on the User that I can use from any View at some point in the future.

Back inside of UserForm.ts I can update the implementation of onSetAgeClick like so:

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

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

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

  onSetAgeClick(): void {
    this.model.setRandomAge();
  }

  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>`;
  }

  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);
  }
}

I get the following error in console:

UserForm.ts:13 Uncaught TypeError: Cannot read property 'setRandomAge' of undefined
    at HTMLButtonElement.UserForm.onSetAgeClick 

The issue is the context of this inside of onSetAgeClick().

Down inside bindEvents(), I eventually set up an event listener on the element I find and I pass in the function that I want to be called. Whenever we pass in a function we want to be called by an event handler about 99% of the time the value of this inside of the function is not going to be what I expect it to be.

It's a classic issue inside of React, in some cases inside of Angular as well, all I need to do is bind the value of this inside the function like so:

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

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

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

  onSetAgeClick = (): void => {
    this.model.setRandomAge();
  };

  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>`;
  }

  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);
  }
}

So now that I am using an arrow function the value of this inside onSetAgeClick() should refer to the view class of UserForm.

Anytime I define an event handler or anything I add to my eventsMap I will have to use an arrow function to avoid the same exact error.

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