DEV Community

Cover image for How Vanilla JS is unscalable, and how can you...
Dario Mannu
Dario Mannu

Posted on

How Vanilla JS is unscalable, and how can you...

If you played with modern vanilla JS, whether you advocate it or oppose it surely you've come across or tried template literals to compose HTML:

const templateString = `
  <h1>this is a title</h1>
  <p>this is the content</p>
  <button>click me</button>
`;


targetElement.innerHTML = templateString;
Enter fullscreen mode Exit fullscreen mode

Today it's not just trendy to talk about components, but an undeniably essential part of scalability: reusable pieces of code that can give you anything from a simple button to a full-featured colour-picker or a file uploader.
You change your component in one place and it behaves the new way everywhere.

const Button = (label) => `
  <button>${label}</button>
`;

document.body.innerHTML = `
  <h1>A button example</h1>
  ${  Button('click me')  }
`;
Enter fullscreen mode Exit fullscreen mode

Simple as 1, 2, 3. So, what's next?
Next is making it interactive.
What if the button is clicked?

const clickHandler = () => alert('you clicked me');

const Button = (label) => {
  return `
    <button id="btn1">${label}</button>
  `;
};

document.body.innerHTML = `
  <h1>A button example</h1>
  ${  Button('click me')  }
`;

document.getElementById('btn1').addEventListener('click', clickHandler);
Enter fullscreen mode Exit fullscreen mode

So far, so good. It's plain, standard vanilla JS, and it just works!

Image description


The dark side of the Truth

Well, you know, the above all works, but try making dozens of components like that. What if you want to remove them? Who calls removeEventListener() every time?
Additionally, each component has two parts: the template on one side, and the even/data bindings somewhere else. Best programming practices mandate that the two be grouped together.

You may now realise that when you solve this problem you just created a UI library. So... welcome to the club, then! 🎆

When you solve any other related problem here (Routing? Inversion of control?), you've just created your shiny new JavaScript framework!

So where does all that "No JavaScript frameworks" hype go, in the end?

I think there's no way to solve this conundrum. Unless your application is never going to be anything more than a simple 1990's page posting a form to a server, you'll always ending up with something like a framework, good or bad, fast or slow, beloved or behated.

A framework is just an architectural construct.

Drawing a line

So, where do you draw a line? If you still refer to your self-evolved-webapp-turned-framework as a Vanilla JS app, how much would let you grow before finally admitting it's no longer vanilla? Once you created a router? Once you start organising your code in anything that looks like components?

Im my opinion, something starts to become a framework once you implement Inversion of Control

Essentially, as soon as you create a router that calls "views", or something like that, you got a framework.

The missing bit from template literals

Before evolving one of my projects into what eventually got to become Rimmel.js, I started a webapp, eaxctly like above. Template literals and a bit of glue code to attach and detach events. I like reactivity, so I used RxJS to treat events as observable streams.

const Component = () => {

  const clickHandler = (event) => doSomethingWith(event);

  return `
    <button onclick="${clickHandler}">click me</button>
  `;
}
Enter fullscreen mode Exit fullscreen mode

Why can we not do this with template literals? That was disappointing at best. The string you'd get from the above is <button onclick="(event) => doSomethingWith(event)">click me</button>, which is clearly not right and not what you'd hope for.

Same with Observables and Promises. We can use strings and numbers as expressions in template literals, but why can we not use Observables and Promises?

So I started to solve this very problem and it was born.

Rimmel does nothing more than just take your tagged templates, if you pass any strings or numbers as expressions then those will be just merged in the HTML. If they are Promises or Observables, they will be bound when the HTML is mounted to the DOM. If your expressions happen to be in a onclick="${ something }" area, then it will call addEventListener for you.

const Component = () => {
  const clickHandler = (event) => doSomethingWith(event);

  return rml`
    <button onclick="${clickHandler}">click me</button>
  `;
}
Enter fullscreen mode Exit fullscreen mode

Note this time we no longer used a template literal, but an rml-tagged template:

  rml`
    <div>your HTML here</div>
  `
Enter fullscreen mode Exit fullscreen mode

By doing so, rml, the entry point of Rimmel, will read your template, take note of all "future" expressions you pass in, like promises, observables and event handlers, and binds them once mounted.

Conclusion

If you're starting off with vanilla JS, you'll inevitably end up with a framework as you grow, either by migrating to an existing one or creating your own.

Some can be quite abstract and deviate more or less significantly from standard HTML/JS, like Angular, VanJS, React, Svelte, Vue. Others can try to stick with the HTML look-and-feel or sit somewhere between the two.
Rimmel.js strikes an interesting balance between sticking with HTML/JavaScript, yet introducing a very powerful twist to template literals, enabling you to conveniently make use of any event handlers, Promises and Observables, in a functional-reactive style.

Top comments (0)