[go: up one dir, main page]

DEV Community

Cover image for LitElement To-Do App
Westbrook Johnson
Westbrook Johnson

Posted on

LitElement To-Do App

And, how it compares to React as well as Vue.

This a cross-post of a Sep 24, 2018 article from Medium that takes advantage of my recent decision to use Grammarly in my writing (so, small edits have been made here and there), thanks for looking again if you saw it there 🙇🏽‍♂️ and welcome if this is your first time.

Yes, you’re right, LitElement needs a logo. What sort of code library gets big without a logo, right?

Yes, you’re right, LitElement needs a logo. What sort of code library gets big without a logo, right?

Edit: (1/20/19) Update code samples and repo to apply lit-element@2.0.0 and associated updates to the API available there. These changes include but are not limited to a pre-established this context of template-based event listeners, support for adoptedStyleSheets, updated property metadata, and default type serialization.

In the standard week of a software engineer, you’d be hard-pressed to avoid a good “this approach versus that” article or two. In the world of frontend, often this takes the shape of how framework or library X compares to the same in Y. This week mine took the shape of A comparison between Angular and React and their core languages. In other weeks it might be three or seven different articles! However, more articles a week does very little towards making sure you find really solid writing, logic, or learning in any one of these articles. I think we feed the self-fulfilling prophecy that the more something is written about the more others will also write about it. The cycle is even faster to the point of being almost unwanted when you focus specifically on what can be perceived as “major” players the likes of Angular, React, or Vue.

Sadly, almost as a rule, the more something is written about, the harder it is to find quality writings on the subject. That’s why it’s quite refreshing when you do find a quality comparison of technical applications in written form, and I did just that several weeks back when I was delivered Sunil Sandhu’s I created the exact same app in React and Vue. Here are the differences. Not only does the writing avoid explicit favoritism, despite Sunil making it clear that he’d worked predominantly with Vue up till the point of his writing, it went the extra step of not comparing the two allegorically but with real code; code with just enough complexity to get to the important points, and just enough simplicity to be parsable by the reader without investing inordinate amounts of time to the process. What’s more, as an engineer that’s only worked around the edges of React applications or on demo code, while having written not a line of Vue, I really felt I had gained a deeper understanding of each upon completing the article.

It’s definitely this sort of quality writing on a subject that inspires others to get into the game; even if it’s just me, it happened and you’re a part of it now, too! Sometimes this is a direct response in the vein of “I’ve got opinions I want to share in this area, too”, but for me over the last few weeks I just could stop thinking, “here’s the beautiful piece talking about React and Vue, where is the article doing to same for technologies I rely on?” As a long time creator of web components, and more recently a heavily invested user of LitElement, currently under furious development by the Polymer Project team at Google, I am keenly aware that there has yet to be built a beautiful library to house the literature on the subject. As it stands today, you might not even need a whole newsstand to store the written work on the subject. Here’s a short list of places you might choose to start:

However, much of this is focused on internal comparison. So, starting from the great work the Sunil had already shared with the world, here is my attempt to take his level headed comparison of these libraries at an application level one step further and include an analysis of the same app built with LitElement.

To that end, let’s get started!


Let’s assume I rewrite this structure someday.

Let’s assume I rewrite this structure someday.

There are certainly some differences in how the files are structured in this application. The Polymer CLI doesn’t support the src/public distinction that was on display in both the React and Vue applications, at least not right out of the box, so I chose not to fight it much. In support of that decision, you will see an index.html file in the top level of our application; this replaces src/main.js that you found in the Vue application and src/index.js in the React application as the entry point to the application. I’ve slimmed it down for the context of this being a demo, but even in the majority of delivery contexts there isn’t much more you need beyond:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="theme-color" content="#000000">
    <title>Lit-Element To Do</title>
    <link rel="stylesheet" href="src/index.css" />
  </head>
  <body>
    <noscript>
      You need to enable JavaScript to run this app.
    </noscript>
    <to-do></to-do>
    <script type="module" src="./src/ToDo.js"></script>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

There are still a few browsing contexts that require polyfills, and I like to rely on the type="module" to nomodule trick to support delivery of the smallest amount of transpilation in modern browsers, but beyond that there’s not much else you could want in an entry point to your to-do application.

Before we dive too deep, let’s take a look at what a LitElement based web component might look like:

Not a whole lot needed when you’re accepting the  raw `deleteItem` endraw  method from the parent element.
Not a whole lot needed when you’re accepting the deleteItem method from the parent element.

Web components can easily take on the single file component approach that you see with Vue, however here I’ve split out the styles into separate files. Uniquely, you’ll notice that the styles are imported from a JS file rather than a CSS file, this is to keep the import system applied herein more closely in line with what is possible in the browser and to take advantage of the capabilities provided by lit-html the rendering engine that underlies this base class offering.

The style import exports a TemplateResult with a full <style/> element for application in any other TemplateResult.

Above you have the styles as applied to a css template tag that supports the implementation of these style via Constructable Stylesheet Objects which allow your custom elements to share the same <style/> tag across multiple instances of itself. Applying your styles in this way will allow for greater performance as this feature becomes available in browsers and is shimmed internal to LitElement for browsers that have yet to implement this API. If you love the Vue approach of single file components, nothing is keeping you from placing this in the same file as your functionality and template. However, having the code split out like this makes the promotion of the styles included to shared styles (those used in multiple components across your code base) very easy.

How do we describe and mutate data?

static get properties() {
  return {
    list: {type: Array},
    todo: {type: String},
  };
}
constructor() {
  super();
  this.list = [
    this.todoItem('clean the house'),
    this.todoItem('buy milk')
  ];
  this.todo = '';
}
todoItem(todo) {
  return {todo}
}
Enter fullscreen mode Exit fullscreen mode

How did LitElement do that?

First things first, LitElement is extending HTMLElement, which means we’re making Custom Elements every time we use it. One of the first superpowers that custom elements give you is access to static get observedAttribute() which allows you to outline attributes on your element to observe. When these attributes change, attributeChangedCallback(name, oldValue, newValue) will be called which allows your element to respond to those changes. When using LitElement the properties listen in static get properties() automatically be added to static get observedAttribute() with the value of that attribute being applied to the property of the same name by default. If you want (or need) to extended functionality here, you can further customize how each property relates to the element’s attributes and relates to the rendering of the element. By adding an attribute key to the definition object, you can set the value to false when you don’t want the property in question to be settable via an attribute, or provide a string to outline a separately named attribute to observe for this property’s value. The converter property is used above to outline a specific how to serialize the value set to the observed attribute, it will default to the appropriate processing when the type property is set to Array, Boolean, Object, Number, String, however you can customize this with a single method for bi-directional serialization or an object with fromAttribute and toAttribute keys to outline the serialization that should occur for both consuming and publishing that attribute. reflect will track as a boolean whether the value of the property should be published directly to the attribute on all changes, and hasChanged allows you to prepare a custom method for testing whether changes to the property’s value should trigger an update to the element’s DOM. When a hasChanged method is not provided, this test is made by strict JS identity comparison meaning that the data managed as properties by LitElement plays well with immutable data libraries. This extended property definition might look like:

static get properties() {
  return {
    roundedNumber: {
      attribute: 'number',
      converter: {
        fromAttribute: (value) => Math.round(parseFloat(value)),
        toAttribute: (value) => value + '-attr'
      },
      reflect: true,
    },
  };
}
Enter fullscreen mode Exit fullscreen mode

Feel free to see that go by in real life via this Glitch. When defined as such, the value of this.roundedNumber would follow a lifecycle much like the pseudo code below:

<my-el                           // the `number` attribute of
  number="5.32-attr"             // <my-el/> is set so we
></my-el>                        // take the value, 5.32-attr
// run fromAttribute method
parseFloat('5.32-attr');         // parseFloat it, 5.32
Math.round(5.32);                // round it, 5
this.roundedNumber = 5;          // store it in `this.roundedNumber`
// CHANGE RECOGNIZED because 5 !== undefined;
// run toAttribute method
5 + '-attr';                     // append '-attr', '5-attr'
this.setAttribute(
  'number',
  '5-attr'
);                               // set it to the attibute
Enter fullscreen mode Exit fullscreen mode

However, this isn’t something we’ll need to take advantage of for a to-do app, so we should dive into that further as part of a future post.

What all this does under the covers is create a getter and a setter for each property to manage its value and to call the appropriate lifecycle methods when the values change as outlined in your hasChanged method. This means you can manipulate the state directly (i.e. this.name = ‘John’;) much like you would with Vue, however you’d fail to trigger an update to the template when not altering the identity of the data (this.list.push({todo:'Does not Mutate Data’}) doesn’t change the identity of the array, which means a new render isn’t triggered). However, additional flexibility in your dirty checking is supported as desired (i.e. hasChanged: (newValue, oldValue) => newValue > oldValue would trigger a change only when your value is increasing, so this.demoValue = this.demoValue + 1 would trigger a change, but this.demoValue = this.demoValue — 1 wouldn’t, if you saw a benefit in it). You also have the option to write your own custom getters and setters, but again...future post.

You’ll also see my addition of the todoItem method to abstracts the creation of a to-do item. This is in no way LitElement specific, but I felt it added both simplification and unification to the to-do code as it is used in initialization as well as in creating new to do items.

How do we create new To Do Items?

createNewToDoItem() {
  this.list = [
    ...this.list,
    this.todoItem(this.todo)
  ];
  this.todo = '';
}
Enter fullscreen mode Exit fullscreen mode

How did LitElement do that?

If the first thing you said was “that looks like a mix of both the React and Vue code to create a new to do item”, then you’d be right. The direct property access provided by Vue is alive and well with this.todo = ''; and the need for unique array/object identities of React is there too with the use of ...this.list, leveraging the spread operator to create an array with a unique identity while still including all of the data from the previous array. In this way, the pushing of data into the DOM and receiving it from an event is very similar to what was going on in the React application with only a few differences.

<input
  type="text"
  .value=${this.todo}
  @input=${this.handleInput}
/>
Enter fullscreen mode Exit fullscreen mode

You’ll notice the .value=${this.todo} syntax. Here you see the template set the property value to the value of this.todo. This is because value is one of the few attributes that doesn’t directly sync to the property of the same name in an <input/> element. While you can get the first value of this.todo to sync appropriately by setting the attribute only, future change (particularly those clearing the <input/> after creating a new to do) would not update the UI as expected. Using the property value (and thus the .value=${...} syntax) rather than the attribute solves that.

After that, you’ll see @input syntax which is very close to the event handling we saw in Vue. Here it is simply template sugaring for addEventListener('input',..., which is used here to trigger the pseudo-2-way binding that manages the value of this.todo. When an input event occurs on the <input/> element, the handleInput method is triggered as follows, setting the value of this.todo to the value of the <input/>. (Note: Here the input event is used as opposed to the change event. This is because change will only trigger after the blur event, which would prevent the Enter button from having data to trigger self-fulfillment of the “form”.)

handleInput(e) {
  this.todo = e.target.value;
}
Enter fullscreen mode Exit fullscreen mode

How do we delete from the list?

deleteItem(indexToDelete) {
  this.list = this.list.filter(
    (toDo, index) => index !== indexToDelete
  );
}
Enter fullscreen mode Exit fullscreen mode

How did LitElement do that?

Array.prototype.filter() is great for working with data in this context because by default it creates an array with a new identity. Here we directly set the value of this.list to the filtered array created by removing the item at index === indexToDelete and a new update to the DOM is requested in response to the change displaying the change.

To make this possible, we’ll first bind the deleteItem method to both this and the key (index) for the item in the array and pass it as a property into the <to-do-item/> element that displays individual to-dos.

<to-do-item
  item=${item.todo}
  .deleteItem=${this.deleteItem.bind(this, key)}
></to-do-item>
Enter fullscreen mode Exit fullscreen mode

This initial pass at the LitElement version was refactored directly from the React application, rather than a generated application, and as such shows how most of the techniques therein were possible in a LitElement context. However, there are some realities that this sort of approach to parent/child interactions that we should go over. So as not to disrupt the conversation around the two approaches relativity, I’ve grouped this with similar ideas in the Or do we have it? section below.

How do we pass event listeners?

<button
  class="ToDo-Add"
  @click=${this.createNewToDoItem}
>+</button>
Enter fullscreen mode Exit fullscreen mode

Here again, we see the Vue shorthand syntax pushing our events into React like handlers. However, as before, there’s only the slightest of magic (just straight sugar) in the template as it applies addEventListener to the element in question. You’ll also notice that the keypress event needs to be handled in its entirety as well.

<input
  type="text"
  @keypress=${this.handleKeyPress}
/>
Enter fullscreen mode Exit fullscreen mode

The event is processed directly for e.key === 'Enter' just like you would with VanillaJS.

handleKeyPress(e) {
  if (e.target.value !== '') {
    if (e.key === 'Enter') {
      this.createNewToDoItem();
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

How do we pass data through to a child component?

<to-do-item
  item=${item.todo}
  .deleteItem=${this.deleteItem.bind(this, key)}
></to-do-item>
Enter fullscreen mode Exit fullscreen mode

For each of our todos we need to pass down the value of item and deleteItem to accurately inflate the UI and trigger functionality on interaction. In both contexts, we’ve simplified the properties by pairing them directly to attributes so you would think that we could apply both directly as an attribute. This idea works great for item which is serialized as a String and as such easily transforms from an attribute to a property, but for the deleteItem method, passing a function this way is no good. That is why you’ll see the .deleteItem syntax signifying that we are setting this value as a property onto the <to-do-item/> element instead of as an attribute. We’ll discuss a caveat of this approach in the Or do we have it? section below.

How do we emit data back to a parent component?

<button class="ToDoItem-Delete"
  @click=${this.deleteItem}>-
</button>
Enter fullscreen mode Exit fullscreen mode

In that we’ve passed a bound method into the value of deleteItem when we hear the click event on our delete button we can call that method straight away and see its side effects in the parent element. As I mentioned in How do we delete from the list? this concept is something we’ll revisit in the Or do we have it? section below.

And there we have it! 🎉

In short order, we’ve reviewed some central concepts around using LitElement, including how we add, remove and change data, pass data in the form of properties and attributes from parent to child, and send data from the child to the parent in the form of event listeners. Hopefully, with the help of I created the exact same app in React and Vue. Here are the differences. this has been able to give you a solid introduction into how LitElement might compare to React or Vue when taking on the same application. However, as Sunil said best,

There are, of course, lots of other little differences and quirks between [these libraries], but hopefully the contents of this article has helped to serve as a bit of a foundation for understanding how [each] frameworks handle stuff

So, hopefully, this is but a beginning to your exploration, no matter which part of the ever-growing JavaScript ecosystem that exploration may take you.

Github link to the LitElement app:

https://github.com/westbrook/lit-element-todo

Github links to both of Sunil’s original apps:

https://github.comsunil-sandhu/vue-todo

https://github.comsunil-sandhu/react-todo

Or do we have it? (reviewing the effect of some differences)

If you have been enjoying the code only comparison of LitElement to React and Vue, please stop here. Beyond here be dragons, as it were. Having built a LitElement to do app in the visage of a React to do app, I wanted to look at what these features would look like relying on more common web component practices, and I wanted to share those in the context of this close comparison.

Reusability contexts

Part of the concept behind the componentization of the web is reusability. We want to be creating components that we can use in this app over and over again, while also having the possibility to use them in other apps both within our organizations and beyond. When thinking about this act as part of a Vue or React application where the only context for use of the components that you are creating is inside of a Vue or React application, it is easy to get caught in the ease and fun of things like passing a method to a child.

<to-do-item
  .deleteItem=${this.deleteItem.bind(this, key)}
></to-do-item>
Enter fullscreen mode Exit fullscreen mode

The parent will always be inside of an application and the child will always be inside of an application, so the technique just makes sense and has become commonplace. So commonplace, that is is often the first question I hear when engineers with experience in React start thinking about working in web components, “How do I pass methods to children?” Well, the answer is above. However, when you choose to do this, you are choosing to take away one of the superpowers of using the platform, and that’s the ability to work outside of an application. Have you ever had issues working with an <input/> element outside of an application? Ok, dumb question. Have those issues ever been something that a little visit to MDN couldn’t fix? However, this LitElement based <to-do-item/> element, and the equivalent <ToDoItem /> in the React app both expect to be delivered a method to call as deleteItem this means there would be no way to apply them with pure HTML that wouldn’t find them erroring out when clicked. <to-do-item></to-do-item> should be given the ability to be used in this to do app, in another to do app, or in anything really, and one of those options is directly in the HTML. To make this possible, we want to take a page out of the Vue to do app, and loosely couple our items without lists.

Loose coupling

Beyond the contexts of reuse, that passing a method into a child prevents, a child requiring a method be provided essentially creates an upward dependency chain that out current tools can’t ensure. import {foo} from './bar.js'; can ensure that the child dependency graph is static, but we have no concept of requiring functionality on a parent. This means that the implementer of our <to-do-item/> component has to grok this reality and manage the parents that it is deployed in as such. A tight coupling. The Vue to do app, avoids this for the most part by instead of calling a provided method it $emits an event when the delete button is clicked:

<div class=”ToDoItem-Delete” @click=”deleteItem(todo)”>-</div>

// ...

deleteItem(todo) {
  this.$emit('delete', todo)
}
Enter fullscreen mode Exit fullscreen mode

This, of course, requires a little more code, but the flexibility that it gives us is amazing. Here is the same code as applied to the LitElement based <to-do-item/>:

<button
  class="ToDoItem-Delete"
  @click=${this.deleteItem}
>-</button>

// ...

deleteItem() {
  const event = new CustomEvent('delete', {
    bubbles: true,
    composed: true,
    detail: {todo: this.todo}
  });
  this.dispatchEvent(event);
}
Enter fullscreen mode Exit fullscreen mode

A further benefit of this approach includes the ability for something other than the immediate parent being able to be listening to the event, something I can’t find adequate documentation on immediately for Vue’s $emit method. (This documentation seems to imply that it creates a non-bubbling event, but it isn’t exactly clear on the subject.) When bubbles === true the event will bubble up your application until e.stopPropagation() is called meaning that it can also be heard outside of your application. This is powerful for triggering far-reaching side effects as well as multiple side effects and having a direct debugging path to actions at various levels in your application or outside of it. Take a look at how that looks in the full application in the event branch.

Delivery size

react-scripts is shipped as a direct dependency of the React to do app in Sunil’s article, and one of the side benefits of that is that a yarn build command points into those scripts and prepares your code for production. The same concept is powered by vue-cli-service in the Vue version of the app. This is great being none of the things that make a developer’s life easier should get in the way of our users’ ease of use, and that includes not shipping development environment code to production. What’s even better is that using the command takes the React app from 388KB (down the wire)/1.5MB (parsed) development app down to just 58KB/187KB, which is a nice win for your users. What’s more, I’m sure the build process is still fairly naive as it comes to build processes and there would be room to shave off further size before actually delivering to production. Along those lines, I hacked preact-compat into the react-scripts based webpack config to see what it could do, and it moved the application to 230KB (over the wire)/875.5KB (parsed) for the development app with the production app clocking in at 19.6KB/56KB, a solid jump towards ideal. I look forward to my having brought it up here inspiring someone to create this app from scratch in Preact where I expect to see even better results! In the Vue app, you see a 1.7MB (over the wire and parsed) development app (there seems to be no GZIP on the Vue development server) taken down to an even smaller 44.5KB (over the wire)/142.8KB (parsed). While these are both great results, approaching the same concept through the use of polymer build (powered by the settings you’ll find in the polymer.json config for the app) takes a development application of 35.7KB (down the wire)/77.5KB (parsed) and turns it into a production-ready 14.1KB/59KB. This means the entire parsed size of the LitElement application is roughly the same as the over the wire size of the React app, and the over the wire size is only 1/3 that of the Vue app, huge wins on both points for your users. Tying these findings to the ideas outlined by Alex Russell in The “Developer Experience” Bait-and-Switch is a whole other post, but is highly important to keep in mind when going from a technical understanding of a library or framework to applying that library or framework in your code. These are the sorts of performance improvements that we won’t see on our $3000 MacBook Pros, but when testing with applied connection and CPU slowdowns on mobile with Lighthouse you start to get an understanding of what this might mean for a fully formed application. Much like high school chemistry, with these 💯 point grades, there is lots of nuance...

React To-Do App

React To-Do App Trace
No HTTP2 was used in the running of the Lighthouse Audit, however at 4 total requests, there shouldn’t have been many gains to be had there.

Preact To-Do App

Preact To-Do App Trace
No HTTP2 was used in the running of the Lighthouse Audit, however at 4 total requests, there shouldn’t have been many gains to be had there.

Vue To-Do App

Vue To-Do App Trace
No HTTP2 was used in the running of the Lighthouse Audit, however at 5 total requests, there shouldn’t have been many gains to be had there.

LitElement To-Do App

LitElement To-Do App Trace
No HTTP2 was used in the running of the Lighthouse Audit, however at 1 total requests, there shouldn’t have been many gains to be had there.

Yes, you‘re seeing that right, the LitElement to-do app gets to CPU Idle almost twice as fast as either the React or Vue applications with similar results across almost all of the metrics deemed important for this audit. Preact comes in at a virtual tie when deployed as a drop-in replacement for React, which most likely means it would run even smaller as the default build dependency. It’ll be interesting if that also cleans up some of the longer “First *” times seen in the audit. This means there is certainly more research to be done in load performance and points to a less clear decision on what is the best choice for managing the UI of your application. I’ll save thoughts on a future where Preact must continue to maintain its own component model and virtual DOM engine while lit-html has the possibility of stripping itself down even further via the pending Template Instantiation proposal for a future post.

Top comments (0)