mirror of
https://github.com/LukeHagar/unicorn-utterances.git
synced 2025-12-10 12:57:46 +00:00
Add lit articles
This commit is contained in:
@@ -1,13 +1,15 @@
|
||||
---
|
||||
{
|
||||
title: "Intro to Web Components: Vanilla JS",
|
||||
title: "Web Components 101: Vanilla JS",
|
||||
description: "",
|
||||
published: '2021-07-15T22:12:03.284Z',
|
||||
authors: ['crutchcorn'],
|
||||
tags: ['javascript', 'html', 'web'],
|
||||
attached: [],
|
||||
license: 'coderpad',
|
||||
originalLink: 'https://coderpad.io/blog/intro-to-web-components-vanilla-js/'
|
||||
originalLink: 'https://coderpad.io/blog/intro-to-web-components-vanilla-js/',
|
||||
series: "Web Components 101",
|
||||
order: 1
|
||||
}
|
||||
---
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
252
content/blog/web-components-101-framework-comparison/index.md
Normal file
252
content/blog/web-components-101-framework-comparison/index.md
Normal file
@@ -0,0 +1,252 @@
|
||||
---
|
||||
{
|
||||
title: "Web Components 101: Framework Comparison",
|
||||
description: "",
|
||||
published: '2021-12-02T22:12:03.284Z',
|
||||
authors: ['crutchcorn'],
|
||||
tags: ['lit', 'vue', 'react', 'angular'],
|
||||
attached: [],
|
||||
license: 'coderpad',
|
||||
originalLink: 'https://coderpad.io/blog/intro-to-web-components-vanilla-js/',
|
||||
series: "Web Components 101",
|
||||
order: 3
|
||||
}
|
||||
---
|
||||
|
||||
Alright alright, I know for a lot of the last article seemed like a big ad for Lit. That said, I promise I’m not unable to see the advantages of other frameworks. Lit is a tool in a web developer’s toolbox. Like any tool, it has its pros and cons: times when it’s the right tool for the job, and other times when it’s less so.
|
||||
|
||||
That said, I’d argue that using an existing framework is more often the better tool for the job than vanilla web components.
|
||||
|
||||
To showcase this, let’s walk through some of these frameworks and compare and contrast them to home-growing web components.
|
||||
|
||||
# Pros and Cons of Vanilla Web Components
|
||||
|
||||
While web frameworks are the hot new jazz - it’s not like we couldn’t make web applications before them. With the advent of W3C standardized web components (without Lit), doing so today is better than it’s ever been.
|
||||
|
||||
Here are some pros and cons of Vanilla JavaScript web components:
|
||||
|
||||
<table class="wp-block-table"> <tbody> <tr> <th> Pros </th> <th> Cons </th> </tr> <tr> <td> <ul> <li><span>No framework knowledge required</span></li> <li><span>Less reliance on framework</span></li> </ul> <ul> <li><span>Maintenance</span></li> <li><span>Bugs</span></li> <li><span>Security issues</span></li> </ul> <ul> <li><span>Smaller “hello world” size</span></li> <li><span>More control over render behavior</span></li> </ul> </td> <td> <ul> <li><span>Re-rendering un-needed elements is slow</span></li> <li><span>Handling event passing is tricky</span></li> <li><span>Creating elements can be overly verbose</span></li> <li><span>Binding to props requires element query</span></li> <li><span>You’ll end up building Lit, anyway</span></li> </ul> </td> </tr> </tbody> </table>
|
||||
|
||||
To the vanilla way of doing things’ credit, there’s a bit of catharsis knowing that you’re relying on a smaller pool of upstream resources. There’s also a lessened likelihood of some bad push to NPM from someone on the Lit team breaking your build.
|
||||
|
||||
Likewise - for smaller apps - you’re likely to end up with a smaller output bundle. That’s a huge win!
|
||||
|
||||
For smaller applications where performance is critical, or simply for the instances where you need to be as close to the DOM as possible, vanilla web components can be the way to go.
|
||||
|
||||
That said, it’s not all roses. After all, this series has already demonstrated that things like event passing and prop binding are verbose compared to Lit. Plus, things may not be as good as they seem when it comes to performance.
|
||||
|
||||
## Incremental Rendering
|
||||
|
||||
|
||||
On top of the aforementioned issues with avoiding a framework like Lit, something we haven’t talked about much is incremental rendering. A great example of this would come into play if we had an array of items we wanted to render, and weren’t using Lit.
|
||||
|
||||
Every time we needed to add a single item to that list, our `innerHTML` trick would end up constructing a new element for every single item in the list. What’s worse is that every subelement would render as well!
|
||||
|
||||
This means that if you have an element like this:
|
||||
|
||||
```html
|
||||
<li><a href=”https://example.com”><div class=”flex p-12 bg-yellow”><span>Go to this location</span></div></a></li>
|
||||
<li><a href=”https://example.com”><div class=”flex p-12 bg-yellow”><span>Go to this location</span></div></a></li>
|
||||
```
|
||||
|
||||
And only needed to update the text for a single item in the list, you’d end up creating 4 more elements for the item you wanted to update… On top of recreating the 5 nodes (including the [Text Node](https://developer.mozilla.org/en-US/docs/Web/API/Text)) for every other item in the list.
|
||||
|
||||
## Building Your Own Framework
|
||||
|
||||
As a result of the downsides mentioned, many that choose to utilize vanilla web components often end up bootstrapping their own home-grown version of Lit.
|
||||
|
||||
Here’s the problem with that: You’ll end up writing Lit yourself, sure, but with none of the upsides of an existing framework.
|
||||
|
||||
This is the problem with diving headlong into vanilla web components on their own. Even in our small examples in the article dedicated to vanilla web components, we emulated many of the patterns found within Lit. Take this code from the article:
|
||||
|
||||
```html
|
||||
<script>
|
||||
class MyComponent extends HTMLElement {
|
||||
todos = [];
|
||||
|
||||
connectedCallback() {
|
||||
this.render();
|
||||
}
|
||||
|
||||
// This function can be accessed in element query to set internal data externally
|
||||
setTodos(todos) {
|
||||
this.todos = todos;
|
||||
this.clear();
|
||||
this.render();
|
||||
}
|
||||
|
||||
clear() {
|
||||
for (const child of this.children) {
|
||||
child.remove();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
this.clear();
|
||||
|
||||
// Do logic
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
customElements.define('my-component', MyComponent);
|
||||
</script>
|
||||
<script> class MyComponent extends HTMLElement { todos = []; connectedCallback() { this.render(); } // This function can be accessed in element query to set internal data externally setTodos(todos) { this.todos = todos; this.clear(); this.render(); } clear() { for (const child of this.children) { child.remove(); } } render() { this.clear(); // Do logic } } customElements.define('my-component', MyComponent);</script>
|
||||
```
|
||||
|
||||
Here, we’re writing our own `clear` logic, handling dynamic value updates, and more.
|
||||
|
||||
The obvious problem is that we’d then have to copy and paste most of this logic in many components in our app. But let’s say that we were dedicated to this choice, and broke it out into a class that we could then extend.
|
||||
|
||||
Heck, let’s even add in some getters and setters to make managing state easier:
|
||||
|
||||
```html
|
||||
<script>
|
||||
// Base.js
|
||||
class OurBaseComponent extends HTMLElement {
|
||||
connectedCallback() {
|
||||
this.doRender();
|
||||
}
|
||||
|
||||
createState(obj) {
|
||||
return Object.keys(obj).reduce((prev, key) => {
|
||||
// This introduces bugs
|
||||
prev["_" + key] = obj[key];
|
||||
prev[key] = {
|
||||
get: () => prev["_" + key],
|
||||
set: (val) => this.changeData(() => prev["_" + key] = val);
|
||||
}
|
||||
}, {})
|
||||
}
|
||||
|
||||
changeData(callback) {
|
||||
callback();
|
||||
this.clear();
|
||||
this.doRender();
|
||||
}
|
||||
|
||||
clear() {
|
||||
for (const child of this.children) {
|
||||
child.remove();
|
||||
}
|
||||
}
|
||||
|
||||
doRender(callback) {
|
||||
this.clear();
|
||||
callback();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script> // Base.js class OurBaseComponent extends HTMLElement { connectedCallback() { this.doRender(); } createState(obj) { return Object.keys(obj).reduce((prev, key) => { // This introduces bugs prev["_" + key] = obj[key]; prev[key] = { get: () => prev["_" + key], set: (val) => this.changeData(() => prev["_" + key] = val); } }, {}) } changeData(callback) { callback(); this.clear(); this.doRender(); } clear() { for (const child of this.children) { child.remove(); } } doRender(callback) { this.clear(); callback(); } }</script>
|
||||
```
|
||||
|
||||
Now our usage should look fairly simple!
|
||||
|
||||
```html
|
||||
<script>
|
||||
// MainFile.js
|
||||
class MyComponent extends OurBaseComponent {
|
||||
state = createState({todos: []});
|
||||
|
||||
render() {
|
||||
this.doRender(() => {
|
||||
this.innerHTML = `<h1>You have ${this.state.todos.length} todos</h1>`
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('my-component', MyComponent);
|
||||
</script>
|
||||
<script> // MainFile.js class MyComponent extends OurBaseComponent { state = createState({todos: []}); render() { this.doRender(() => { this.innerHTML = `<h1>You have ${this.state.todos.length} todos</h1>` }) } } customElements.define('my-component', MyComponent);</script>
|
||||
```
|
||||
|
||||
That’s only 13 lines to declare a UI component!
|
||||
|
||||
Only now you have a bug with namespace collision of state with underscores, your `doRender` doesn’t handle async functions, and you still have many of the downsides listed below!
|
||||
|
||||
You could work on fixing these, but ultimately, you’ve created a basis of what Lit looks like today, but now you’re starting at square one. No ecosystem on your side, no upstream maintainers to lean on.
|
||||
|
||||
# Pros and Cons of Lit Framework
|
||||
|
||||
With the downsides (and upsides) of vanilla web components in mind, let’s compare the pros and cons of what building components using Lit looks like:
|
||||
|
||||
<table class="wp-block-table"> <tbody> <tr> <th> Pros </th> <th> Cons </th> </tr> <tr> <td> <ul> <li><span>Faster re-renders* that are automatically handled</span></li> <li><span>More consolidated UI/logic</span></li> <li><span>More advanced tools after mastery</span></li> <li><span>Smaller footprint than other frameworks</span></li> </ul> </td> <td> <ul> <li><span>Framework knowledge required</span></li> <li><span>Future breaking changes</span></li> <li><span>Not as widely known/used as other frameworks (Vue, React, Angular)</span></li> </ul> <p><span></span></p> </td> </tr> </tbody> </table>
|
||||
|
||||
While there is some overlap between this list of pros and cons and the one for avoiding Lit in favor of home-growing, there’s a few other items here.
|
||||
|
||||
Namely, this table highlights the fact that Lit isn’t the only framework for building web components. There’s huge alternatives like React, Vue, and Angular. These ecosystems have wider adoption and knowledge than Lit, which may make training a team to use Lit more difficult.
|
||||
|
||||
However, Lit has a key advantage over them, ignoring being able to output to web components for a moment - we’ll come back to that.
|
||||
|
||||
|
||||
Even compared to other frameworks, Lit is uniquely lightweight.
|
||||
|
||||
Compare the bundle sizes of Vue - a lightweight framework in it’s own right - compared to Lit.
|
||||
|
||||

|
||||
|
||||
While tree shaking will drastically reduce the bundle size of Vue for smaller applications, Lit will still likely win out for a simple component system.
|
||||
|
||||
# Other Frameworks
|
||||
|
||||
Lit framework isn’t alone in being able to output to web components, however. In recent years, other frameworks have explored and implemented various methods of writing code for a framework that outputs to web components.
|
||||
|
||||
|
||||
For example, the following frameworks have official support for creating web components without changing implementation code:
|
||||
|
||||
- [Vue](https://v3.vuejs.org/guide/web-components.html#definecustomelement)
|
||||
- [Angular](https://angular.io/guide/elements)
|
||||
- [Preact](https://github.com/preactjs/preact-custom-element)
|
||||
|
||||
Vue 3, in particular, has made massive strides in improving the web component development experience for their users.
|
||||
|
||||
What’s more is that these tools tend to have significantly larger ecosystems. Take Vue for example.
|
||||
|
||||
Want the ability to change pages easily? [Vue Router](https://router.vuejs.org/)
|
||||
|
||||
Want a global store solution? [Vuex
|
||||
](https://vuex.vuejs.org/)Prefer similar class based components? [Vue Class Component Library](https://class-component.vuejs.org/)
|
||||
|
||||
Prebuilt UI components? [Ant Design](https://www.antdv.com/docs/vue/introduce/)
|
||||
|
||||
While some ecosystem tools might exist in Lit, they certainly don’t have the same breadth.
|
||||
|
||||
That’s not to say it’s all good in the general web component ecosystem. Some frameworks, like React, [have issues with Web Component interop](https://custom-elements-everywhere.com/), that may impact your ability to merge those tools together.
|
||||
|
||||
# Why Web Components?
|
||||
|
||||
You may be asking - if you’re going to use a framework like Vue or React anyway, why even bother with web components? Couldn’t you instead write an app in one of those frameworks, without utilizing web components?
|
||||
|
||||
You absolutely can, and to be honest - this is how most apps that use these frameworks are built.
|
||||
|
||||
But web components play a special role in companies that have multiple different projects: Consolidation.
|
||||
|
||||
Let’s say that you work for BigCorp - the biggest corporation in Corpville.
|
||||
|
||||
BigCorp has dozens and dozens of full-scale applications, and not all of them are using the same frontend framework. This might sound irresponsible of BigCorp’s system architects, but in reality, sometimes a framework is better geared towards specific applications. Additionally, maybe some of the apps were part of an acquisition or merger that brought them into the company.
|
||||
|
||||
After all, the user doesn’t care (or often, know) about what framework a tool is built with. You know what a user does care about? The fact that each app in a collection all have vastly different UIs and buttons.
|
||||
|
||||

|
||||
|
||||
While this is clearly a bug, if both codebases implement the buttons on their own, you’ll inevitably end up with these types of problems; this being on top of the work-hours your teams have to spend redoing one-another’s work for their respective frameworks.
|
||||
|
||||
And that’s all ignoring how difficult it can be to get designers to have consistency between different project’s design components - like buttons.
|
||||
|
||||
Web Components solve this problem.
|
||||
|
||||
If you build a shared component system that exports web components, you can then use the same codebase across multiple frameworks.
|
||||
|
||||
Once the code is written and exported into web components, it’s trivial to utilize these new web components in your application. Like, it can be a [single line of code trivial.](https://v3.vuejs.org/guide/web-components.html#tips-for-a-vue-custom-elements-library)
|
||||
|
||||
From this point, you’re able to make sure the logic and styling of these components are made consistent between applications - even if different frameworks.
|
||||
|
||||
# Conclusion
|
||||
|
||||
While web components have had a long time in the oven, they came out swinging! And while Lit isn’t the only one at the table, they’ve certainly found a strong foothold in capabilities.
|
||||
|
||||
Lit’s lightweightness, paired with web component’s abilities to integrate between multiple frameworks is an incredible one-two punch that makes it a strong candidate for any shared component system.
|
||||
|
||||
What’s more, the ability to transfer knowledge from other frameworks makes it an easy tool to place in your toolbox for usage either now or in the future.
|
||||
|
||||
Regardless; whether you’re using Vue, React, Angular, Lit, Vanilla Web Components, or anything else, we wish you happy engineering!
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
566
content/blog/web-components-101-lit-framework/index.md
Normal file
566
content/blog/web-components-101-lit-framework/index.md
Normal file
@@ -0,0 +1,566 @@
|
||||
---
|
||||
{
|
||||
title: "Web Components 101: Lit Framework",
|
||||
description: "",
|
||||
published: '2021-11-04T22:12:03.284Z',
|
||||
authors: ['crutchcorn'],
|
||||
tags: ['web components', 'lit'],
|
||||
attached: [],
|
||||
license: 'coderpad',
|
||||
originalLink: 'https://coderpad.io/blog/web-components-101-lit-framework/',
|
||||
series: "Web Components 101",
|
||||
order: 2
|
||||
}
|
||||
---
|
||||
|
||||
Recently we talked about [what web components are and how you can build a web app utilizing them with only vanilla JavaScript](https://coderpad.io/blog/intro-to-web-components-vanilla-js/).
|
||||
|
||||
While web components are absolutely usable with only vanilla JavaScript, more complex usage, especially pertaining to value binding, can easily become unwieldy.
|
||||
|
||||
One potential solution might be using a web component framework such as VueJS or React. However, web-standard components can still be a massive boon to development.
|
||||
|
||||
As such, there’s a framework called [“Lit”](https://lit.dev/) that is developed specifically to leverage web components. With [Lit 2.0 recently launching as a stable release](https://lit.dev/blog/2021-09-21-announcing-lit-2/), we thought we’d take a look at how we can simplify web component development.
|
||||
|
||||
# HTML
|
||||
|
||||
One of the greatest strengths of custom elements is the ability to contain multiple other elements. This makes it so that you can have custom elements for every scale: from a button to an entire page.
|
||||
|
||||
To do this in a vanilla JavaScript custom element, you can use `innerHTML` to create new child elements.
|
||||
|
||||
```html
|
||||
<script>
|
||||
class MyComponent extends HTMLElement {
|
||||
connectedCallback() {
|
||||
this.render();
|
||||
}
|
||||
|
||||
render() {
|
||||
this.innerHTML = '<p>Hello!</p>';
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('hello-component', MyComponent);
|
||||
</script>
|
||||
|
||||
<hello-component></hello-component>
|
||||
```
|
||||
|
||||
This initial example looks fairly similar to what the Lit counterpart of that code looks like:
|
||||
|
||||
```html
|
||||
<script type="module">
|
||||
import { html, LitElement } from "https://cdn.skypack.dev/lit";
|
||||
|
||||
export class HelloElement extends LitElement {
|
||||
render() {
|
||||
return html`
|
||||
<p>Hello!</p>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define('hello-component', HelloElement);
|
||||
</script>
|
||||
|
||||
<hello-component></hello-component>
|
||||
```
|
||||
|
||||
<iframe src="https://app.coderpad.io/sandbox?question_id=194516" width="640" height="480" loading="lazy"></iframe>
|
||||
|
||||
There are two primary differences from the vanilla JavaScript example. First, we no longer need to use the `connectedCallback` to call `render`. The LitElement’s `render` function is called by Lit itself whenever needed - such as when data changes or for an initial render - avoiding the need to manually re-call the render method.
|
||||
|
||||
That said, Lit components fully support the same lifecycle methods as a vanilla custom elements.
|
||||
|
||||
The second, easier-to-miss change from the vanilla JavaScript component to the Lit implementation, is that when we set our HTML, we don’t simply use a basic template literal (`<p>test</p>`): we pass the function `html` to the template literal (`html\`<p>test</p>\``).
|
||||
|
||||
This leverages [a somewhat infrequently used feature of template literals called tagged templates](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#tagged_templates). Tagged templates allow a template literal to be passed to a function. This function can then transform the output based on the string input and expected interpolated placeholders.
|
||||
|
||||
Because tagged templates return a value like any other function, you can assign the return value of `html` to a variable.
|
||||
|
||||
```javascript
|
||||
render {
|
||||
const el = html`
|
||||
<p>Hello!</p>
|
||||
`;
|
||||
return el;
|
||||
}
|
||||
```
|
||||
|
||||
If you were to `console.log` this value, you’d notice that it’s not an [HTMLElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement). Instead, it’s a custom value that Lit utilizes to render to proper DOM nodes.
|
||||
|
||||
# Event Binding
|
||||
|
||||
“If the syntax is so similar, why would I add a framework to build custom elements?”
|
||||
|
||||
Well, while the Vanilla JavaScript and Lit custom element code look similar for a small demo: The story changes dramatically when you look to scale up.
|
||||
|
||||
For example, if you wanted to render a button and add a click event to the button with vanilla JavaScript, you’d have to abandon the `innerHTML` element assignment method.
|
||||
|
||||
First, we’ll create an element using `document.createElement`, then add events, and finally utilize [an element method like `append`](https://developer.mozilla.org/en-US/docs/Web/API/Element/append) to add the node to the DOM.
|
||||
|
||||
```html
|
||||
<script>
|
||||
class MyComponent extends HTMLElement {
|
||||
connectedCallback() {
|
||||
this.render();
|
||||
}
|
||||
|
||||
sayHello() {
|
||||
alert("Hi there!");
|
||||
}
|
||||
|
||||
render() {
|
||||
const button = document.createElement('button');
|
||||
button.innerText = "Say Hello!";
|
||||
button.addEventListener('click', this.sayHello);
|
||||
this.append(button);
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define('hello-component', MyComponent);
|
||||
</script>
|
||||
|
||||
<hello-component></hello-component>
|
||||
```
|
||||
|
||||
While this works for the initial render, it doesn’t handle any of the edgecases that,at scale,can cause long-term damage to your app’s maintainability & performance.
|
||||
|
||||
For example, future re-renders of the element will duplicate the button. To solve this, you must iterate through all of the element’s [`children`](https://developer.mozilla.org/en-US/docs/Web/API/Element/children) and [`remove`](https://developer.mozilla.org/en-US/docs/Web/API/Element/remove) them one-by-one.
|
||||
|
||||
Further, once the element is removed from the DOM, the click listener is not implicitly removed in the background. Because of this, it’s never released from memory and is considered a memory leak. If this issue continued to occur during long-term usage of your app, it would likely bloat memory usage and eventually crash or hang.
|
||||
|
||||
To solve this, you’d need to assign a variable for every `addEventListener` you had present. This may be simple for one or two events, but add too many and it can be difficult to keep track.
|
||||
|
||||
And all of this ignores the maintenance standpoint: What does that code do at a glance?
|
||||
|
||||
It doesn't look anything like HTML and as a result, requires you to consistently context shift between writing standard HTML in a string and using the DOM APIs to construct elements.
|
||||
|
||||
Luckily, Lit doesn’t have these issues. Here’s the same button construction and rendering to a custom element using Lit instead of vanilla JavaScript:
|
||||
|
||||
```html
|
||||
<script type="module">
|
||||
import { html, LitElement } from "https://cdn.skypack.dev/lit";
|
||||
|
||||
export class HelloElement extends LitElement {
|
||||
sayHello() {
|
||||
alert("Hi there!");
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<button @click=${this.sayHello}>Say Hello!</button>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define('hello-component', HelloElement);
|
||||
</script>
|
||||
|
||||
<hello-component></hello-component>
|
||||
```
|
||||
|
||||
<iframe src="https://app.coderpad.io/sandbox?question_id=194518" width="640" height="480" loading="lazy"></iframe>
|
||||
|
||||
Yup, that’s all. Lit allows you to bind elements by using the `@` sign and passing the function as a placeholder to the `html` tagged template. Not only does this look much HTML-like, it handles event cleanup, re-rendering, and more.
|
||||
|
||||
# Attributes & Properties
|
||||
|
||||
As we learned before, there are two ways to pass values between and into components: attributes and values.
|
||||
|
||||
Previously, when we were using vanilla JavaScript, we had to define these separately. Moreover, we had to declare which attributes to dynamically listen to value changes of.
|
||||
|
||||
```javascript
|
||||
class MyComponent extends HTMLElement {
|
||||
connectedCallback() {
|
||||
this.render();
|
||||
}
|
||||
|
||||
static get observedAttributes() {
|
||||
return ['message'];
|
||||
}
|
||||
|
||||
attributeChangedCallback(name, oldValue, newValue) {
|
||||
this.render();
|
||||
}
|
||||
|
||||
render() {
|
||||
const message = this.attributes.message.value || 'Hello world';
|
||||
this.innerHTML = `<h1>${message}</h1>`;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In Lit, we declare attributes and properties using a static getter and treat them as normal values in any of our functions.
|
||||
|
||||
```javascript
|
||||
import { html, LitElement } from "https://cdn.skypack.dev/lit";
|
||||
|
||||
export class HelloElement extends LitElement {
|
||||
static get properties() {
|
||||
return {
|
||||
message: {type: String},
|
||||
};
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.message = 'Hello world';
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<h1>${this.message}</h1>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define('hello-component', HelloElement);
|
||||
```
|
||||
|
||||
For starters, we no longer have to manually call “render” when a property’s value is changed. Lit will re-render when values are changed.
|
||||
|
||||
That’s not all, though: Keen eyed readers will notice that we’re declaring a type associated with the `message` property.
|
||||
|
||||
Unlike the [React ecosystem’s PropTypes](https://github.com/facebook/prop-types), the `type` subproperty doesn’t do runtime type validation. Instead, it acts as an automatic type converter.
|
||||
|
||||
This can be of great help as the knowledge that attributes can only be strings can be difficult to remember while debugging.
|
||||
|
||||
For example, we can tell Lit to convert an attribute to a Number and it will migrate from a string that looks like a number to an actual JavaScript type number.
|
||||
|
||||
```html
|
||||
<script type="module">
|
||||
import { html, LitElement } from "https://cdn.skypack.dev/lit";
|
||||
|
||||
export class HelloElement extends LitElement {
|
||||
static get properties() {
|
||||
return {
|
||||
val: {type: Number},
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<h1>${this.val} is typeof ${typeof this.val}</h1>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define('hello-component', HelloElement);
|
||||
</script>
|
||||
|
||||
<!-- This will show "123 is typeof number" -->
|
||||
<hello-component val="123"></hello-component>
|
||||
<!-- This will show "NaN is typeof number" -->
|
||||
<hello-component val="Test"></hello-component>
|
||||
```
|
||||
|
||||
<iframe src="https://app.coderpad.io/sandbox?question_id=194519" width="640" height="480" loading="lazy"></iframe>
|
||||
|
||||
## Attribute Reactivity
|
||||
|
||||
|
||||
One of the biggest benefits of not having to call `render` manually is that Lit is able to render contents when they need to update.
|
||||
|
||||
For example, given this example, the contents will render properly to update with new values.
|
||||
|
||||
```javascript
|
||||
import { html, LitElement } from "lit";
|
||||
|
||||
export class ChangeMessageElement extends LitElement {
|
||||
static get properties() {
|
||||
return {
|
||||
message: {type: String},
|
||||
};
|
||||
}
|
||||
|
||||
changeSelectedMsg() {
|
||||
const newMsg = msgs[Math.floor(Math.random() * msgs.length)];
|
||||
this.message = newMsg;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.message = 'Hello world';
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<button @click="${this.changeSelectedMsg}">Toggle</button>
|
||||
<hello-component message=${this.message}></hello-component>
|
||||
`;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<iframe src="https://app.coderpad.io/sandbox?question_id=181069" width="640" height="480" loading="lazy"></iframe>
|
||||
|
||||
# Reactive Data Binding
|
||||
|
||||
This reactivity comes with its own set of limitations. While numbers and strings are able to be set fairly trivially, objects (and by extension arrays) are a different story.
|
||||
|
||||
This is because, in order for Lit to know what properties to update in render, an object must have a different reference value from one to another. [This is just how React and other frameworks detect changes in state as well.](https://www.coletiv.com/blog/dangers-of-using-objects-in-useState-and-useEffect-ReactJS-hooks/)
|
||||
|
||||
```javascript
|
||||
export class FormElement extends LitElement {
|
||||
constructor() { /* ... */ }
|
||||
static get properties() {
|
||||
return {
|
||||
todoList: {type: Array},
|
||||
inputVal: {type: String},
|
||||
};
|
||||
}
|
||||
|
||||
_onSubmit(e) {
|
||||
e.preventDefault(); /* This works, because we’re changing the object reference */
|
||||
this.todoList = [...this.todoList, this.inputVal]; /* But this would not, because we aren’t */
|
||||
// this.todoList.push(this.inputVal); this.inputVal = '';
|
||||
}
|
||||
|
||||
_onChange(e) {
|
||||
this.inputVal = e.target.value;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<form @submit="${this._onSubmit}">
|
||||
<input .value="${this.inputVal}" @change="${this._onChange}" type="text" />
|
||||
<button type="submit">Add</button>
|
||||
</form>
|
||||
<todo-component todos=${this.todoList}></todo-component>
|
||||
`;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<iframe src="https://app.coderpad.io/sandbox?question_id=181090" width="640" height="480" loading="lazy"></iframe>
|
||||
|
||||
You may also notice that we’re binding both the user’s input and output to set and reflect the state. [This is exactly how other frameworks like React also expect you to manage user state](https://coderpad.io/blog/master-react-unidirectional-data-flow/).
|
||||
|
||||
# Prop Passing with Lit’s Dot Synax
|
||||
|
||||
HTML attributes are not the only way to pass data to a web component. Properties on the element class are a way to pass more than just a string to an element.
|
||||
|
||||
While the `type` field can help solve this problem as well, you’re still limited by serializability, meaning that things like functions won’t be able to be passed by attributes.
|
||||
|
||||
While properties are a more robust method of data passing to web components, they’re seldomly used in vanilla JavaScript due to their complexity in coding.
|
||||
|
||||
For example, this is a simple demonstration of passing an array.
|
||||
|
||||
```html
|
||||
<html>
|
||||
<head>
|
||||
<!-- Render object array as "ul", passing fn to checkbox change event -->
|
||||
<script>
|
||||
class MyComponent extends HTMLElement {
|
||||
property = [];
|
||||
|
||||
connectedCallback() {
|
||||
this.render();
|
||||
}
|
||||
|
||||
render() {
|
||||
this.innerHTML = `<h1>${this.property.length}</h1>`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('my-component', MyComponent);
|
||||
</script>
|
||||
|
||||
<script>
|
||||
function changeElement() {
|
||||
const compEl = document.querySelector('#mycomp');
|
||||
compEl.property = [
|
||||
'Testing',
|
||||
'Second',
|
||||
'Another'
|
||||
];
|
||||
compEl.render();
|
||||
}
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<my-component id="mycomp"></my-component>
|
||||
<button onclick="changeElement()">Change to 3</button>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
First, you have to get a reference to the element using an API like `querySelector`. This means you need to introduce a new reference to the component and make sure the IDs match in both parts of code.
|
||||
|
||||
Then, just as is the case with updating attribute values, we need to manually call the “render” function in order to update the UI.
|
||||
|
||||
But those complaints aside, there’s still one more: It places your data and component tags in two different areas. Because of this, it can be more difficult to debug or figure out what data is being passed to what component.
|
||||
|
||||
Lit takes a different approach. Within a Lit `html` tagged template, add a period before an attribute binding and suddenly it will pass as a property instead.
|
||||
|
||||
```html
|
||||
<script type="module">
|
||||
import { html, LitElement } from "https://cdn.skypack.dev/lit";
|
||||
|
||||
class MyElement extends LitElement {
|
||||
static get properties() {
|
||||
return {
|
||||
property: {type: Array},
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<h1>${this.property.length}</h1>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define('my-component', MyElement);
|
||||
|
||||
class ChangeMessageElement extends LitElement {
|
||||
static get properties() {
|
||||
return {
|
||||
array: {type: Array},
|
||||
};
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.array = [];
|
||||
}
|
||||
|
||||
changeElement() {
|
||||
this.array = [
|
||||
'Testing',
|
||||
'Second',
|
||||
'Another'
|
||||
];
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<!-- If "property" didn't have a period, it would pass as attribute -->
|
||||
<my-component .property=${this.array}></my-component>
|
||||
<button @click=${this.changeElement}>Change to 3</button>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define('change-message-component', ChangeMessageElement);
|
||||
</script>
|
||||
|
||||
<change-message-component></change-message-component>
|
||||
```
|
||||
|
||||
<iframe src="https://app.coderpad.io/sandbox?question_id=194520" width="640" height="480" loading="lazy"></iframe>
|
||||
|
||||
This works because properties and attributes are both created at the same time with Lit.
|
||||
|
||||
However, due to the period binding not being HTML standard, it comes with the side effect of having to use a Lit template in order to bind properties. This tends not to be a problem in applications - since many tend to use and compose components throughout their applications.
|
||||
|
||||
# Array Rendering
|
||||
|
||||
In our article about vanilla JavaScript web components, we built a simple todo list. Let’s take another look at that example, but this time using Lit for our component code. We’ll get started with a parent `FormElement`, which will manage the data and user input.
|
||||
|
||||
```javascript
|
||||
class FormElement extends LitElement {
|
||||
static get properties() {
|
||||
return {
|
||||
todoList: {type: Array},
|
||||
inputVal: {type: String},
|
||||
};
|
||||
}
|
||||
|
||||
_onSubmit(e) {
|
||||
e.preventDefault();
|
||||
this.todoList = [...this.todoList, {name: this.inputVal, completed: false}];
|
||||
this.inputVal = '';
|
||||
}
|
||||
|
||||
// ...
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<button @click=${this.toggleAll}>Toggle all</button>
|
||||
<form @submit=${this._onSubmit}>
|
||||
<input .value=${this.inputVal} @change=${this._onChange} type="text" />
|
||||
|
||||
<button type="submit">Add</button>
|
||||
</form>
|
||||
<!-- Notice the period in ".todos" -->
|
||||
<todo-component .todos=${this.todoList}></todo-component>
|
||||
`;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Now that we have a form that contains an array, an important question arises: how do we iterate through an array in order to create individual elements for a list?
|
||||
|
||||
Well, while [React has `Array.map](https://reactjs.org/docs/lists-and-keys.html)` and [Vue has `v-for`](https://v3.vuejs.org/guide/list.html#mapping-an-array-to-elements-with-v-for), Lit uses a `repeat` function. Here’s an example:
|
||||
|
||||
```javascript
|
||||
class TodoElement extends LitElement {
|
||||
// ...
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<ul>
|
||||
${repeat(this.todos, (todo) => html`
|
||||
<li>
|
||||
<input type="checkbox" .checked=${todo.completed}/>
|
||||
${todo.name}
|
||||
</li>
|
||||
`)}
|
||||
</ul>
|
||||
`;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<iframe src="https://app.coderpad.io/sandbox?question_id=181092" width="640" height="480" loading="lazy"></iframe>
|
||||
|
||||
# Passing Functions
|
||||
|
||||
Before we step away from code to talk pros and cons about Lit itself (shh, spoilers!); let’s take a look at a code sample that demonstrates many of the benefits over vanilla JavaScript web components we’ve talked about today.
|
||||
|
||||
Readers of the previous blog post will remember that when passing an array of objects to a web component, things looked pretty decent.
|
||||
|
||||
It wasn’t until we tried binding event listeners to an array of objects that things got complex (and messy). Between needing to manually create elements using `document`, dealing with `querySelector` to pass properties, manually calling “render”, and needing to implement a custom “clear” method - it was a messy experience.
|
||||
|
||||
Let’s see how Lit handles the job.
|
||||
|
||||
```javascript
|
||||
class TodoElement extends LitElement {
|
||||
// ...
|
||||
|
||||
render() {
|
||||
const headerText = this.todos
|
||||
.filter(todo => todo.completed).length;
|
||||
|
||||
return html`
|
||||
<h1>${headerText}</h1>
|
||||
<ul>
|
||||
${repeat(this.todos, (todo) => html`
|
||||
<li>
|
||||
<input type="checkbox" @change=${todo.onChange} .checked=${todo.completed}/>
|
||||
${todo.name}
|
||||
</li>
|
||||
`)}
|
||||
</ul>
|
||||
`;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<iframe src="https://app.coderpad.io/sandbox?question_id=181093" width="640" height="480" loading="lazy"></iframe>
|
||||
|
||||
You will notice that we’re using a `filter` within our `render` method. Because this logic is within the `render` method, it will run on every UI update. This is important to note in case you have expensive operations: you should avoid running those within the render method.
|
||||
|
||||
Outside of this, however - that’s all there is! It reads just like HTML would (with the added benefit of cleanup and prop passing), handles dynamic data, and more!
|
||||
|
||||
# Conclusion
|
||||
|
||||
The ability to leverage Lit in an application makes maintaining and improving a project easier than rolling web components yourself.
|
||||
|
||||
Lit demonstrates significant growth in web components from the early days of [Polymer](http://polymer-project.org/). This growth is in no small part due to the Lit team themselves, either!
|
||||
|
||||
Before it was a fully fledged framework, the project started from the `lit-html` package, which was an offshoot of Polymer. The Polymer team was instrumental in standardizing the modern variant of web components.
|
||||
|
||||
The ability to use Lit can strongly enhance web component development, but there are other options out there. Next time, we’ll talk about what the competitors are doing, what the pros and cons of each are, and how you can make the best choice for your applications.
|
||||
Reference in New Issue
Block a user