Reactive HTML through the OtoReact framework

Introduction

Summary

OtoReact is a library that brings reactivity straight into your HTML file.
The first application above is the result of this piece of Reactive HTML (RHTML):


    

The Reactive HTML is placed in an HTML document that loads the OtoReact library, e.g. like this:


        

That's all you need.

Full framework functionality, offered by a library.

Nothing to install, to configure, to build, and not a single line of required JavaScript code.

Running like a charm.

What is it?

'Reactive web applications', or 'Single Page Applications', are web applications that, when possible, react immediately on user input, timer events etc, or the receival of server data requested by the application, instead of having the web server generate and send a new HTML page.
This results in both a much better user experience and much less server load.

Two tiny examples you see above; please enter some data.
This website as a whole is also a reactive web application, written in OtoReact.

Reactivity is attained by means of JavaScript programming code running inside the web browser. The JavaScript code has to manipulate the so-called "DOM" ("Document Object Model"), which is the internal object model of a web page ("document").
Writing such JavaScript by hand can get quite complicated and might result in very cluttered programming code. A framework makes it much easier to create reactive web applications.

OtoReact is a small and fast framework library to attain reactivity by loading so-called Reactive HTML, or RHTML, straight into the browser:

  • Reactive HTML is based on HTML; JavaScript is needed only for data manipulation and event handling. Not for DOM manipulation.
    Even components are defined in HTML without a single line of JavaScript.
  • Reactive HTML is parsed by the browser itself, and compiled and executed by OtoReact within milliseconds.
  • Reactive HTML is easy to learn, and one doesn't need to install a compiler, a bunch of libraries, node.js, or anything.
    Just a text editor (like Visual Studio Code) suffices.
  • Reactive HTML makes it easy to build dynamic pages based on data, even if no reactivity is needed.
  • Reactive HTML makes it easy to follow the Model-view-controller design pattern, separating your data model and program logic from the presentation.
  • Reactive HTML can build dynamic CSS style sheets; you may not need a separate CSS framework.
  • Reactive HTML has a powerful component system, URL routing, global error handling, some persistence, et cetera.
    • Components are defined right within the HTML, without needing any JavaScript statements
    • Components are lightweight and have local scope.
    • Components can be (mutually) recursive
    • Component definitions can be nested: a component definition can contain its own local component definitions
  • OtoReact can be combined with other tooling: it can be used for fragments of a larger application, and does not modify any native objects (prototypes) which might cause conflicts. It defines a few global variables but does not depend on them.
See also Why OtoReact.

Please note that you need at least a basic understanding of HTML and JavaScript to work with a framework like OtoReact.

Samples

What's your name

Let's return to the tiny example above left.
You can modify the source code below, and on every keystroke the modified application will be recompiled and re-executed.

Explanation:

  • DEFINE rvar='yourName' introduces a "reactive variable" yourName, that can contain state information of the application.
    • yourName.V denotes the value of the variable.
    • We have now added store='sessionStorage', which makes the value persist when the application is reloaded.
  • @value="yourName.V" binds the value of the reactive variable to the value of the input element. Whenever the input element receives input, OtoReact will update the reactive variable and all document content that reacts on it.
  • IF cond=… is an RHTML conditional. Whenever the JavaScript condition is 'truthy', in this case when yourName.V is non-empty, then the body of the conditional is rendered.
  • \{yourName.V\} and \{yourName.V.length\} are embedded expressions. The rendered document will contain the value of the expression between the braces.
In the bottom right corner, the compile and initial build time of the example are shown. The time used for updates, when data is entered into (or asynchronously received by) the application is not measured.

Multiplication tables

You might increment the numbers, and see how fast the DOM is updated.

Explanation:

  • DEF rvar=maxY … is an abbreviation of DEFINE …. It declares a reactive variable maxY with initial value 10, to persist in sessionStorage.
  • Attributing INPUT type=number @valueAsNumber=maxY.V declares that, at any input event, the numeric value of the input element shall be assigned to maxY.V. The rest of the application shall react on the new value.
  • FOR let=y of="range(1,maxY.V)" lets local variable y iterate through the values of range(1,maxY.V), which are the numbers 1 to maxY.V.

Tic-Tac-Toe

Study the source code after you have read more of this documentation.

Working with server data

Here is an example of dynamically building a table based on server data, with a bit of animation as well:

Remarks

Other frameworks

There exist quite a number of alternative libraries and frameworks to make it easier to build reactive web applications. I distinguish two main categories:

  1. Some, like JQuery and React, offer tools to make it easier to manipulate the DOM.
    All manipulation is still done from within the JavaScript you write, and you need a good understanding of DOM manipulation and JavaScript in general.
    The respective libraries can in some cases be loaded straight into the browser without compilation.
  2. Others, like Angular, Svelte, and Vue (with a build step), make a clean separation between the programming code and presentation. The JavaScript you write contains the data model and data manipulation you need; the actual web page is described by a template file containing HTML enriched with directives and other stuff.
    You don't need to write code to manipulate the DOM at all anymore; this is done by the framework.
    The source code files have to be compiled on you development machine. You need to install the compiler, and quite some modules for each application (which for Angular may take more than 1GB per application).

Reactive HTML combines features of both approaches, giving you the advantages of describing the desired reactive layout by a HTML template, separate from your programming code, but without the hassle of installing and using a compiler and managing quite a number of configuration files, and you need just a basic understanding of JavaScript.

Reactive !== Responsive

Reactive is not the same thing as responsive, though there is some overlap.
Responsive web design is a phrase used to indicate web pages that adapt themselves to the device they are viewed on.

This document for example changes its layout when being printed or viewed on a narrow screen: the table of contents moves to the top, and on a mobile device font sizes are adapted.

Responsiveness is usually attained by using CSS media queries, which is perfectly possible in Reactive HTML.
When you get stuck with media queries, then reactivity may come to the rescue, to make responsive adaptations to your document or style definitions not possible with media queries.

No server-side functionality

OtoReact does not provide server-side functionality. You need other tooling for that, like Node.js (+ Express), Python, PHP, .Net, or perhaps you can use existing web API's.

Document setup

  1. Download OtoReact.js from the download page and place it somewhere on your server.
    It's less than 40 kilobyte, and has no dependencies.
  2. Include the OtoReact compiler into your application:
    <{}script type=module src="path/to/OtoReact.js"><{}/script>
  3. Mark the element(s) of your document you wish to compile with attribute rhtml:
    <{}body rhtml>
    , then OtoReact will implicitly compile and render the document body at the first event cycle.
  4. Optionally, add an option list to the rhtml attribute.
  5. You may also want to hide all or part of your application until it has been built, e.g. by using: <{}body rhtml hidden>.
    OtoReact will unhide its target after it has been built.

That's all!

Why OtoReact?

Some advantages of using OtoReact above other frameworks:
  1. We think it is conceptually simpler to have the full document definitions in enriched HTML, rather than requiring JavaScript to invoke the framework, define components, link everything together, etc.
  2. Installation (i.e. downloading a single file) is much simpler, and takes much less disk space, than with most other frameworks.
  3. State variables (RVAR's) can be defined in any local HTML scope, while in other frameworks state variables are only available at the component level.
    So if, for example, you need a state variable within a FOR iteration, then you can simply define a single RVAR within that FOR block, and OtoReact will repeatedly instantiate the RVAR.
    In other frameworks you would either have to define a whole array of state variables, or define a separate component just for the purpose of having a local state variable.
  4. Components can be defined in any local HTML scope too, meaning you can easily use them to avoid local code repetitions.
    Local component definitions can refer to local variables visible at the point of definition, without needing these to be passed as parameters.
  5. Just as in any modern programming language, OtoReact (and Svelte) have control structures for conditional and repeated code that surround the code blocks they apply to.
    Angular and Vue depend on attributes to be inserted inside the element to be repeated or conditionally rendered.

    Compare:

    Angular:
    <{}li *ngFor="let item of items">
        \{\{ item.message }}
    <{}/li>
    
    Svelte:
    \{#each items as item}
        <{}li>
            \{ item.message }
        <{}/li>
    \{/each}
    
    Vue:
    <{}li v-for="item in items">
        \{\{ item.message }}
    <{}/li>
    
    React:
    items.map( item => (
        <{}li>
            \{ item.message }
        <{}/li>
    ))
    
    OtoReact:
    <{}for let="item" of="items">
        <{}li>
            \{ item.message }
        <{}/li>
    <{}/for>
    
  6. There are several small nifty features you won't find in other frameworks. E.g. the simple fact that null and undefined are not rendered simplifies many embedded expressions.

Some drawbacks are:

  1. OtoReact does not yet do dependency analysis. Depending on the size and complexity of your application, it may be desirable or required to insert hints (attributes reacton and hash) that tell OtoReact which RHTML parts should react on which state variables.
  2. The HTML parser does not allow OtoReacts control structures inside table, select, and datalist (see Parser limitations), so one has to use alternative element names table., select., etc. instead.
  3. Runtime compilation is theoretically slower than serving a precompiled application. However:
    • Compilation time, usually ranging from 5 - 100 milliseconds, is quite small in relation to the overall load time of a web application.
    • In many or most cases, compilation can be done while the application waits for other resources like images or data, so it doesn't increase the overall load time at all.

Search engine compatibility

Static content in an OtoReact main file is just HTML and can be indexed by any search engine. This is a great advantage over frameworks like Angular and Svelte, where all content is generated by JavaScript.

Dynamic content generated by any JavaScript framework including OtoReact, or just any client-side JavaScript code, can only be indexed by a search engine if that engine is able to execute the JavaScript.
Now Google does a reasonably good job at executing JavaScript, and may be able to index your dynamic OtoReact site, provided each page has its own URL and is reachable through normal links.
Most other engines, including Bing I think, won't do so.

Static content in included files can only be loaded through JavaScript, and can only be indexed by search engines executing JavaScript.
If you want your static content be split over multiple files ánd want it to be indexable by any search engine, you could think of the following solutions:

  • Use server-side technology, like Server Side Includes (SSI) or PHP, to include all included and imported RHTML files in your main file.
  • Use a tool to bundle multiple RHTML files into a single file before deployment.
    Such a tool is not available yet, but it's not difficult to develop one.

Playground(s)

Here you have a playground to enter your own Reactive HTML.

All code you enter will be saved by your browser in localStorage, and should be available when you return to this page.

The OtoReact framework

RHTML Concepts

This chapter introduces and documents a number of basic RHTML concepts.

Sections and paragraphs marked with a '*' are advanced and may be skipped on first reading.

Notation

means:
Source text "{idemo}" produces output "".

String interpolation: text with embedded expressions

All text content and all HTML attributes inside RHTML code, except scripts, expressions and event handlers that are already JavaScript, may contained embedded JavaScript expressions between braces:
This is called string interpolation or, in JavaScript terminology, 'template literals'.

The expressions are evaluated, converted to string, and inserted as text; there is absolutely no risk of code injection.

Within JavaScript, you can of course use the JavaScript syntax for template literals, using backquotes and dollar signs:

let x = `Some text ${expression} et cetera`;

If you prefer, you may add a dollar sign in RHTML as well:

Notes*

  • Literal (normal) braces can be shown by preceeding them with a backslash:

    In all other cases, backslashes stand for themselves:

  • To include a literal backslash or dollar sign right in front of an embedded expression, insert a dollar sign in between: ,
  • If you set the option 'bDollarRequired', then the dollar sign becomes compulsory, and you can write literal braces without backslashes.
  • Expression values null and undefined are not shown: .

    This is unlike JavaScript template literals, where null and undefined are spelled out:

  • You may omit the expression, like \{ \}, which comes in handy if you want the parser to not recognize an HTML tag:

RHTML constructs overview

RHTML defines a number of new constructs, which dynamically build your HTML page:

  • DEF or DEFINE introduces a local variable or reactive variable
  • IF specifies a conditional block of RHTML
  • CASE specifies a series of alternative conditional blocks of RHTML
  • FOR specifies a repeating block of RHTML, with a number of additional features
  • COMPONENT defines a user-defined construct.
    Components may be recursive and may have slots, which are themselves full-fledged constructs.
  • IMPORT asynchronously imports components defined in a separate file (a module) into your application
  • INCLUDE asynchronously includes a separate RHTML file into your application
  • RHTML dynamically compiles a string as RHTML.
    This is used in the demo component you see on this page.
  • REACT on optionally allows you to specify which blocks of RHTML should react on which variables, optimizing your application.
    The RHTML attribute reacton does the same thing.
  • DOCUMENT name defines a separate reactive child document that can be opened in separate (popup) windows, or can be printed without being opened.
  • ELEMENT inserts a variably named element.
  • ATTRIBUTE adds a variably named attribute to its parent HTML element.
  • COMMENT inserts a comment containing variable text.
Note: We write construct names in uppercase and attribute names in lowercase here, but as in HTML, both are case independent.

RHTML functions

The OtoReact module makes the following functions available for import. They are added to the global environment as well.
The question marks indicate optional arguments.
  • RVAR(name?, initialValue?, store?) creates a reactive variable.
  • range(start?, count, step?) yields an iterable range of count numerical values: start, start+step, …, start+(count-1) * step.
    You can use this with FOR.
  • reroute() and docLocation are used with URL routing.
  • * RFetch(resource, init?) is the same as fetch(resource, init?), except that it throws an error when an HTTP error status is received.
  • * RCompile(HTMLElement, options?) is available as an alternative way to initiate RHTML compilation.
    It compiles and builds the given HTMLElement as RHTML, using the given options.

All of these are exported by the OtoReact module. TypeScript type declarations are available on the download page.

Local and global variables, 'globalThis'

Ordinary JavaScript in a browser environment distinguishes between local variables, module variables and global variables:
  • Local variables are visible within a single block \{ … \} of JavaScript code
  • Module variables are visible within a single module, and may be exported and imported into other modules
  • Global variables are visible in all JavaScript code running in the context of a single browser window:
    • Embedded scripts
    • External scripts
    • HTML inline event handlers
Browser-based JavaScript also has a distinction between classic JavaScript and module JavaScript, more about that in our section on SCRIPT.

RHTML local variables

RHTML adds one other kind of variable: RHTML local variables; these are visible in all local JavaScript code within a block of RHTML code.

These are introduced by the following RHTML constructs:

  • Constructs DEFINE, SCRIPT type="otoreact/local", SCRIPT type="otoreact/static", IMPORT defines=…, and DOCUMENT name introduce local variable(s) visible in the surrounding block, following the construct closing tag.
  • Constructs FOR, CASE value with capturing, COMPONENT templates with parameters, and DOCUMENT params? window? introduce local variables visible within the construct block.

RHTML local variable names obey strict lexical scoping rules, see DEFINE for a demonstration.

RHTML local variables are not visible within non-local SCRIPT embedded or external scripts; these scripts are executed just once in global scope.

Global variables in RHTML

Global variables are visible in RHTML inline scripts, of course.

Global variables are created:

  • Either as properties of the JavaScript global object, which is preferably refered to as globalThis:
    globalThis.varName = value;
    In web browsers, the global object is commonly named "window", but in other JavaScript environments it is named otherwise, and using "window" causes confusion with real window properties. Hence ECMAScript 2020 introduced the new cross-platform name globalThis to refer to the global object, and we suggest you use this rather than window.
  • Or by writing SCRIPT type="otoreact/global" defines=….

RVAR's: Reactive VARiable's

Reactive variables (RVAR's) are objects containing variable data on which RHTML elements may react.
Anytime the value of an RVAR changes, the reacting elements will be updated.

RVAR's are created:

  • either by calling RVAR(name?, initialValue?, store?, subscriber?, storename?) from JavaScript,
  • or by using the define rvar construct.

RVAR arguments

name
If you supply a name, the RVAR will be registered in the global environment under that name and will be visible anywhere.
So if you write:
    <{}script type=module>
        const x = RVAR('X');
        …
, then it will be available as x just inside this module, and also as X anywhere (though normally one shouldn't use different local and global names).
initialValue
Provides the initial value of the RVAR.
When initialValue is a Promise, then the value of the RVAR will initially be undefined, and when the promise resolves to a value, then the RVAR will be set to that value.
store*
Provides simple persistence functionality, see Persistence.
subscriber*
A routine which is subscribed to the RVAR for immediate execution, as by x.Subscribe(subscriber, true).
storename*
See Persistence.

You may or must inform OtoReact which fragments of RHTML should react on which RVAR's, by using the REACT element or the reacton attribute, see below.
For RVAR's created by calling RVAR(), this is necessary.
For RVAR's created by DEFINE rvar, if there are no explicit react's and no other subscribers, then all RHTML code following the DEFINE rvar, i.e. the range where the RVAR is visible, will implicitly react on the RVAR.

Properties and methods

An RVAR x is an object, distinct from the value of the variable. It has the following properties and methods:

x.V
To get or set the value of an RVAR x, one writes x.V.
When x.V is set to a different value than it had before, then the RVAR is marked as dirty, and all RHTML code that reacts on it will be updated.
x.U
When you modify not the value of a RVAR, but properties of the value, you may want the RVAR to be marked as dirty as well.
You can do this by writing x.U to get the value of the RVAR.

E.g., if x.V is an array, you can write x.U.push(e) or x.U[i] = e to add an or modify an array element, and the DOM will react on the modified array. So you don't have to assign to x.V to trigger a reaction.

Exception: within an HTML attribute or property definition, accessing x.U does not mark the RVAR as dirty. This is so that one can use x.U within RHTML two-way bindings, and the RVAR will only be marked dirty when the property is modified by an event, not when it is being set by the RHTML engine.

Setting x.U sets the value of x and marks it as dirty even when the value is equal to the previous value.

x.SetDirty()
The RVAR can be explicitly set dirty by calling x.SetDirty().
x.Subscribe(subs, bImmediate?, bInit?) *
Routine subs, when not null, is registered as a subscriber to x, so subs(x.V) will be executed whenever x has been set dirty.

When bImmediate is truthy, subs will be called immediately every time x is being set dirty; otherwise it will be called once at the next event cycle.

When bInit is truthy, then subs will initially be called when it is registered. The default value for bInit is the value of bImmediate.

The return value is x, so calls to Subscribe can be chained.

x.Unsubscribe(subs) *
Unregisters a routine subs previously registered as a subscriber to x.
x.Set(value) *
Sets x.V either synchronously, or asynchronously when value is a Promise.

When it is a Promise, then x.V will initially be undefined, and when the promise resolves to a value, then x.V will be set to that value.

x.Set *
Provides a routine that sets the value of x, i.e. v => x.Set(v).

This is handy to create an errorhandler. E.g., if errMsg is an RVAR that should receive error messages, then you can write doSomething().catch(errMsg.Set) to catch the errors of some asynchronous routine, or you can add an attribute #onerror="errMsg.Set" to catch all errors within a block of RHTML.

x.Clear *
Provides a routine that clears the value of x, except when x has just been set dirty in the same event loop.

You can e.g. add an attribute #onsuccess="errMsg.Clear" to clear any error message when any event handler succeeds without error.

Persistence*

When you provide a store parameter to RVAR() or to define rvar, then the variable value will be retrieved from that store when the variable is created, and stored at every update.

store can be:

  • sessionStorage, meaning that the value will be restored when the user reloads the page, or leaves it and returns again while staying in the same browser window
  • localStorage, meaning that the value will be preserved in local browser storage and restored when the user returns to the same site in the same browser on the same machine
  • Any other object that supports the setItem and getItem methods of the Storage interface.
    It could be an object that saves values to a database.

The RVAR must have a unique storename; the default is `RVAR_$\{name\}`, where the prefix "RVAR_" can be changed using option 'store_Prefix'.

An example using sessionStorage can be found in the Multiplication demo: modify the numbers; then either reload the page or modify the source code, which triggers a reload too, and see that the modified numbers have persisted.

Scripts in RHTML

Scripts can be included anywhere using the SCRIPT type? element just as in HTML.
Depending on the script type, they are either executed just once, or every time the surrounding element is being instantiated (built).

Scripts in OtoReact can export variables, so that these variables are either globally defined or locally visible in RHTML code.

See <{}SCRIPT> for details about SCRIPT in RHTML.

Style sheets

Internal or external STYLE style sheets can be included anywhere and you can choose whether they are effective in local or global scope.

With RSTYLE, one can dynamically generate style sheets, using all RHTML constructs and embedded expressions. One can for example:

  • Define a style based on variable data and use this style on multiple places in one or more stylesheets,
  • Use a FOR loop to define styles for all columns in a table, based on column specs fetched from a database,
  • Create style sheets that react on state changes.

See <{}STYLE> and <{}RSTYLE> for details.

How it works

OtoReact's RCompile does not, as one might perhaps expect, translate the whole chunk of RHTML into one large string of JavaScript.
Rather:
  1. Each snippet of JavaScript is compiled separately into a routine, by offering it to JavaScripts eval function (in global scope, of course).
    E.g., an RHTML property expression a+b is compiled by calling eval("([a,b]) => (a+b)").

    OtoReact does not itself parse and analyse the JavaScript, so it is unaware which variables are actually used and which are not.

  2. While traversing the RHTML document tree, all these tiny routines are combined by JavaScript functions into "builder" routines, which can build ór update the DOM tree according to the RHTML. Each RHTML source node corresponds to a separate builder routine.
  3. The root builder routine is called to perform the initial build.
  4. Each time a builder routine is called to build a DOM node, or a range of DOM nodes, it creates a so-called 'Range' object that refers to the created range of nodes and contains other meta-information as needed. This information is used when the builder routine is called again to update the same range of nodes.
  5. While building, any part of the resulting DOM tree which is marked to react on some RVAR, is registered with the RVAR together with its local 'environment' containing the value of all local variables at that point, and a local builder routine which can update just that part of the DOM tree.
  6. After the DOM tree has been built, the browser engine will compute the graphical document layout and graphically render its contents.
    It will do so again anytime the DOM tree changes.
  7. Whenever some RVAR is being marked dirty by a user event or otherwise, it will schedule (by setTimeout) all reacting DOM tree parts to be updated by their registered builder routine in its registered environment.
    The updating takes place at the next 'event cycle'.
    While updating, all present HTML elements will remain in place as much as possible, so that any elements that had received focus or input data will keep that.

Limitations inflicted by the parser

Using the browser HTML parser inflicts on us some limitations, as the parser isn't tailored for RHTML. Such as:
  • Element arrangement
    The HTML parser forces elements in some cases to be arranged according to HTML rules, and will silently rearrange them if they are not.
    Notably:
    • Table-related elements TABLE, THEAD, TBODY, TFOOT, TR may only contain appropriate children.
      The same holds for elements SELECT, DATALIST, OPTION, and perhaps others.
      If, e.g., a table would contain a for loop containing a tr:
      
                      , then the (Chromium) parser will move the for before the table, while leaving the tr inside.
                      
      Thus OtoReact sees:
      
                      , and will give a misleading error about x being undefined.
                      
      To get the loop right, OtoReact allows you to put one (or more) dots after any tag name, so the parser won't recognize this as a table and won't interfere:
      
                      When building the DOM tree, the dots will be removed.
                  
    • The paragraph-element p does not allow block-level elements inside, but allows unknown elements.
      Thus, if you write
      
                      , without closing the p, perhaps because you know the parser will automatically close it when needed, or because you didn't know block-level element ul is not allowed inside p, then the parser will silently close the p before the ul but after the if, because if is unknown to the parser.
                      
      So the ul ends up outside the if, without you or OtoReact being informed:
      
                      To avoid this, make a habit of always closing all elements.
                  
  • Attribute names are always lowercase
    As attribute names in HTML are case-insensitive, the parser makes them all lowercase.
    • As RHTML sometimes uses attribute names as bound variables, such variables will be lowercase, even if you wrote them with uppercase, and OtoReact can't warn you about this.
    • There is also a feature for conditional classes (#class:someclass="someBoolean"), that will only work for class names in lowercase.
    • RHTML allows you to use attributes to set DOM and Style properties, but these property names are case sensitive. So OtoReact receives the property name in lowercase and has to restore proper casing.
  • Missing error messages
    There are more cases where the browser doesn't inform you of errors, which may result in strange behaviour.

I imagine a formalism similar to Reactive HTML one day being natively supported by the browser. In that case, these limitations can be lifted.

Problem solving

  • When you get weird errors, you may have forgotten to add dots where needed.
    Otherwise, you may want to check how the browser has parsed your document:
    1. Outcomment or remove your compiler invocation, like _RHTML or // RCompile(…)
    2. Reload your application
    3. Inspect the parsed DOM-tree using your browsers development tools. Either:
      • Right click in your document and select 'Inspect'
      • Press F12 or Ctrl-Shift-I and select tab 'Elements' (Chrome) or 'Inspector' (Firefox)
  • When your application doesn't react on events, you may have forgotten some reacton attribute.
  • Errors while building the DOM are by default inserted into the DOM output, but may sometimes be invisible or get overlooked. Look in the development console to be sure you haven't missed any error messages, or enable option bAbortOnError.
  • When an event handler or some other piece of JavaScript doesn't work as expected, you might insert the debugger statement, to get a breakpoint at that point when the browser development tool is opened.
Page '{docLocation.subpath}' not found
This site uses cookies from Google Analytics.
You can find more about me on my LinkedIn profile,
and on my personal homepage in Dutch