2024 05 05 refactoring and managing htmlelements in a component based architecture with typescript - bizkite-co/handterm GitHub Wiki


original file name: Codeium Chat - ik1Xg0Utno3IipxueIpjJE7ZXYKz0ybQ.md summary: The chat discussed refactoring code related to moving functions between classes and managing HTMLElements in a component-based architecture. The user considered having each component create its own HTMLElements and discussed a flexible design where components could either create their own elements or accept existing ones passed from a parent component. They also explored the idea of allowing the overriding of default elements with a setter method that performs validation. The conversation provided examples and suggestions on how to implement these design patterns effectively in TypeScript. date: 2024-05-05T13:52

Codeium Chat Conversation

Note: This is purely the output of the chat conversation and does not contain any raw data, codebase snippets, etc. used to generate the output.

User at 1:52 pm, May 5, 2024

I have the nextCharsDisplay displaying in XtermAdapter now, so abracting nextCharsDisplay from HandChord worked. Thanks for your help

Now I am realizing that in order for nextCharsDisplay to function as a separate component, I think I need this code to also be in class NextCharsDisplay, rather than class HandChord

  public setNext = () => {
    const nextIndex = this.getFirstNonMatchingChar();

    if (nextIndex < 0) {
      return;
    }
    // Remove the outstanding class from the previous chord.
    Array
      .from(this.wholePhraseChords?.children ?? [])
      .forEach((chord, i) => {
        chord.classList.remove("next");
      }
      );
    if (this.wholePhraseChords && nextIndex > this.wholePhraseChords.children.length - 1) return;

    let nextCharacter = `<span class="nextCharacter">${this.phrase?.value.substring(nextIndex, nextIndex + 1).replace(' ', '&nbsp;')}</span>`;

    console.log("nextIndex:", nextIndex);
    this.nextCharsDisplay.updateDisplay(nextIndex);

    const next = this.wholePhraseChords?.children[nextIndex] as HTMLElement;
    if (next) {
      if (this.nextChar) this.nextChar = next.getAttribute("name")?.replace("Space", " ") ?? "";
      next.classList.add("next");
      // If we're in test mode and the last character typed doesn't match the next, expose the svg.
      Array.from(next.childNodes)
        .filter((x): x is HTMLImageElement => x.nodeName == "IMG")
        .forEach((x: HTMLImageElement) => {
          x.width = 140;
          let charSvgClone = x.cloneNode(true) as HTMLImageElement;
          charSvgClone.hidden = this.testMode?.checked ?? false;
          if (this.chordImageHolder) this.chordImageHolder.replaceChildren(charSvgClone);

        });
    }
    if (this.svgCharacter && next) {
      const nameAttribute = next.getAttribute("name");
      if (nameAttribute) {
        this.svgCharacter.innerHTML = nameAttribute
          .replace("Space", spaceDisplayChar)
          .replace("tab", "↹");
      }
    }
    if (this.svgCharacter && !this.testMode?.checked) {
      this.svgCharacter.hidden = false;
    }
    this.setWpm();
    return next;
  };

Codeium at 1:53 pm, May 5, 2024

The method setNext in class HandChord seems to perform several tasks related to the wholePhraseChords element, updating the nextCharsDisplay, and managing SVG characters. If you want to move this functionality to class NextCharsDisplay, you will need to ensure that NextCharsDisplay has access to all necessary elements and state.

Here's a strategy for moving setNext into NextCharsDisplay:

  1. Move any relevant state and utility methods (getFirstNonMatchingChar, etc.) from HandChord to NextCharsDisplay.
  2. Pass any necessary elements (wholePhraseChords, chordImageHolder, svgCharacter) to NextCharsDisplay either in the constructor or via setter methods.
  3. Ensure that NextCharsDisplay has a way to communicate back to HandChord if it needs to perform actions outside of its scope (for example, calling setWpm).

Here's an example of how you might start to refactor the setNext function into NextCharsDisplay. This example assumes that you have moved or will move the necessary state and utility methods to NextCharsDisplay:

class NextCharsDisplay {
  // ... existing fields and methods ...

  // New fields for moved state and elements
  private wholePhraseChords: HTMLElement | null;
  private chordImageHolder: HTMLElement | null;
  private svgCharacter: HTMLElement | null;
  private testMode: HTMLInputElement | null;
  private setWpmCallback: () => void;

  // Example constructor modification to accept new elements and callback
  constructor(nextCharsElement: HTMLElement, wholePhraseChords: HTMLElement | null, chordImageHolder: HTMLElement | null, svgCharacter: HTMLElement | null, testMode: HTMLInputElement | null, setWpmCallback: () => void) {
    // ... existing constructor code ...
    this.wholePhraseChords = wholePhraseChords;
    this.chordImageHolder = chordImageHolder;
    this.svgCharacter = svgCharacter;
    this.testMode = testMode;
    this.setWpmCallback = setWpmCallback;
  }

  public setNext = () => {
    // ... moved setNext logic with modifications to work within NextCharsDisplay ...
  };

  // ... additional methods as needed ...
}

You will need to adjust the implementation details based on the actual responsibilities of NextCharsDisplay and how it interacts with the rest of your system. Remember to handle any dependencies setNext has on HandChord and ensure that it only manipulates state and elements that are relevant to the display of the next characters.


User at 8:56 pm, May 5, 2024

I am moving several functions from HandChord to NextCharsDisplay and I've already done that with other functions earlier today.

Each time I do it I have to move related HTMLElements between the classes, or to pass them to the other classes through the constructor.

What I am wondering is if it might not be better to have each class create and expose it's own HTMLElements and then let the other components append or prepend the other components HTMLElements into it's own, if it needs them, and show or hide them if it needs to.


Codeium at 8:57 pm, May 5, 2024

Having each class manage its own HTMLElements is a common and effective design pattern known as component-based architecture. This approach encapsulates the logic and presentation within each component, making your code more modular, reusable, and easier to maintain. Here are some benefits to this approach:

  • Encapsulation: Components control their own state and presentation, reducing the complexity of external interactions.
  • Reusability: Components can be reused in different parts of the application without modification.
  • Separation of Concerns: Each component has a specific responsibility, making the codebase easier to understand and debug.

Here's how you could refactor your code to follow this pattern:

  1. Adjust each class to create its own HTMLElements and expose them through public methods or getters.
  2. Allow components to accept child elements or components to be inserted into their DOM structure.

Here's an example of how you might refactor NextCharsDisplay to follow this approach:

class NextCharsDisplay {
  // ... existing fields ...

  // HTMLElements created by NextCharsDisplay
  private nextCharsElement: HTMLElement;
  private chordImageHolder: HTMLElement;

  constructor() {
    // Create your own HTMLElements
    this.nextCharsElement = this.createNextCharsElement();
    this.chordImageHolder = this.createChordImageHolder();
  }

  // Method to create the nextCharsElement
  private createNextCharsElement(): HTMLElement {
    const element = document.createElement('div');
    // ... additional setup ...
    return element;
  }

  // Method to create the chordImageHolder
  private createChordImageHolder(): HTMLElement {
    const element = document.createElement('div');
    // ... additional setup ...
    return element;
  }

  // Public getter to expose the nextCharsElement
  public getNextCharsElement(): HTMLElement {
    return this.nextCharsElement;
  }

  // Public getter to expose the chordImageHolder
  public getChordImageHolder(): HTMLElement {
    return this.chordImageHolder;
  }

  // ... additional methods as needed ...
}

// Usage in another class or component
const nextCharsDisplay = new NextCharsDisplay();
const someContainerElement = document.getElementById('some-container');
someContainerElement.prepend(nextCharsDisplay.getNextCharsElement());

When integrating components, you would use methods or getters to retrieve the elements and then append or prepend as needed. This makes it clear which components own which elements and reduces the coupling between components.

Remember to update any references to the moved HTMLElements in both NextCharsDisplay and HandChord to reflect the new ownership and ensure the components communicate effectively when necessary.


User at 9:35 pm, May 5, 2024

I think there is a way with that arrangement that I could have each component create it's own element, or let the parent component assign an already existing HTMLElement.


Codeium at 9:35 pm, May 5, 2024

Yes, that's a flexible design choice. You can allow each component to either create its own element or accept an existing one passed from a parent component. This pattern is often used in frameworks like React, where a component can either generate its own DOM or be composed with children elements passed as props.

Here is how you could implement this in TypeScript:

class NextCharsDisplay {
  private nextCharsElement: HTMLElement;
  private chordImageHolder: HTMLElement;

  constructor(nextCharsElement?: HTMLElement, chordImageHolder?: HTMLElement) {
    this.nextCharsElement = nextCharsElement || this.createNextCharsElement();
    this.chordImageHolder = chordImageHolder || this.createChordImageHolder();
  }

  private createNextCharsElement(): HTMLElement {
    const element = document.createElement('div');
    // ... additional setup ...
    return element;
  }

  private createChordImageHolder(): HTMLElement {
    const element = document.createElement('div');
    // ... additional setup ...
    return element;
  }

  public getNextCharsElement(): HTMLElement {
    return this.nextCharsElement;
  }

  public getChordImageHolder(): HTMLElement {
    return this.chordImageHolder;
  }

  // ... additional methods as needed ...
}

// Usage in a parent component
const existingNextCharsElement = document.getElementById('next-chars-display');
const existingChordImageHolder = document.getElementById('chord-image-holder');

// Pass existing elements to the constructor
const nextCharsDisplay = new NextCharsDisplay(existingNextCharsElement, existingChordImageHolder);

// Or let the component create its own elements
const nextCharsDisplayWithNewElements = new NextCharsDisplay();

This approach gives you the best of both worlds: the convenience of components creating and managing their own elements, along with the flexibility to integrate with existing DOM structures when needed.

Keep in mind that when you allow an external element to be passed in, you need to handle it carefully to avoid breaking the encapsulation of your component. Make sure that the external interactions are well-documented and that the component's integrity is preserved when integrating with elements it doesn't own.


User at 9:39 pm, May 5, 2024

I think I want to create all the required elements in the constructor, without requiring any parameter, but then also allow that same element to be overridden with a setNextCharsElement(nextCharsElement: HTMLElement) that can even validate that the passed in element is of the right type. This will give me maximum flexibility while refactoring.


Codeium at 9:39 pm, May 5, 2024

That sounds like a good approach, balancing flexibility with encapsulation. You can provide a default element creation in the constructor and then allow for the element to be set or replaced later using a setter method that can also perform validation.

Here's an example of how you can implement this in TypeScript:

class NextCharsDisplay {
  private nextCharsElement: HTMLElement;
  private chordImageHolder: HTMLElement;

  constructor() {
    this.nextCharsElement = this.createNextCharsElement();
    this.chordImageHolder = this.createChordImageHolder();
  }

  private createNextCharsElement(): HTMLElement {
    const element = document.createElement('div');
    // ... additional setup ...
    return element;
  }

  private createChordImageHolder(): HTMLElement {
    const element = document.createElement('div');
    // ... additional setup ...
    return element;
  }

  public setNextCharsElement(nextCharsElement: HTMLElement): void {
    if (this.validateNextCharsElement(nextCharsElement)) {
      this.nextCharsElement = nextCharsElement;
    } else {
      throw new Error('Invalid element passed to setNextCharsElement');
    }
  }

  private validateNextCharsElement(element: HTMLElement): boolean {
    // Perform validation logic to ensure it's the right type of element
    // For example, you might check for a specific class name or data attribute
    return element.classList.contains('expected-class-name');
  }

  public getNextCharsElement(): HTMLElement {
    return this.nextCharsElement;
  }

  public getChordImageHolder(): HTMLElement {
    return this.chordImageHolder;
  }

  // ... additional methods as needed ...
}

// Usage
const nextCharsDisplay = new NextCharsDisplay();

// Later, if you need to replace the nextCharsElement with an existing one:
const existingNextCharsElement = document.getElementById('next-chars-element');
nextCharsDisplay.setNextCharsElement(existingNextCharsElement);

In this setup, NextCharsDisplay starts with creating its own elements, but allows replacement with setNextCharsElement. The validateNextCharsElement method ensures that only suitable elements are assigned as nextCharsElement. This method can be customized based on your specific validation needs.

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