watchlight
v1.1.2-b
Published
A light weight, comprehensive, reactive framework for business logic.
Downloads
22
Maintainers
Readme
Introduction to Watchlight
A light-weight, comprehensive, reactive framework for business logic and when things change.
Watchlight
provides a range of approaches to support reactive programming beyond the DOM and user interface with a
light-weight JavaScript module (14K minified, 4.6K gzipped).
- Event listeners on any reactive object via
addEventListener
. - Observers via functions wrapping reactive objects,
e.g.
observer(() => console.log(myObject.name))
will log thename
every time it changes. - A range of
Observable
capability similar toRxJs
. - Inference rules similar to Drools or
Rools and modeled after the
Promise
paradigm. - Spreadsheets ... no reactive library would be complete without them.
The spreadsheet and rules are provided as separate files, rule.js
and ./sheet.js
which are not included in
the 4.5K size stated above. They are XXX and YYY respectively.
Watchlight
does not use any intermediate languages or transpilation; hence, you can debug all of your code as written
using a standard JavaScript debugger.
Installation
Watchlight
is provided as a JavaScript module. It can be loaded directly in a modern browser or used directly in
NodeJS.
npm install watchlight
The repository is at https://github.com/anywhichway/watchlight
Transpiling and minifying is left to the developer using the library.
Using The Examples
There are examples in the examples directory and sub-directories. Most examples
can be run by both loading an HTML file and running the command node examplefilename.js
. The HTML files just load the
same JavaScript files that are fed to NodeJS on the command line.
Psuedo Classes
Watchlight
makes extensive use of Proxy
or other constructs around objects and functions you provide. These
constructs supplement the behavior of your functions and classes, but instanceof
will only be true for your original
symbols, i.e. nothing will ever be an instanceof
a psuedo-class.
The psuedo-classes include:
- Observable
- Observer
- Subscription
- Rule
- Partial
- Sheet
- Dimension
- Cell
Observable Objects, Constructors and Functions
The core of Watchlight
is the psuedo-class Observable
. Observable objects are reactive, they drive application in
a non-procedural manner. They can have event listeners and subscribers attached, be the
subject of observers, and be referenced by inference rules.
When the properties of Observable objects contain sub-objects, the sub-objects are returned as reactive objects when accessed.
If a class is made Observable and observeInstances:true
is passed as an option, it will return
an Observable instance when it is called to create a new instance.
Reactive Object API
Observables
Observable Observable(target:object|function [, {global:boolean, observeInstances:boolean}])
Wraps target with a Proxy and makes it Observable, i.e. a subject of Subscriptions, Observers, and Rules.
Setting global
to true
when the type of the target is a function
will make the function available in the globalThis
context.
Setting observeInstances
to true
will automatically make any instances created by a class constructor Observable. Note,
this will only work for classes defined using class <className> {}
, not old style JavaScript classes.
There are actually some other options, but they are for internal use and remain un-documented for now.
Returns: Observable (Remember, this is a pseudo-class. It is a Proxy around the target.)
Observers
Watchlight
supports many of the functions of RxJs, but is also supports a somewhat simpler
reactive concepts, the Observer. Observers are functions that get invoked automatically every time
the properties on the Observeable objects they reference change in value. They are more powerful that event listeners
because they can operate across multiple objects.
Those familiar with RxJs
can think of observers
as functions that automatically subscribe to an Observable
when the
Observable
detects that the observer
accesses some of its properties. What is super powerful about Observer
is that
it will automatically subscribe across multiple Observables
.
Observers are the cornerstone of the watchlight
spreadsheet functionality. A slimmed down
version is also used in the Lightview reactive UI library.
Observer Examples
const user = Observable({name: "mary"});
const hello = Observer(() => {
console.log("Hello", user.name);
})
user.name = "joe";
logs
Hello mary
Hello joe
Nested property access automatically creates child reactors, changes to which will invoke the observer so long as the
changes are made via navigation through the Observable user
.
const user = Observable({name: "mary", contactInfo: {phone: "555-555-5555"}});
Observer(() => {
console.log(JSON.stringify(user)); // recursively accesses every property
})
user.contactInfo.phone = "999-999-9999";
logs
{"name":"mary","contactInfo":{"phone":"555-555-5555"}}
{"name":"mary","contactInfo":{"phone":"999-999-9999"}}
You can call an Observer
directly:
const user = Observable({name: "mary", contactInfo: {phone: "555-555-5555"}}),
logUser = Observer(() => {
console.log(JSON.stringify(user)); // recursively accesses every property
})
logUser();
logs
{"name":"mary","contactInfo":{"phone":"555-555-5555"}}
{"name":"mary","contactInfo":{"phone":"555-555-5555"}}
Observer API
Observer Observer(aFunction:function [,thisArg:object,...args:any])
Creates an Observer from aFunction
you provide. The Observer will be called any time the properties on the objects it
references change in value. You can also call the Observer directly like it was the original function.
Observers are indexed internally by name. Creating an observer from a function with the same name as a previous observer will overwrite the old observer. Anonymous functions are not overwritten.
You can pass a default thisArg
and ...args
when creating an Observer
.
Synchronously invoked sub-functions that access Observable data will create reactive dependencies that can cause
invocation of the Observer at a later time. Use unobserve
to avoid creating a reactive dependency. Asynchronously
invoked sub-functions, i.e. those inside setTimeout
will not cause reactive dependencies.
import {reactive, observer} from "../../watchlight.js";
const user = reactive({name: "mary"});
const hello = observer(() => {
console.log("Hello", user.name);
})
const world = reactive({});
observer(function (message) {
this.user = user.name
console.log(message, user.name);
}, world, "Welcome to the world")
observer(() => {
if (world.user) console.log(`${world.user} owns the world.`)
})
user.name = "joe";
hello();
logs
Hello mary
Welcome to the world mary
mary owns the world.
Hello joe
joe owns the world.
Welcome to the world joe
Hello joe
void observer.stop()
Stops observer
from executing when the objects it references change.
void observer.start()
Restarts the observer
so it will respond when the objects it references change.
Observer observer.withOptions( {onerror:function} )
Observer
error handling defaults to re-throwing errors thrown by wrapped functions. This can be changed to swallow the
error by passing {onerror:()=>{}}
or use the error as the value by passing {onerror:(e) => e}
.
any unobserve( aFunction:function )
You can nest unobserve
inside an observer if you do not want changes to a particular object or property to cause
invocation of the observer.
Functions wrapped in unobserve
are transient and will get the this
context of the enclosing observer so long as you
define them using =>
.
Returns: The value returned by the function you provide.
unobserve
is useful when you need to use arrays but do not want index modification or access to cause an observer to
be re-invoked or when you want to use JSON.stringify.
import {reactive, observer, unobserve} from "../../watchlight.js";
const tasks = reactive([
{name: "task1", duration: 2000},
{name: "task2", duration: 3000},
{name: "task3", duration: 1000},
{name: "task4", duration: 2000}]);
const doTasks = observer(() => {
const task = tasks.currentTask = unobserve(() => tasks.shift());
if (task) {
// complete the task in the defined duration
setTimeout(() => task.complete = true, task.duration);
// will access all properties
console.log("doing:", unobserve(() => JSON.stringify(tasks.currentTask)));
observer(() => { // called whenever current task completion is changed
if (tasks.currentTask?.complete) {
// will access all properties
console.log("completed:", unobserve(() => JSON.stringify(tasks.currentTask)));
doTasks();
}
})
} else {
console.log("Waiting for more tasks ...");
const interval = setInterval(() => {
if (tasks.length > 0) { // poll for length change, not reactive since in setInterval
clearInterval(interval);
doTasks();
}
})
}
})
setTimeout(() => tasks.push(reactive({name: "task5", duration: 2000})), 10000);
Subscriptions and Event Listeners
Reactive objects created using Observable(target)
can dispatch event listeners similar to those used in web browsers.
Event listeners are added via subscribe
. They can be revoked via unsubscribe
. If the target
supports
addEventListener
, e.g. if you make a DOMElement
Observable, then addEventListener
is also supported. Listeners
are indexed internally based on their text representation; hence, if you plan to overwrite them,
you should not use functions that contain closure values and count on the functions being preserved as different event
handlers.
Subscription subscribe(subscription: function|string|ObservableEventDescriptor, target:Observable)
You will usually pass a function as the value for subscription
when subscribing. The name of the function should be the event
type you wish to subscribe to. If you pass an un-named function, it will be invoked for all events.
Passing a string for subscription
is only useful for pipelined subscriptions and is covered in more detail elsewhere.
The function or string passed is actually just a shorthand for an ObservableEventDescriptor
which has the surface
{eventType: string, listener: function}
.
class Person {
constructor({name, age}) {
this.name = name;
this.age = age;
}
}
Person = Observable(Person);
const joe = Person({name: "joe", age: 27}); // joe is a reactive object
subscribe(function change({target,property,value,oldValue}) {
console.log(`${target.name}'s ${property} is changing from ${oldValue} to ${value}`)
},joe);
Functions can be made into Observables. When they are, you can subscribe to the invocation.
function helloWorld() {
console.log("Hello world!");
}
subscribe(function apply({target,thisArg,argsList}) {
console.log(`${target.name} is about to execute`);
},helloWorld);
You may have noted from the above that subscriptions are notified prior to an activity occuring, this allows the activity to be cancelled just like DOM events.
subscribe(function change({target,property,value,oldValue,preventDefault}) {
if(CURRENTUSER.name!==target.name) {
event.preventDefault();
alert(`You can only change your own name!`);
}
},joe);
That's the basics, we cover more advanced use of Subscriptions later.
void observableInstance.addEventListener( eventType:string, listener:function, options:Object)
Only available if the target of the observableInstance
supports addEventListener
like a DOM Element.
Adds a function
as an event listener on the eventName
. The listener will receive an ObservableEvent
when the
eventName
occurs on the observableInstance
, i.e. the listener has the signature ({event,....rest})
.
The options
argument has the surface {synchronous,once}
.
Returns: void
. If you want chaining, use subscribe
.
boolean observableInstance.hasEventListener( eventType:string, listener:function)
Only available if the target of the observableInstance
supports addEventListener
like a DOM Element.
Checks for existence of function with the same string representation as a listener for eventName
on the reactiveObject
.
Returns: The true
or false
.
boolean observableInstance.removeEventListener( eventType:string, listener:function)
Only available if the target of the observableInstance
supports removeEventListener
like a DOM Element.
Removes a listener for eventName
with the same text representation as listener
.
Returns: The true
if the listener
was found and removed, otherwise false
.
ObservableEvent
ObservableEvent(config:object)
An object with the string property type
containing an event name, e.g. {type:"change"}
. Other properties vary based
on event type and may include:
target
- the reactive proxy generating the eventcurrentTarget
- thetarget
or object further up the tree as a result of bubblingproperty
- the property impacted on thetarget
value
- the current value of theproperty
oldValue
- the previous value of theproperty
before the event
Typically, ObservableEvents
are created automatically by watchlight
, rather than by an application developer. However,
it is possible to add custom event types.
Events will bubble up from an object to its containing objects. For the data below, subscribers registered on
object
will get events for changes to aPerson.name
.
const object = reactive({person: {name: "joe", age: 27}}),
aPerson = object.person;
aPerson.name = "mary";
The ObservableEvent
API is very similar to the
browser Event API. However, unlike DOM nodes, regular objects can be contained by multiple parents; hence,
bubbling can propagate more widely.
void observableEvent.preventDefault()
Prevents the event type from occurring. For example, if there is a change
Subscription (a.k.a. listener) calling
preventDefault
will stop the change from occurring. The event will still bubble.
void observableEvent.stopPropagation()
Stops bubbling to parent objects, but all subscribers on the current object will continue to get the event.
void observableEvent.stopImmediatePropagation()
Stops bubbling when called from a subscription, and all subsequent subscriptions will be blocked.
Interface ObservableEventDescriptor
{eventType:string, listener:function}
The built-in event types are described below. Also see custom event types.
*
A wild card that will match any event.
apply
Listeners on the event name apply
are invoked when an Observable function is about to execute.
change
Listeners on the event name change
are invoked whenever a property value is changing on an Observable.
defineProperty
Listeners on the event name defineProperty
are invoked whenever a new property is being defined on an Observable. A new property
is assumed if the previous value of a property is undefined
.
delete
Listeners on the event name delete
are invoked whenever a property is deleted from an Observable.
fire
A special event supported by Inference Rules when their conditions are satisfied.
retract
A special event supported by Observables that have been asserted for use by Inference Rules. Fires when the object is being removed from imnstances tracked by its constructor.
Event Listener Example
const aPerson = reactive({name: "joe", age: 27});
aPerson.addEventListener("defineProperty", ({type, target, reactor, property, value}) => {
console.log(type, target);
});
aPerson.addEventListener("change", ({type, target, reactor, property, value, oldValue}) => {
console.log(type, target);
});
aPerson.addEventListener("delete",
function myDelete({type, target, reactor, property, oldValue}) {
console.log(type, target);
},
{synchronous: true});
aPerson.married = true; // invokes the defineProperty handler asynchronously using setTimeout
aPerson.age = 30; // invokes the change handler asynchronously using setTimeout with the oldValue as 27
delete aPerson.age; // invokes the delete handler synchronously with the oldValue as 30 (due to the change above)
aPerson.removeEventListener("change", ({type, target, reactor, property, value, oldValue}) => {
console.log(type, target);
});
aPerson.removeEventListener("delete", "myDelete"); // removes the delete event listener
aPerson.removeEventListener("delete", function myDelete() {
}); // also removes the delete event listener
Custom Event Types
You can add custom event types by using Reactor.registerEventType(eventName)
. You can then add and use event listeners
that will automatically get invoked and support the standard API when events are posted
using reactiveObject.postMessage(eventName,options={})
.
Advanced Subscription Use
Subscriptions support the routing and piping of events.
The below watches for clicks on a button, ignores clicking faster than 1 every second, delays 5 seconds and logs the click to the console.
registerEventTypes("click");
const observableInstance = Observable(document.getElementById("mybutton"));
subscribe("click" ,observableInstance)
.pipe([timeThrottle(1000),delay(5000)])
.subscribe((event) => {
console.log(event);
});
The function subscribe
knows to expect an Observable as the second argument. So, unless you need to reference
your Observable elsewhere, you can shorten the above code and the Observable
will be automatically created.
registerEventTypes("click");
subscribe("click" ,document.getElementById("mybutton"))
.pipe([timeThrottle(1000),delay(5000)])
.subscribe((event) => {
console.log(event);
});
Above pipe
is a method on Subscription returned by subscribe
. Watchlight
also exposes pipe
and route
as
top level functions. So, you can make your code even shorter.
registerEventTypes("click");
pipe([timeThrottle(1000),delay(5000)],document.getElementById("mybutton"))
.subscribe(click(event) => {
console.log(event);
});
And, since you are wrapping a DOM Element, you can use addEventListener
if you prefer.
pipe([timeThrottle(1000),delay(5000)],document.getElementById("mybutton"))
.addEventListener("click",(event) => {
console.log(event);
});
Watchlight
also supports route
, which behaves the same way as most event or http routers with middleware.
Observable(document.getElementById("myInput"))
.subscribe("change")
.route(({target}) => target.value==="joe", ({target}) => ... do something)
.route(({target}) => target.value==="mary", ({target}) => ... do something)
.route(() => throw new TypeError(`${target.value} must be mary or joe`))
Routes are committed to once the first function in the route succeeds. Then the remaining functions are called until
one returns undefined
or calls a preventDefault
, stopPropagation
, or stopImmediatePropagation
on the event it
gets as an argument.
Under the hood, pipe
just creates a single route and then locks the subscription so that no more routes can be added.
You will find many of the same pipeline operators as provided by RxJs, e.g.
count
, debounce
, delay
, filter
, timeThrottle
, map
, etc. There are also some additional operators, e.g.
sum
, average
.
Since the list is long and each requires its own explanation, they are provided in a separate file
so that we can move on to inference rules.
Inference Rules
Inference rules can match across multiple objects up and down the inheritance hierarchy. They can chain across
multiple then
and catch
statements similar to Promises
. These chained statements can add new objects or change
existing objects. Rules also respond to the addition and removal of new objects in a prioritized manner. Objects can
even be automatically removed if data changes and the rules that created the objects no longer have their conditions
satisfied.
To avoid the creation of a special language or the representation of operators like "==" and ">" as strings, the inference engine does not use the Rete Algorithm or a derivative like most rule engines. However, it is small (4K minified/gzipped) and fast. And, this means you can use the JavaScript debugger to step through all of your code as it is written.
Watchlight
is currently in beta, but tests on an 8 MB Ryzen 4000 5 show that 250,000+ rule tests can be
processed per second in Firefox, Chrome, Edge and NodeJS, even when the potential rule matches exceed 1 million
combinations of objects. The number of rules that actually fire per second is entirely dependent on the nature of the
logic being modelled. If no rule conditions are satisfied, no rules will fire! Head-to-head comparisons of different
rule processing engines can only be made using the same rule and data sets.
Anatomy of A Rule
Rules consist of:
condition
- A single synchronous function that accepts one object as an argument and must returntrue
orfalse
. The property names effectively represent variables in the condition. The values of the properties must be instances created from classes defined usingclass <classname> { }
. Conditions should be side effect free. Create new objects or call non-synchronous or side effect producing functions in conditions at your own risk.domain
- An object with the same properties as the argument tocondition
. The values of the properties are the expected classes of the values in thecondition
argument.options (optional)
- Configuration data for the rule.actions
- A series of chainedthen
statements, the first of which gets the same argument as thecondition
. Subsequentactions
get the return value of the precedingaction
as their arguments. Chaining stops when anaction
returnsundefined
.exception handlers
- One or morecatch
statements interspersed withactions
, although usually just the last statement.
when(
({person1, person2}) => { // start condition
return person1.name !== person2.name &&
typeof (person1.age) === "number" &&
typeof (persone2.age) === "number"
}, // end condition
{person1: Person, perrdon2: Person},// domain
{priority:10} // options
)
.then(({person1, person2}) => { // first action
return {person1, person2, avgAge: person1.age / person2.age}
}
)
.then(({person1, person2, avgAge}) => { // chained action
console.log(person1.name, person2.name, avgAge)
})
Rule Processing
Rules are processed in a cycle with a run limit that may be Infinity:
- Match all rules to all combinations of objects in rule accessible memory, a.k.a. "working memory", by rule domain
- Add matched rules to a rule agenda
- Sort rule agenda by rule priority
- For each rule on the agenda
- For each combination of objects
- remove combination from combinations
- Test the condition with the combination
- if failed, goto next rule
- else fire rule and process actions (add, modify, remove objects, call functions)
- if action adds a higher priority rule to agenda goto 3
- else goto next combination
- No more combinations goto next rule
- For each combination of objects
- No more rules
- if runlimit exceeded, stop
- else set timeout to watch for new rules added to the rule agenda
Rule Examples
import {when,Observable} from "./rule.js";
class Person {
constructor({name,age}) {
if(name==null || age==null) throw new TypeError("Person requires both name and age")
this.name = name;
this.age = age;
}
}
Person = Observable(Person);
when(({object}) => true, {object: Object})
// runs every time a new Object is added or changed
.then(({object}) => console.log(object))
new Person({name: "joe"});
logs
Person {name:"joe"}
Note the import of Observable
from rule.js
rather than watchlight.js
. This version of Observable
has been
enhanced to support rule processing. Specifically it ensures the Observable classes keep track of all their instances
in a manner that makes rules the most efficient. It also enables the creation of pseudo-class Partial objects (see below).
when(({person}) => person.age < 21, {person: Person})
// runs every time a new person is added with an age < 21
// or a person's age changes to < 21
.then(({person}) => console.log(person, "is a minor"))
Combo = Observable(Combo);
when(({person1, person2}) => {
// creates pairs of people, automatically removes pair
// if a person's name changes or a person is removed
// Combo has an equals methods on it so that it is reflexive
return person1.name !== person2.name && not(Combo(person1, person2));
}, // then, create pair
({person1, person2}) => {
return this.justifies({person1,person2},new Combo(person1, person2))
})
.then((combo) => console.log("A pair:", combo))
Note the use of Combo
without the word new
in the rule test. Combo
will test like it is an instance of Combo,
but it is not created with the Combo constructor. Watchlight
support a concept called Partials
, which are partially
populated instanced of classes that will not throw construction errors or trigger other rules.
Rules API
any rule.catch( errorHandler:function )
errorHandler
has the call signature (error:Error)
.
If the error errorHandler
returns undefined
, the error will be swallowed.
If the errorHandler
returns anything else, it will be used as the input argument to the next action in the chain.
If the errorHandler
throws, the next catch
statement will be sought.
boolean exists( object:Object [,test:function] )
Checks to see if an object or partial object exists. Typically, used as part of rule condition.
let joe = reactive(new Person({name: "joe", age: 20})),
mary = reactive(new Person({name: "mary", age: 27})),
joe = assert(joe);
// true
joeexists = exists(joe);
// true because of joe
namedjoeexists = exists(Person({name: "joe"})); // a Partial not really a Person, will not throw error or trigger rules
// false because joe is 20 and mary is not asserted
rightageexists = exists(Person({age: 21}));
// false, because mary was not asserted to rule memory
namedmaryexists = exists(Person({name: "mary"}));
// true, because a Person that has all the same properties and values, i.e. mary, exists
deepequalexists = exists(Person({name: "mary", age: 27}));
any rule.then(action:function)
action
has the call signature (data:any)
.
data
is typically an object with multiple properties the values of which are other objects, e.g.
{
person:Person({name: "joe", age: 27}),
table: Table({number: 12, capacity: 10})
}
If the action
returns undefined
, action processing will cease.
If the action
returns anything else, it will be used as the input argument to the next action in the chain.
Inside the action
function you can use:
this.justifies(justification,conclusion)
where justification
is an object holding the facts that must remain constant
for the facts in the conclusion to remain in place. See the examples/rules/diagnostic-confidence.js
Returns: the return value of action
.
boolean not( object:Object )
A convenience, equivalent to !exists(object)
.
boolean retract( object:Object )
Stops the object from being tracked by the Observer class that created it. As a result, rules will not have access to
it and any objects created in the scope of justifies
, where the object
was part of a justification will be removed.
Returns: Reactive true
if the object
was being tracked, i.e. was not previously retracted. Otherwise, false,
Rule when(condition:function, domain:Object [,{priority:number, confidence:float])
The condition
can be an anonymous or named function. The call signature of condition
is (object:Object)
where
object
must be an Object with one or more properties. The condition
MUST return true
or false
indicating if the
members of the object
satisfy the rule conditions.
The domain
MUST be an Object with the same properties as the object
argument to condition
. The values of the
properties MUST be classes or constructors.
confidence
sets a confidence on a rule or Observable data. This is available to the this.justifies
function and also
via this.withConfidence
in the then
statements of a rule.
a confidence
= minimum confidence of data used to fire the rule * confidence of the rule. You can run the
example diagnostic confidence or view its
source.
Returns: Reactive Proxy
for condition
, i.e. a Rule
.
Instance Bound Rules
Reactive objects can have instance bound rules associated with them in addition to event handlers and observers. Unlike event handlers and observers, these rules get added to the rule processing agenda.
There are two options for binding. The first is to provide a rule that applies only to the object it is bound to:
const joe = assert(new Person({name: "joe", age: 27}));
joe.when((joe) => joe.age > 27)
.then((joe) => console.log("joe too old", joe.age));
Note the lack of domain and the un-parametrized object as an argument.
If Joe's age changes before the rule has an opportunity to fire (perhaps due to a higher priority rule), then the console message will not be written.
The second option is to allow comparing with other objects:
const joe = assert(new Person({name: "joe", age: 27}));
// This rule will match Joe with all possible partners.
joe.when(({bound, partner}) => {
return partner.name !== bound.name
}, {partner: Person})
.then(({bound, partner}) => {
console.log("joe partner", partner)
});
Note the domain and the parameterized object as an argument.
The property bound
MUST be present in the condition argument. And, MUST NOT be present in the domain
.
Rule Example Files
Fibonacci sequence generation: source.
Pair matching beyond the examples in this document.: source.
Spreadsheet
Spreadsheet like functionality is provided through a separately loaded module ./sheet.js
. The functionality is
headless and depends on object access paths for its notation. It is also n-dimensional and sparse. Formulas can be set
at any level in a sheet's data hierarchy and any legal property names can be used for navigation through the hierarchy.
Any type of data can be stored in cells. There is no support for selecting, cutting, pasting, etc.; although, these
could be provided by a wrapper.
Dimension and Cell
Dimension
is a psuedo-class, i.e. you can't use instanceof
to check if something is a Dimension
. Any time an
undefined property or sub-property is accessed on a Sheet
a Dimension
is created. If a Dimension
is directly
assigned a value or a function, it is converted into an instance of the psuedo-class Cell
. Cells
only exist at leaf nodes of Dimensions
. Existing Dimensions
can be overridden and converted into a Cell
by direct assignment of a value or function.
Cells in a Sheet
with functions assigned, provide a method withFormat
that can take either a string or a function as
an argument. If a string, then it should be an un-interpolated string template literal that accesses
this.valueOf()
. If a function, it will get the cell as its this
value, so it can call this.valueOf()
. It should
return a string.
The code below can be run or viewed in the examples directory.
import {Sheet} from "../sheet.js";
const sheet = Sheet();
sheet.A[0]; // no assignment is made, so sheet.A[0] will automatically be a Dimension when accessed
sheet.A[1] = 1; // sheet.A[1] is a Cell. Dimensions and Cells are created automatically
sheet.A[2] = 1;
sheet.A[3] = () => A[1] + A[2]; // Note, there is no need to include sheet; watchlight manages the resolution
sheet.A[3].withFormat("$${this.valueOf().toFixed(2)}");
sheet.A[4] = 1;
sheet.B[1] = () => sum(values(A, 2, 3));
sheet.B[2] = () => sum(A);
console.log(sheet.B[1].valueOf()); // logs 3
console.log(sheet.B[2].valueOf()); // logs 5
console.log(sheet.A[3].valueOf()); // logs 2
console.log(sheet.A[3].format()); // logs $2.00
sheet.A[2] = 2;
console.log(sheet.A[3].valueOf()); // logs 3
sheet[1][2][1] = () => {
return A[3] + 1
}; // completely different dimension approach
console.log(sheet[1][2][1].valueOf()); // logs 4
sheet.A[2] = 4;
setTimeout(() => { // let recalculation settle out
console.log(sheet.A[3].valueOf()); // logs 5
console.log(sheet.A[3].format()); // logs $5.00
console.log(sheet[1][2][1].valueOf()); // logs 6
console.log(sheet.B[1].valueOf()); // logs 9
})
Sheet
functions behave like their similarly named counterparts in MS Excel and Google Sheet.
Most functions will automatically convert cell references to iterables when necessary.
Some functions require a cell or a value for an argument and not a Dimension
. If you call a function that can't take
a Dimension
with a Dimension
, you will get an error similar to this:
TypeError: isnumber(A.5) 'A.5' is a Dimension not a value or Cell
Self Referencing Formulas
Directly circular formulas are automatically avoided by excluding the cell in which the formula is defined from the range it may reference, e.g.
const sheet = Sheet();
sheet.tab1.A[1] = 1;
sheet.tab1.A[2] = 2;
sheet.tab1.A[3] = 3;
sheet.tab1.A[4] = () => sum([tab1.A]); // 5
Paths
The path to a Dimension
or Cell
is available as a property:
const sheet = Sheet();
console.log(sheet.tab1.A[1].path); // logs "tab1.A.1"
Logical and Info Functions
number count(values:Array|Dimension,{start:number|string,end:number|string})
number counta(values:Array|Dimension,{start:number|string,end:number|string})
any iff(test:truthy, value1:any, value2:any)
boolean isdimension(value:any)
boolean isblank(value:any)
boolean isboolean(value:any)
boolean isempty(value:any)
boolean islogical(value:any)
boolean isnumber(value:any)
boolean isobject(value:any)
boolean isstring(value:any)
boolean isundefined(value:any)
number len(value:any)
Throws TypeError
if value
does not have a length
or size
property or function.
Math Functions
number average(values:array|Dimension,{start:number|string,end:number|string})
number exp(value:number,power:number)
number log10(value:number)
number max(values:Array|Dimension,{start:number|string,end:number|string})
number median(numbers:Array)
number min(values:Array|Dimension,{start:number|string,end:number|string})
number product(values:Array|Dimension,{start:number|string,end:number|string})
number stdev(values:Array|Dimension,{start:number|string,end:number|string})
number sum(values:Array|Dimension,{start:number|string,end:number|string})
number variance(values:Array|Dimension,{start:number|string,end:number|string})
number zscores(values:Array|Dimension,{start:number|string,end:number|string})
Returns Array.
Trigonometry Functions
number acos(value:number)
number acosh(value:number)
number asin(value:number)
number asinh(value:number)
number atan(value:number)
number atan2(value:number)
number cos(value:number)
number cosh(value:number)
number pi()
number rand()
number sin(value:number)
number tan(value:number)
number tanh(value:number)
Coercion Functions
number int(value:string|number)
number float(value:string|number)
string lower(value:string)
Array numbers(source:Array|Dimension, start:number|string, end:number|string)
source
can be an Array or a Sheet
dimension. If end
is less that start
the return value is reversed.
Returns an array of all numbers from the object based on the keys between and including start
and end
.
Array numbersa(values:Array|Dimension, {start:number|string, end:number|string})
source
can be an Array or a Sheet
dimension. If end
is less that start
the return value is reversed.
Returns an array of all values coercible into numbers from the object based on the keys between and including
start
and end
. Strings are parsed as floats and booleans are converted to 1s and 0s.
string upper(value:string)
number value(data:any)
Array values(values:Array|Dimension, {start:number|string, end:number|string})
source
can be an Array or a Sheet
dimension. If end
is less that start
the return value is reversed.
Returns an array of values from the object based on the keys between and including start
and end
.
License
Watchlight
is dual licensed:
AGPLv3
or
A custom commercial license. Contact [email protected].
Change History
Reverse Chronological Order
2022-04-25 v1.1.2b Optimized subscribe
, made more conformant with RxJs
. Deleted kitchensink example since
there are now many more structured examples and it was out of date with API. Many RxJs
operators added and unit tested,
but yet to be documents. This release has seen some performance degradation in rules. Should be able to optimize back in.
2022-04-20 v1.1.1b Modified naming to be more consistent with RxJs
. Added a range of RxJs
operators. Corrected
issue where bubbles
and defaultPrevented
were ignored on events.
Split rule functionality into a separate file. Deprecated whilst
for simpler justifies
approach. Optimized rule
processing with some light-weight indexing to support faster retract
and not
. Eliminated assert
, creating an
Obervable
object automatically invokes the rules it may match. Eliminated withOptions
on rules to simplify API.
Modified Sheet
so that closures can ultimately be supported. This required changing the way default tabs can be established for
cell formula references. Added many functions for use in formulas.
2022-04-03 v1.0.17b Updated license token to more standard form.
2022-04-03 v1.0.16b Added automation of existing tests to package.json.
2022-03-27 v1.0.15b Modified event bubbling to be consistent with browser approach. preventDefault()
will no longer
stop bubbling. Use stopPropagation()
or stopImmediatePropagation()
to stop bubbling.
2022-03-27 v1.0.14b Fixed issue with ReactorEvent
properties not being enumerable, which prevent spread and assign
copying.
2022-03-27 v1.0.13b Support for custom event types added.
2022-03-26 v1.0.12b More rule examples. Added foundation for confidence based, a.k.a. "fuzzy", reasoning.
Modified result
portions of whilst
for more flexible results return. Adjusted TOC layout and scrolling.
2022-03-25 v1.0.11b Documentation content updates.
2022-03-25 v1.0.10b Documentation content updates. Improved swipe behavior of TOC.
2022-03-25 v1.0.9b Documentation content updates. Renamed main entry point to watchlight.js
. More unit tests and event
bubbling work. Fixed issue with observers not stopping when requested.
2022-03-25 v1.0.8b Documentation layout. Added event bubbling. Renamed event
property in ReactorEvent
to type
.
2022-03-24 v1.0.7b Documentation layout. More unit tests. Fixed issues with checking presence of and removing event handlers.
2022-03-24 v1.0.6b Documentation TOC tray added.
2022-03-23 v1.0.5b Documentation style updates.
2022-03-23 v1.0.4b More unit tests. Documentation content and style updates.
2022-03-23 v1.0.3b Unit tests, fixed bug in proxy property lookup that was creating extra reactive sub-objects when
value was false. Minor rule performance improvement. Added observer.withOptions
.
2022-03-22 v1.0.2b Documentation updates.
2022-03-22 v1.0.1b Documentation updates, added Partial and Sheet
2022-03-20 v0.0.7b Documentation updates, observer examples, renaming of some internals
2022-03-19 v0.0.6b Documentation updates, examples added, enhanced whilst with onassert
2022-03-19 v0.0.5b Documentation updates, enhanced instance bound rules
2022-03-19 v0.0.4b Documentation updates, performance improvements
2022-03-18 v0.0.3b Documentation updates
2022-03-18 v0.0.2b Documentation updates
2022-03-18 v0.0.1b Initial public release