reactive-scriptable-components
v0.0.36
Published
light-weight reactive scriptable web components
Downloads
35
Maintainers
Readme
reactive-scriptable-components (RSC)
light-weight reactive scriptable web components
Warning: this framework is currently under active development - consider it as incomplete pre-alpha software: anything may change, some changes may even break existing applications (although I don't expect many of them). Thus, please stay tuned and come back here from time to time to see if there is a (well-documented) version that you may safely use...
The idea behind this framework is to allow for the rapid development of small reactive web applications. To give you an idea of what these web apps could look like, consider the following example (which implements a simple calculator that converts temperatures between °Celsius and °Fahrenheit, see live demo):
<rsc-applet>
<rsc-title>Temperature Converter</rsc-title>
<rsc-tabular columns="2">
<rsc-label>°Celsius:</rsc-label>
<rsc-native-number-input $$value="Applet:observed.Celsius"></rsc-native-number-input>
<rsc-label>°Fahrenheit:</rsc-label>
<rsc-native-number-input $$value="Applet:observed.Fahrenheit"></rsc-native-number-input>
</rsc-tabular>
<script type="rsc-script">
const observed = Object.assign(this.observed,{
Celsius:0,
Fahrenheit:0
})
reactively(() => observed.Fahrenheit = observed.Celsius * 9/5 + 32)
reactively(() => observed.Celsius = 5/9 * (observed.Fahrenheit-32))
</script>
</rsc-applet>
The example basically consists of two number input controls, a bit of visual "decoration" and some "business logic".
What makes it interesting is how the logic works:
$$value
attributes make the number input controls "reactive" (in both directions), i.e., user input changes the specified variable and variable changes will be reflected in the UI - and, yes, the circularity of the dependencies shown above causes no problem- every "reactive scriptable component" (which is a standard web component) may contain its own
observed
andunobserved
(state) variables - in this trivial example, only the applet itself provides some "state", whereas the input controls do not - whenever an
observed
variable is changed, all functions using that variable may bereactively
recalculated - in this example, changes of theCelsius
variable will recompute theFahrenheit
variable and vice-versa - and the$value
reactivity will automatically update the number input fields.
This approach allows to write simple web applications within minutes - the author uses it for his computer science lectures at Stuttgart University of Applied Sciences in order to demonstrate various concepts and algorithms or give students the possibility to practice what they learned. You probably won't use "reactive-scriptable-components" to implement the next office package, but simple tools can be written with very little effort and in a way that may easily be understood even by inexperienced or casual programmers.
NPM users: please consider the Github README for the latest description of this package (as updating the docs would otherwise always require a new NPM package version)
Just a small note: if you like this module and plan to use it, consider "starring" this repository (you will find the "Star" button on the top right of this page), so that I know which of my repositories to take most care of.
Features
"reactive-scriptable-components" offer the following fundamental features:
- Script Attributes(small) scripts may be directly provided as an HTML attribute of a component - this keeps a component and it's functionality together
- Script Elements(larger) scripts may be provided as a
<script type="rsc-script"/>
element within the component they belong to - e.g., below all other inner HTML elements. This approach keeps the internal structure of an RSC component visible and still allows a component and its code to be kept close together - Delegated Scriptsif you want to separate the "look" from its "feel", you may provide "delegated scripts" (
<script type="rsc-script" for="..."/>
) for components that can be identified by a CSS selector (e.g.,#id
,.class
,[attr="value"]
etc.) - Behaviour Scriptsif you have multiple RSC components that share the same functionality, you may define a "behaviour" and provide the shared code in a separate
<script type="rsc-script" for-behaviour="..."/>
element. If there are both a behaviour and a component script for a given RSC component, the behaviour script is executed before the component script. - Observed and Unobserved VariablesRSC components usually have to store some values they need for their operation. For that purpose, RSC provides both an
observed
and anunobserved
data structure for every component which can be freely used as required. "Observed" entries may then be used to trigger "reactive functions" or update "reactive attributes" whenever their values change - Reactive Functions"reactive functions" (defined using
reactively(() => ...)
) are functions that will be automatically invoked whenever any of the observed(!) values they use internally have changed - Reactive Attributes"reactive attributes" have names starting with one or two dollar signs (e.g.,
$value
or$$value
) and establish a "reactive binding" between a reactive variable of the component itself (observed.value
in this example) and another reactive variable in an outer RSC component - both a reference to that outer component and the path to the other reactive variable have to be specified in the attribute itself - Event Handlers as Function Callssometimes, RSC components do not directly change other (reactive) variables but initiate an activity - to support such use cases, RSC components may trigger events or handle them. In contrast to DOM events, however, RSC events may be used like function calls, i.e., it is allowed to provide arbitrary arguments and possible to wait for a result from event handling
- Error Indicatorsoften, it is difficult to recognize and track errors which occured in behaviour or component scripts, or during event handling. For that reason, RSC marks faulty components with an "error indicator": just click on such an indicator to reveal details about the detected error
Built-in Controls
Browser Requirements (and Polyfills)
RSC is based on relatively modern web technologies which should already be available in most browsers out-of-the-box - but for those that lack these features (particularily Safari versions < 16.4 or devices with iOS versions < 16.4), polyfills have been included in the examples to plug these holes:
- Import Maps (see availability across browsers)for a polyfill see https://github.com/guybedford/es-module-shims
- Custom Elements v1 (see availability across browsers)for a polyfill see https://github.com/webcomponents/webcomponentsjs
Inlined Dependencies
"reactive-scriptable-components" are based on the following (brilliant!) libraries and packages:
- HTM (Hyperscript Tagged Markup) - for easy HTML markup using JavaScript template strings,
- PREACT - from which its efficient and light-weight DOM diffing is used, and
- Hyperactiv - a light-weight reactive library which even handles circular dependencies
All these dependencies have been bundled into a single module for faster loading and a predictable user experience.
Nota bene: while it may be advisable to know how to use HTM, there is no immediate need to learn any of the above to write an RSC application.
The final distributables were built using the marvellous microbundle.
Installation
In order to avoid initial flashing of "custom Elements" (aka "Web Components") you should always add the following lines at the beginning of the <head/>
section in your HTML file:
<style>
:not(:defined) { visibility:hidden }
</style>
This trick applies to all kinds of Web Components, not just those presented here.
Most modern browsers already support import maps and web components out-of-the-box - except Safari browsers < 16.4 or (any browsers on) devices with iOS < 16.4. If you need to support these browsers as well, you should add the following lines directly after the <style/>
section mentioned above:
<!-- Import Map Polyfill from https://github.com/guybedford/es-module-shims -->
<script src="https://rozek.github.io/reactive-scriptable-components/polyfills/es-module-shims.js"></script>
<!-- Web Components Polyfill from https://github.com/webcomponents/webcomponentsjs -->
<script src="https://rozek.github.io/reactive-scriptable-components/polyfills/webcomponents-bundle.js"></script>
Using RSC in a "No-Build Environment" (e.g., directly in the Browser)
If you don't use any kind of build tool but create your web application directly in the browser or in an HTML file, you should first provide an "import map" that allows scripts to import modules by name rather than by URL. Just append the following lines to the <head/>
section of your HTML file:
<script type="importmap">
{
"imports": {
"reactive-scriptable-components": "https://rozek.github.io/reactive-scriptable-components/dist/reactive-scriptable-components.modern.js",
"RSC": "https://rozek.github.io/reactive-scriptable-components/dist/reactive-scriptable-components.modern.js"
}
}
</script>
Then, if you don't use any package that already imports the RSC module, load it with the following lines:
<script type="module"
src="https://rozek.github.io/reactive-scriptable-components/dist/reactive-scriptable-components.modern.js"
></script>
Otherwise, just load your package, e.g. the full-bundle
with all predefined RSC behaviours:
<script type="module"
src="https://rozek.github.io/reactive-scriptable-components/behaviours/full-bundle.js"
></script>
Using RSC with a Module Bundler
(t.b.w)
Applet Creation
(t.b.w)
Component Usage
(t.b.w) (lifecycle handling, nesting, containment validation)
Additional Element Properties and Methods
Compared to standard HTML elements, RSC components provide a few additional properties and methods which simplify behaviour and component scripts:
Applet
is a getter which returns a reference to the closest visual ofthis
one with behaviour "Applet"Card
is a getter which returns a reference to the closest visual ofthis
one with behaviour "Card"outerVisual
is a getter which returns a reference to the next outer visual ofthis
oneoutermostVisual
is a getter which returns a reference to the outermost visual ofthis
oneclosestVisualWithBehaviour (BehaviourName)
returns a reference to the closest visual ofthis
one with the givenBehaviourName
- please note, that the "closest" may also bethis
visualclosestOuterVisualWithBehaviour (BehaviourName)
returns a reference to the closest outer visual ofthis
one with the givenBehaviourName
closestVisualMatching (Selector)
returns a reference to the closest visual ofthis
one matching the givenSelector
- please note, that the "closest" may also bethis
visualclosestOuterVisualMatching (Selector)
returns a reference to the closest outer visual ofthis
one matching the givenSelector
innerVisuals
is a getter which returns a (possibly empty) list of all visuals which are direct children ofthis
oneinnerVisualsWithBehaviour (BehaviourName)
returns a (possibly empty) list of all visuals with the givenBehaviourName
which are direct children ofthis
oneinnerVisualsMatching (Selector)
returns a (possibly empty) list of all visuals which are direct children ofthis
one and match the givenSelector
innerElements
is a getter which returns a (possibly empty) list of all elements (i.e., not only RSC components) which are direct children ofthis
oneinnerElementsMatching (Selector)
returns a (possibly empty) list of all elements (i.e., not only RSC components) which are direct children ofthis
one and match the givenSelector
Component Scripts
(t.b.w) (script as function bodies, script attributes, script elements, delegated scripts)
function (
my,me, RSC,JIL, onAttributeChange, onAttachment,onDetachment,
toRender, html, on,once,off,trigger, reactively
) {
// this is where scripts are inserted
}
my
contains a reference tothis
visual (i.e., the one in whose context the current script is running). If you define getters and setters for observed and unobserved variables, inside these accessors,this
will refer to the data structure rather than to the visual - in such situations,my
will help you refering to the actual visual. Additionally, you may usemy
to let your code look like ordinary english: e.g.,my.observed.Value = ...
me
is just a synonym formy
and may be used wherever the resulting code will then read more like ordinary english: e.g., like inmy.render.bind(me)
RSC
contains a reference to RSC itself - thus, if you want to use any of its exported functions, you don't have to import the module yourself in your behaviour or component scriptsJIL
since RSC uses the javascript-interface-library internally anyway, you may use this reference to that library in order to avoid having to import it in your scripts yourself in your behaviour or component scriptsonAttributeChange
onAttributeChange((normalizedName,newValue) => ...)
can be used to install a function (with the given signature) that will be called whenever an attribute of an RSC component was changed. Only one callback function can be installed, later invocations ofonAttributeChange
overwrite previously registered callbacksonAttachment
onAttachment(() => ...)
can be used to install a function that will be called whenever an RSC component is added to the DOM while RSC is running (and all behaviours have already been defined). Only one callback function can be installed, later invocations ofonAttachment
overwrite previously registered callbacksonDetachment
onDetachment(() => ...)
can be used to install a function that will be called whenever an RSC component is removed from the DOM. Only one callback function can be installed, later invocations ofonDetachment
overwrite previously registered callbackstoRender
toRender(() => ...)
can be used to install a function that will be called whenever an RSC component has to be (re-)rendered. Only one callback function can be installed, later invocations oftoRender
overwrite previously registered callbackshtml
is a reference to the htm markup function prepared for being used with preact - i.e., within RSC scriptson
on(events, selectors, data, (Event) => ...)
can be used to install a handler for the given (comma-separated) list ofevents
, sent from RSC components or DOM elements identified by any of the (optionally) given (comma-separated) selectors ...once
off
trigger
reactively
reactively(() => ...)
Behaviour Scripts
(t.b.w) (behaviour registry, behaviour definition, behaviour and component scripts together)
Observed and Unobserved Variables
(t.b.w) (accessors, scope)
Reactive Functions
(t.b.w) (reactively, initial invocation, variable tracking, see hyperactiv)
Reactive Attributes
(t.b.w) (access path, unidirectional/bidirectional binding)
Rendering
(t.b.w) (see htm, DOM diffing by preact, initial rendering, automatic vs. manual re-rendering)
RSC Events
(t.b.w) (event handler registration on/once/off, selectors, event triggering, arguments, results, bubbling)
Error Indicators
(t.b.w)
Pre-defined Behaviours
(t.b.w) (full-bundle)
Basic Components
(t.b.w)
Layout Components
(t.b.w)
Native HTML Controls
(t.b.w)
Various Other Components
(t.b.w)
Examples
(t.b.w)
API Reference
(t.b.w)
assign
isRunning ():boolean
ValueIsDOMElement (Value:any):boolean
allow/expect[ed]DOMElement (Description:string, Argument:any):Element|null|undefined
ValueIsVisual (Value:any):boolean
allow/expect[ed]Visual (Description:string, Argument:any):RSC_Visual|null|undefined
ValueIsName (Value:any):boolean
allow/expect[ed]dName (Description:string, Argument:any):RSC_Name|null|undefined
ValueIsErrorInfo (Value:any):boolean
allow/expect[ed]ErrorInfo (Description:string, Argument:any):RSC_ErrorInfo|null|undefined
newUUID ():RSC_UUID
outerVisualOf (DOMElement:HTMLElement):RSC_Visual|undefined
VisualContaining (DOMElement:HTMLElement):RSC_Visual|undefined
outermostVisualOf (DOMElement:HTMLElement):RSC_Visual|undefined
closestVisualWithBehaviour(DOMElement:HTMLElement, BehaviourName:RSC_Name):RSC_Visual|undefined
closestVisualMatchingclosestVisualMatching (DOMElement:HTMLElement, Selector:Textline):RSC_Visual|undefined
innerVisualsOf (DOMElement:HTMLElement):RSC_Visual[]
registerBehaviour(Name:RSC_Name, SourceOrExecutable:Text|Function|undefined, observedAttributes:RSC_Name[] = []):void
Global Reactivity
(t.b.w)
observed
unobserved
Convenience Functions for Behaviour (and Visual) Scripts
(t.b.w)
throwReadOnlyError (Name:string):never
throws an error which indicates that the property calledName
can not be modifiedBooleanProperty(my:RSC_Visual, PropertyName:string, Default?:boolean, Description?:string, readonly:boolean = false):object
BooleanListProperty(my:RSC_Visual, PropertyName:string, Default?:boolean[], Description?:string, readonly:boolean = false):object
NumberProperty(my:RSC_Visual, PropertyName:string, Default?:number, Description?:string, readonly:boolean = false):object
NumberListProperty(my:RSC_Visual, PropertyName:string, Default?:number[], Description?:string, readonly:boolean = false):object
NumberPropertyInRange(my:RSC_Visual, PropertyName:string, lowerLimit?:number, upperLimit?:number, withLower:boolean = false, withUpper:boolean = false, Default?:number, Description?:string, readonly:boolean = false):object
NumberListPropertyInRange(my:RSC_Visual, PropertyName:string, lowerLimit?:number, upperLimit?:number, withLower:boolean = false, withUpper:boolean = false, Default?:number[], Description?:string, readonly:boolean = false):object
IntegerProperty(my:RSC_Visual, PropertyName:string, Default?:number, Description?:string, readonly:boolean = false):object
IntegerListProperty(my:RSC_Visual, PropertyName:string, Default?:number[], Description?:string, readonly:boolean = false):object
IntegerPropertyInRange(my:RSC_Visual, PropertyName:string, lowerLimit?:number, upperLimit?:number, Default?:number, Description?:string, readonly:boolean = false):object
IntegerListPropertyInRange(my:RSC_Visual, PropertyName:string, lowerLimit?:number, upperLimit?:number, Default?:number[], Description?:string, readonly:boolean = false):object
StringProperty(my:RSC_Visual, PropertyName:string, Default?:string, Description?:string, readonly:boolean = false):object
StringListProperty(my:RSC_Visual, PropertyName:string, Default?:string[], Description?:string, readonly:boolean = false):object
StringPropertyMatching(my:RSC_Visual, PropertyName:string, Pattern:RegExp, Default?:string, Description?:string, readonly:boolean = false):object
StringListPropertyMatching(my:RSC_Visual, PropertyName:string, Pattern:RegExp, Default?:string[], Description?:string, readonly:boolean = false):object
TextProperty(my:RSC_Visual, PropertyName:string, Default?:string, Description?:string, readonly:boolean = false):object
TextlineProperty(my:RSC_Visual, PropertyName:string, Default?:string, Description?:string, readonly:boolean = false):object
OneOfProperty(my:RSC_Visual, PropertyName:string, allowedValues:string[], Default?:string, Description?:string, readonly:boolean = false):object
OneOfListProperty(my:RSC_Visual, PropertyName:string, allowedValues:string[], Default?:string[], Description?:string, readonly:boolean = false):object
URLProperty(my:RSC_Visual, PropertyName:string, Default?:string, Description?:string, readonly:boolean = false):object
URLListProperty(my:RSC_Visual, PropertyName:string, Default?:string[], Description?:string, readonly:boolean = false):object
handleBooleanAttribute(reportedName:string, reportedValue:string|undefined, my:RSC_Visual, Name:string, PropertyName?:string):boolean
handleBooleanListAttribute(reportedName:string, reportedValue:string|undefined, my:RSC_Visual, Name:string, PropertyName?:string):boolean
handleNumericAttribute(reportedName:string, reportedValue:string|undefined, my:RSC_Visual, Name:string, PropertyName?:string):boolean
handleNumericListAttribute(reportedName:string, reportedValue:string|undefined, my:RSC_Visual, Name:string, PropertyName?:string):boolean
handleLiteralAttribute(reportedName:string, reportedValue:string|undefined, my:RSC_Visual, Name:string, PropertyName?:string):boolean
handleLiteralListAttribute(reportedName:string, reportedValue:string|undefined, my:RSC_Visual, Name:string, PropertyName?:string):boolean
handleLiteralLinesAttribute(reportedName:string, reportedValue:string|undefined, my:RSC_Visual, Name:string, PropertyName?:string):boolean
handleSettingOrKeywordAttribute (reportedName:string, reportedValue:string|undefined, my:RSC_Visual, Name:string, permittedValues:string[], permittedKeywords?:string[], PropertyName?:string):boolean
handleJSONAttribute(reportedName:string, reportedValue:string|undefined, my:RSC_Visual, Name:string, PropertyName?:string):boolean
handleJSONLinesAttribute(reportedName:string, reportedValue:string|undefined, my:RSC_Visual, Name:string, PropertyName?:string):boolean
Script Templates
The following code templates may be quite practical when writing custom behaviours - you don't have to use them, but they may save you some typing.
Initialization
Explicitly setting the initial state (and using accessors for any further state changes, as shown below) makes code that uses this state leaner. You may use
this.unobserved.XXX = ...
if you have a single state variable only, or
Object.assign(this.unobserved,{
XXX:...,
YYY:...,
... // add as many variables as you need
})
if you have more of them.
State Access
It is always a good idea to protect a visual's state against faulty values. You may use the following template to define your own custom accessors:
const my = this // "my" is relevant in the following getters and setters
Object.assign(my.observed,{
get XXX () { return my.unobserved.XXX },
set XXX (newValue) {
... // add your validation logic here
my.unobserved.XXX = newValue
},
... // add as many accessors as you need
})
Attribute Mapping
Internally, RSC works with arbitrary JavaScript values as their state, but initially, you may want to configure your components using element attributes (which are always strings). You may use the following code to map attributes to state variables
onAttributeChange((Name, newValue) => {
if (Name === 'xxx') {
this.observed.XXX = newValue
return true
}
}) // not returning "true" triggers automatic mapping
if you only need to map a single attribute, or
onAttributeChange((Name, newValue) => {
switch (Name) {
case 'xxx': this.observed.XXX = newValue; break
case 'yyy': this.observed.YYY = newValue; break
... // add as many mappings as you need
default: return false // triggers automatic mapping
}
return true
})
if you want to map more of them.
Please, keep in mind, that you may have to parse given attributes before they can be assigned to state variables. Typical "parsers" include:
parseFloat(newValue)
parseInt(newValue,10)
JSON.parse(newValue)
Don't forget, that parsing may fail - you may want to handle parser errors explicitly, but RSC will catch exceptions in onAttributeChange
and present an error indicator for any unhandled error.
Important: don't forget to add all relevant attribute names to the
observed-attributes
attribute of your behaviour script element
<script type="rsc-script" for-behaviour="..." observed-attributes="xxx, yyy, ...">
or
onAttributeChange
will never be invoked.
Nota bene: if internal names and attribute names of all variables are the same (except for capitalisation) and you also do not have to parse any of the attributes (e.g., because all variables are of type
string
anyway), then there is no need for an explicitonAttributeChange
handler: RSC will map such attributes automatically.
Custom Rendering
In almost any case, you may want to render your new visual in a custom way: use
toRender(() => html`...`)
for simple one-liners without additional rendering logic, or
toRender(() => {
... // add your logic here
return html`...`
})
for anything else.
Complete (Behaviour) Script Template
Just for the sake of convenience, here is the complete template for a behaviour script
<script type="rsc-script" for-behaviour="..." observed-attributes="xxx, yyy, ...">
Object.assign(this.unobserved,{
XXX:...,
YYY:...,
... // add as many variables as you need
})
const my = this // "my" is relevant in the following getters and setters
Object.assign(my.observed,{
get XXX () { return my.unobserved.XXX },
set XXX (newValue) {
... // add your validation logic here
my.unobserved.XXX = newValue
},
... // add as many accessors as you need
})
onAttributeChange((Name, newValue) => {
switch (Name) {
case 'xxx': this.observed.XXX = newValue; break
case 'yyy': this.observed.YYY = newValue; break
... // add as many mappings as you need
default: return false // triggers automatic mapping
}
return true
})
toRender(() => {
const { XXX,YYY,... } = this.observed
... // add your logic here
return html`...`
})
</script>
If you want to create a script element for a specific visual, simply
- remove
for-behaviour="..."
(or replace it byfor="..."
for a delegated script) and - remove
observed-attributes="..."
(because only behaviours can observe element attributes)
That's it!
Build Instructions
You may easily build this package yourself.
Just install NPM according to the instructions for your platform and follow these steps:
- either clone this repository using git or download a ZIP archive with its contents to your disk and unpack it there
- open a shell and navigate to the root directory of this repository
- run
npm install
in order to install the complete build environment - execute
npm run build
to create a new build
If you made some changes to the source code, you may also try
npm run agadoo
in order to check if the result is still tree-shakable.
You may also look into the author's build-configuration-study for a general description of his build environment.