0.0.36 • Published 3 months ago

reactive-scriptable-components v0.0.36

Weekly downloads
-
License
MIT
Repository
github
Last release
3 months ago

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):

Screenshot of the Temperature Converter Example

  <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 and unobserved (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 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.

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 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 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

Screenshot of built-in native HTML 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:

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:

  • Appletis a getter which returns a reference to the closest visual of this one with behaviour "Applet"
  • Cardis a getter which returns a reference to the closest visual of this one with behaviour "Card" 
  • outerVisualis a getter which returns a reference to the next outer visual of this one
  • outermostVisualis 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 
  • innerVisualsis 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
  • innerElementsis 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

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
}
  • mycontains 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 = ...
  • meis 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) 
  • RSCcontains 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
  • JILsince 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 
  • onAttributeChangeonAttributeChange((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
  • onAttachmentonAttachment(() => ...) 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
  • onDetachmentonDetachment(() => ...) 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 
  • toRendertoRender(() => ...) 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
  • htmlis a reference to the htm markup function prepared for being used with preact - i.e., within RSC scripts 
  • onon(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 
  • reactivelyreactively(() => ...)

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):neverthrows 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.

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 explicit onAttributeChange 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 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 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.

License

MIT License

0.0.36

3 months ago

0.0.32

3 months ago

0.0.33

3 months ago

0.0.34

3 months ago

0.0.35

3 months ago

0.0.30

3 months ago

0.0.31

3 months ago

0.0.28

3 months ago

0.0.29

3 months ago

0.0.26

3 months ago

0.0.27

3 months ago

0.0.22

3 months ago

0.0.23

3 months ago

0.0.24

3 months ago

0.0.25

3 months ago

0.0.20

3 months ago

0.0.17

3 months ago

0.0.18

3 months ago

0.0.19

3 months ago

0.0.16

3 months ago

0.0.10

3 months ago

0.0.11

3 months ago

0.0.12

3 months ago

0.0.13

3 months ago

0.0.14

3 months ago

0.0.15

3 months ago

0.0.9

3 months ago

0.0.8

3 months ago

0.0.5

4 months ago

0.0.7

3 months ago

0.0.6

3 months ago

0.0.4

4 months ago

0.0.3

4 months ago

0.0.2

4 months ago

0.0.1

4 months ago