Home > Software engineering >  Using OOP to create UI in Typescript
Using OOP to create UI in Typescript

Time:08-11

My goal is to create an Inventory App in pure typescript. The "App" class is the starter class. Specifically, I make two instances to initialize ProductView and CategoryView. I do this because I need to generate tables(show/hide by CSS style) based on clicked menus. For example, if a user clicks on the Products menu, I should display the table's and the modal's products. When I clicks on each menu, I got this kind of error:

Uncaught TypeError: Cannot read properties of undefined (reading 'initUI')

AND

Uncaught TypeError: Cannot read properties of undefined (reading 'initCategoryUI')

Github Repository

import { selectedMenu, productMenu, categoryMenu } from "./dom";
import { ProductView } from "./productview";
import { CategoryView } from "./categoryview";
import { Types } from "./entity";
import "./assets/scss/style.scss";
import Entity from "./entity";

class App {
  private _productView: ProductView;
  private _categoryView: CategoryView;
  constructor() {
    this._productView = new ProductView(new Entity<IProduct>(Types.IProduct));
    this._categoryView = new CategoryView(
      new Entity<ICategory>(Types.ICategory)
    );
    console.log("check ::", this._categoryView);
    selectedMenu.forEach((el: HTMLAnchorElement) =>
      el.addEventListener("click", this._selectedMenuHandler)
    );
    this._init();
  }
  private _selectedMenuHandler(): void {
    if (this instanceof Element) {
      if (this.classList.contains("menu__products")) {
        this.classList.add("menu__selected");
        categoryMenu?.classList.remove("menu__selected");
        console.log("what happend in ::", this._productView); //undefined
        this._productView.initUI();
      } else if (this.classList.contains("menu__categories")) {
        this.classList.add("menu__selected");
        productMenu?.classList.remove("menu__selected");
        console.log("what happend in ::", this._categoryView); //undefined
        this._categoryView.initCategoryUI();
      }
    }
  }

  private _init() {
    this._productView.initUI();
  }
}

const app = new App();

import { modalPopup } from "./dom";

export class View {
  private _span: HTMLElement;

  constructor() {
    this._span = document.querySelector<HTMLElement>(".close")!;
    this._span.addEventListener("click", this._closeModal);
    document.addEventListener("click", (e: Event) =>
      this._closeModalWindowClicked(e)
    );
  }

  protected _closeModalWindowClicked(e: Event) {
    if (e.target === modalPopup) modalPopup.style.display = "none";
  }

  protected _openModal() {
    modalPopup.style.display = "block";
  }
  protected _closeModal() {
    modalPopup.style.display = "none";
  }
}

import {
  btn,
  tableThead,
  tableBody,
  btnSubmit,
  inputTitle,
  categoryElement,
  inputQuantity,
  modalContentCategory,
  modalContentProduct,
  modalHeader,
} from "./dom";
import { Product } from "./product";
import Entity from "./entity";
import { View } from "./view";
btn?.classList.add("btn-product");

export class ProductView extends View {
  private _categoryValue: string = "";
  private _productInventory: Entity<IProduct>;
  constructor(inventory: Entity<IProduct>) {
    super();
    console.log("create Product::", this);

    this._productInventory = inventory;
    btn?.addEventListener("click", this._openModal);
    btnSubmit?.addEventListener("click", this._addButtonHandler.bind(this));
    categoryElement?.addEventListener("change", this._selectCategoryHandler);
  }

  public initUI() {
    this._createModal();
    this._createHeaderTable();
    this._renderTable();
  }

  private _createModal() {
    modalContentCategory?.classList.add("hidden");
    modalContentProduct?.classList.remove("hidden");
    modalHeader!.innerHTML = "Add Product";
  }
  private _createHeaderTable(): void {
    if (tableThead) {
      tableThead.innerHTML = `<th>#</th>
        <th>Title</th>
        <th>Quantity</th>
        <th>Category</th>
        <th></th>`;
    }
  }
  private _tableUIBody(item: Product, id: number) {
    return `<tr odd" : ""}">
    <td>${id}</td>
    <td>${item.title}</td>
    <td>${item.quantity}</td>
    <td>${item.category}</td>
    <td><button data-id="${item.id}"  >
    <i ></i>
    <span>Delete</span>
    </button> <button data-id="${item.id}" >
    <i ></i>
    <span>Edit</span>
    </button></td>
  </tr>`;
  }
  private _selectCategoryHandler(e: Event) {
    const select = document.querySelector<HTMLSelectElement>(
      ".form__select-category"
    );
    this._categoryValue = select?.options[select?.selectedIndex].value!;
  }
  private _addButtonHandler() {
    const newProduct = new Product(
      inputTitle?.value!,
      this._categoryValue,
      Number.parseInt(inputQuantity?.value!)
    );
    this._productInventory?.add(newProduct);
    this._renderTable();
    this._closeModal();
  }
  private _renderTable(): void {
    tableBody!.innerText = "";
    const categories = this._productInventory.storage;
    const allCategory = categories.map((category: ICategory, index) => {
      return this._tableUIBody(<Product>category, index);
    });
    tableBody!.innerHTML  = allCategory.join("");
  }
}

import {
  tableBody,
  tableThead,
  inputTitle,
  btn,
  btnSubmit,
  modalContentCategory,
  modalContentProduct,
  modalHeader,
} from "./dom";
import Entity, { Types } from "./entity";
import { Category } from "./category";
import { View } from "./view";

export class CategoryView extends View {
  private _categoryInventory: Entity<ICategory>;
  constructor(categoryInventory: Entity<ICategory>) {
    super();
    console.log("create Category::", this);
    this._categoryInventory = categoryInventory;
    btn?.addEventListener("click", this._openModal);
    btnSubmit?.addEventListener("click", this._addButtonHandler);
  }

  initCategoryUI() {
    this._createModal();
    this._createHeaderTable();
    this._renderTable();
  }

  private _createModal() {
    modalContentProduct?.classList.add("hidden");
    modalContentCategory?.classList.remove("hidden");
    modalHeader!.innerHTML = "Add Category";
  }
  private _createHeaderTable(): void {
    if (tableThead) {
      tableThead.innerHTML = `<th>#</th>
        <th>Title</th>
        <th></th>`;
    }
  }
  _addButtonHandler() {
    const newCategory = new Category(inputTitle?.value!);
    this._categoryInventory.add(newCategory);
    this._renderTable();
    this._closeModal();
  }

  private _renderTable(): void {
    tableBody!.innerText = "";
    const categories = this._categoryInventory.storage;
    const allCategory = categories.map((category: ICategory, index) => {
      return this._tableUIBody(<Category>category, index);
    });
    tableBody!.innerHTML  = allCategory.join("");
  }
  private _tableUIBody(item: Category, id: number) {
    return `<tr odd" : ""}">
    <td>${id}</td>
    <td>${item.title}</td>
    <td><button data-id="${item.id}"  >
    <i ></i>
    </button> <button data-id="${item.id}" >
    <i ></i>
    </button></td>
  </tr>`;
  }
}

CodePudding user response:

el.addEventListener("click", () => this._selectedMenuHandler())

try to use arrow function which can retains the this value of the enclosing lexical context

  • Related