LEGO: Modern Web-Components
LEGO (Lightweight Embedded Gluten-free Objects) is a NodeJS tool to build
Lego is:
π Minimalist:7461 lines of readable code in its core (non-optimised, uncompressed, no cheating).π± Low dependency: its single third-party is the minimalist Petit-Dom which itself has no dependencyβ»οΈ? Reactive: updating the state recalculate the Virtual Dom when neededπ fast: using virtual dom through a thin layer makes it close to bare-metalπ§ Simple: that's Vanilla, there isn't much to know, it's a raw class to extend; no magicβ¨ π?‘ Native: webcomponents are actual native webcomponents, you benefit from all the today's and tomorrow's possibilites (slot, encapsulation, β¦).
Lego is not (and will never be):
π?― A full bloated frontend framework with routing. Others do it well.π? A website builder with SSR or similar complexities.π? An HTML replacement that locks you into a specific technology.
View the demo and their source
Lego is inspired from the native Web-Component spec and Riot.
It's just much lighter with simplicity, source that are easy to read, to hack and to maintain. The core lib is only 61 LOC! Lego is as light as 3Kb for the full bundle!
Demo: view in action
Installation
Lego is based on npm and the latest node.
You need to install the compiler with npm i @polight/lego
Quick start
Hello World
Create a file called bricks/hello-world.html:
<template>
<p>Hello ${state.name}</p>
</template>
<script>
init() { this.state = { name: "World!" } }
</script>Compile with npx lego bricks
And use it in your index.html
<script src="./dist/index.js" type="module"></script>
<hello-world />Run a local web server (ie: python3 -m http.server) and display your index.html (ie: http://localhost:8000).
More Advanced Web-Component Example
bricks/user-profile.html
<template>
<div :if="state.registered">
<h1>${state.firstName} ${state.lastName}'s profile</h1>
<p>Welcome ${state.firstName}!</p>
<section :if="state.fruits.length">
<h3>You favorite fruits:</h3>
<ul>
<li :for="fruit in state.fruits">${fruit.name} ${fruit.icon}</li>
</ul>
</section>
<button :if="!state.registered" @click="register">Register now</button>
</div>
</template>
<script>
init() {
this.state = {
registered: false,
firstName: 'John',
lastName: 'Doe',
fruits: [{ name: 'Apple', icon: 'π?' }, { name: 'Pineapple', icon: 'π??' }]
}
}
register() {
this.render({ registered: confirm('Do you want to register?') })
}
</script>Compile this component: npx lego bricks
Then include it in your page:
index.html
<user-profile></user-profile>
<script src="./dist/index.js" type="module"></script>Run your webserver and see your little app!
When developing you may want to automatically watch files changes. In that case pass the
-wflag:npx lego -w bricks
Tip: you probably want to store this task with a shortcut like
npm run watch. To do so just add"watch": "lego -w bricks"in you package.json scripts.
Understanding Webcomponents
A component can optionally have 3 parts: some HTML in a <template> tag, some JavaScript
in a <script> tag and some CSS in a <style> tag.
Template tag
An HTML template is written within a <template> tag.
These superpowers can be recognized with their : or @ prefixes.
The possible directives are:
:ifto display a tag based on a condition:forto repeat a tag:to evaluate a string@to bind an event
Note that
:ifand:forattributes, when used in the same tag should be used with an order in mind:<a :if="user" :for="user in state.users">won't work, but<a :if="state.users.length" :for="user in state.users">will first evaluate if theusersarray has items, or<a :for="user in users" :if="user">will check that each individual user has a value.
<a :if="state.isAdmin" :for="user in state.users">won't loop at all ifisAdminis false.
:if Directive
Conditionally display a tag and its descendants.
Example: <p :if="state.count < 5">Less than 5</p> will be displayed if the condition is met.
:for Directive
Repeat a tag based on a property.
The syntax is as follow: :for="item in state.items".
The item value will be available trough ${item} within the loop.
If you need an incremental index i, use :for="item, i in state.items".
Example: <li :for="attendee in state.attendees">${attendee}</li> with a state as
this.state = { attendees: ['John', 'Mary'] } will display <li>John</li><li>Mary</li>
: Custom Directive
A custom directive will interpret in JS whatever you pass as value.
<template>
<a :href="this.getUrl('144')">Visit Profile</a>
</template>
<script>
getUrl(id) { return `/user/${id}` }
</script>outputs
<a href="/user/144">Visit Profile</a>
Example: <input type=checkbox :checked="state.agreed" :required="state.mustAgree">.
With the following state: this.state = { agreed: false, mustAgree: true } would render
<input type=checkbox required="required">.
@ Directive for binding Events
<template>
<button @click="sayHi" name="the button">click</button>
<script>
sayHi(event) {
alert(`${event.target.getAttribute('name')} says hi! ππ?Ό`)
}
</script>Reactive Properties
The state is where the reactiveness takes place.
declare a state object in the init() function with default values:
init() {
this.state = {
user: { firstname: 'John', lastname: 'Doe' },
status: "Happy π"
}
}Displaying a state value is as simple as writing ${state.theValue} in your HTML.
When you need your component to react, call the this.render() method
with your updated state:
itemSelected(event) {
this.render({ selected: "apple", isAdmin: true })
}
This will refresh your component where needed.
When state is just mutated, the changed(changedProps) is called.
This changed() method is called before (re-)rendering.
Component Attributes
Attributes declared on the components will be all be accessible through the state.
If the property is initialized in the this.state, the attribute will be reactive:
<x-user status="thinking π€"><x-user>status will therefore be reactive and the thinking
state won't be reactive.
These properties can be accessed through this.getAttribute() from within the component.
After all, these components are just native!
Slots
Slots are part of the native web-component. Because Lego builds native web-components, you can use the standard slots as documented.
Example:
index.html
<user-profile>
<span>This user is in Paris</span>
<user-profile>bricks/user-profile.html
<template>
<h1>User profile</h1>
<p>important information: <slot></slot></p>
</template>Will write β¦<p>important information: <span>This user is in Paris</span></p>
Reactive CSS Style
CSS is much more fun when it's scoped. Here it come with the web-components.
Here again, no trick, just the full power of web-components and scoping styles.
Well, you should know that the css is reactive too!
Writing CSS is as easy as
<template>
<h1>Bonjour!</h1>
</template>
<script>
init() {
this.state = { fontScale: 1 }
}
</script>
<style>
:host {
font-size: ${state.fontScale}rem;
}
h1 {
padding: 1rem;
text-align: center;
}
</style>Host
:host is a native selector
for web-components.
It allows to select the current component itself.
Variables
You can use variables in your CSS just like in your templates.
Example:
<template>
<h1>Bonjour<h1>
</template>
<script>
init() {
this.state = { color: '#357' }
}
</script>
<style>
h1 {
color: ${ state.color };
}
</style>will apply the #357 color onto h1.
Compiling
LEGO_URL=</url/to/lego.min.js> npx lego <source_path> <target_file_path>Would compile the source_path file or folder (recursively) into target_file_path js file using lego.min.js from the declared url.
As mentioned before, when developing you probably want to watch for changes with the -w
option: npx lego -w <source_path> <target_file_path>
source_path: either a file or a directory (relative or absolute). If it's a directory, it will recursively read all the .html files and compile them into the target_file.
target_file_path: (default: components.js) the path (relative or absolute) to a .js file. That file will be created and contain all the components.
Naming a component
The name of the file will be the name of the component.
Example: components/x-button.html will create <x-button> component.
However in some cases you may want to give your component a different name than the file.
To do so, you may give your template a name with the name attribute.
Example:
components/x-button.html:
<template name="my-super-button"></template>Will create a <my-super-button> component.
Note that because it builds native web-components, the naming convention must respect the ones from the standards (lowercase, with a dash in the name, starting with a letter, β¦)
Testing
Running tests 
Just install node dev dependencies (npm install) and run the tests (npm test).
Under the hood
Native web-components
Because Lego is actual native web-components, all its native possibilities (like slots), :host and whatever exists or will exist are on board.
Browser compatibility
Lego is based on native customElements. Support for customElement is spreading and shall increase in time.
When building a web-app you may have control of the browsers. If you're building a more general website you may need to increase the overall browser compatibility and install the custom-element polyfill.
Dependencies
It is still fully compatible with native custom elements. No magic behind the scene, no complexity, just a couple of useful methods to write native web-components easier.
Using a compiled component has no dependency, nothing extra injected in the browser. Compiling depends on jsdom.
