Appendix part 4
Appendix
This appendix introduces further details about parts of FOAM that aren’t necessary for the main tutorial.
- Properties
- Properties-on-properties
- Property Binding
- Slots
- Listening to Properties
- Types
- Requires, Imports, Exports, and Contexts
- Requiring Dependencies
- Contexts and Dependency Injection
- Contextual Creation
- Renaming
- Methods on the Class
- Listeners
- Actions
- Methods on all objects
- DAOs
1. Properties
a. Properties-on-properties
Properties are objects too which means they have their own methods and properties.
Several of these “properties on properties” are very useful when writing your own classes. Here are some of them, in roughly descending order of usefulness:
postSet: function(old, nu) { ... }is called with the old and new values of this property, after it has changed.preSet: function(old, nu) { ... }is called with the old and new values of the property when it’s about to change. The return value ofpreSetis the value which is actually stored.value: Provide a fixed default value for this property. It won’t actually be stored on each object, saving memory and bandwidth.expression: function() { ... }: A function that’s called every time the default value is required. Can usethisto refer to the instance in question, so you can compute the default based on some other properties.factory: function() { ... }is called once when a new object is created, the value returned becomes the value of this propertyviewspecifies the view that should be used to render this property. Defaults toTextFieldfor properties with no specifiedclass. Properties with eg.StringArrayas their model may have other defaults.- You can specify the
viewin several ways. The most common two ways are:- By name:
view: { class: 'DAOList', rowView: 'MyCitationView' } - With a view factory:
{ class: ‘ViewFactory’, name: ‘listFactory’, value: ‘DAOList’ }
- By name:
- You can specify the
required: trueindicates that this field is required for the model to function correctly.transient: trueindicates that this field should not be stored by DAOs.hidden: trueindicates that this field should not be rendered by views.label: 'string'gives the label that views should use to label this property, if applicable. Defaults tothis.name, naturally.documentation: Gives developer documentation for this property.getter: function() { ... }is called each time the property is accessed, and its return value is the value of the property.- When this is used, the property is a “pseudoproperty” that has no real value, and therefore no value is stored.
setter: function(nu) { ... }is called to set the value of the property.- See above about pseudoproperties.
aliases: ['string', 'array']defines other names for this property. They can be used as if they were real properties, but they access the same underlying value.
There are some more having to do with tables, i18n, autocomplete and more. See core/Property.js for the complete definition of Property. core/types.js adds Int property and friends, and some of those have more properties specific to their type.
b. Property Binding
i. Slots
In addition to things like setter and postSet, you can listen for updates to any property.
For every property foo on a FOAM object, there is a foo$ which is a “Value” for the property. Setting two objects to share this Value, rather than the literal value, is like passing by reference instead of by value. To illustrate:
var o1 = Foo.create({ bar: 'abc' });
var o2 = Foo.create({ bar: o1.bar });
console.log(o1.bar); // prints 'abc'
o2.bar = 'def';
console.log(o2.bar); // 'def'
console.log(o1.bar); // 'abc'
In the above, the value of `o1.bar` is copied to `o2.bar`. In the below, `o1.bar` and `o2.bar` are the same underlying property:
var o1 = Foo.create({ bar: 'abc' });
var o2 = Foo.create({ bar$: o1.bar$ });
console.log(o1.bar); // prints 'abc'
o2.bar = 'def';
console.log(o2.bar); // 'def'
console.log(o1.bar); // 'def'
To make an UI component update every time some property changes:
this.add(obj.prop$)
To create a listener function on some properties:
obj.slot(function[, slots])
objectis the object this property belongs to. It serves asthis, effectively.slotsare the properties that the return value depends on.
This makes it convenient to eg. bind a view to a property from a larger class.
ii. Listening to Properties
In addition to things like setter and postSet, you can listen for updates to any property, like so:
foo.bar$.sub(function(sub, prop, topic, object) { ... });
objectis the object this property belongs to. It serves asthis, effectively.topicis the reason for the event. For a property listener, it’s always the property’s name.oldValueis the value from before the change.newValueis the value after the change.
iii. Property Types
There are many more types of properties other than example IntProperty provided above . Most of you can easily guess what they do:
`String`, `Boolean`, `Date`, `DateTime`,
`Int`, `Float`, `Function`, `Array`,
`Reference`, `StringArray`, `DAOProperty`.
There are many more; most of these are defined in core/types.js.
2. Requires, Imports, Exports, and Contexts
Most FOAM classes depend on others, often many of them. FOAM supports this in a declarative way. It also supports a style of dependency injection using contexts.
a. Requiring Dependencies
As shown in the main tutorial, FOAM models should require their dependencies. A class has a requires array containing the names of those classes it needs:
foam.CLASS({
name: 'SomeClass',
requires: [
'SomeView',
'AnotherClass'
]
});
These classes are made available as this.SomeView and this.AnotherClass. Where this is available, it is best to use these rather than the globals
SomeView and AnotherClass, because then they can be overridden with drop-in replacements by other classes, in a dependency injection fashion.
b. Contexts and Dependency Injection
Every instance in FOAM has a context. This is an object, spelled this.__context__, which is supplied at creation time. You generally won’t need to reference this.__context__ directly.
Instead, you can add an imports array to a class:
foam.CLASS({
name: 'SomeClass',
imports: ['foo']
});
At instance creation time, the supplied context will be checked for foo, and if found, it will be copied into a property on SomeClass, also called foo.
Therefore inside SomeClass, you should refer to this.foo, not this.__context__.foo.
If you want to export one of your properties to descendant objects, you can use exports:
foam.CLASS({
name: 'MyController',
requires: [
'GestureManager'
],
exports: [
'gestureManager'
],
properties: [
{
name: 'gestureManager',
factory: function() { return this.GestureManager.create(); }
}
]
});
c. Contextual Creation
All instances have a context, but it’s rare to explicitly specify the context. The context for an instance is determined by the following rules in order:
- If you supply the optional second argument to
createthat context will be used:SomeClass.create({ foo: 'bar', someContext) - If the class being instantiated was fetched from a context that context will be used:
var instance = someX.SomeClass.create(),instance.__context__issomeX. - If the class being instantiated was
required thenthis.__context__will be used:var instance = this.SomeClass.create()theninstance.__context__ === this.__context__. - If none of the above apply, eg.
SomeClass.create(), the global context (window.__context__) is used.- These global models (
window.SomeClass) are planned to be removed later on and the global context might also disappear.
- These global models (
d. Renaming
In requires, imports and exports, you can rename a value. Examples:
foam.CLASS({
name: 'MyClass',
requires: [
'CViewActionButton as ActionButton',
'XHR'
],
imports: [
'somethingOrOther as somethingElse'
],
exports: [
'as myClass',
'authXHR as XHR'
],
properties: [
{
name: 'authXHR',
factory: function() {
return this.XHR.xbind({ authAgent: ... });
}
}
]
});
- In this class and all its descendants,
ActionButtonis actuallyCViewActionButton. - Similarly,
XHRin all descendants will be the modified, authenticatedXHR. - Note the
as myClassexport, which exports this instance to its children asmyClass.
3. Methods on the Class
On classes themselves, statically, there are a handful of useful methods and properties.
SomeClass.nameis the name of the class.SomeClass.create()creates a new instance of the class.SomeClass.isInstance(o)checks ifois an instance of the class (or a subclass).SomeClass.isSubModel(OtherClass)returnstrueifOtherClassis a descendant ofSomeClass.
4. Listeners
Listeners are like methods, but this is always bound to the object, making them easier to pass as event handlers.
foam.CLASS({
name: 'Mouse',
properties: [ 'x', 'y' ],
methods: {
function connect(element) {
element.addEventListener('mousemove', this.onMouseMove);
}
},
listeners: [
{
name: 'onMouseMove',
code: function(evt) {
this.x = evt.offsetX;
this.y = evt.offsetY;
}
}
]
});
The listener is attached to the object like a normal method, which can be called directly with this.onMouseMove(). Under the hood, however, there are several differences.
- Listeners always have
thisbound properly, so they can be passed as callbacks, as above, without being explicitly bound. - Listeners can be merged, or batched. The first event that comes in starts the clock, when the timer expires, your code is called once with the most recent event.
isMerged: true, mergeDelay: 100will merge events and fire the real code 100ms after the first event arrives. After that time expires, another event arriving will start the clock again. This is useful to avoid spamming database or network updates.isFramed: truewill merge events and fire your code on the next animation frame. This is useful to avoid redrawing more than once per frame. Your code receives the most recent event, same asisMerged.
5. Actions
Actions are guarded, GUI-friendly methods. FOAM will run code you supply to determine whether the button for this action should be hidden, visible but disabled, or enabled.
foam.CLASS({
// ...
actions: [
{
name: 'start',
label: 'Start' Process',
help: 'Start the timer',
isAvailable: function() { return true; },
isEnabled: function() { return ! this.isStarted; },
action: function() { this.isStarted = true; }
}
],
// ...
});
By default, an action is always visible and enabled (so the isAvailable above is unnecessary). This button is always visible but only enabled when this.isStarted is false. When the button is clicked while enabled, action is called. If the button is disabled, nothing happens on a click.
6. Methods on all objects
FOAM includes several properties and methods on all objects:
model_: Every object has a pointer to itsModel. This is the Javascript representation of its class, the same object you passed toCLASS().- These representations have their own model,
Model.
- These representations have their own model,
o.equals(x)comparesoandxo.compareTo(x)returns the usual -1, 0 or 1.o.hashCode()is similar to Java.o.diff(x)returns a diff ofoagainstx, property by property.o.clone()returns a shallow copy ofo.o.deepClone()is of course a deep copy.o.toJSON()ando.toXML()return JSON or XML as a string. Parsers are included to read them in again.o.write(document)writes the default view of the object into the document.
7. DAOs
The DAO interface looks like this, if you pretend Javascript supports interfaces:
interface DAO extends Sink {
Promise<Object> put(obj);
Promise remove(id);
Promise<Object> find(query);
Promise<sink> select(sink);
Promise removeAll();
void listen(sink);
void pipe(sink): // select() + listen()
DAO where(query);
DAO limit(count);
DAO skip(count);
DAO orderBy(comparators...);
}
a Sink looks like this:
interface Sink {
void put(obj, [opt_flowControl]);
void remove(obj, [opt_flowControl]);
void eof();
void error(msg);
}
Here’s an example of using the DAO interface to make a query:
dao
.skip(200)
.limit(50)
.orderBy(EMail.TIMESTAMP)
.where(
AND(
EQ(EMail.TO, 'kgr@google.com'),
EQ(EMail.FROM, 'adamvy@google.com'),
GT(EMail.TIMESTAMP, startOfYear)))
.select(
GROUP_BY(EMail.SUBJECT, COUNT()));
This is generally SQL-like, but instead of parsing a string it constructs the query directly. This has no parsing overhead, and completely avoids SQL injection. It also adds some typechecking, though Javascript can only take that so far.
This query syntax works on all DAOs, including plain Javascript arrays. It is also extensible - the MLang syntax - AND, EQ, and so on - are simple expressions, and you can write new ones if needed.
Tutorial Menu:
- Getting Started
- Core Concepts
- Applied Learning: Build a Basic App with FOAM