0

Please advise how to implement an extension class for DOM elements in typescript. Lets say I want to add two methods enable and disable to the standard DOM element methods.

So far, i have to do this:

export class FormElement {
  public element: HTMLElement

  constructor(selector: string) {
    this.element = document.querySelector(selector)
  }

  public disable = (): void => {
    this.element.setAttribute('disabled', 'disabled')
  }

  public enable = (): void => {
    this.element.removeAttribute('disabled')
  }
}

At some place I want to get a DOM element extended by the FormElement class

class SomeComponent {
  ...
  private get nameInput(): FormElement {
    return new FormElement('input#company_msisdn')
  }  

  private disableNameInput = (): void => {
    this.nameInput.disable()
  }
}

Looks good, but if you need to use the DOM methods of an element, it does not look very good:

  ...
  private initializeNameInput = (): void => {
    this.nameInput.element.addEventListener('change', this.onNameInputChange)
  }

Tell me if there is a way to implement the FormElement class so that the call to the newly added methods and to the DOM methods of the element looked the same:

  this.nameInput.enable()
  this.nameInput.addEventListener('change', this.onNameInputChange)

?

For example i'm try create class with genric:

class FormElement<T = HTMLElement> {
  private this.element

  constructor(selector: string) {
    this.element = document.querySelector(selector) as T
  }

  ...
}

const nameInput = new FormElement<HTMLInputElement>('#name-input')
nameInput.disable()
nameInput.addEventListener('change', this.onNameInputChange)

But it is not right code, please advise how to implement class and generic.

4
  • As far as I understood, you want to get rid of intermediate element property? correct? I still don't understand what is wrong with this.nameInput.element.addEventListener('change', this.onNameInputChange) Commented Nov 26, 2021 at 7:53
  • .element. isnt clear solution for me. Commented Nov 26, 2021 at 8:17
  • 1
    Is that class really needed, since you are only encapsulating a private field, which is, as I understand, the thing, that you want to get rid of - a plain function like the following could help: const getElementFromDOM = (selector: string) => document.querySelector(selector); where as you can use it: const nameInput = getElementFromDOM(someSelector); nameInput.addEventListener(...)? What I mean is, is a functional approach feasible for you? Commented Nov 26, 2021 at 13:09
  • If you are doing OOP wither way, why not just inherit from the HTMLElement constructor? Commented Nov 26, 2021 at 13:41

1 Answer 1

3

I see your pain with the intermediate property 'element'. If your requirement isn't fixed on 'it must be a class'. I'd suggest to you, to some alternatives, e.g. inject functions into the prototype or complete functional style.

Below you find three few approaches on how this could be solved.

Approach 1 - Inject methods into the prototype

Consider this approach as syntactic sugar and can be very controversial.

// Types for your new methods, this extends the original HTMLElement interface
interface HTMLElement {
  enable: () => void;
  disable: () => void;
}

// Add concrete implementation of the new HTMLElement functions
HTMLElement.prototype.disable = function () {
  this.setAttribute('disabled', 'disabled');
}

HTMLElement.prototype.enable = function () {
  this.removeAttribute('disabled')
}

const getElementFromDOM = (selector: string): HTMLElement | null => document.querySelector(selector);

// usage
const nameInput = getElementFromDOM("input#name");
nameInput?.disable();
nameInput?.enable();

Downside: Newly added functions could interfere with already existing methods, for example, adding 'some' to the Array prototype would cause interference. This can be hard to track down or confuse others. A good keyword to mention here is 'prototype pollution'.

Approach 2 - Functional style

Alternatively, instead of expanding the HTMLElement prototype, you could provide extra methods which take a HTMLElement as parameter to disable/enable elements, this could look like this:

const disable = (element: HTMLElement) => element.setAttribute('disabled', 'disabled');
const enable = (element: HTMLElement) => element.removeAttribute('disabled');

// usage
const nameInput = getElementFromDOM("input#name");
disable(nameInput);
enable(nameInput);

Downside: There is nothing like nameInput.disable() and therefore it cannot be mixed with HTMLElement functions as stated in your question (e.g. nameInput.setAttribute(...)).

Approach 3 - Create a new element derived from HTMLElement

And if the previous approaches aren't an option for you and you definitely want a class the best approach to get rid of .element. is, to define a completely new element.

class FormElement implements HTMLElement {
 // Here you would have to implement all methods etc, which HTMLElement already has
}

Downside: I hope this is obvious to all, a huge amount of 're-implementing' already existing functionality.

Sign up to request clarification or add additional context in comments.

3 Comments

I like second approach the most. Dont think that changing the prototype is good idea
My personal approach would be to go with the second one. It's the one that's less likely to cause much re-work or confusion and seems the cleanest. Thanks for pointing out, I've added a small 'downside' description to each of the approaches.
Option 2 is probably the easiest to unit test too, so I'd go with that.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.