light-weight reactive scriptable web components
npm install reactive-scriptable-componentslight-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):
!Screenshot of the Temperature Converter Example
``html
`
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 problemobserved
* every "reactive scriptable component" (which is a standard web component) may contain its own and unobserved (state) variables - in this trivial example, only the applet itself provides some "state", whereas the input controls do notobserved
* whenever an variable is changed, all functions using that variable may be reactively recalculated - in this example, changes of the Celsius variable will recompute the Fahrenheit 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.
"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 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 Scripts
if you want to separate the "look" from its "feel", you may provide "delegated scripts" () for components that can be identified by a CSS selector (e.g., #id, .class, [attr="value"] etc.)
- Behaviour Scripts
if you have multiple RSC components that share the same functionality, you may define a "behaviour" and provide the shared code in a separate 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
- Observed and Unobserved Variables
RSC components usually have to store some values they need for their operation. For that purpose, RSC provides both an and an unobserved 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 changereactively(() => ...)
- Reactive Functions
"reactive functions" (defined using ) are functions that will be automatically invoked whenever any of the observed(!) values they use internally have changed$value
- Reactive Attributes
"reactive attributes" have names starting with one or two dollar signs (e.g., 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 Calls
sometimes, 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 Indicators
often, 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
!Screenshot of built-in native HTML Controls
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
"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.
In order to avoid initial flashing of "custom Elements" (aka "Web Components") you should always add the following lines at the beginning of the
section in your HTML file:`html
`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
section mentioned above:`html
`$3
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
section of your HTML file:`html
`Then, if you don't use any package that already _imports_ the RSC module, _load_ it with the following lines:
`html
src="https://rozek.github.io/reactive-scriptable-components/dist/reactive-scriptable-components.modern.js"
>
`Otherwise, just _load your package_, e.g. the
full-bundle with all predefined RSC behaviours:`html
src="https://rozek.github.io/reactive-scriptable-components/behaviours/full-bundle.js"
>
`$3
(t.b.w)
Applet Creation ##
(t.b.w)
$3
(t.b.w) (lifecycle handling, nesting, containment validation)
$3
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 of this one with behaviour "Applet"
- Card
is a getter which returns a reference to the closest visual of this one with behaviour "Card"
- outerVisual
is a getter which returns a reference to the next outer visual of this one
- outermostVisual
is a getter which returns a reference to the outermost visual of this one
- closestVisualWithBehaviour (BehaviourName)
returns a reference to the closest visual of this one with the given BehaviourName - please note, that the "closest" may also be this visual
- closestOuterVisualWithBehaviour (BehaviourName)
returns a reference to the closest _outer_ visual of this one with the given BehaviourName
- closestVisualMatching (Selector)
returns a reference to the closest visual of this one matching the given Selector - please note, that the "closest" may also be this visual
- closestOuterVisualMatching (Selector)
returns a reference to the closest _outer_ visual of this one matching the given Selector
- innerVisuals
is a getter which returns a (possibly empty) list of all visuals which are direct children of this one
- innerVisualsWithBehaviour (BehaviourName)
returns a (possibly empty) list of all visuals with the given BehaviourName which are direct children of this one
- innerVisualsMatching (Selector)
returns a (possibly empty) list of all visuals which are direct children of this one and match the given Selector
- innerElements
is a getter which returns a (possibly empty) list of all elements (i.e., not only RSC components) which are direct children of this one
- innerElementsMatching (Selector)
returns a (possibly empty) list of all elements (i.e., not only RSC components) which are direct children of this one and match the given Selector$3
(t.b.w) (script as function bodies, script attributes, script elements, delegated scripts)
`javascript
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 to this 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 use my to let your code look like ordinary english: e.g., my.observed.Value = ...
- me
is just a synonym for my and may be used wherever the resulting code will then read more like ordinary english: e.g., like in my.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 scripts
- JIL
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 scripts
- onAttributeChange
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 of onAttributeChange overwrite previously registered callbacks
- onAttachment
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 of onAttachment overwrite previously registered callbacks
- onDetachment
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 of onDetachment overwrite previously registered callbacks
- toRender
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 of toRender overwrite previously registered callbacks
- html
is a reference to the htm markup function prepared for being used with preact - i.e., within RSC scripts
- on
on(events, selectors, data, (Event) => ...) can be used to install a handler for the given (comma-separated) list of events, sent from RSC components or DOM elements identified by any of the (optionally) given (comma-separated) selectors ...
- once
- off
- trigger
- reactively
reactively(() => ...)$3
(t.b.w) (behaviour registry, behaviour definition, behaviour and component scripts together)
$3
(t.b.w) (accessors, scope)
$3
(t.b.w) (reactively, initial invocation, variable tracking, see hyperactiv)
$3
(t.b.w) (access path, unidirectional/bidirectional binding)
$3
(t.b.w) (see htm, DOM diffing by preact, initial rendering, automatic vs. manual re-rendering)
$3
(t.b.w) (event handler registration on/once/off, selectors, event triggering, arguments, results, bubbling)
$3
(t.b.w)
Pre-defined Behaviours ##
(t.b.w) (full-bundle)
$3
(t.b.w)
$3
(t.b.w)
$3
(t.b.w)
$3
(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
$3
(t.b.w)
*
observed
* unobserved
$3
(t.b.w)
*
throwReadOnlyError (Name:string):never
throws an error which indicates that the property called Name can not be modified
* BooleanProperty(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.
$3
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
`javascript
this.unobserved.XXX = ...
`if you have a single state variable only, or
`javascript
Object.assign(this.unobserved,{
XXX:...,
YYY:...,
... // add as many variables as you need
})
`if you have more of them.
$3
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:
`javascript
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
})
`$3
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
`javascript
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
`javascript
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:
`javascript
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
>
> If you want to create a script element for a specific visual, simply
* remove
for-behaviour="..." (or replace it by for="..." 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:
1. either clone this repository using git or download a ZIP archive with its contents to your disk and unpack it there
2. open a shell and navigate to the root directory of this repository
3. run
npm install in order to install the complete build environment
4. execute npm run build to create a new buildIf 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.