Welcome to FrameJS! This documentation tries to help answer any questions you may have about what FrameJS is, how to use it and what its APIs are.
Build encapsulated elements that manage their own state, then reuse them in any web project to make complex UIs. With its small size (~1.5kb gzipped) it fits well for simple elements as well as for complex components.
FrameJS tries to make it easy and safe to build UI elements you can use across projects and frameworks.
It doesn't rely on specific build pipelines or compilers, so it fits right into existing projects. It supports and provides decorators for Typescript, using JSX and other templating languages. And you can choose to use features like shadow dom and rendering per element as fits.
It won't stop you from using any existing techniques for custom elements. Its purpose is to aid you as a developer by providing tested functionality and speed up your work.
Try FrameJS online or set up your local development environment.
If you’re interested in playing around with FrameJS, you can use an online code playground. Try a Hello World template on Stackblitz
Other Hello World templates on stackblitz:
Demos on stackblitz:
Make sure you've installed and/or updated Node before continuing.
It's recommended for setting up tools to bundle and minify for production. A modern build pipeline typically consists of:
-
A package manager, such as Yarn or NPM. It lets you take advantage of a vast ecosystem of third-party packages, and easily install or update them.
-
A bundler, such as Webpack or Browserify. It lets you write modular code and bundle it together into small packages to optimize load time.
The custom elements API dictates elements to we written as es6 classes. A good practice is to let a consumer application transform the code to es5, or compile to both. The browser runtime cannot mix es5 and es6 custom elements.
Install with npm
npm install @framejs/core
// hello-world.js
import { FrameElement } from './node_modules/@framejs/core/dist/frame-element.js';
class HelloWorld extends FrameElement {
render() {
return `<h1>Hello World!</h1>`
}
}
customElements.define('hello-world', HelloWorld);
<!-- index.html -->
<hello-world></hello-world>
<script type="module" src="./hello-world.js">
The default render function replaces innerHTML, and it's recommended to use an advanced renderer like the lit-html renderer or preact renderer. See examples on stackblitz
For more information on the polyfills, see the web components polyfill documentation.
These examples expect that you are using a module bundler of some kind.
import { FrameElement } from '@framejs/core';
class HelloWorld extends FrameElement {
elementDidMount() {
console.log('Hello!');
}
elementDidUnmount() {
console.log('Bye!')
}
render() {
return `Hello World!`
}
}
customElements.define('hello-world', HelloWorld);
Properties are available as this.props[property]
, this[property]
and destructuring
import { FrameElement } from '@framejs/core';
class HelloWorld extends FrameElement {
static get props() {
return {
greeting: 'Hello World!'
}
}
static get propTypes() {
return {
greeting: String
}
}
render() {
return `${this.props.greeting}`;
}
}
customElements.define('hello-world', HelloWorld);
This sets the attribute greeting="Hello World!"
on the element.
Changing the attribute on the element updates the prop
and triggers are re-render.
import { FrameElement } from '@framejs/core';
class HelloWorld extends FrameElement {
static get props() {
return {
greeting: 'Hello World!'
};
}
static get propTypes() {
return {
greeting: String
};
}
static get reflectedProps() {
return ['greeting'];
}
render() {
return `${this.props.greeting}`;
}
}
customElements.define('hello-world', HelloWorld);
Prop observers are running every time a specified prop changes
import { FrameElement } from '@framejs/core';
class HelloWorld extends FrameElement {
static get props() {
return {
greeting: 'Hello World!'
};
}
static get propTypes() {
return {
greeting: String
};
}
static get propObservers() {
return {
greeting: '_greetingObserver'
};
}
_greetingObserver(oldValue, newValue) {
console.log(oldValue, newValue)
}
render() {
return `${this.props.greeting}`;
}
}
customElements.define('hello-world', HelloWorld);
Event listeners are added on connectedCallback and removed on disconnectedCallback.
The syntax for a listener is:
'Event'
- event listener on element'Event:#child
- event listener on child element. the string after:
is used for selecting the element.
import { FrameElement } from '@framejs/core';
class HelloWorld extends FrameElement {
static get props() {
return {
greeting: 'Hello World!'
};
}
static get propTypes() {
return {
greeting: String
};
}
static get eventListeners() {
return {
'click': '_handleClick',
'click:#myButton': '_handleButtonClick'
};
}
_handleClick(event) {
console.log(event, 'element clicked!')
}
_handleButtonClick(event) {
console.log(event, 'button clicked!')
}
render() {
return `${this.props.greeting} <button id="myButton">Click me!</button>`;
}
}
customElements.define('hello-world', HelloWorld);
import { FrameElement } from '@framejs/core';
class HelloWorld extends FrameElement {
static get style() {
return `
:host {
color: dodgerBlue;
}
`;
}
render() {
return html`
<style>${this.constructor.style}</style>
Hello World!`;
}
}
customElements.define('hello-world', HelloWorld);
You should not set
<style>
in the template if you are using VDOM like renderer-preact
import { FrameElement } from '@framejs/core';
class HelloWorld extends FrameElement {
static get shadow() {
return false;
}
}
customElements.define('hello-world', HelloWorld);
import { FrameElement } from '@framejs/core';
class HelloWorld extends FrameElement {
static get shadowMode() {
return 'closed';
}
}
customElements.define('hello-world', HelloWorld);
Manually trigger re-render by using this.invalidate();
import { FrameElement } from '@framejs/core';
class HelloWorld extends FrameElement {
_invalidateOnPropChanges = false;
}
customElements.define('hello-world', HelloWorld);
this
is passed to render(), but you can still reference them manually via this
.
import { FrameElement } from '@framejs/core';
class HelloWorld extends FrameElement {
static get props() {
return {
greeting: 'Hello'
}
}
render({ greeting }) {
return `${greeting} World!`
}
}
customElements.define('hello-world', HelloWorld);
It's possible to use type reflection if loading the reflect-metadata polyfill, alternatively pass in type to @Attribute({type: String})
;
@Attribute()
automatically gets reflected as attributes and can be of type: String
| Boolean
| Number
.
@Attribute() myProp: String;
will be set as attribute my-prop
;
import {
Define,
FrameElement,
Property,
Attribute,
Listen,
Observe,
Event,
EventEmitter
} from '@framejs/core';
@Define({
tag: 'hello-world',
style: `:host { color: dodgerBlue; }`, // optional
shadow: true, // default true
mode: 'open', // default 'open'
invalidateOnPropChanges: true // default true
})
class HelloWorld extends FrameElement {
@Property() greeting: String = 'Hello';
@Attribute() target: String = 'World'!
@Event() targetChanged: EventEmitter;
@Observe('target')
_handleTargetChange(oldValue, newValue) {
this.targetChanged.emit(newValue);
}
@Listen('click')
_handleClick(event) {
console.log('clicked!')
}
}
If you are unsure about the typescript configuration, take a look at the tsconfig.json used in FrameJS.
lit-html example on Stackblitz
npm install @framejs/renderer-lit-html
import { FrameElement } from '@framejs/core';
import { withLitHtml, html } from '@framejs/renderer-lit-html';
class HelloWorld extends withLitHtml(FrameElement) {
render() {
return html`<h1>Hello World!</h1>`
}
}
customElements.define('hello-world', HelloWorld);
To be able to use JSX you need to either use babel (output to es6) or typescript. This example is using typescript with --jsx --jsxFactory h
.
npm install @framejs/renderer-preact
import { FrameElement } from '@framejs/core';
import { withPreact, h } from '@framejs/renderer-preact';
class HelloWorld extends withPreact(FrameElement) {
render() {
return <h1>Hello World!</h1>
}
}
customElements.define('hello-world', HelloWorld);
The built-in renderer is very simple: it receives the returned value and replaces innerHTML with the new template when updated.
See the code for renderer-preact for implementation details.