Scoped JS is a DOM feature that makes it possible to scope a script to its immediate host element and completely out of the global browser scope.
- Scoped Scripts
- Variable Bindings
- Selective Execution
- Globals
- Runtime
- Error Handling
- Isomorphic Rendering
Scoped scripts have their this
variable implicitly bound to their host element. They are defined with the scoped
MIME type.
<div id="alert">
<script type="scoped">
// this === #alert
</script>
</div>
This lets us place behaviours just where we need them! This way, we are able to keep the main application layer void of the implementation details of the UI.
Here's an #alert component with a "remove* feature.
<div id="alert">
<div class="message"></div>
<div class="exit" title="Close this message.">X</div>
<script type="scoped">
// details of how the #alert block should behave...
this.querySelector('.exit').addEventListener('click', () => {
this.remove();
});
</script>
</div>
Besides the this
variable being implicitly bound to the script's host element, other variables in a scoped script are to be explicitly-bound to external values; variables are bound by name.
Below, we're implementing a message
variable in our #alert component.
<body>
<div id="alert">
<div class="message"></div>
<div class="exit" title="Close this message.">X</div>
<script type="scoped">
// where to place the message within the alert block...
this.querySelector('.message').innerHTML = message;
// details of how the alert block should behave...
this.querySelector('.exit').addEventListener('click', () => {
this.remove();
});
</script>
</div>
<script>
document.querySelector('#alert').bind({
message: 'This task is now complete!',
});
</script>
</body>
As shown above, an application simply binds its hard-earned values and is done!
Scoped JS follows the normal top-down execution of a script. Calling the .bind()
method with different variable-bindings reruns the script top-down. But as a UI binding langauge, it also features Selective Execution where an update to a variable gets to rerun only the corresponding statements within the script - skipping the other statements. This makes for the most-efficient way to keep a block of the UI in sync with little updates from an application.
To update a variable or multiple variables, call .bind()
with a params
object as a second paremeter and set params.update
to true
.
alertEl.bind({
variable2: 'New value',
variable5: 'New value',
}, {update:true});
Also, Scoped JS exposes a new DOM property .bindings
for selectively updating an element's bindings.
alertEl.bindings.variable5 = 'New value',
This is illustrated in the clock below.
<body>
<div id="clock">
<div class="greeting"></div>
<div class="current-time"></div>
<script type="scoped">
this.querySelector('.greeting').innerHTML = greeting;
this.querySelector('.current-time').innerHTML = currentTime;
</script>
</div>
<script>
let clockEl = document.querySelector('#clock');
clockEl.bind({
greeting: 'Good Afternoon!',
currentTime: '00:00:00',
});
// Clock ticks
setInterval(() => {
clockEl.bindings.currentTime = (new Date).toLocaleString();
}, 100);
</script>
</body>
Scoped JS also supports the Observer API for object observability. With Observer, Scoped JS is able to respond to mutations made directly to the bound data object. So, the #clock above could be ticked by directly updating the data object.
<script>
let clockState = {
greeting: 'Good Afternoon!',
currentTime: '00:00:00',
};
document.querySelector('#clock').bind(clockState);
// Clock ticks
setInterval(() => {
Obs.set(clockState, 'currentTime', (new Date).toLocaleString());
}, 100);
</script>
Statements may also reference deep mutations made on the bound data object, as in the clock.currentTime
reference below.
<body>
<div id="clock">
<div class="greeting"></div>
<div class="current-time"></div>
<script type="scoped">
this.querySelector('.greeting').innerHTML = clock.greeting;
this.querySelector('.current-time').innerHTML = clock.currentTime;
</script>
</div>
<script>
let state = {
clock: {
greeting: 'Good Afternoon!',
currentTime: '00:00:00',
},
};
document.querySelector('#clock').bind(state);
// Clock ticks
setTimeout(() => {
Obs.set(state.clock, 'currentTime', (new Date).toLocaleString());
}, 100);
</script>
</body>
Within the script, the dependency chain is followed even when broken into local variables. Below, a change to clock.currentTime
will still propagate through variable1
and variable2
. (The first and last statements in the script are left untouched touched, as expected.)
<body>
<div id="clock">
<div class="greeting"></div>
<div class="current-time"></div>
<script type="scoped">
this.querySelector('.greeting').innerHTML = clock.greeting;
let variable1 = clock.currentTime;
let variable2 = variable1;
this.querySelector('.current-time').innerHTML = variable2;
this.style.color = 'blue';
</script>
</div>
</body>
By default, scoped scripts have no access to anything besides what is explicitly bound into the scope. But they also have an idea of a global scope - that is, bindings seen by every scoped script. This global scope is created by binding on the document
object itself, using a new document.bind()
method.
document.bind({
greeting: 'Good Afternoon!',
});
To update a global or multiple globals, call document.bind()
with a params
object as a second paremeter and set params.update
to true
.
document.bind({
greeting: 'Good Afternoon!',
}, {update:true});
There is also the document.bindings
property for selectively updating globals.
document.bindings.greeting = 'Good Evening!';
By design, Scoped JS parses scoped scripts immediately they land on the DOM, but runs them only after the global scope has been initialized with document.bind()
or the document.bindings
property. Newer scipts are run immediately after this global runtime initilization. But the runtime of an individual script will begin before the global one on calling the element's .bind()
method or assigning to its .bindings
property.
Alternatively, the autorun=true
directive may be set on the CHTML META tag. The autorun
Boolean attribute may also be set on individual script elements.
<html>
<head>
<meta name="chtml" content="autorun=true;" />
</head>
<body>
<div id="alert">
<script type="scoped" autorun>
...
</script>
</div>
</body>
</html>
Also, it is allowed for an element to receive bindings before its scoped script is appended or is ready to run. The element's runtime begins the first time both are available.
alertEl.bind({
message: 'This task is now complete!',
});
// Sometime later
alertEl.append('<script scoped>this.innerHTML = message</script>');
Scoped JS features a way to handle syntax or reference errors that may occur with scoped scripts. Normally, these are shown in the console as warnings. But they can be silently ignored by setting a directive on the CHTML META tag. Individual scripts may also be given a directive, to override whatever the global directive is.
<html>
<head>
<meta name="chtml" content="script-errors=0;" />
</head>
<body>
<h1></h1>
<script type="scoped" errors="1">
this.querySelectorSelectorSelector('h1').innerHTML = headline;
</script>
</body>
</html>
The script tag of a scoped script is not always needed for the lifetime of the page. They are discarded by default after parsing. But when a page is rendered on the server and has to be hydrated by the browser, it becomes necessary to retain these scripts for revival on the browser. This feature is designed to be explicitly turned on with a directive on the CHTML META tag.
<html>
<head>
<meta name="chtml" content="isomorphic=true;" />
</head>
<body>
<h1></h1>
<script type="scoped">
this.querySelector('h1').innerHTML = headline;
</script>
</body>
</html>
Now, this binding will always be there for when we run the code document.bind({headline: 'Hello World'})
- whether on the server and on the browser.
Environment-Specific Bindings
Sometimes, we want certain bindings to apply only on the server; sometimes, only on the browser. For example, animation is only a thing in the browser. This is the perfect use-case for conditionals.
<div>
<script type="scoped">
if (condition) {
this.animate(...);
}
</script>
</div>
Above, condition
could be a simple question about the current environment, and this can be acheived by simply binding a global variable, env
, for example: document.bind({env:'server', headline: 'Hello World'})
.
<div>
<script type="scoped">
if (env !== 'server') {
this.anumate([
{color:'red'},
{color:'blue'},
], {duration:600,});
}
</script>
</div>