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
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(' ', ' ')}</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
:
- Move any relevant state and utility methods (
getFirstNonMatchingChar
, etc.) fromHandChord
toNextCharsDisplay
. - Pass any necessary elements (
wholePhraseChords
,chordImageHolder
,svgCharacter
) toNextCharsDisplay
either in the constructor or via setter methods. - Ensure that
NextCharsDisplay
has a way to communicate back toHandChord
if it needs to perform actions outside of its scope (for example, callingsetWpm
).
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:
- Adjust each class to create its own
HTMLElements
and expose them through public methods or getters. - 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.