part 7
This appendix introduces further details about some parts of FOAM that aren’t necessary for the main tutorial.
Properties
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 ofpreSet
is the value which is actually stored.defaultValue
: Provide a fixed default value for this property. It won’t actually be stored on each object, saving memory and bandwidth.defaultValueFn: function() { ... }
: A function that’s called every time the default value is required. Can usethis
to refer to the instance in question, so you can compute the default based on some other properties.factory: function() { ... }
is called once duringinit
after creating a new object, the value returned becomes the value of this property.- This is commonly used as
factory: function() { return []; }
to make each object have its own empty array.defaultValue: []
would make all instances share one array!
- This is commonly used as
view
specifies the view that should be used to render this property. Defaults toTextFieldView
for properties with no specifiedmodel_
. Properties with eg.StringArrayProperty
as their model may have other defaults.- You can specify the
view
in several ways, the commonest two are:- By name:
view: 'DAOListView'
- With a “factory” object:
{ factory_: 'DAOListView', rowView: 'MyCitationView' }
- By name:
- You can specify the
required: true
indicates that this field is required for the model to function correctly.transient: true
indicates that this field should not be stored by DAOs.hidden: true
indicates 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.help: 'string'
explanatory help text for this property, which could go in a tooltip.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.
dynamicValue: function() { ... }
is passed toEvents.dynamic
, which turns this property into a spreadsheet cell. The function you provide will be re-run every time any of its inputs changes, and the return value becomes the value of the property.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/mm2Property.js
for the complete definition of Property
. core/mm3Types.js
adds IntProperty
and friends, and some of those have more properties specific to their type.
Property Binding
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:
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:
This makes it convenient to eg. bind a view to a property from a larger class.
Listening to Properties
In addition to things like setter
and postSet
, you can listen for updates to
any property, like so:
object
is the object this property belongs to. It serves asthis
, effectively.topic
is the reason for the event. For a property listener, it’s always the property’s name.oldValue
is the value from before the change.newValue
is the value after the change.
This functionality is used by things like Events.dynamic
to register listeners
on properties changing.
Property Types
We showed IntProperty
above; there are many more types of properties. Most you
can easily guess what they do:
StringProperty
, BooleanProperty
, DateProperty
, DateTimeProperty
,
IntProperty
, FloatProperty
, FunctionProperty
, ArrayProperty
,
ReferenceProperty
, StringArrayProperty
, DAOProperty
,
ReferenceArrayProperty
.
There are many more; most of these are defined in core/mm3Types.js
.
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.
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:
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.
Async Loading
In order to load a class, FOAM may need to perform async operations:
- Fetch external templates via XHR.
- Compile templates, inline or external, in a setting without synchronous
eval
, such as a Chrome app.
Because of this, FOAM requires that all classes be arequire()
d by name, like
this:
But you won’t need to do that yourself in most apps, for two reasons:
<foam>
tags automaticallyarequire
what they need.- When a class is
arequire
d, everything from itsrequires
list isarequire
d too.
Therefore, if you use a <foam>
tag at the top level, and specify requires
on all your classes, your app should load with no explicit arequire
s.
Contexts and Dependency Injection
Every instance in FOAM has a context. This is an object, spelled this.X
,
which is supplied at creation time. You generally won’t need to reference
this.X
directly.
Instead, you can add an imports
array to a class:
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.X.foo
.
If you want to export one of your properties to descendant objects, you can use exports:
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
create
, that 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.X
issomeX
. - If the class being instantiated was
require
d, thenthis.X
will be used:var instance = this.SomeClass.create()
theninstance.X === this.X
. - If none of the above apply, eg.
SomeClass.create()
, the global context (window.X
) is used.- These global models (
window.SomeClass
) are planned to be removed later on, and the global context might also disappear.
- These global models (
Renaming
In requires
, imports
and exports
, you can rename a value. Examples:
- In this class and all its descendants,
ActionButton
is actuallyCViewActionButton
. - Similarly,
XHR
in all descendants will be the modified, authenticatedXHR
. - Note the
as myClass
export
, which exports this instance to its children asmyClass
.
Methods on the Class
On classes themselves, statically, there are a handful of useful methods and properties.
SomeClass.name
is the name of the class.SomeClass.create()
creates a new instance of the class.SomeClass.isInstance(o)
checks ifo
is an instance of the class (or a subclass).SomeClass.isSubModel(OtherClass)
returnstrue
ifOtherClass
is a descendant ofSomeClass
.
Listeners
Listeners are like methods, but this
is always bound to the object, making
them easier to pass as event handlers.
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
this
bound 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: 100
will 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: true
will 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
.
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.
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.
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)
compareso
andx
o.compareTo(x)
returns the usual -1, 0 or 1.o.hashCode()
is similar to Java.o.diff(x)
returns a diff ofo
againstx
, 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.
DAOs
The DAO interface looks like this, if you pretend Javascript supports interfaces:
a Sink
looks like this:
Note that every DAO is therefore also a Sink, making it trivial to pull data
from one DAO into another: sourceDAO.select(targetDAO)
.
Here’s an example of using the DAO interface to make a query:
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.