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.