Reusable View Logic - ldco2016/microurb_web_framework GitHub Wiki

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

export class UserForm {
  constructor(public parent: Element, public model: User) {
    this.bindModel();
  }

  bindModel(): void {
    this.model.on("change", () => {
      this.render();
    });
  }

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

  onSetNameClick = (): void => {};

  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 class="set-name">Change Name</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 {
    this.parent.innerHTML = "";

    const templateElement = document.createElement("template");
    templateElement.innerHTML = this.template();

    this.bindEvents(templateElement.content);

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

I am going to start to think about extracting some reusable logic out of this thing.

I only have one view right now.

I am going to focus on easily creating views and then worry about nesting one view inside another.

How do I extract reusable logic out of user form and how do I do it using composition and then inheritance?

Quick reminder of all the methods and properties inside of UserForm:

Screen Shot 2021-10-13 at 5 17 11 PM

I definitely have a lot of properties and methods right now and some of the methods are absolutely reusable in nature such as the render() method. All the logic probably needs to be done for all views created.

onSetNameClick and onSetAgeClick only belong in UserForm.

So using composition I am going to separate these methods by creating a new class called HtmlRenderer, essentially this class would encapsulate all the current reusable view logic that I have in UserForm which would then have a reference to an instance of HtmlRenderer like so, renderer: HtmlRenderer which would then be an instance to the class HtmlRenderer.

When I try to render class UserForm, it will delegate responsibility off to its instance of HtmlRenderer.

I would have to teach or put functionality inside the renderer to tell it exactly how to form up some HTML, put it in the DOM, respond to user events and so on.

I will go through the above properties and methods one by one and figure out which ones go into HtmlRenderer.

parent: Element - reusable? Well, everything that wants to be shown inside the DOM needs to have this parent reference.

model: User - this one is a bit confusing. On the one hand I do want every HTML render to have a model it can bind to so it can automatically re-render itself anytime some data tied to this model changes, but right now its custom just to the UserForm. I could use generics to make this model more reusable.

template(): string - This one is also a bit confusing. The HtmlRenderer will probably have to have a template method or a method that returns a string to be turned into some HTML, however, the template method will be custom to each view put together. So template(): string will have to stay with class UserForm and I will have to have some kind of way to have HtmlRenderer access that template() method.

render(): void - Without a doubt, everything that needs to be show within the DOM is needs to be contained or accessed via the render() method.

eventsMap(): { key: () => void } - eventsMap() is the method that is going to return an object that describes all the different events and elements to bind the events to inside of the specific view, so this is another one where the HtmlRenderer is going to be responsible for binding the events, but the actual events definitiions will be different for each view class I create, so I will leave this one in class UserForm.

bindEvents(): void - That's the actual event binding logic so it goes inside class HtmlRenderer.

bindModel(): void - Every view I create is going to have to bind to a model so that will be in class HtmlRenderer.

onSetNameClick(): void - this is a method that is custom for class UserForm. onSetAgeClick(): void - this is a method that is custom for class UserForm.

renderer: HtmlRenderer - will stay with class UserForm.

So this would be one possible way to split up my composition.

The only issue with this approach is that the render(): void method and the bindEvents(): void methods expect to call something called template() or they need a template to work correctly. So render(): void needs access to template(): string and bindEvents(): void needs access to eventsMap(): { key: () => void }. So, not only do I have UserForm that needs a reference over to HtmlRenderer, but I am imagining a scenario where in order for my class HtmlRenderer to work correctly it would also have to have a reference back over to class UserForm. So maybe I would give my HtmlRenderer a view: View property and the View interface says you have to have a template method that returns a string and an eventsMap method that returns an object as well, so I am kind of talking about a bi-directional relationship. This is usually a sign that maybe composition is not the best idea or the division of methods between the two is not the best idea. So maybe composition is not a great idea because for it to work is to have a bi-directional relationship.

A way to get around that would be to say that the render() method of the HtmlRenderer would have to be called like so: render(template: string): void and bindEvents({}): void has to be called with all those different events that it needs to be bound.

So if I set that up, I can then ensure that there is some delegation inside of UserForm so maybe I would re-implement a render(): void method inside of UserForm and bindEvents(): void. All they would do is call either template or eventsMap and pass the results of those on to the render(template: string): void method of HtmlRenderer.

I am trying to build a web framework here and generally, I want to make building out Views as easy as possible. So if I have to talk about render() and bindEvents() has to put together that delegation then I am expecting the user to implement these methods for me, I mean the users of the framework, the developers that will use my code.

I don't think thats realistic so once again I don't think composition is a good solution here.

So how would I do this if I wanted to use Inheritance?

I would probably create a class View rather than calling it HtmlRenderer and I will have UserForm extend class View. SO then all the logic currently on UserForm I would move over to class View. So parent: Element, model: User, render(): void, bindEvents(): void and bindModel(): void would all go inside class View.

My render(): void and bindEvents(): void as well, try to call some render methods that presumably are not going to be implemented by class View. However, class View should not be in charge of implementing template(): string and eventsMap(): { key: () => void }, those are going to be two very custom methods that have to be defined by UserForm. Nevertheless, I will have references to them inside of class View. So if I just try to call those methods without them being defined, I will run into an error message. So what I am saying is I will have to set up View as an abstract class View.

So I am never going to instantiate View directly, it is only going to be used by being extended by another class. So I would want to add an abstract template signature and abstract eventsMap as well. In this scenario, a developer who wants to use this framework, all they have to do is extend class View and when they do, they get an error message saying class UserForm has to implement a method called template(): string and a method called eventsMap(): { key: () => void }. Using inheritance here makes a lot of sense.

We always have to pull in the right design pattern for the job and inheritance makes sense here.

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