ThreeWay
is a Meteor package that provides three-way data-binding. In particular, database to view model to view.
Learn more at the demo/guide site.
The objective of writing this package is to provide a powerful (e.g.: dynamic data-binding), flexible (e.g.: transformations of view model data for presentation, ancestor/descendant/sibling data-access) and Blaze-friendly tool for doing data-binding (i.e.: no tip-toeing around the package).
Database to view model connectivity is provided by Meteor methods (with signatures function(id, value)
), with "interface transforms" for server-to-client and client-to-server. Actually, it is richer than that. One may configure fields for data-binding with wild cards and send the data back with meteor methods that take more arguments (e.g.: methods with signature function(id, value, param1, param2, ...)
).
The user is responsible for ensuring the right subscriptions are in place so ThreeWay
can retrieve records from the local database cache.
The data binding responds to changes in the DOM. So Blaze can be used to generate and change data bindings.
Presentation of data is facilitated by "pre-processors" which map values (display-only bindings) and may do DOM manipulation when needed (e.g.: with Semantic UI dropdowns and certain animations). This feature allows for great flexibility in displaying data, enabling one to "easily" (and typically declaratively) translate data to display.
Somehow links in Atmosphere get messed up. Navigate this properly in GitHub.
- Install
- The Demo/Guide Site
- Usage
- Documentation
- Referring to Fields in Documents
- Default Values
- Dynamic Data-Binding
- Updaters to the Server
- Transforms: Translation from/to Database to/from View Model
- Binding to the View
- Helpers, Template Helpers and Binding
- Event Bindings
- View Model to View Only Elements
- Instance Methods
- Additional Template Helpers
- Pre-processor Pipelines
- Data Validation
- "Family Access": Ancestor and Descendant Data
- Data Migration on Hot Code Push
- Debug
- Extras
- Notes
- To Do
This is available as convexset:three-way
on Atmosphere. (Install with meteor add convexset:three-way
.)
If you get an error message like:
WARNING: npm peer requirements not installed:
- package-utils@^0.2.1 not installed.
Read more about installing npm peer dependencies:
http://guide.meteor.com/using-packages.html#peer-npm-dependencies
It is because, by design, the package does not include instances of these from npm
to avoid repetition. (In this case, meteor npm install --save package-utils
will deal with the problem.)
See this or this for more information.
Now, if you see a message like
WARNING: npm peer requirements not installed:
[email protected] installed, underscore@^1.8.3 needed
it is because you or something you are using is using Meteor's cruddy old underscore
package. Install a new version from npm
. (And, of course, you may use the npm
version in a given scope via require("underscore")
.)
The repository contains the source for the demo/guide site as well as the package proper.
The site uses semantic:ui
which requires a bit of initialization. Start Meteor, do a trivial edit of client/lib/semantic-ui/custom.semantic.json
, and save it to generate Semantic UI.
It provides a view of the database via an #each
block iterating over a cursor
Here are some an example set-ups. Some of this will be clear immediately, the rest will become clear soon.
Here are some an example set-ups. Some of this will be clear immediately, the rest will become clear soon.
Let's start with a simple vanilla set-up.
ThreeWay.prepare(Template.DemoThreeWay, {
// The relevant Mongo.Collection
collection: DataCollection, // alt: string with collection name or null
// Meteor methods for updating the database
// The keys being the respective fields/field selectors for the database
// The method signature for these methods being
// function(_id, value, ...wildCardParams)
updatersForServer: {
'name': 'update-name',
'tags': 'update-tags',
},
// (Global) initial values for fields that feature only in the local view model
// and are not used to update the database
viewModelToViewOnly: {
'vmOnlyValue': '',
},
});
One then proceeds to bind input and display elements like so:
<ul>
<li>Name: <input type="text" data-bind="value: name"></li>
<li>Name in View Model: <span data-bind="html: name"></span></li>
<li>vmOnlyValue: <input type="text" data-bind="value: vmOnlyValue"></li>
<li>vmOnlyValue in View Model: <span data-bind="text: vmOnlyValue"></span></li>
</ul>
Once ready to bind to a document with id _id
in the database on a template instance instance
, simply call:
instance._3w_.setId(_id);
... or instantiate the relevant template like so:
{{> DemoThreeWay _3w_id="the-relevant-item-id"}}
or
{{> DemoThreeWay _id="the-relevant-item-id"}}
or
<!-- assuming someObject is an object with an _id property -->
{{> DemoThreeWay someObject}}
... and things will happen.
Note: When a document _id
is passed in through the data context, _3w_id
takes precedence over _id
for "reasonable reasons". For example, an existing template might take as input an object with an _id
property, but that might not be the desired _id
since data passed in is typically "static" and probably comes from another collection.
Here is a sample data-binding string:
<input data-bind="value: email"></div>
... and here is one which gives a sense of typical use...
<input data-bind="value: email; style: {color: emailValidationErrorText|trueIfNonEmpty|redIfTrue}"></div>
<div data-bind="html: emailValidationErrorText; visible: emailValidationErrorText|trueIfNonEmpty" style="color: red;"></div>
this will be elaborated upon later.
Now here are more of the settings, including:
- data transformations from view model to server and from server to view model
- helper functions that "display"-type (one-way) bindings like
html
,visible
anddisabled
, as well as theclass
,style
andattr
bindings can use for input - pre-processors for values in "input" elements and for "display" elements
ThreeWay.prepare(Template.DemoThreeWay, {
// The relevant Mongo.Collection
collection: DataCollection, // alt: string with collection name or null
// Meteor methods for updating the database
// The keys being the respective fields/field selectors for the database
// The method signature for these methods being
// function(_id, value, ...wildCardParams)
updatersForServer: {
'name': 'update-name',
'emailPrefs': 'update-emailPrefs',
'personal.particulars.age': 'update-age',
'tags': 'update-tags',
'personal.someArr.*': 'update-some-arr',
'personal.someArr.1': 'update-some-arr-1', // More specific than the previous, will be selected
'personal.otherArr.*.*': 'update-other-arr',
},
// Transformations from the server to the view model
// In this example, "tags" are stored in the view model as a comma
// separated list in a string, while it is stored in the server as
// an array
dataTransformToServer: {
tags: x => x.split(',').map(y => y.trim())
},
// Transformations from the view model to the server
// (Transform and call the updater Meteor method)
// In this example, "tags" are stored in the view model as a comma
// separated list in a string, while it is stored in the server as
// an array
dataTransformFromServer: {
tags: arr => arr.join && arr.join(',') || ''
},
// Pre-processors for data pre-render (view model to view)
preProcessors: {
// this takes a string of comma separated tags, splits, trims then
// joins them to make the result "more presentable"
tagsTextDisplay: x => (!x) ? '' : x.split(',').map(x => x.trim()).join(', '),
// this maps a key to the corresponding long form description
mapToAgeDisplay: x => ageRanges[x],
// this maps an array of keys to the corresponding long form
// descriptions and then joins them
// emailPrefsAll is of the form {"1_12": "1 to 12", ...})
mapToEmailPrefs: prefs => prefs.map(x => emailPrefsAll[x]).join(", "),
// These processors support visual feedback for validation
// e.g.: the red "Invalid e-mail address" text that appears when
// an invalid e-mail address has been entered
trueIfNonEmpty: x => x.length > 0,
grayIfTrue: x => (!!x) ? "#ccc" : '',
redIfTrue: x => (!!x) ? "red" : '',
},
// (Global) initial values for fields that feature only in the local view
// model and are not used to update the database
// Will be overridden by value tags in the rendered template of the form:
// <data field="sliderValue" initial-value="50"></data>
viewModelToViewOnly: {
sliderValue: "0",
"tagsValidationErrorText": '',
},
});
Returning to the previous example:
<input data-bind="value: email; style: {color: emailValidationErrorText|trueIfNonEmpty|redIfTrue}"></div>
<div data-bind="html: emailValidationErrorText; visible: emailValidationErrorText|trueIfNonEmpty" style="color: red;"></div>
... the "pipes" take data (from helpers or data fields) and pass them through a pipeline of pre-processors. For "display bindings" like html
and text
, the final value is displayed.
Suppose emailValidationErrorText
were ""
then piping it through trueIfNonEmpty
would lead to false
, and piping that through redIfTrue
would return ''
. So in the event of "no validation error", the color of the text would remain the default inherited value.
For "value bindings" like value
and checked
, the pipelines are only used to do DOM manipulation. This is useful for custom elements such as Semantic UI dropdowns.
More on this in a bit.
At this point, one might have a look at the full parameter set. Which will include:
- injection of default values for selected fields
- data validation
- event bindings
- database update settings
- suppressing and reporting the update of a focused field
Further elaboration is available in the documentation below.
ThreeWay.prepare(Template.DemoThreeWay, {
// The relevant Mongo.Collection
collection: DataCollection, // alt: string with collection name or null
// Meteor methods for updating the database
// The keys being the respective fields/field selectors for the database
// The method signature for these methods being
// function(_id, value, ...wildCardParams)
updatersForServer: {
'name': 'update-name',
'emailPrefs': 'update-emailPrefs',
'personal.particulars.age': 'update-age',
'tags': 'update-tags',
'personal.someArr.*': 'update-some-arr',
'personal.someArr.1': 'update-some-arr-1', // More specific than the previous, will be selected
'personal.otherArr.*.*': 'update-other-arr',
},
// Inject default values if not in database record
injectDefaultValues: {
'name': 'Unnamed Person',
// for wildcard fields, the last part of the field name cannot be a wildcard
'personal.otherArr.*.a': '100',
},
// Transformations from the server to the view model
// In this example, "tags" are stored in the view model as a comma
// separated list in a string, while it is stored in the server as
// an array
dataTransformToServer: {
tags: x => x.split(',').map(y => y.trim())
},
// Transformations from the view model to the server
// (Transform and call the updater Meteor method)
// In this example, "tags" are stored in the view model as a comma
// separated list in a string, while it is stored in the server as
// an array
dataTransformFromServer: {
tags: arr => arr.join && arr.join(',') || ''
},
// Validators under validatorsVM consider view-model data
// Useful for making sure that transformations to server values do not fail
// Validators under validatorsServer consider transformed data (for the server)
//
// validators have method signature:
// function(value, matchInformation, vmData)
// success/failure call backs have signature:
// function(value, matchInformation, vmData)
// all are called with the template instance as context
//
// matchInformation takes the form:
// {
// "fieldPath": "personal.otherArr.0.a",
// "match": "personal.otherArr.*.*",
// "params": ["0","a"]
// }
validatorsVM: {
// tags seems to be a decent candidate for one here
// but see below
},
validatorsServer: {
tags: {
validator: function(value, matchInformation, vmData) {
// tags must begin with "tag"
return value.filter(x => x.substr(0, 3).toLowerCase() !== 'tag').length === 0;
},
success: function(value, matchInformation, vmData) {
var instance = this;
instance._3w_.set('tagsValidationErrorText', '');
},
failure: function(value, matchInformation, vmData) {
var instance = this;
instance._3w_.set('tagsValidationErrorText', 'Each tag should begin with \"tag\".');
},
},
},
// determines whether to re-validate repeated values
validateRepeats: false, // (default: false)
// Helper functions that may be used as input for display-type bindings
// Order of search: three-way helpers, then template helpers, then data
// Called with this bound to template instance
// (be aware that arrow functions are lexically scoped)
helpers: {
altGetId: function() {
return Template.instance()._3w_.getId()
}
}
// Pre-processors for data pre-render (view model to view)
preProcessors: {
// this takes a string of comma separated tags, splits, trims then
// joins them to make the result "more presentable"
tagsTextDisplay: x => (!x) ? '' : x.split(',').map(x => x.trim()).join(', '),
// this maps a key to the corresponding long form description
mapToAgeDisplay: x => ageRanges[x],
// this maps an array of keys to the corresponding long form
// descriptions and then joins them
// emailPrefsAll is of the form {"1_12": "1 to 12", ...})
mapToEmailPrefs: prefs => prefs.map(x => emailPrefsAll[x]).join(", "),
// These processors support visual feedback for validation
// e.g.: the red "Invalid e-mail address" text that appears when
// an invalid e-mail address has been entered
trueIfNonEmpty: x => x.length > 0,
grayIfTrue: x => (!!x) ? "#ccc" : '',
redIfTrue: x => (!!x) ? "red" : '',
},
// (Global) initial values for fields that feature only in the local view
// model and are not used to update the database
// Will be overridden by value tags in the rendered template of the form:
// <data field="sliderValue" initial-value="50"></data>
viewModelToViewOnly: {
sliderValue: "0",
"tagsValidationErrorText": '',
},
// Event Handlers bound like
// <input data-bind="value: sliderValue; event: {mousedown: dragStartHandler, mouseup: dragEndHandler|saySomethingHappy}" type="range">
eventHandlers: {
dragStartHandler: function(event, template, vmData) {
console.info("Drag Start at " + (new Date()), event, template, vmData);
},
dragEndHandler: function(event, template, vmData) {
console.info("Drag End at " + (new Date()), event, template, vmData);
},
saySomethingHappy: function() {
console.info("Let\'s chill. (Second mouseup event to fire.)");
},
},
// Database Update Parameters
// "Debounce Interval" for Meteor calls; See: http://underscorejs.org/#debounce
debounceInterval: 300, // default: 500
// "Throttle Interval" for Meteor calls; See: http://underscorejs.org/#throttle ; fields used for below...
throttleInterval: 500, // default: 500
// Fields for which updaters are throttle'd instead of debounce'd
throttledUpdaters: ['emailPrefs', 'personal.particulars.age'],
// Reports updates of focused fields
// default: null (i.e.: update focused field and do nothing;
// feel free to do something and update the field anyway
// via instance._3w_set)
// fieldMatchParams take the form like:
// {
// fieldPath: "personal.otherArr.0.a",
// match: "personal.otherArr.*.*",
// params: ["0", "a"]
// }
updateOfFocusedFieldCallback: function(fieldMatchParams, newValue, currentValue) {
console.info("Update of focused field to", newValue, "from", currentValue, "| Field Info:", fieldMatchParams);
},
// Reactively update _id of document with the return value of this
// function (default: null; to not use this feature)
idGetter: function reactiveIdGetter(c) {
// return something based on data in collections
if (Template.instance().subscriptionsReady()) {
/* return something here? */
}
// watch path and set _id (here's a FlowRouter example)
// it is recommended to give _id params in routes different names from
// _id, such as "userId", "itemId", ...
return FlowRouter.getParam('itemId');
// to avoid setting (changing) the _id
// return null;
// c is a Tracker.Computation, and can be stopped via
// c.stop(), which means that following the return of this function
// the _id of the bound document will no longer be reactively updated
}
});
And we are back to that example:
<input data-bind="value: email; style: {color: emailValidationErrorText|trueIfNonEmpty|redIfTrue}"></div>
<div data-bind="html: emailValidationErrorText; visible: emailValidationErrorText|trueIfNonEmpty" style="color: red;"></div>
... the example options above are a little disjoint from this snippet, but it should be clear that defining a validator will inform ThreeWay
of whether input is valid or not, and then set emailValidationErrorText
to a non-empty string if there is a validation error. So when there is a validation error, the error message will be displayed and the color of the input field will be set to red.
To prevent debug and preparation functions from being misused by end users, one can block their use. One way is to call "irreversible stopping functions" after all ThreeWay.prepare
calls are done. Meteor.startup
works in this case:
Meteor.startup(function() {
ThreeWay._preventSubsequentPrepareCalls(true);
ThreeWay.utils._preventInstanceEnumeration(true);
});
If one wants to do this adaptively, then pass in a function that returns a truthy value. For instance, a function that checks Meteor.settings
and returns true
(to block subsequent uses) and false
(to allow, for instance, in development).
But the simplest way to do this would be something like...
Meteor.startup(function() {
if (Meteor.isClient) {
if (Meteor.settings.public.appName.indexOf('(Development)') !== -1) {
ThreeWay._preventSubsequentPrepareCalls(false);
ThreeWay.utils._preventInstanceEnumeration(false);
} else {
ThreeWay._preventSubsequentPrepareCalls(true);
ThreeWay.utils._preventInstanceEnumeration(true);
}
}
});
Consider the following Mongo document. The relevant fields may be referred to with the identifiers in the comments:
{
topLevelField: 'xxx', // "topLevelField"
topLevelArray: [ // "topLevelArray"
'a', // "topLevelArray.0"
'b', // "topLevelArray.1"
'c', // "topLevelArray.2"
],
topLevelObject: { // "topLevelObject"
nestedField: 'xxx', // "topLevelObject.nestedField"
nestedArray: [ // "topLevelObject.nestedArray"
1, // "topLevelObject.nestedArray.0"
2, // "topLevelObject.nestedArray.1"
3, // "topLevelObject.nestedArray.2"
],
}
}
Here is an example of binding to one of them: <span data-bind="value: topLevelObject.nestedArray.2"></span>
.
However, it would be clunky to have to specify each of "topLevelObject.nestedArray.0"
thru "topLevelObject.nestedArray.2"
(or more) in the set-up options. Therefore, options.updatersForServer
accepts wildcards (in key names) such as topLevelObject.nestedArray.*
where *
matches numbers (for arrays) and "names" (for objects; but of course, it's all objects anyway).
Note that in the case of multiple matches, the most specific match will be used, enabling "catch-all" updaters (which can be somewhat dangerous if not managed properly).
Note that having a more specific match is a signal to distinguish a field (or family of fields) from the more generic matches. This means that the more generic validators will not be used. There is no "fall through"... at the moment...
If certain fields require a value but it is possible that database entries have such missing fields, they can be "injected" (on the view model side) with defaults. These may be specified in the set up as follows:
// Inject default values if not in database record
injectDefaultValues: {
'name': 'Unnamed Person',
// for wildcard fields, the last part of the field name cannot be a wildcard
'personal.otherArr.*.a': '100',
},
As indicated, for wildcard fields, the last part of the field name cannot be a wildcard. Otherwise, it would be impossible to determine exactly what field to add. The "non-tail" part of existing fields are used to figure out if there is missing data.
The data binding responds to changes in the DOM. So Blaze can be used to generate and change data bindings. For example:
{{#each fields}}
<div>{{name}}: <input data-bind="value: particulars.{{field}}"></div>
{{/each}}
... might generate...
<div>Name: <input data-bind="value: particulars.name"></div>
<div>e-mail: <input data-bind="value: particulars.email"></div>
<div>D.O.B.: <input data-bind="value: particulars.dob"></div>
... and should a new field be added, data binding will take effect.
Dynamic data binding works without a hitch (hopefully) when a template is operating in a vacuum. Multiple ThreeWay
instances (See: "Family Access": Ancestor and Descendant Data for more information) work fine in the absence of dynamic data binding. But when DOM elements (to be data bound) are being added and removed dynamically, it is important to create certainty about which ThreeWay
instance a given DOM element should be bound to.
Briefly, the start of the template life cycle for a template and a child template is as follows: (i) parent created, (ii) child created, (iii) child rendered, (iv) parent rendered. Monitoring call backs work on a "first-come-first-bound" basis, with child nodes getting the first pick.
To ensure proper bindings, it is advisable for every template to be wrapped in a div
element which will be watched for changes within.
A design decision was made to not require such a root node, but to leave it to the user to handle this matter.
In the absence of a root node, all individual DOM elements in the template are observed for changes.
(This is why having a wrapping element is useful.)
In the event that the user would (inexplicably) like to observe disjoint parts of a template for changes, the _3w_setRoots(selectorString)
method should be used to select root nodes (via a template-level jQuery selector). This might be done in an onRendered
hook or after rendering completes.
For even more specificity, the restrict-template-type
attribute can be set (with a comma separated list of template names) on DOM elements to specify which ThreeWay
-linked template types should be used to data bind individual elements.
Data is sent back to the server via Meteor methods. This allows one to control matters like authentication and the like. What they have in common is method signatures taking the _id
of the document, the updated value next, and a number of additional parameters equal to the number of wildcards in the field specification.
The keys of options.updatersForServer
are the respective fields (or fields specified through wild cards) for the database. The method signature for these methods is function(_id, value, ...wildCardParams)
.
In the event that a method is associated with a "wildcard match" field name, such as "ratings.3.rating"
matched to "ratings.*.rating"
, then the matching wildCardParams
will be passed into the method as well. In that example, one would end up with a call like:
Meteor.call('update-ratings.*.rating', _id, newValue, "3");
(Don't mind the string representation of array indices, it doesn't really matter because Mongo field specifiers are strings.)
Here are more examples:
updatersForServer: {
'x': 'update-x',
'someArray.*': 'update-someArray.*',
'anotherArray.*.properties.*': 'update-anotherArray.*.properties.*'
},
... which might be associated with the following methods:
Meteor.methods({
'update-x': function(id, value) {
if (someAuthCheck(this.userId)) {
DataCollection.update(id, {
$set: {
x: value
}
});
}
},
'update-someArray.*': function(id, value, k) {
var updater = {};
updater['someArray.' + k] = value;
DataCollection.update(id, {
$set: updater
});
},
'update-anotherArray.*.properties.*': function(id, value, k, fld) {
var updater = {};
updater['anotherArray.' + k + '.properties.' + fld] = value;
DataCollection.update(id, {
$set: updater
});
}
});
(Also, please don't ask for regular expressions... Do that within your own updaters.)
To use callbacks or otherwise deviate from the use of Meteor methods, use the following Extended Notation for Updaters.
Aside from plain string names Also allowed are updater descriptions of the following form:
{
'name': function(id, value) {
console.info('[update-name]', id, "to", value);
Meteor.call('update-name', id, value);
},
'personal.someArr.1': {
method: 'update-personal.someArr.1',
callback: function(err, res, info) {
console.info('[update-personal.someArr.1]', err, res, info);
}
},
}
In the latter case, info
takes the form:
{
instance: instance, // template instance
id: _id, // _id
value: v, // update value
params: params, // wildcard params
methodName: methodName, // method name
updateTime: updateTime, // time update started
returnTime: returnTime, // time update completed
};
... admittedly, that is a little excessive.
Update methods are, by default, debounced. These can be customized. The example below should be reasonably self-explanatory:
{
...
// Database Update Parameters
// "Debounce Interval" for Meteor calls; See: http://underscorejs.org/#debounce
debounceInterval: 300, // default: 500
// "Throttle Interval" for Meteor calls; See: http://underscorejs.org/#throttle ; fields used for below...
throttleInterval: 500, // default: 500
// Fields for which updaters are throttle'd instead of debounce'd
throttledUpdaters: ['emailPrefs', 'personal.particulars.age'],
...
}
The format that data is stored in a database might not be the most convenient for use in the view model (e.g.: sparse representation "at rest"), as such it may be necessary to do some translation between database and view model.
Consider the following example:
dataTransformFromServer: {
tags: arr => arr.join && arr.join(',') || ''
},
dataTransformToServer: {
tags: x => x.split(',').map(y => y.trim())
},
In this example, for some reason, tags
is stored in the view model as a string-ified comma separated list, while it is stored as an array on the server. When the underlying observer registers a change to the database, the new value is converted and placed into the view model. When the database is to be updated, the view model value is transformed back into an array before it is sent back via the relevant Meteor method.
Note that transformations actually take two parameters, the first being the value in question and the second being all the view model data. Thus the complete method signature is function(value, vmData)
.
When the template is rendered, the reactive elements are set up using data-bind
attributes in the "mark up" part of the template.
For example, <span data-bind="html: name"></span>
, binds the "name" field to the innerHTML
property of the element. Also, <span data-bind="text: something"></span>
, binds the "something" field to the text
property of the element.
But "pre-processors" can be applied to view model data to process content before it is rendered. For example,
<span data-bind="html: emailPrefs|mapToEmailPrefs"></span>
<span data-bind="text: emailPrefs|mapToEmailPrefs"></span>
where mapToAgeDisplay
was described as x => ageRanges[x]
(or, equivalently, function(x) {return ageRanges[x];}
) and ageRanges
is a dictionary (object) mapping keys to descriptions.
Pre-processors actually take up-to four arguments, (value, elem, vmData, dataSourceInfomation)
and return a value to be passed into the next pre-processor, or rendered on the page. This actually features in ThreeWay.preProcessors.updateSemanticUIDropdown
, used in the demo, where the element itself has to be manipulated (see: this) to achieve the desired result. More on pre-processors later.
There's usually nothing much to say about this simple binding...
<input name="name" data-bind="value: name">
(It works with input
and textarea
tags.)
This one too, although the helper does bear some explaining. repackageDictionaryAsArray
takes a dictionary (object) and maps it into an array of key-value pairs. That is, an array with elements of the form {key: "key", value: "value"}
. So the below example lays out the various options as checkboxes and binds checked
to an array.
{{#each repackageDictionaryAsArray emailPrefsAll}}
<div class="ui checkbox">
<input type="checkbox" name="emailPrefs" value="{{key}}" data-bind="checked: emailPrefs">
<label>{{value}}</label>
</div>
{{/each}}
In the case of radio buttons, checked
is bound to a string.
<div class="inline fields">
{{#each repackageDictionaryAsArray ageRanges}}
<div class="ui radio checkbox">
<input type="radio" name="age" value="{{key}}" data-bind="checked: age">
<label>{{value}}</label>
</div>
{{/each}}
</div>
The ischecked
binding is similar to the checked
binding. However, it relates to individual elements rather than the collection of radio buttons or checkboxes. Checked elements map to true
values and unchecked elements map to false
values.
One can apply certain modifiers to value
and checked
bindings such as:
<input name="name" data-bind="value#donotupdateon-input: name">
<input name="comment" data-bind="value#throttle-1000: name">
By default, value
bindings update the view model on change
and input
. But the latter can be suppressed with a donotupdateon-input
option. In the first example, the view model is only updated on change
such as a loss of focus after a change is made.
For the comment
input element, updates can happen as one is typing (due to updates being made on input
), however, those updates to the view model are throttled with a 1 second interval.
The following modifiers are available and are applied in the form <binding>#<modifier>-<option>#<modifier>-<option>: <view model field>
:
updateon
: also updates the view model when a given event fires (e.g.updateon-<event name>
)donotupdateon
: do not update the view model when a given event fires (e.g.donotupdateon-<event name>
); the only valid option isinput
and this only applies tovalue
bindings.throttle
: throttles (e.g.throttle-<interval in ms>
); does not apply tochecked
bindingsdebounce
: (e.g.debounce-<interval in ms>
); does not apply tochecked
bindings
(For checked
bindings, it would be rather sketchy to apply throttling or debouncing due multiple elements forming a composite checked
widget.)
visible
and disabled
can be bound to any boolean (or truthy) variable, and stuff disappears/gets disabled when it is set to false
(false-ish).
<div data-bind="visible: something">...</div>
<div data-bind="disabled: something">...</div>
focus
deals with whether an element is focused. (Personally not all too keen on this one.)
<input type="text" name="name" data-bind="focus: nameHasFocus">
<div data-bind="visible: nameHasFocus">name has focus</div>
Style bindings are done via: data-bind="style: {font-weight: v1|preProc, font-size: v2|preProc; ...}"
. Things work just like the above html binding.
Attribute bindings are done via: data-bind="attr: {disabled: v1|preProc, ...}"
. Things also work just like html bindings. Set a value to null
or undefined
to ensure that an attribute is not present at all.
Class bindings are done via: data-bind="class: {class1: bool1|preProc; ...}"
. However, things work more like the visible and disabled bindings in that the values to be bound to will be treated as boolean-ish.
Alternatively, one can bind each of the above directly to a single object. This can be done as follows:
data-bind="styles: stylesObject"
wherestylesObject
might be{ "font-family": "Courier New", "font-size": "200%", }
data-bind="classes: classesObject"
whereclasses
might be{ "red": false, "loading": true, }
data-bind="attributes: attributesObject"
whereattributesObject
might beAs above, set a value to{ "disabled": true, "data-id": "xxxxxx", }
null
orundefined
to get rid of the respective attribute.
Helper functions may be used as input for display-type bindings.
Such bindings include html
, visible
, disabled
, as well as the class
, style
and attr
bindings.
For such bindings, the order of search is helpers first, then template helpers, then data. (so ThreeWay
helpers shadow template helpers)
Helpers are called with this
bound to template instance, and Template.instance()
is also accessible. (Note: Be careful of lexically scoped arrow functions that overrides call
/apply
/bind
.)
It is useful to highlight _3w_hasData
, which is automatically added to the set of template helpers.
<div data-bind="visible: _3w_hasData">...</div>
<button data-bind="disabled: _3w_hasData">...</button>
One might find it to be particularly useful.
A tenuous design decision has been made not to phase out helpers. A less tenuous design decision is to not unify helpers with pre-processors based on their different method signatures.
Helpers may be inherited.
Sometimes one variable alone is not enough to determine the state of a DOM property. For example, to determine whether a phone number is valid, might depend both on the number and on the country. On the other hand, that example is faulty since a validation callback can do the relevant computations with full access to the view model.
But anyway, usefulness aside, this is one example of such a binding:
<div data-bind="style: {background-color: colR#colG#colB|makeRGB}">
... it is also an example that you might find in the demo. (Look for the bit asking you to some nodes to the DOM via the console.)
Event bindings may be achieved via: data-bind="event: {change: cbFn, keyup:cbFn2|cbFn3, ...}"
where callbacks like cbFn1
have signature function(event, template, vmData)
(vmData
being, of course, the data in the view model).
The event handlers may be specified in the set up as follows:
eventHandlers: {
dragStartHandler: function(event, template, vmData) {
console.info("Drag Start at " + (new Date()), event, template, vmData);
},
dragEndHandler: function(event, template, vmData) {
console.info("Drag End at " + (new Date()), event, template, vmData);
},
saySomethingHappy: function() {
console.info("Let\'s chill. (Second mouseup event to fire.)");
},
},
... and bound as follows:
<input data-bind="value: sliderValue; event: {mousedown: dragStartHandler, mouseup: dragEndHandler|saySomethingHappy}" type="range">
Event handlers may be inherited.
Template-level defaults may be specified in the configuration like so:
// (Global) initial values for fields that feature only in the local view model
// and are not used to update the database
viewModelToViewOnly: {
'vmOnlyValue': 'something',
},
... or a generic customization via the template instance (fired at onCreated
)...
viewModelToViewOnly: function (templateInstance) {
return {
'vmOnlyValue': someComputation(templateInstance.data.something),
};
},
However, since those are "template-level defaults", it may be useful at times to customize (update) them at instantiation. This may be achieved through template instance data:
{{> DemoThreeWay _3w_additionalViewModelOnlyData=helperWithAdditionalData}}
The following methods are crammed onto each template instance in an onCreated
hook. They may be accessed via instance._3w_
(where instance
is the relevant template instance).
setRoots(selectorString)
: selects the root of theThreeWay
instance using a selector string (Template.instance().$
will be used); child nodes of the single node (the method throws an error if more than one node is matched), present and forthcoming, will be watched for changes (and the respective data bindings updated); See Using Dynamic Data Binding with MultipleThreeWay
instances for more information
-
get3wInstanceId()
: gets the instance id of theThreeWay
instance -
getId()
: gets the id of the document bound to -
setId(id)
: sets the id of the document to bind to -
get(prop)
: gets a property -
getWithDefault(prop, defaultValue)
: gets a property and returnsdefaultValue
ifundefined
. -
set(prop, value)
: sets a property -
get_NR(prop)
: gets a property "non-reactively" -
getAll_NR
: gets all the data "non-reactively" -
withArray(prop, methodName, ...args)
: invokes the method with namemethodName
(e.g.: push) on the array in a property with argumentsargs
and returns the result (throws if no array is found)- e.g.:
instance._3w_.withArray('numbers', 'push', 5); /* returns resulting length */
- e.g.:
instance._3w_.withArray('numbers', 'pop', 5); /* returns 5 */
- e.g.:
-
mapData(prop, mapFunction, ...additionalArgs)
: invokes the functionmapFunction
with the data of a property as the first argument and additional argumentsadditionalArgs
, sets the property to the result, and returns the result- e.g.:
instance._3w_.mapData('someNumber', (x,y) => x+y, 5); /* increments someNumber by 5 */
- e.g.:
-
isPropVMOnly(prop)
: gets all view-model only data "non-reactively" -
getAll_VMOnly_NR
: gets all view-model only data "non-reactively" -
fetch(prop)
: attempts to retrieve propertyprop
from the view model and, failing that, attempts to read from the bound document -
fetchExtended(prop)
: attempts to retrieve propertyprop
in the following order of precedence:- evaluating a ThreeWay helper
- evaluating a ThreeWay helper on (the closest) ancestor
- evaluating a Blaze helper
- the entire document (
prop = "*"
) - the entire view model (
prop = "@"
) - from some view model field
- from some document field
-
isSyncedToServer(prop)
: returnstrue
ifThreeWay
can be sure that data for the field with nameprop
has been received and written by the server. -
allSyncedToServer
: returnstrue
ifThreeWay
can be sure that all data has been received and written by the server. -
isNotInvalid(prop)
: returnstrue
if data is not invalid (i.e.: available validators returntrue
). -
expandParams(fieldSpec, params)
: takes a wild card field specification (like'friends.*.name'
) and parameters (like[3]
) to generate a field specifier (likefriends.3.name
). -
focusedField()
: returns the currently focused field. -
focusedFieldUpdatedOnServer(prop)
: indicates whether fieldprop
was updated on the server while the relevant field was in focus (and aupdateOfFocusedFieldCallback
callback was defined inoptions
) and hence the field is out of sync -
resetVMOnlyData
: resets view-model only data to initial values (including those from the optional field_3w_additionalViewModelOnlyData
of the data context)
-
parentDataGet(p, levelsUp)
: returns propertyp
from parent instancelevelsUp
levels up (default: 1) -
parentDataGetAll(p, levelsUp)
: returns all data from parent instancelevelsUp
levels up (default: 1) -
parentDataSet(p, v, levelsUp)
: sets propertyp
on parent instance tov
levelsUp
levels up (default: 1) -
parentDataGet_NR(p, levelsUp)
: (non-reactively) returns propertyp
from parent instancelevelsUp
levels up (default: 1) -
parentDataGetAll_NR(levelsUp)
: (non-reactively) returns all data from parent instancelevelsUp
levels up (default: 1) -
getInheritedHelper(name)
: seeks out closest ancestor (self included) with helper having namename
and returns it if available. -
getInheritedEventHandler(name)
: seeks out closest ancestor (self included) with event handler having namename
and returns it if available. -
getInheritedPreProcessor(name)
: seeks out closest ancestor (self included) with pre-processor having namename
and returns it if available.
-
childDataGetId(childNameArray)
: returns id from descendant instance wherechildNameArray
gives a sequential list of successive descendant names (alternatively a string in the special case of a direct child) -
childDataSetId(id, childNameArray)
: sets id from descendant instance wherechildNameArray
gives a sequential list of successive descendant names (alternatively a string in the special case of a direct child) -
childDataGet(p, childNameArray)
: returns propertyp
from descendant instance wherechildNameArray
gives a sequential list of successive descendant names (alternatively a string in the special case of a direct child) -
childDataGetAll(childNameArray)
: returns all data from descendant instance wherechildNameArray
gives a sequential list of successive descendant names (alternatively a string in the special case of a direct child) -
childDataSet(p, v, childNameArray)
: sets propertyp
from descendant instance wherechildNameArray
gives a sequential list of successive descendant names (alternatively a string in the special case of a direct child) -
childDataGet_NR(p, childNameArray)
: (non-reactively) returns propertyp
from descendant instance wherechildNameArray
gives a sequential list of successive descendant names (alternatively a string in the special case of a direct child) -
childDataGetAll_NR(childNameArray)
: (non-reactively) returns all data from descendant instance wherechildNameArray
gives a sequential list of successive descendant names (alternatively a string in the special case of a direct child) -
getAllDescendants_NR(levels)
: (non-reactively) returns all descendant instances and information on them as objects. For example...
[
{
id: "kiddy",
instance: [Blaze.TemplateInstance],
level: 1,
templateType: "Template.DemoThreeWayChild",
},
{
id: "grandkiddy",
instance: [Blaze.TemplateInstance],
level: 2,
templateType: "Template.DemoThreeWayGrandChild",
},
]
-
siblingDataGet(p, siblingName)
: returns propertyp
from sibling instance wheresiblingName
gives the name of the relevant sibling -
siblingDataGetAll(siblingName)
: returns all data from sibling instance wheresiblingName
gives the name of the relevant sibling -
siblingDataSet(p, v, siblingName)
: sets propertyp
from sibling instance wheresiblingName
gives the name of the relevant sibling -
siblingDataGet_NR(p, siblingName)
: (non-reactively) returns propertyp
from sibling instance wheresiblingName
gives the name of the relevant sibling -
siblingDataGetAll_NR(siblingName)
: (non-reactively) returns all data from sibling instance wheresiblingName
gives the name of the relevant sibling
getTemplateByPath(path)
: obtains theThreeWay
-linked template instance given the specified relative path (as an array), for exampletemplateInstance._3w_.getTemplateByPath(['..', 'grand-child-2'])
(use".."
to indicate going one level up)callOnTemplateByPath(path, methodName, arg_1, arg_2, ..., arg_n)
: invokes the method with namemethodName
(with the relevant arguments) on theThreeWay
-linked template instance specified by the pathpath
applyOnTemplateByPath(path, methodName, args)
: invokes the method with namemethodName
(with the relevant arguments as an arrayargs
) on theThreeWay
-linked template instance specified by the pathpath
-
_3w_3wInstanceId
: returns the instance id of theThreeWay
instance -
_3w_id
: returns the_id
of the document selected (if any) -
_3w_hasData
: returns a boolean indicating whether the view model has data yet -
_3w_get
: See previous section. -
_3w_getWithDefault
: See previous section. -
_3w_getAll
: See previous section. -
_3w_getAll
: See previous section. -
_3w_parentDataGet
: See previous section. -
_3w_parentDataGetAll
: See previous section. -
_3w_childDataGet
: See previous section. -
_3w_childDataGetAll
: See previous section. -
_3w_siblingDataGet
: See previous section. -
_3w_siblingDataGetAll
: See previous section. -
_3w_isSyncedToServer
: See previous section. -
_3w_notSyncedToServer
: The negation of_3w_isSyncedToServer
. See previous section. -
_3w_allSyncedToServer
: See previous section. -
_3w_isNotInvalid
: See previous section. -
_3w_isInvalid
: The negation of_3w_isNotInvalid
. See previous section. -
_3w_validValuesSynced(prop)
:true
if synced or data is not valid. (See previous section.) -
_3w_validValuesNotSynced(prop)
:true
if data is not invalid (ok, valid) and not synced. (See previous section.) -
_3w_expandParams
: See previous section. -
_3w_focusedField
: returns the currently focused field. -
_3w_focusedFieldUpdatedOnServer(prop)
: indicates whether fieldprop
was updated on the server while the relevant field was in focus (and aupdateOfFocusedFieldCallback
callback was defined inoptions
) and hence the field is out of sync -
_3w_display(displayDescription)
: See Pre-processor Pipelines in Blaze.
Presentation of data is facilitated by "pre-processors" which map values (display-only bindings) and may do DOM manipulation when needed (e.g.: with Semantic UI dropdowns). This feature allows for great flexibility in displaying data, enabling one to "easily" (and typically declaratively) translate data to display.
However, because of the impurity of side-effects is rather directly enabled, in principle, one could use pre-processor side-effects render a d3.js diagram responding to changes in data (but for simple charts there are easier ways to do things (e.g.: this and this).
Display (one-directional) bindings like html
and visible
(later class
, style
and attr
) use pre-processing pipelines to generate items for display. Consider the following examples:
preProcessors: {
mapToAgeDisplay: x => ageRanges[x],
toUpperCase: x => x.toUpperCase(),
alert: function(x) {
alert(x);
return x;
},
// This is something special to make the Semantic UI Dropdown work
// More helpers will be written soon...
updateSemanticUIDropdown: ThreeWay.preProcessors.updateSemanticUIDropdown
},
... a binding like <span data-bind="html: age|mapToAgeDisplay"></span>
would, if in the view model age === '13_20'
and ageRanges['13_20'] === '13 to 20'
display the text "13 to 20".
... and a binding like <span data-bind="html: age|mapToAgeDisplay|alert|toUpperCase"></span>
would, under the same circumstances, annoy the user with an alert with text "13 to 20" and then display the text "13 TO 20". (Please don't do something like that.)
Multi-way data-bindings such as value
and checked
use pre-processing pipelines to deal with DOM manipulation only (e.g.: Semantic UI dropdowns via ThreeWay.preProcessors.updateSemanticUIDropdown
). Pipeline functions do not manipulate value.
Pre-processors have method signature function(value, elem, vmData, dataSourceInfomation)
(function(v1, v2, ..., vn, elem, vmData, dataSourceInfomation)
for the first pre-processor in a multi-variable binding) where value
is the value in the view model, elem
is the bound element, vmData
is a dictionary containing all the data from the view model, and dataSourceInfomation
contains information on the source of the data in the form:
{
"type": 'field',
"name": 'personal.otherArr.0.a',
"fieldPath": "personal.otherArr.0.a", // applicable if source is a field from database
"match": "personal.otherArr.*.*", // applicable if source is a field from database
"params": ["0","a"] // applicable if source is a field from database
}
Example Use Case: Consider an input field with some validator. An invalid value might cause some validation error message to be set to be non-empty, and that change in view model data might trigger various forms of presentation. For example:
<div data-bind="html: tagsValidationErrorText; visible: tagsValidationErrorText|trueIfNonEmpty; style: {color: tagsValidationErrorText|trueIfNonEmpty|redIfTrue}"></div>
Pre-processors are called with this
bound to template instance, and Template.instance()
is also accessible. (Note: Be careful of lexically scoped arrow functions that overrides call
/apply
/bind
.)
Pre-processors may be inherited.
One can data-bind to a sub-object/array/the entire document and pass it through a pre-processor. At times, only side-effects are sought. For this purpose, the process
binding was created. For example:
<div data-bind="process: vertices|plotDiagram"></div>
This gives a pre-processor plotDiagram
the value of vertices
and the div
element on which the binding is defined. What follows might be that plotDiagram
empties out the div
(via jQuery's .empty()
) and fills it up with a plot of the vertices. Pure side-effects with no annoying hackery.
Furthermore, for the process
pre-processor, it will be possible to bind the entire document. Wherein all transformations from MiniMongo will be available.
<div data-bind="process: *|visualizeDocument"></div>
In addition, one may also bind to the full content of the view-model, wherein data is presented in a "flat" form.
<div data-bind="process: @|describeViewModelContent"></div>
There are some fundamental differences between binding to the entire view model (@
) and binding to the associated document (*
).
Binding to the View Model (@ ) |
Binding to the Associated Document (* ) |
---|---|
All view model data | Just data in the associated document |
Flat data representation | An object with "depth" and transformations, if any |
Updated on view model update | Updated when MiniMongo is updated (from the server; hence latency) |
Pre-processors may also be used with the _3w_display
blaze-helper. For example,
<!-- Maps an array of e-mail preference codes to human readable
names and wraps with STRONG tags if there are more than one-->
{{{_3w_display 'emailPrefs|mapToEmailPrefs|boldIfMoreThanOne'}}}
<!-- Spaces out a comma separated list of tags -->
{{_3w_display 'tags|tagsTextDisplay'}}
<!-- Maps (3) numerical intensity values to a RGB string -->
{{_3w_display 'colR#colG#colB|makeRGB'}}
This use of Handlebars expressions is an alternative to display bindings in tags. While the two methods are very similar and their use would largely be a matter of preference, it should be noted that:
- Handlebars expressions cannot be styled as straightforwardly as adding
style
andclass
bindings - Ownership of Handlebars expressions to a template (or correspondence with a template instance) is always clear
Data validators are defined as follows:
// Validators under validatorsVM consider view-model data
// Useful for making sure that transformations to server values do not fail
// Validators under validatorsServer consider transformed data (for the server)
//
// validators have method signature:
// function(value, matchInformation, vmData)
// success/failure call backs have signature:
// function(value, matchInformation, vmData)
// all are called with the template instance as context
//
// matchInformation takes the form:
// {
// "fieldPath": "personal.otherArr.0.a",
// "match": "personal.otherArr.*.*",
// "params": ["0","a"]
// }
validatorsVM: {
// tags seems to be a decent candidate for one here
// but see below
},
validatorsServer: {
tags: {
validator: function(value, matchInformation, vmData) {
// tags must begin with "tag"
return value.filter(x => x.substr(0, 3).toLowerCase() !== 'tag').length === 0;
},
success: function(value, matchInformation, vmData) {
var instance = this;
instance._3w_.set('tagsValidationErrorText', '');
},
failure: function(value, matchInformation, vmData) {
var instance = this;
instance._3w_.set('tagsValidationErrorText', 'Each tag should begin with \"tag\".');
},
},
},
validateRepeats: false, // (default: false)
In options
one can set the value of validateRepeats
to determines whether successive identical values are validated. Deals with the issue of validation firing on change and then for the server updates.
Recall that in the previous section, the following example was described:
<div data-bind="html: tagsValidationErrorText; visible: tagsValidationErrorText|trueIfNonEmpty; style: {color: tagsValidationErrorText|trueIfNonEmpty|redIfTrue}"></div>
The validation flow is as follows:
1. a change is made in the view which propagates to the view model
2. validation starts
3. view-model level validation using data in the view model and success/failure call-backs fire
4. if the view-model level check does not fail, server validation is run and success/failure call-backs fire
5. the overall result is returned
If the change is a candidate for a database update (e.g.: the value is not the same as the previous known value in the database), then validity is used as a requirement for an update. (As is common sense.)
The reasons for having two separate checks is to deal with include the possibility that a user may want to guard against a transformation being done on invalid data, and that checks may be more convenient in one form or another. (Not to mention silly stuff relating to wild card matching shenanigans.)
ThreeWay
-linked template instances can be connected in parent-child relationships. Data can be accessed across template boundaries in the following ways (and more):
- ancestor (any number of levels up)
- descendant (any number of levels down; requires knowledge of the relevant template instance identifiers of successive descendants passed into each template as
_3w_name
in the data context) - sibling (requires knowledge of the relevant template instance identifier passed into template as
_3w_name
in data context)
In principle, as long at the template instance of the "highest-level" ancestor can be acquired, then any connected instance may be straight-forwardly accessed. "Sibling" data-access is syntactic sugar for this.
See Instance Methods for more information.
ThreeWay
migrates data between reloads triggered by hot code push (or the reload
package). To do this properly, instances should not have instance ids that collide.
There are a few sufficient conditions for non-collision such as the following all being true, all ThreeWay
-enabled template instances are in a common template tree (there is a ThreeWay
-enabled such that all others are descendants. This may be achieved trivially by calling:
ThreeWay.prepare(Template.BigPapaRootTemplateLayout, {});
... on the root template, which may be a layout that swaps components in and out with Template.dynamic
. (Do look at Using Dynamic Data Binding with Multiple ThreeWay
instances to ensure that this is done properly.)
Recall that one may customize ids manually by passing _3w_name
into the data context of each template instance. For instances with the same name (instance id), that get created and destroyed dynamically, only the first instance will get the data from the previous migration.
One may pass _3w_ignoreReloadData
(boolean) into the data context of each template instance to indicate whether to ignore migrated data (true
to ignore).
ThreeWay.DEBUG_MODE.setOn()
- Turns debug mode on
ThreeWay.DEBUG_MODE.setOff()
- Turns debug mode off
ThreeWay.DEBUG_MODE.isOn
- Returns whether debug mode is on
ThreeWay.DEBUG_MODE.selectAll()
- Show all debug messages (initially none)
ThreeWay.DEBUG_MODE.selectNone()
- Reset selection of debug message classes to none
ThreeWay.DEBUG_MODE.select(aspect)
- More granular control of debug messages, debug messages fall into the following classes:
'parse'
'bind'
'tracker'
'new-id'
'observer'
'db'
'default-values'
'validation'
'data-mirror'
'vm-only'
'reload'
'bindings'
'value'
'checked'
'focus'
'html-text'
'visible-and-disabled'
'style'
'attr'
'class'
'event'
'process'
The above is obtainable from ThreeWay.DEBUG_MODE.MESSAGE_HEADINGS
.
ThreeWay.utils.allInstances
: a description of all instances an array of instances as follows
{
instanceId: ...,
dataId: ...,
data: ...,
document: ...,
template: instance,
templateType: instance.view.name,
}
ThreeWay.utils.allInstancesByTemplateType
: ThreeWay.utils.allInstances
grouped by template type (name)
Extra processors may be accessed via the ThreeWay.preProcessors
namespace (e.g.: ThreeWay.preProcessors.updateSemanticUIDropdown
). All "extra" pre-processors will be included by default if no pre-processor with the same name is defined.
truthy
: returns a boolean which reflects the "truthiness" of the valuenot
: returns a boolean which reflects the "falsiness" of the valueisNonEmptyString
: returns the described true/false valueisNonEmptyArray
: returns the described true/false valuetoUpperCase
: transforms the value to a string (undefined
to''
) and returns it in upper casetoLowerCase
: transforms the value to a string (undefined
to''
) and returns it in lower caseupdateSemanticUIDropdown
: does the necessary DOM manipulation that enables the use of Semantic UI dropdowns; (Previously, this used.dropdown("set exactly", ...)
that would trigger value changes and unnecessary updates.)undefinedToEmptyStringFilter
: mapsundefined
's to empty strings and passes other values
Similar to the above, but these are generators for pre-processors: they each accept one or more arguments and return a pre-processor. They may be accessed via the ThreeWay.preProcessorGenerators
namespace (e.g.: ThreeWay.preProcessorGenerators.undefinedFilterGenerator
).
undefinedFilterGenerator(defaultValue)
: a function that returns a function that mapsundefined
's todefaultValue
and passes other valuesmakeMap(map, defaultValue)
: a function that mapsk
tomap[k]
(and returnsdefaultValue
ifmap
does not have propertyk
)
Built-in transformations, for mapping from server to view model and back, may be accessed via the ThreeWay.transformations
namespace. (e.g.: ThreeWay.transformations.dateToString
or ThreeWay.transformations.dateFromString
) Generally, the naming convention is understood to be "server-side value" on left and "view model value" on right.
arrayFromCommaDelimitedString
: maps a comma delimited string to an array of a separated values (empty string maps to empty array)arrayToCommaDelimitedString
: maps an array to a comma delimited string
The following are available but unnecessary because one can use input
elements of type=date
, type=month
, type=datetime-local
and ThreeWay
will make things work with Date
-typed view-model values.
dateFromString
: maps a string of the form "YYYY-MM-DD" to aDate
dateToString
: maps aDate
to a string of the form "YYYY-MM-DD"datetimeFromString
: maps a string of the form "YYYY-MM-DDThh:mm" to aDate
datetimeToString
: maps aDate
to a string of the form "YYYY-MM-DDThh:mm"monthFromString
: maps a string of the form "YYYY-MM" to aDate
monthToString
: maps aDate
to a string of the form "YYYY-MM"
Similar to the above, but these are generators for transformations that take one or more parameters and return a transformation. They may be accessed via the ThreeWay.transformationGenerators
namespace (e.g.: ThreeWay.transformationGenerators.booleanFromArray
).
arrayFromDelimitedString(delimiter)
: generates transformations likearrayFromCommaDelimitedString
abovearrayToDelimitedString(delimiter)
: generates transformations likearrayToCommaDelimitedString
abovearrayFromIdKeyDictionary(idField)
: generates a transform function that maps dictionaries (objects) like:
[
{_id: 'abc', f1: 'a', f2: 1}
{_id: 'def', f1: 'b', f2: 2}
{_id: 'ghi', f1: 'c', f2: 3}
]
... to ...
{
'abc': {_id: 'abc', f1: 'a', f2: 1}
'def': {_id: 'def', f1: 'b', f2: 2}
'ghi': {_id: 'ghi', f1: 'c', f2: 3}
}
... (here idField
is _id
).
arrayToIdKeyDictionary(idField)
: generates the reverse transformation ofarrayFromIdKeyDictionary(idField)
.booleanFromArray(trueIndicator)
: generates a transform function that returnstrue
if the input is an array with a single element taking valuetrueIndicator
) andfalse
otherwise.booleanToArray(trueIndicator)
: generates a transform function that evaluates the "truthiness" of the input and returns[trueIndicator]
if true and[]
otherwise.numberFromString(defaultValue)
: generates a function that maps a string to a number with a default value in the event of a casting error.
These are generators for event handlers via specialization of existing DOM events.
They take an event handler and wrap a filter around it, returning a new event handler that is called on the parent event, but checks to see whether the user specified handler should be called.
These generators may be accessed via the ThreeWay.eventGenerators
namespace (e.g.: ThreeWay.eventGenerators.returnKeyHandlerGenerator
).
Generally, these generators take, as first argument, an event handler with signature function(event, template, vmData)
(see Event Bindings).
-
keypressHandlerGenerator(handler, keyCodes, specialKeys)
: calls event handler if the pressed key is in the arraykeyCodes
and the special keys (SHIFT, CTRL, ALT) pressed match those inspecialKeys
(bind the result tokeydown
,keyup
and similar handlers) -
keypressHandlerGeneratorFromChars(handler, chars, specialKeys)
: calls event handler if the pressed key is in the stringchar
and the special keys (SHIFT, CTRL, ALT) pressed match those inspecialKeys
(bind the result tokeydown
,keyup
and similar handlers) -
returnKeyHandlerGenerator(handler, specialKeys)
: calls event handler if the pressed key was RETURN and the special keys (SHIFT, CTRL, ALT) pressed match those inspecialKeys
(bind the result tokeydown
,keyup
and similar handlers)
For example:
var ctrlReturnKey = ThreeWay.eventGenerators.returnKeyHandlerGenerator(() => console.info('[CTRL-ENTER handler]'), {
ctrlKey: true,
altKey: false,
shiftKey: false,
});
The following trigger when the relevant key is pressed in a keyup
:
backspaceKey
tabKey
returnKey
escapeKey
pageUpKey
pageDownKey
endKey
homeKey
leftArrowKey
upArrowKey
rightArrowKey
downArrowKey
insertKey
deleteKey
f1Key
f2Key
f3Key
f4Key
f5Key
f6Key
f7Key
f8Key
f9Key
f10Key
f11Key
f12Key
For finer grained control, the above may be prefixed with keydown_
or keyup_
(e.g.: keydown_backspaceKey
and keyup_tabKey
).
Currently, binding to database fields only occurs if the required field is already in the database. So fields bound on in the DOM do not drive the binding. However, sometimes records in the database have missing values that should be filled in.
As of v0.1.17, a compromise solution was included in the form of the injectDefaultValues
option, where missing fields are filled in with default values.
Pre-v0.1.2, there was the issue of a race condition when multiple fields with the same top level field (e.g.: particulars.name
and particulars.hobbies.4.hobbyId
) would be updated tens of milliseconds apart.
The observer callbacks would send entire top level sub-documents even if a single primitive value deep within was updated.
For a time, an attempt was made to address the problem by (i) queueing via promise chains of Meteor methods grouped by top-level fields plus a delay before next Meteor method being triggered, and (ii) field specific updaters (with individual throttling/debouncing) to avoid inadvertent skipping of updates from sub-fields (due to debounce/throttle effects on a method being used to update multiple sub-fields).
Pre-v0.1.14, the above race condition was still not fully solved. The "comprehensive solution" was to store snapshots of entire sub-documents with the expectation that stuff would get sent back and data sent back from the server matching existing values (that were not too old) could be "ignored".
As of v0.1.20, promise chains/bins were no longer used.
Pre-v0.1.9, dynamic rebinding was incomplete and carried out by polling the DOM. As of v0.1.9, Mutation Observers have been used to deal with things in an event-driven manner.
The mixing of dynamic data-binding and the possibility of multiple ThreeWay
instances poses some challenges with regards to the question of which ThreeWay
instance a new DOM element should be data bound with.
See the discussion in Using Dynamic Data Binding with Multiple ThreeWay
instances for more information.
Pre-v0.1.20, late creation of child templates posed a problem. They were outside of the normal order of the template life cycle:
- Parent created
- Child created
- Grandchild created
- Grandchild rendered
- Child rendered
- Parent rendered
... which enabled appropriate creation of
MutationObserver
's inonRendered
hooks, so the most junior nodes (in order of creation or "age") would get first bite at new nodes, which makes sense by default. (See the discussion in Using Dynamic Data Binding with MultipleThreeWay
instances for more information on how to create nodes in a child template but have them bound to a parent.)
The late creation problem was solved by introducing something of a "bind auction" for added and modified nodes. The bid value for each template instance involved being its level of depth in its ThreeWay
family tree. Ties are broken arbitrarily (actually, on a first created first served basis).
(Debouncing)[http://underscorejs.org/#debounce] Meteor methods for updates ensures that updates are sent after a "pause in editing", such as with a text field. Due to the fact that cursors send entire sub-documents when changes are made, and to reduce the number of Meteor calls made, There is a sense in which one might combine updates into single debounced calls (e.g.: {$set: {field1: value1, field2: value2}}
instead of {$set: {field1: value1}}
and {$set: {field2: value2}}
).
However, this is when check
and authentication causes a bit of a problem. The user should not be expected to write a general method that does schema and authentication checks. In principle, given a schema, appropriate methods can be generated, but ThreeWay
is not the place for automatic method generation (based on schemas).
- Consider how to perform binding to web components (possibly via a call-back interface)
- Consider enabling "hard-links" between pieces of data (possibly via a SSOT)
- Consider events executed at most n times
- Consider events "limited"/"filtered" by a truthy view model field
- Reconsider group debounced updates (given that auto-generation of a general updater in
convexset:collection-tools
is done)