We have a html
tag function, which helps to construct HTML templates:
import { html } from '@in-wave/symbiote';
let mySymbioteTemplate = html`<div>Hello world!</div>`;
More detailed template syntax example:
<h1>{{heading}}</h1>
<button ${{onclick: 'onButtonClicked'}}>Click me!</button>
<my-component ${{
textContent: 'someText',
'style.border': 'myComponentBorder',
'$.data': 'someData',
}}></my-component>
<ul ${{list: 'myListData'}}>
<li>{{listItemName}}</li>
</ul>
Now we can use native JavaScript string interpolation and simple objects to map element's attributes, properties (including any nested property) to our data models.
Symbiote.js now support the new, cutting edge, browser styling technology - adoptedStyleSheets. It helps to style your web-components as easy and flexible like never before:
import { css } from '@in-wave/symbiote';
// External component styles are set in higher level CSS root (document or shadow):
MyComponent.rootStyles = css`
my-component {
border: 1px solid currentColor;
}
`;
// Styles for component's Shadow DOM (optional):
MyComponent.shadowStyles = css`
:host {
display: block;
padding: 10px;
color: #f00;
}
`;
rootStyles and shadowStyles could be used separately and together as well. Or you can use any other styling approach you familiar with.
Important notice: adoptedStyleSheets interface does not conflict with CSP (Content Security Policy) in common, so its safe to use CSS definitions in JavaScript directly.
renderCallback
If you need to interact with some DOM elements, created by Symbiote component, you need a reliable way to know when those elements are ready. In case of postponed rendering defined in some parent class, that was a messy thing sometimes. Now we have a simple dedicated renderCallback lifecycle method for that.
^
One of the unique features of Symbiote.js is ability to lay on DOM structure for component behavior definition.
Now we can bind our handlers and properties to upper component's state directly and use cascade data model in your application:
MyComponent.template = html`
<button ${{onclick: '^upperLevelClickHandler'}}>Click me!</button>
`;
This example will find the first upper-level component's state, where "upperLevelClickHandler" is defined.
It helps to significantly simplify a lot of complicated interaction cases and to write less code.
Summary: use the
^
property token to get access to upper level properties.
+
In Symbiote.js 2.x you can define computed properties which are calculated
on any other local property change:
class MyComponent extends Symbiote {
init$ = {
a: 1,
b: 1,
'+sum': () => this.$.a + this.$.b;
}
}
MyComponent.template = html`<div>{{+sum}}</div>`;
Property calculation flow is optimized and won't be invoked while all other changes made in synchronous cycle will not be complete.
Summary: use the
+
property prefix to define computed properties.
--
Now you can use special property tokens to initiate component data with a values
defined as CSS Custom Property:
class TestApp extends Symbiote {}
TestApp.rootStyles = css`
test-app {
--header: 'CSS Data';
--text: 'Hello!';
}
`;
TestApp.template = html`
<h1>{{--header}}</h1>
<div>{{--text}}</div>
`;
This feature helps to create and provide configurations for the components.
class MyComponent extends Symbiote {
isVirtual = true;
}
When isVirtual flag is enabled, your component will be rendered as a part of DOM without any wrapping Custom Element in structure. In this case, component's custom tag will be used as a placeholder only, and will be removed at rendering stage.
Built-in list rendering is now support any kind of reactive web-components, not the Symbiote-components only. That helps to achieve maximum performance in that cases, when it is very important, for example, in big dynamic tables. Here is an example:
// Create lightweight web-component for each table row:
class TableRow extends HTMLElement {
set rowData(data) {
data.forEach((cellContent, idx) => {
if (!this.children[idx]) {
this.appendChild(document.createElement('td'));
}
this.children[idx].textContent = cellContent;
});
}
}
window.customElements.define('table-row', TableRow);
// Than render big dynamic table with Symbiote:
class MyTable extends Symbiote {
init$ = {
tableData: [],
}
initCallback() {
window.setInterval(() => {
let data = [];
for (let i = 0; i < 10000; i++) {
let rowData = [
i + 1,
Date.now(),
];
data.push({rowData});
}
this.$.tableData = data;
}, 1000);
}
}
MyTable.rootStyles = css`
table-row {
display: table-row;
}
td {
border: 1px solid currentColor;
}
`;
MyTable.template = html`
<h1>Hello table!</h1>
<table ${{itemize: 'tableData', 'item-tag': 'table-row'}}></table>
`;
MyTable.reg('my-table');
In this example we made a performant dynamic table of 20000 reactive cells.
We've removed the alternative binding description support because it required an excess property name transformations, which are not obvious for the developers sometimes.
This is not working anymore:
<button set -onclick="onButtonClicked"></button>
Use the new tag function helper instead:
<button ${{onclick: 'onButtonClicked'}}></button>
Browser runtime is the most reliable source of information about what happens in your code. So, in addition to static code analysis, we use runtime type checks to prevent issues in some complicated cases. Now, if you accidentally change the type of your state property or initiate your property with a wrong type, you will be warn about that.
Bundled code and single type definitions endpoint are not provided as a part of package anymore. We build our library that way, what allows to simplify build process in your development environment or to use code CDNs to use any module as build endpoint. That is much more flexible and modern approach.
We've renamed some API entities according to developers feedback.
The major rename is that
BaseComponent
class is nowSymbiote
.
Raw HTML template changes (if don't use "html" tag):
<div set="textContent: textVariableName"></div>
→ now it becomes →
<div bind="textContent: textVariableName"></div>
<my-component ctx-name="my_data_ctx"></my-component>
or
<my-component style="--ctx-name: 'my_data_ctx'"></my-component>
→ now it becomes →
<my-component ctx="my_data_ctx"></my-component>
or
<my-component style="--ctx: 'my_data_ctx'"></my-component>
Dynamic list:
<ul repeat="data" repeat-item-tag="list-item"></ul>
→ now it becomes →
<ul itemize="data" item-tag="list-item"></ul>
or
<ul ${{itemize: 'data', 'item-tag': 'list-item'}}></ul>
Light DOM slots support is removed from the default template processing pipeline. Now, if
you need to use slots without Shadow DOM, you need to connect slotProcessor
manually:
import Symbiote from '@in-wave/symbiote';
import { slotProcessor } from '@in-wave/symbiote/core/slotProcessor.js';
class MyComponent extends Symbiote {
constructor() {
super();
this.addTemplateProcessor(slotProcessor);
}
}
The reason is that this function can trigger unexpected lifecycle callbacks in the nested DOM structure and should be used with attention to that.
For the most cases, when slots are necessary, use components with a Shadow DOM mode enabled.
Symbiote.js is very easy to use with SSR:
import Symbiote from '@in-wave/symbiote';
class MyComponent extends Symbiote {
ssrMode = true;
}
Now you can create the markup for your components on the server and connect it to the Symbiote.js
state just with a one simple flag - ssrMode
.
Now yo can use the @
token not for the one-way property-to-attribute binding only, but
for the attribute dependent property initiation itself:
class MyComponent extends Symbiote {
init$ = {
'@my-attribute': 'some initial value...',
}
}
// or use the direct template initiation:
class MyOtherComponent extends Symbiote {}
MyOtherComponent.template = html`
<h1>{{@my-attribute}}</h2>
`;
allowTemplateInits
flag in now allows to initiate properties from the templates directly.
Example:
class MyComponent extends Symbiote {
initCallback() {
// Property is already exists:
this.$.myProp = 'new value';
}
}
MyComponent.template = html`
<h1>{{myProp}}</h1>
`;
The default allowTemplateInits
value is true
.
Unlike many other frontend libraries and frameworks, Symbiote.js is designed as a DOM API higher level extension, not an independent external abstraction. And this is a main idea.
We are not inventing the wheel or to reinvent the web platform, we evolve principles, that already existing as the native Web Platform features, and have proved their efficiency. We don't create the new runtimes, new compilers or the new language syntax. We just adding features to the existing modern DOM API to make your DX better.
Symbiote.js encourages the loosely coupled architecture and abstraction, allowing the creation of highly flexible and easily extensible solutions.
Symbiote.js is built upon a modern web platform features and utilize native browser APIs instead of using an excess abstractions. Symbiote.js - is what you get after Occam's razor done it's work.
Symbiote.js is agnostic. It works in browser and that means it works with any other technology that interacts with browser APIs. It makes it useful for manage complex interactions between multiple system parts without a pain. It makes Symbiote the best choice for building embedded solutions and to build integration products.
Symbiote.js is minimal, but has everything you need. You don't need to search and install more and more libraries for the data management, table rendering, component styling, dynamic application localization or many of other common purposes. You have everything you need out of the box, because browser have. We just make your work more convenient.
Symbiote.js doesn't hide platform APIs behind opaque abstraction levels. You have an access to everything you see as the result of component rendering.
Symbiote.js is extensible. You can extend your base component class to feat your needs. You can add your own features. You can integrate your application APIs into the base class with ease.
Symbiote.js is friendly for the new kind of dependency sharing. It's easy to share common code among the different subsystems (micro-frontends, widgets, meta-applications).
We believe that simple things should stay simple and all the complications should fit to their purposes.
This example contains two embedded Symbiote applications.
HTML code example:
<ims-photo-spinner data="DATA_JSON_URL"></ims-photo-spinner>
HTML code example:
<lr-file-uploader-regular css-src="./~/css/uploader/index.css"></lr-file-uploader-regular>
These widgets are provided by different vendors, but they connected to the one common workflow. Symbiote.js allows you to do it with ease and the high level of flexibility.
You can try to upload your own animation sequence (frame images) to the Uploadcare CDN and see the result.
Note, that files should have names applicable for proper sorting.
Here we provide a set of the basic recommendations for the development environment setup. Note, that this is only recommendations, you can chose any other approach which is more relevant to your experience or needs.
We use standard JavaScript template literals for the component's templates description. To highlight HTML inside template literals we use IDE extensions, that allows to identify such templates with tag functions or JavaScript comments.
Example:
let template = html`<div>MY_TEMPLATE</div>`;
let styles = css`
div {
color: #f00;
}
`;
We strongly recommend to use TypeScript static analysis in all your projects.
We use JSDoc format and *.d.ts files for the types declarations.
JSDoc annotation example:
/**
* @param {Boolean} a
* @param {Number} b
* @param {String} c
*/
function myFunction(a, b, c) {
...
}
Check the details at https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html
Esbuild - is our choice for the code bundling and minification. Esbuild is very performant and easy to configure solution that can prepare your JavaScript and CSS code for the distribution.
Network imports is a very powerful platform feature that helps to use the common code parts for the different functional endpoints in the big applications with ease. You don't need to setup complex build workflows to share the dependencies anymore.
Example:
import { Symbiote, html, css } from 'https://esm.run/@in-wave/symbiote';
export class MyAppComponent extends Symbiote {}
export { html, css }
Use any local server you like, that can serve static files. Symbiote.js is agnostic and it doesn't require any special tool for ESM modules resolving or for the anything else.
You can use the simple relative paths for your modules or import-maps
for the
module path mapping.
This feature is supported in all modern browsers.