loading...

The Vanilla Javascript Component Pattern

megazear7 profile image megazear7 Updated on ・6 min read

I started delving into web components about a year ago. I really liked the idea of getting a reference to a custom element and then calling methods and setting values right on the custom element. After that I looked into Polymer 3.0, which layered on a number of conveniences and best practices. These specifically came in the area of templating, life cycle management, and property / attribute reflection. I proceeded away from Polymer 3.0 to using lit-element, and then finally just lit-html. I continued this process of stripping away the technologies while leaving the patterns, schemes, and best practices that I had learned. What I arrived at is something of a Vanilla Javascript Component Pattern (I might need a more specific name).

This pattern doesn't even use web components, as I wanted something that could be deployed across browsers without polyfills or any additional code that would need delivered to the browser. Not that this is difficult, or should be a barrier to usage of web components on a greenfield project, however I wanted something that could be used anywhere and everywhere.

Below is a very simply example of such a component. It uses ES6 classes and a plain template literal for producing the markup. It does some fancy stuff inside the constructor, and this code is essentially boilerplate that makes sure that each DOM element only has a single JavaScript object representing it. It does this by setting a data-ref attribute with a randomly generated ID. Then, when the ExampleComponent class is used and an instance of this class already exists for the provided DOM element, the reference to the already existing object is returned from the constructor. This allows a DOM element to be passes to this classes constructor multiple times, and only one instance of the class will ever exist.

export default class ExampleComponent {
  init(container) {
    this.container = container;
    this.render();
  }

  render() {
    this.container.innerHTML = ExampleComponent.markup(this);
  }

  static markup({}) {
    return `
      <h1>Hello, World!</h1>
    `;
  }

  constructor(container) {
    // The constructor should only contain the boiler plate code for finding or creating the reference.
    if (typeof container.dataset.ref === 'undefined') {
      this.ref = Math.random();
      ExampleComponent.refs[this.ref] = this;
      container.dataset.ref = this.ref;
      this.init(container);
    } else {
      // If this element has already been instantiated, use the existing reference.
      return ExampleComponent.refs[container.dataset.ref];
    }
  }
}

ExampleComponent.refs = {};

document.addEventListener('DOMContentLoaded', () => {
  new ExampleComponent(document.getElementById('example-component'))
});

You will notice that this renders the static "Hello, World!" value in an <h1> tag. However, what if we want some dynamic values? First, we'll update the class as shown below:

export default class ExampleComponent {
  set title(title) {
    this.titleValue = title;
    this.render();
  }

  get title() {
    return titleValue;
  }

  init(container) {
    this.container = container;
    this.titleValue = this.container.dataset.title;
    this.render();
  }

  render() {
    this.container.innerHTML = ExampleComponent.markup(this);
  }

  static markup({title}) {
    return `
      <h1>${title}</h1>
    `;
  }

  constructor(container) {
    // The constructor should only contain the boiler plate code for finding or creating the reference.
    if (typeof container.dataset.ref === 'undefined') {
      this.ref = Math.random();
      ExampleComponent.refs[this.ref] = this;
      container.dataset.ref = this.ref;
      this.init(container);
    } else {
      // If this element has already been instantiated, use the existing reference.
      return ExampleComponent.refs[container.dataset.ref];
    }
  }
}

ExampleComponent.refs = {};

document.addEventListener('DOMContentLoaded', () => {
  new ExampleComponent(document.getElementById('example-component'))
});

We now initialize the value with the data-title attribute on the container DOM element that is provided to the constructor. In addition, we provide setter and getter methods for retrieving and updating the value, and whenever the value is updated, we re-render the component.

However, what if we want sub components rendered as a part of this component?

export default class ExampleComponent {
  set title(title) {
    this.titleValue = title;
    this.render();
  }

  get title() {
    return titleValue;
  }

  init(container) {
    this.container = container;
    this.titleValue = this.container.dataset.title;
    this.render();
  }

  render() {
    this.container.innerHTML = ExampleComponent.markup(this);
    this.pageElement = this.container.querySelector('.sub-component-example');
    new AnotherExampleComponent(this.pageElement);
  }

  static markup({title}) {
    return `
      <h1>${title}</h1>
      <div class="sub-component-example"></div>
    `;
  }

  constructor(container) {
    // The constructor should only contain the boiler plate code for finding or creating the reference.
    if (typeof container.dataset.ref === 'undefined') {
      this.ref = Math.random();
      ExampleComponent.refs[this.ref] = this;
      container.dataset.ref = this.ref;
      this.init(container);
    } else {
      // If this element has already been instantiated, use the existing reference.
      return ExampleComponent.refs[container.dataset.ref];
    }
  }
}

ExampleComponent.refs = {};

document.addEventListener('DOMContentLoaded', () => {
  new ExampleComponent(document.getElementById('example-component'))
});

Notice that this time around, we add a div with a unique class name to the markup method. Then in the render method we get a reference to this element, and initialize an AnotherExampleComponent with that DOM element. Note: I have not provided an implementation here for AnotherExampleComponent. Lastly, what if we want our component to propagate events out of the component into parent components, or whatever code initialized or has a reference to our component?

export default class ExampleComponent {
  set title(title) {
    this.titleValue = title;
    this.render();
  }

  get title() {
    return titleValue;
  }

  init(container) {
    this.container = container;
    this.titleValue = this.container.dataset.title;
    this.render();
  }

  render() {
    this.container.innerHTML = ExampleComponent.markup(this);
    this.pageElement = this.container.querySelector('.sub-component-example');
    this.clickMeButton = this.container.querySelector('.click-me');
    new AnotherExampleComponent(this.pageElement);

    this.addEventListeners();
  }

  static markup({title}) {
    return `
      <h1>${title}</h1>
      <button class="click-me">Click Me</div>
      <div class="sub-component-example"></div>
    `;
  }

  addEventListeners() {
    this.clickMeButton().addEventListener('click', () =>
      this.container.dispatchEvent(new CustomEvent('click-me-was-clicked')));
  }

  constructor(container) {
    // The constructor should only contain the boiler plate code for finding or creating the reference.
    if (typeof container.dataset.ref === 'undefined') {
      this.ref = Math.random();
      ExampleComponent.refs[this.ref] = this;
      container.dataset.ref = this.ref;
      this.init(container);
    } else {
      // If this element has already been instantiated, use the existing reference.
      return ExampleComponent.refs[container.dataset.ref];
    }
  }
}

ExampleComponent.refs = {};

document.addEventListener('DOMContentLoaded', () => {
  new ExampleComponent(document.getElementById('example-component'))
});

Notice that we have now added an addEventListeners method which listens for events within the component. When the button is clicked, it dispatches an event with a custom name on the container, so that client code can listen to the specialized set of custom named events on the container, and does not need to be aware of the implementation details of the component itself. This is to say, that the container is the border between the client code and the implementation. The class itself should never reach outside of it's own container, and client code should never reach inside of the container for data or events. All data and events should be provided to the client through an interface of getter methods and events dispatched from the container.

All of this separation of concerns, encapsulation, and componetized development is possible in vanilla JS with no libraries, frameworks, or polyfills. Schemes and patterns are always better than frameworks and libraries, as I say all of the time. We also did not need web components to do this. However, where do the the benefits of web components and libraries come in?

First, web components are a platform enhancement, that turn the schemes and patterns presented here into rules for the platform. This means that with web components, the encapsulation and separation of concerns shown here cannot be broken down by client code, because the platform will enforce it. So if web components can be used, these best practices should be updated for web components (a blog post on that coming soon!).

Secondly, libraries can be helpful. So, if you have the room in your data budget for how much code to deliver to the client there are a few libraries that can assist us. Currently with this scheme its nothing other than the actual project code itself, as no libraries were needed. The main issue with this scheme is rendering the markup. Currently to re-render is expensive, and complex views can be complex to represent in a plain template literal. However we can use a tagged template literal library such as hyperHTML or lit-html in order to simplify the rendering process and speed up the re-rendering process. Keep in mind that while hyperHTML has been in production for over a year, lit-html is currently on the fact track for a 1.0 release.

I have this same post on my blog where I talk more about the latest and greatest web development patterns over frameworks.

Discussion

pic
Editor guide
Collapse
davidmulder profile image
David Mulder

There are indeed situations where it's sensible not to use libraries or frameworks, but those situations are rare and should only ever be touched upon by experienced senior developers. If I gave the code presented in this blog post to most of my colleagues I can guarantee that within a couple of months it will be completely and utterly unmaintainable. I am not sure I would be able to keep this code clean. This reminds me of the messes we had 10+ years ago.

If anything I think this blog post represents the trap most passionate (and often inexperienced) developers fall in (including me). We hate the overhead of libraries and/or think we can do better. It's good to reinvent the wheel once every while, because it's possible to improve, but we shouldn't reinvent the wheel ever single time we build a car. If the goal is to make private components for a project with a strong data pressure forget about web components and pick any library in the react-style (be careful with your transpilation settings). If you don't care about the amount of data go for a library in the angular-style. And if the component should be used by lots of people go for web components (including huge polyfill), or if the data thing is a huge problem make it a backend rendered component (yes, there is still a place for those).

The only thing you should never do (professionally) is go at it vanilla. There is nothing inheritly better about vanilla. Vanilla is just the set of base libraries decided upon by a small group of people. What's important is to pick the right set of tools. I mean, I get how tempting vanilla is and sometimes it can be the right set of tools if the requirements are extreme enough. Years ago I build a private application that actually performed and looked like native on mobile when that claim was still just an empty promise. The cost was that it was 100% unmaintainable.

Point is, I don't think you are doing anybody a favor with a post like this. It will just put more inexperience developers on the wrong path.

Collapse
quanla profile image
Quan Le

I agree that Vanilla component is a wrong and amateur idea, but, my friend, I think you are too fond of using frameworks, and that is much greater threat to your projects and your career.

I have 14+ years of experience and was a heavy framework user once, but now I only have ReactJS and NodeJS in my toolbox. Most frameworks today are created by amateurs that dont know how to solve problems in a simpler way, and promoted/adopted by a huge community of amateurs who know no better but love to make noise.

Don't start with questioning my skills or capabilities, I finishes hundreds of man month effort in days

Collapse
puritanic profile image
Darkø Tasevski

I would love to see your comment on reddit :)

Thread Thread
quanla profile image
Quan Le

Sorry, but I don't know what you mean, I which reddit post?

Thread Thread
puritanic profile image
Darkø Tasevski

r/reactjs, r/programming, r/javascript :) No post in particular but it would be fun to watch as your comment gets downvoted into oblivion.
Do you really think that Facebook's React team and Google's Angular team are made of amateurs? Lol

Thread Thread
quanla profile image
Quan Le

Ha ha. That should be fun. I would love to see it too. Pretty sure it will happen. About Angular and React team, I dont think they are amateurs, but still, our definition of amateur are very different

Collapse
kepta profile image
Kushan Joshi

David couldn’t agree with you more.
Code Maintainabilty is the most underrated thing in the world. While doing something like making a fancy vanilla Javascript is cool and will get you some pat on the back, it is not pragmatic and I doubt you would ever use beyond more than a blog post example.

Collapse
megazear7 profile image
megazear7 Author

I think frameworks work very well in software focused small to medium sized companies that can scope out there technological future very well, and can cultivate teams experienced in the framework that fits their scenario. However, imagine a scenario where you are working on something for a very large non-software focused company. You can't introduce a framework as the scope of the work is too small to make such long lasting decisions. You also want to build something that is still relevant 5 years down the line. You don't know who will be working on it or what there expertise will be. The platform stays relevant forever, frameworks do not. However like I said, frameworks provide an amazing benefit when used in an organizations that, you might say, respects their power.

So I think we might see eye to eye on this, if provided a specific scenario to problem solve for. However the only thing I would really take issue with is maintainability. If certain rules are followed in regards to what code is responsible for what, then this is certainly maintainable. JavaScript by it's nature has always been poor at enforcing separation of concerns, leaving it up to self governance from the developers. Luckily this is changing in updates to the language and web components.

Collapse
jmyrons profile image
jmyrons

This is the approach I have used and advocated for for the last 10 years however 'class' was not available until ecmascript 6 so I have learned how to do it using what's available by leveraging the power and nature of first class functions, closures, and the other features available in the Javascript language in the earlier versions of ecmascript. I applaud your approach because I have always written Javascript with the intent to avoid making my code dependent on frameworks and libraries. It would be nice if companies would come to realize this is the better approach to writing frontend code and not insisting on the use of frameworks!

Collapse
megazear7 profile image
megazear7 Author

I absolutely agree, especially with the comment about what companies are looking for. I keep in contact with a few recruiters in order to stay in touch with what companies are looking for. I don't know how many times I've had to explain to a interviewer that they need to look for people competent in the JavaScript language, not for people who have committed themselves to a particular framework.

Collapse
misterhtmlcss profile image
Roger K.

Now even as a junior developer I can attest to that frustration. It's crazy how focused they are on platform skills when that's just a doc away from doing x or y. I spend so much time trying to develop my JavaScript skills now. I don't even like calling myself a front-end developer because I feel like it had a frivolous connotation, like I can only do HTML, CSS and MAYBE a bit of JS. I'm like no, I love JS and I'm good at it. Anyway great article, but the discussion had been even more enjoyable.

Collapse
vitalyt profile image
Vitaly Tomilov

In the same spirit, to create DOM components in pure JavaScript, I implemented a small library just to wrap all my code into DOM components, per se:

github.com/vitaly-t/excellent

You still write everything directly for DOM, but it helps with reusability + lifespan + isolation.

Collapse
cashpipeplusplus profile image
cashpipeplusplus

Using innerHTML like this looks like a massive XSS vulnerability waiting to happen. Please don't do components like this. Your users will suffer the consequences when your page contents get hijacked.

Collapse
misterhtmlcss profile image
Roger K.

I thought innerHTML was only an issue if you were applying it with user input? I thought if it's my code and my data then I can use innerHTML. Crap! Please give me a link or a little more on this as I'm using it wrong and I thought I was being careful.

Collapse
cashpipeplusplus profile image
cashpipeplusplus

Of course, it is possible to use innerHTML without an XSS vulnerability, but it's much easier to reduce your XSS attack surface when you don't use it at all. You have to make that call for your project.

In my professional JS projects, we aren't building an end-user application, and we won't ultimately be in control, so we ban the use of innerHTML project-wide.

As an alternative, you can/should construct individual elements with createElement, and use innerText or createTextNode for the textual parts. For example:

const div1 = document.createElement('div');
div1.appendChild(document.createTextNode('It may be a pain, but this is '));

const em1 = document.createElement('em');
em1.innerText = 'worth';
div1.appendChild(em1);

div1.appendChild(document.createTextNode(' it!'));

document.body.appendChild(div1);

But if you find yourself doing this often... consider doing something else, like using a templating library or something. When we use this pattern at work, we're building a middle-ware library that constructs a DOM hierarchy with UI controls for something the app developer is building. It could have been done as a web component, but we just fill in an app-supplied div instead. There's not much text involved, except for labels and aria attributes, so it's not terribly burdensome. YMMV.

Thread Thread
megazear7 profile image
megazear7 Author

I have updated the contents of this blog on the source blog post to more directly address and discuss some of these concerns: alexlockhart.me/2018/07/the-vanill...

I agree, in most cases a trusted template library should be used. The point I wanted to stress in the blog post is that I think that in my opinion template literals should be the preferred method of rendering html from JavaScript, and that libraries can be layered on top of that basic scheme.

Collapse
megazear7 profile image
megazear7 Author

Correct you would need to do both JS and HTML encoding for security reasons. Ideally using a template library would also alleviate many conerns as well.

Collapse
hamzahamidi profile image
Hamza Hamidi

It's always fun to see a new VanillaJs component approach. You could also use Inheritance to reduce the amount of line in the CTOR, introduce lifecycles. But this could also become quickly unmaintainable.

Collapse
Sloan, the sloth mascot
Comment deleted
Collapse
megazear7 profile image
megazear7 Author

Yep, I think as a JavaScript community we've haven't done a good enough job teaching people when to apply patterns and when to integrate frameworks.

Collapse
quanla profile image
Quan Le

This post is great if you don't know what Reactive programming or how it can hugely improve your app architecture

Collapse
megazear7 profile image
megazear7 Author

I think updating DOM in a reactive way would be a great addition instead of rerendering the whole template each time. How might you suggest integrating reactive programming into the pattern I've described in the post?

Collapse
quanla profile image
Quan Le

I am not suggesting, ReactJS has done this very well (updating Dom in a reactive way) and it is 1 of the lib that I have to keep in my toolbox these day

Collapse
lgraziani2712 profile image
Luciano Graziani

I like this! Thank you.

Collapse
tscherrer05 profile image
tscherrer05

Great article !
Small mistake in your second example. It should be :
get title() {
return this.titleValue;
}

Collapse
frankdspeed profile image
Frank Lemanschik

@megazar7 you inspired me to introduce a other usefull pattern what do you think about that? dev.to/frankdspeed/the-html-compon...

Collapse
studiospindle profile image
Remi Vledder

Interesting approach. Using MVC to make this setup would perhaps be more scalable. Any thoughts on this @megazear7 ?

Collapse
gypsydave5 profile image
David Wickes

Schemes and patterns are always better than frameworks and libraries, as I say all of the time.

Amen.