Blog

Inheritance vs Composition in Web Components

By  
Nii Yeboah
Nii Yeboah
·
On Jun 8, 2022 10:14:32 AM
·

blue background with a tree grid

As developers, we strive for efficiency – not only in our code, but also in the software development process itself. One way to achieve this efficiency is to avoid redundancy and prioritize code reuse, so that we do not end up reinventing the wheel.

Two tools that we have in our arsenal to achieve this are inheritance and composition. In two of my recent consulting projects, I found myself using inheritance to implement new, enhanced versions of Vaadin’s web components with some additional features. Those projects are both closed source but, in the past, I have used the same method to implement an open-source enhanced version of the Vaadin Dialog

In all these cases, I believe that my choice of utilizing inheritance led to much more readable and clean code and was crucial to delivering these projects in a timely fashion. The final products were also far more maintainable and extensible than their composition counterparts (not to mention the DRY-principle-violating, redundancy-causing choice of forking a whole codebase simply to add a couple features). 

This got me thinking about how a lot of frontend developers (especially coming from the React world), having read tips like “Composition over Inheritance”, treat them like dogmas, applying them unquestioningly to all situations, without taking the time to consider the nuances of both approaches and when it might be appropriate to use each (or both). In this tutorial, I show how we can create a couple of new components from the Vaadin Text Field using both techniques, and discuss the pros and cons of each.

Inheritance

Inheritance in OOP is when we derive a new class from an existing base class. The new class inherits all public and protected properties and methods from the parent class and can then  add its own new ones. The common explanations of when to use inheritance and composition talk about “has a” and “is a” relationships: "If you have an is-a relationship, use inheritance. If you have a has-a relationship, use composition." I would like to offer a slightly different perspective. 

The key aspects of inheritance to focus on are the inherited methods and properties or, in other words, the inherited API of the component. Web components are still relatively new in the industry and are only now starting to gain traction. However, they already have an undisputed major use case in design systems and making reusable framework-agnostic components that can be used across many web applications.

When making reusable web components and design systems, the API is probably the most important thing to consider. This is why, when trying to craft new versions of existing web components, very often inheritance is not only the best approach but probably the only correct way to do it. I refer to this as framework-level development. This is as opposed to application-level development, where we craft new components to be used in specific applications which might not be used anywhere else.

@customElement('search-field')
export class SearchField extends TextField {
  ready() {
    super.ready();
    this._setType('search');
    render(html`<vaadin-icon icon="lumo:search" slot="suffix"></vaadin-icon>`, this);
  }
}

In the code snippet above, we directly extend the Vaadin Text Field, call a private method to set the type of the internal native input field to be a search field, and then, finally, add a search icon for visual clarity. The specifics of this implementation aren’t important but, as you can see, with just those few lines of code, we have created a new component which provides users with a text field for searching that has better UX on mobile devices or any device with a dynamic keyboard.

Screenshot of a UI view with search field, URL field a text field

The Enter key gets replaced with a search icon, and a clear button is also added by default. More importantly, this new component will behave exactly like the Vaadin Text Field, with the same API and complete feature parity with the original. It can therefore be plugged into any app that uses a text field and be used in the exact same way.

Pro tip: if you would like to try extending Vaadin or other web components in a similar way, you may run into problems when using TypeScript. The IDE and TS compiler may complain about using private properties or methods if those components have not exposed them as protected. Instead of using // @ts-ignore comments, I would suggest creating a type declaration file in your codebase, similar to this:

declare module '@vaadin/text-field/src/vaadin-text-field.js' {
  import { TextField as PrivateTextField } from '@vaadin/text-field';
  class TextField extends PrivateTextField {
$: any;
protected ready(): void;
_setType(propName: string): void;

  }
  export { TextField };
}

Composition

We will now create a similarly simple new component from the text field – a URL field. However, this time, we will be using composition:

@customElement('url-field')
export class UrlField extends LitElement {
  @property({ type: Object, attribute: true }) ariaTarget?: HTMLElement;
  @property({ type: String, attribute: true })
  autocapitalize: 'on' | 'off' | 'none' | 'characters' | 'words' | 'sentences' = 'none';
  @property({ type: String, attribute: true }) autocomplete?: string;
  @property({ type: String, attribute: true }) autocorrect?: 'on' | 'off';
  @property({ type: Boolean, attribute: true }) autoselect = false;
  @property({ type: Boolean, attribute: 'clear-button-visible' }) clearButtonVisible = false;
  @property({ type: Boolean, attribute: true }) disabled = false;
  @property({ type: String, attribute: 'error-message' }) errorMessage?: string;
  @property({ type: String, attribute: 'helper-text' }) helperText = '';
  @property({ type: Boolean, attribute: true }) invalid = false;
  @property({ type: String, attribute: true }) label = '';
  @property({ type: Number, attribute: 'max-length' }) maxlength?: number;
  @property({ type: Number, attribute: 'min-length' }) minlength?: number;
  @property({ type: String, attribute: true }) name = '';
  @property({ type: String, attribute: true })
  pattern = '^((https?|ftp|smtp)://)?(www.)?[a-z0-9]+.[a-z]+(/[a-zA-Z0-9#]+/?)*$';
  @property({ type: String, attribute: true }) placeholder = '';
  @property({ type: Boolean, attribute: 'prevent-invalid-input' }) preventInvalidInput?: boolean;
  @property({ type: Boolean, attribute: true }) readonly = false;
  @property({ type: Boolean, attribute: true }) required = false;
  @property({ type: Object, attribute: 'state-target' }) stateTarget?: HTMLElement;
  @property({ type: String, attribute: true }) title = '';
  @property({ type: String, attribute: true }) value = '';
  @property({ type: String, attribute: true }) readonly theme?: string;
  @query('[part="input"]') inputElement?: TextField;
  readonly type = 'url';

The code snippet above only shows the properties required by our new URL field in order for it to retain the Text Field API and then also have the feature parity and interchangeability that the extension-based search field offered. This took an unreasonable amount of time to do and involved inspecting a lot of mixins and class hierarchies to find all the properties that needed to be mapped.

protected firstUpdated() {
    this.inputElement?._setType(this.type);
  }
  render() {
    return html`
      <vaadin-text-field
    part="input"
    .ariaTarget="${this.ariaTarget}"
    .autocapitalize="${this.autocapitalize}"
    .autocomplete="${this.autocomplete}"
    .autocorrect="${this.autocorrect}"
    .autoselect="${this.autoselect}"
    .clearButtonVisible="${this.clearButtonVisible}"
    .disabled="${this.disabled}"
    .errorMessage="${this.errorMessage}"
    .helperText="${this.helperText}"
    .invalid="${this.invalid}"
    .label="${this.label}"
    .maxlength="${this.maxlength}"
    .minlength="${this.minlength}"
    .name="${this.name}"
    .pattern="${this.pattern}"
    .placeholder="${this.placeholder}"
    .preventInvalidInput="${this.preventInvalidInput}"
    .readonly="${this.readonly}"
    .required="${this.required}"
    .stateTarget="${this.stateTarget}"
    .title="${this.title}"
    .value="${this.value}"
    .theme="${this.theme}"
      >
    <vaadin-icon icon="vaadin:link" slot="suffix"></vaadin-icon>
      </vaadin-text-field>
    `;
  }

We then create a render method with a Vaadin Text Field inside and map all the properties of our new component onto it. Note that we also had to use the Text Field’s private _setType method to set the text field’s internal native input to be a URL field. Even with all this code, the new component also needs some additional styling and may even require some event handlers to truly achieve feature parity and interchangeability. If you would like to see the full code for this and play with the demo, you can find it here.

Screenshot of a UI view with a search field, URL field that says "not a url" and a text field

Conclusion

In the world of frontend – or, more accurately, JavaScript and TypeScript – development, there has been a growing trend to abandon ‘old’, ‘boring’ OOP methodologies. I attribute this to the popularity of React.js and Redux and the growing influence of functional programming concepts in the industry. When using React, avoiding inheritance is definitely advised, but most developers probably haven't considered why this is.

React development is usually application-level development, so you are usually trying to craft components for use in that specific application. Therefore, maintaining an intuitive API is not that important. Web components could also be used in that way. But, when trying to create reusable components for design systems or frameworks to be used across multiple applications, inheritance becomes a necessary tool.

Ready to give Vaadin a try? Get started today!