The Brains@Play ESCode project is a collection of ECMAScript libraries intended to further the Web as a Universal Development Engine by allowing you to program and share composable web applications using any WebAssembly-supported language.
Note: As of January 2023, all development related to the ESCode project has been moved to the graphscript repository. All NPM packages are still available, but have not been updated since the release of ESCode: A First Look in November 2022.
escode implements the ES Components specification—a variant of graphscript—which allows you to define special properties on a hierarchy of reactive objects.
- Write Software Faster: Use existing Components to start your journey
- Visualize your Code: See how your code is organized at a high level—and change it
- Share Code with Others: Contribute to a growing community
- Move as One: Pull updates from a wide array of other programmers
Unlike libraries that use hooks like useEffect (React) and watchEffect (Vue), ESCode monitors arbitrary objects for changes to their values based on specified listeners—meaning we don't require explicitly registering references or using returned objects.
To create a component, pass an object to the create
function:
import { create } from 'escode';
const button = {
__element: 'button',
__attributes: {
onclick: function (input) { console.log(this) }
}
}
const component = create(button, {__parent: document.body})
component.__element.click()
These objects are deep cloned, meaning that all properties attached to the object itself are independent across instantiations.
If you prefer to work with classes, these will also be instanced using this function:
import { create } from 'escode';
const shared = {
value: 0
}
class MyButton {
shared = shared
__element = 'button'
__attributes = {
onclick: function (input) {
this.value++
console.log(this.value)
}
}
}
const component = create(MyButton, {__parent: document.body})
component.__element.click() // shared.value = 1
However, class instances are assumed to be sufficiently instanced by the user. As such, local objects attached to the class itself will be shared across instances.
const secondComponent = create(MyButton, {__parent: document.body})
secondComponent.__element.click() // shared.value = 2
To avoid this, you can simply pass an instance of the class using the new
keyword:
const component = create(new MyButton(), {__parent: document.body})
component.__element.click()
Classes are inherently suited for shallow composition because top-level properties that are reset on an extension are overwritten—even if they are objects with shared properties.
On the other hand, the escode-compose-loader allows you to compose classes deeply, meaning that properties that are objects are merged as well as strings are loaded from source.
Although the escode-compose-loader is native to ES Components, it's worth noting that classes already have some mechanism for loading—so, in some cases, they may be better suited to your needs.
In specific cases, an array may be useful to apply bulk operations to independent Components:
import { create } from 'escode';
const components = create([button, myButton, button], {
__parent: document.body,
__attributes: {
onclick: function (input) { console.log(this) }
}
})
components.forEach(component => component.__element.click())
A string can be passed to grab the Component from a local JavaScript file—or, with additional utilities, compile from source text:
import { create } from 'escode';
import * as esm from 'esmpile'
const button = './index.esc.js'
const component = create(button)
const component = create(button, {__parent: document.body}, { utilities: { bundle: {function: esm.bundle.get}}})
component.__element.click()
A function can be passed directly as a Component, which wraps it as the default
function of a new component:
import { create } from 'escode';
const fn = (input) => {
console.log(input)
return input
}
const component = create(fn)
component.default()
If you're looking to listen to a function, you can simply add it as a property inside a valid ES Component object.
import { create } from 'escode';
const reactive = {
fn,
latest: undefined,
__listeners: {
fn: 'latest'
}
}
const component = create(reactive)
component.fn(1)
Elements can be passed to apply Components to existing DOM elements:
import { create } from 'escode';
const button = document.createElement('button')
button.innerText = 'I will respond to clicks using ESCode'
document.body.appendChild(button)
const component = create(button, {
__attributes: {
onclick: function (input) { console.log(this) }
}
})
component.__element.click()
GraphScript properties refer to special properties that are used to instantiate the Component. These properties are prefixed with __
and are recognized with loaders than can be used to experiment with new Component behaviors
See the ES Components specification for a full list of default properties.
All ES Components have at least one GraphScript property at instantiation. All other properties throughout an ES Component are listeneable by the root Component.
Note: This includes classes and functions. Classes will not be instantiated without a static
__
property. On the other hand, functions will not converted to adefault
property without a__
property set—though they will still be listenable without it.
Active components are recognized by the presence of the __
property on them. This provides access to utilities such as run
and subscribe
—as well as retains a record of read-only properties maintained by the library itself.
All other __
properties are considered GraphScript properties, and are used to program the behavior of the Component.
Components are created using the create
factory function, which accepts any object (e.g. Object, Array, or Class) and outputs an analogous object.
Unlike graphscript, we do not return a standard class from the create
function. Instead, the Component is returned based on the type of the input object.
Objects are extensively instanced and treated as templates. This means that all properties attached to the object itself are independent across instantiations.
Classes are instanced using the new
keyword. The resulting instancing behavior is assumed to be appropriate for the Component. This allows for minimal performance overhead when using classes.
Arrays are iterated over and each item is passed to the factory function. The resulting array is returned.
Additional properties can be added using the loaders argument for escode:
import { create } from 'escode';
const component = create(input, undefined, {
loaders: [ myLoader ]
})
You can incrementally integrate ESCode into your existing projects by wrapping existing functional components and using our listener system to trigger messages between different aspects of the app:
import { create } from 'escode';
import * as existing from './app.js'
const component = {
producer: existing.producer,
consumer: existing.consumer,
__listeners: {
'producer': 'consumer'
}
}
const component = create(existing, {__parent: document.body})
component.producer()
Relatedly, you can also use ESCode more directly as an event manager:
import { create } from 'escode';
import * as existing from './app.js'
const component = {
producer: existing.producer,
consumer: existing.consumer,
__listeners: {
'producer': (result) => {
const res = existing.consumer(result)
console.log(res)
return res
}
}
}
const component = create(existing, {__parent: document.body})
component.producer()
Both of these strategies are particularly useful for integrating with published ES Components that you'd like to use in your project.
The esmpile library allows you to compile ESM code from their text sources. This allows you to track a list of active imports.
The esmonitor library allows you to receive notification about changes to objects and their values via a simple plain-text subscription interface for arbitrary object properties.
The escode library allows you to transform ESM into Web Components that send messages to each other using the ECMAScript Components (ESC) specification.
The escompose library allows you to convert between JS, JSON, and HTML declarations of ESC.
The escode-ide library is a visual programming system to visualize and edit ESC files.
Metric | escode | graphscript |
---|---|---|
Core Size - bundled | 86kb | 39kb |
Core Size - minified | 37kb | 20kb |
Instantiation Time | 3.15ms | — |
Instantiation w/ Explicit Children | 2.7963ms | 3.15ms |
Listener Reaction Time | 0.026ms | 0.015ms |
Generally, we would like to introduce composers to provide additional ways to load and instantiate Components. This would allow for more complex behaviors to be added to the library, such as:
After creating a component, you can serialize it to a JSON object:
const json = component.toJSON()
{
"value": 0,
"fn": "function(){this.value++}",
"__listeners": {
"fn": "value"
}
}
This can be used to reconstruct the component:
import { create } from 'escode';
const component = create(json)
component.fn()
After creating a component, you can export it to HTML text:
const htmlString = component.toHTML()
This can be used to reconstruct the component:
import { create } from 'escode';
const component = create(htmlString)
Additionally, you can load the HTML text as a file and hydrate your components:
<!DOCTYPE html>
<html>
<head>
<script type="module">
import { create } from 'https://cdn.jsdelivr.net/npm/escode';
const component = create()
component.fn()
</script>
</head>
<body>
<div .value=0 .fn="function(){this.value++}" __listeners.fn="value" escomponent>
</body>
</html>
Both of the aforementioned methods could additionally from knowing where accompanying source files actually sit. When exporting a component, you could optionally use esmpile to reference source files rather than exhaustively enumerating all of your properties in JSON or HTML.
Garrett Michael Flynn 🤔 💻 💼 |
This is intended to be an official repository of ES Components.
In the near future, we will switch to the registration of ES Components through NPM via standardized use of the graphscript
and escomponent
keywords. These existing components will be published and distributed into independent repositories.
To learn more about the publication workflow, see the escomponent template repository.
Our work at Brains@Play is sustained by a wide range of contract work and the generous support of our community through Open Collective:
Support us with a monthly donation and help us continue our activities!
Become a sponsor and get your logo here with a link to your site!