Skip to content

Commit 6cc2659

Browse files
committed
Commit 84 (v0.9.84 - Beta)
Feature improvements: - Major update for custom tags - including support for two-way binding to multiple args and props See samples: http://www.jsviews.com/#samples/tag-controls/jqui/draggable-droppable@jsv-draggable and tests: http://www.jsviews.com/test/unit-tests-jsviews.html?testId=f46a512b and also jQuery UI tag control samples such as http://www.jsviews.com/#samples/tag-controls/jqui/resizable and http://www.jsviews.com/#samples/tag-controls/jqui/draggable-droppable. Also many code examples in https://www.jsviews.com/download/sample-tag-controls/jsviews-jqueryui-widgets.js A tag definition can include: tag.bindTo - which specifies two-way bound parameters (one or more args and/or props) tag.linkedElement - which specifies selectors for linkedElems for any of the two-way bound parameters tag.linkedCtxParam - which specifies/names tag contextual parameters for any of the two-way bound parameters mainElement - which specifies selector for an element used for setting id, width or height (otherwise, linkedElem is used) A tag onBind event can set: tag.linkedElem tag.linkedElems tag.mainElem - which is the element used for setting id, width or height (otherwise, linkedElem is used) tag.displayElem - which is the element used for setting class (otherwise, mainElem/linkedElem is used) The following tag methods are available: tag.bndArgs() - which returns array of 'external' bound parameters (after convert, if convert=... is specified) tag.update(...) - which observably updates any ('external') bound parameters (using convertBack, if specified) tag.setValue(...) - which sets tag itself to new ('internal') values of bound parameters tag.getValue(...) - which returns array of current ('internal') values of bound parameters New 'tag contextual parameters' feature - see samples and explanation: http://www.jsviews.com/#samples/tag-controls/jqui/resizable and http://www.jsviews.com/#samples/tag-controls/jqui/draggable-droppable@tag-ctxl-params - Other custom tag improvements: New 'tag boundProps' feature: Tag definition can include: tag.boundProps - which specifies any additional props that are bound, without needing ^myprop=... notation. See sample: http://www.jsviews.com/#samples/tag-controls/range New 'tag contentCtx' feature: Tag definition can include: tag.contentCtx - which specifies the data context within the tag block content. Set contentCtx = true for inheriting parentView context, or to a function, for specifying other context New 'tag argDefault' feature: Tag definition can include: tag.argDefault: false - which prevents the first arg defaulting to current data New 'tag onUpdate: false' feature: Tag definition can include: tag.onUpdate: false - which is equivalent to an onUpdate handler which does nothing but returns false, so tag does not rerender when bound args or props update - Major update for jQuery UI widget-based tag controls library: Extensive new jQueryUI widget controls support and accompanying samples/documentation: http://www.jsviews.com/#download/jqueryui-tagcontrols http://www.jsviews.com/#samples/tag-controls/jqui - New lateRender=true feature: set lateRender=true on any tag, to make the tag render only after completing the data-linking pass. Useful for using expressions such as #childTags('myTag') in data-link expressions. See tests: http://www.jsviews.com/test/unit-tests-jsviews.html?testId=6be4ff95 and sample: http://www.jsviews.com/#samples/tag-controls/jqui/selectable@late-render - {^{on ...}} with no content allows setting width, height, id and class Perf improvements for sorting, moving, refreshing: - Much improved perf for {^{for myArray}}...{{/for}} when making observable changes to underlying array. When using $.observable(myArray).refresh(...) or $.observable(myArray).move(...), the resulting changes to the rendered items no longer result in deleting and re-rendering the moved items. Instead the rendered HTML elements are simply moved to their new positions under the parent element. Bug fixes: - #361: Breaking change (radiobuttons) - both radiogroup and directly data-linked radio buttons now behave identically. Neither of them coerce value to string - Bug with setting tag.depends=... (Breaking change: Note that setting fn.depends="myArray" updates for array changes as well as prop change. See: http://www.jsviews.com/test/unit-tests-jsviews.html?testId=303f5bfd - #354: Minor 'onError' bug - #360: Datepicker bug fix for correct behavior when data-linking to a value of type Date - Several minor bug fixes Unit tests: - Several additional unit tests Other minor breaking changes: - The undocumented lazyLink feature has been removed - The undocumented init: false feature for custom tags has been removed.
1 parent 2581e59 commit 6cc2659

32 files changed

+13976
-8347
lines changed

demos/step-by-step/04_form-elements_if-binding.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ <h3>Ticket order form</h3>
7979
orderDetails = {
8080
name: "",
8181
selectedMovie: "none",
82-
selectedCurrency: 1,
82+
selectedCurrency: "1",
8383
request: ""
8484
},
8585

jquery.observable.js

+69-77
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
/*! JsObservable v0.9.83 (Beta): http://jsviews.com/#jsobservable */
1+
/*! JsObservable v0.9.84 (Beta): http://jsviews.com/#jsobservable */
22
/*
33
* Subcomponent of JsViews
44
* Data change events for data-linking
55
*
6-
* Copyright 2016, Boris Moore
6+
* Copyright 2017, Boris Moore
77
* Released under the MIT License.
88
*/
99

@@ -44,7 +44,7 @@ if (!$ || !$.fn) {
4444
throw "JsObservable requires jQuery"; // We require jQuery
4545
}
4646

47-
var versionNumber = "v0.9.83",
47+
var versionNumber = "v0.9.84",
4848
$observe, $observable,
4949

5050
$views = $.views =
@@ -120,7 +120,8 @@ if (!$.observe) {
120120
: data;
121121
},
122122

123-
resolvePathObjects = function(paths, root, callback) {
123+
dependsPaths = function(paths, root, callback) {
124+
// Process depends = ... paths to resolve objects, and recursively process functions.
124125
paths = paths
125126
? $isArray(paths)
126127
? paths
@@ -136,7 +137,7 @@ if (!$.observe) {
136137
for (i = 0; i < l; i++) {
137138
path = paths[i];
138139
if ($isFunction(path)) {
139-
out = out.concat(resolvePathObjects(path.call(root, root, callback), root));
140+
out = out.concat(dependsPaths(path.call(root, root, callback), root, callback));
140141
continue;
141142
} else if ("" + path !== path) {
142143
root = nextObj = path;
@@ -150,6 +151,10 @@ if (!$.observe) {
150151
}
151152
out.push(path);
152153
}
154+
if (out.length) {
155+
out.unshift({_ar: 1}); // Switch on allowArray, for depends paths.
156+
out.push({_ar: -1});
157+
}
153158
return out;
154159
},
155160

@@ -161,7 +166,7 @@ if (!$.observe) {
161166
delete cbBindingsStore[cbBindingsId]; // This binding collection is empty, so remove from store
162167
},
163168

164-
onObservableChange = function(ev, eventArgs) {
169+
onDataChange = function(ev, eventArgs) {
165170
function isOb(val) {
166171
return typeof val === OBJECT && (paths[0] || allowArray && $isArray(val));
167172
}
@@ -174,7 +179,7 @@ if (!$.observe) {
174179
ctx = ev.data,
175180
observeAll = ctx.observeAll,
176181
cb = ctx.cb,
177-
allowArray = !cb.noArray,
182+
allowArray = ctx.arOk,
178183
paths = ctx.paths,
179184
ns = ctx.ns;
180185

@@ -188,17 +193,17 @@ if (!$.observe) {
188193
parentObs = [ev.target].concat(observeAll.parents());
189194

190195
if (isOb(oldValue)) {
191-
observe_apply(allowArray, ns, [oldValue], paths, cb, true, filter, [parentObs], allPath); // unobserve
196+
observe_apply(undefined, ns, [oldValue], paths, cb, true, filter, [parentObs], allPath); // unobserve
192197
}
193198
if (isOb(value)) {
194-
observe_apply(allowArray, ns, [value], paths, cb, undefined, filter, [parentObs], allPath);
199+
observe_apply(undefined, ns, [value], paths, cb, undefined, filter, [parentObs], allPath);
195200
}
196201
} else {
197202
if (isOb(oldValue)) { // oldValue is an object, so unobserve
198-
observe_apply(allowArray, ns, [oldValue], paths, cb, true); // unobserve
203+
observe_apply(undefined, ns, [oldValue], paths, cb, true); // unobserve
199204
}
200205
if (isOb(value)) { // value is an object, so observe
201-
observe_apply(allowArray, ns, [value], paths, cb);
206+
observe_apply(undefined, ns, [value], paths, cb);
202207
}
203208
}
204209
ctx.cb(ev, eventArgs);
@@ -262,6 +267,7 @@ if (!$.observe) {
262267
updatedTgt = undefined;
263268
cb.apply(this, arguments); // Observe this object (invoke the callback)
264269
}
270+
wrappedCb._wrp = 1;
265271

266272
var l, isObject, newAllPath, nextParentObs, updatedTgt, obId,
267273
notRemoving = !objMap || objMap.un || !unobserve; // true unless it is an observeAll call (not unobserveAll) and we are removing a listener (not adding one)
@@ -329,7 +335,7 @@ if (!$.observe) {
329335

330336
$unobserve = function() {
331337
[].push.call(arguments, true); // Add true as additional final argument
332-
return $observe.apply(this, arguments);
338+
return $observe.apply(undefined, arguments);
333339
};
334340

335341
$observe = function() {
@@ -355,21 +361,22 @@ if (!$.observe) {
355361
data = events[el] && events[el].data;
356362
if (data && (off && data.ns !== initialNs
357363
// When observing, don't unbind dups unless they have the same namespace
358-
|| !off && data.ns === initialNs && data.cb && data.cb._cId === callback._cId))
359-
// When observing and doing array binding, don't bind dups if they have the same namespace (Dups can happen e.g. with {^{for people ~foo=people}})
364+
|| !off && data.ns === initialNs && data.cb && data.cb._cId === callback._cId && (!callback._wrp || data.cb._wrp)))
365+
// When observing and doing array binding, don't bind dups if they have the same namespace (Dups can happen e.g. with {^{for people ^~foo=people}})
360366
{
361367
return;
362368
}
363369
}
364370
}
365371
if (unobserve || off) {
366-
$(boundObOrArr).off(namespace, onObservableChange);
372+
$(boundObOrArr).off(namespace, onDataChange);
367373
} else {
368374
evData = isArrayBinding ? {}
369375
: {
370376
fullPath: path,
371377
paths: pathStr ? [pathStr] : [],
372-
prop: prop
378+
prop: prop,
379+
arOk: allowArray
373380
};
374381
evData.ns = initialNs;
375382
evData.cb = callback;
@@ -393,7 +400,7 @@ if (!$.observe) {
393400
filter: filter
394401
};
395402
}
396-
$(boundObOrArr).on(namespace, null, evData, onObservableChange);
403+
$(boundObOrArr).on(namespace, null, evData, onDataChange);
397404
if (cbBindings) {
398405
// Add object to cbBindings
399406
cbBindings[$data(object).obId || $data(object, "obId", observeObjKey++)] = object;
@@ -441,7 +448,8 @@ if (!$.observe) {
441448

442449
function bindArray(arr, unbind, isArray, relPath) {
443450
if (allowArray) {
444-
// This is a call to observe that does not come from observeAndBind (tag binding), so we allow arrayChange binding
451+
// allowArray is 1 if this is a call to observe that does not come from observeAndBind (tag binding), or is from a `depends` path,
452+
// so we allow arrayChange binding. Otherwise allowArray is zero.
445453
var prevObj = object,
446454
prevAllPath = allPath;
447455

@@ -516,12 +524,12 @@ if (!$.observe) {
516524

517525
while (initNsArrLen--) {
518526
initialNs = initNsArr[initNsArrLen];
519-
if (root && (path = paths[0], !path || path + "" !== path)) {
527+
if (root && !paths[0]) {
520528
if ($isArray(root)) {
521-
bindArray(root, unobserve, true);
522-
} else if (unobserve) {
523-
// remove onObservableChange handlers that wrap that callback
524-
observeOnOff(ns, "");
529+
bindArray(root, unobserve, true); // observe(array, handler)
530+
}
531+
if (unobserve) {
532+
observeOnOff(ns, ""); // unobserve(objectOrArray[, handler])
525533
}
526534
}
527535
if (unobserve && !l && !root) { // unobserve() - unobserves all
@@ -532,7 +540,6 @@ if (!$.observe) {
532540
if ($isArray(object)) {
533541
bindArray(object, unobserve, unobserve);
534542
} else {
535-
// remove onObservableChange handlers that wrap that callback
536543
observeOnOff(ns, "");
537544
}
538545
}
@@ -544,9 +551,13 @@ if (!$.observe) {
544551
if (path === "") {
545552
continue;
546553
}
554+
if (path && path._ar) {
555+
allowArray += path._ar; // Switch on allowArray for depends paths, and off, afterwards.
556+
continue;
557+
}
547558
if (path && path._cp) { // Contextual parameter
548-
contextCb = $sub._gccb(path[0]); // getContextCb: Get context callback for the contextual view (where contextual param evaluated/assigned)
549-
origRoot = root = path[0].data; // Contextual data
559+
contextCb = $sub._gccb(path[0]); // getContextCb: Get context callback for the contextual view (where contextual param evaluated/assigned)
560+
origRoot = root = path[0].data; // Contextual data
550561
path = path[1];
551562
}
552563
object = root;
@@ -585,7 +596,6 @@ if (!$.observe) {
585596
// This is a compiled function for binding to an object returned by a helper/data function.
586597
// Set current object on exprOb.ob, and get innerCb for updating the object
587598
innerCb = unobserve ? path.cb : getInnerCb(path);
588-
innerCb.noArray = !allowArray;
589599
// innerCb._ctx = callback._ctx; Could pass context (e.g. linkCtx) for use in a depends = function() {} call, so depends is different for different linkCtx's
590600
innerCb._cId = callback._cId;
591601
// Set the same cbBindingsStore key as for callback, so when callback is disposed, disposal of innerCb happens too.
@@ -600,8 +610,6 @@ if (!$.observe) {
600610
}
601611
path = origRoot;
602612
object = undefined;
603-
} else {
604-
object = path; // For top-level calls, objects in the paths array become the origRoot for subsequent paths.
605613
}
606614
}
607615
parts = [root = path];
@@ -664,23 +672,21 @@ if (!$.observe) {
664672
allPath += "." + prop;
665673
}
666674
prop = object[prop];
675+
if (!parts[0]) {
676+
bindArray(prop, unobserve); // [un]observe(object, "arrayProperty") observes array changes on property of type array
677+
}
667678
}
668679
if ($isFunction(prop)) {
669680
if (dep = prop.depends) {
670-
// This is a computed observable. We will observe any declared dependencies
671-
innerObserve([object], resolvePathObjects(dep, object, callback), callback, contextCb, unobserve);
681+
// This is a computed observable. We will observe any declared dependencies.
682+
// Pass {_ar: ...} objects to switch on allowArray, for depends paths, then return to contextual allowArray value
683+
innerObserve([object], dependsPaths(dep, object, callback), callback, contextCb, unobserve);
672684
}
673685
break;
674686
}
675687
object = prop;
676-
if (unobserve && object === root && (i>l-2 || paths[i+1] + "" !== paths[i+1])) {
677-
// unobserve all handlers of object, if not followed by string path.
678-
// e.g.$.unobserve(object1, object2, "path", object3) will unobserve all from object1 and object3, and just "path" listener from object2
679-
observeOnOff(ns, "");
680-
}
681688
}
682689
}
683-
bindArray(object, unobserve);
684690
}
685691
}
686692
if (cbId) {
@@ -692,9 +698,9 @@ if (!$.observe) {
692698
}
693699

694700
var initialNs,
695-
allowArray = this != false, // If this === false, this is a call from observeAndBind - doing binding of datalink expressions. We don't bind
701+
allowArray = this == 1 ? 0 : 1, // If this == 1, this is a call from observeAndBind - doing binding of datalink expressions. We don't bind
696702
// arrayChange events in this scenario. Instead, {^{for}} and similar do specific arrayChange binding to the tagCtx.args[0] value, in onAfterLink.
697-
// Note deliberately using this != false, rather than this !== false because of IE<10 bug- see jsviews/issues/237
703+
// Note deliberately using this == 1, rather than this === 1 because of IE<10 bug- see jsviews/issues/237
698704
paths = slice.call(arguments),
699705
origRoot = paths[0];
700706

@@ -719,23 +725,6 @@ if (!$.observe) {
719725

720726
//========================== Initialize ==========================
721727

722-
$sub.getDeps = function() {
723-
var args = arguments;
724-
return function() {
725-
var arg, dep,
726-
deps = [],
727-
l = args.length;
728-
while (l--) {
729-
arg = args[l--];
730-
dep = args[l];
731-
if (dep) {
732-
deps = deps.concat($isFunction(dep) ? dep(arg, arg) : dep);
733-
}
734-
}
735-
return deps;
736-
};
737-
};
738-
739728
$.observable = $observable;
740729
$observable._fltr = function(allPath, object, parentObs, filter) {
741730
if (filter && $isFunction(filter)
@@ -824,25 +813,25 @@ if (!$.observe) {
824813
}
825814
}
826815

827-
if (property !== value || nonStrict && property != value) { // Optional non-strict equality, since serializeArray, and form-based editors can map numbers to strings, etc.
816+
if ((property !== value || nonStrict && property != value)
817+
// Optional non-strict equality, since serializeArray, and form-based editors can map numbers to strings, etc.
828818
// Date objects don't support != comparison. Treat as special case.
829-
if (!(property instanceof Date) || property > value || property < value) {
830-
if (setter) {
831-
setter.call(leaf, value); //set
832-
value = getter.call(leaf); //get updated value
833-
} else if (removeProp = value === remove) {
834-
if (property !== undefined) {
835-
delete leaf[path];
836-
value = undefined;
837-
} else {
838-
path = undefined; // If value was already undefined, don't trigger handler for removeProp
839-
}
840-
} else if (path) {
841-
leaf[path] = value;
842-
}
843-
if (path) {
844-
this._trigger(leaf, {change: "set", path: path, value: value, oldValue: property, remove: removeProp});
819+
&& (!(property instanceof Date && value instanceof Date) || property > value || property < value)) {
820+
if (setter) {
821+
setter.call(leaf, value); //set
822+
value = getter.call(leaf); //get updated value
823+
} else if (removeProp = value === remove) {
824+
if (property !== undefined) {
825+
delete leaf[path];
826+
value = undefined;
827+
} else {
828+
path = undefined; // If value was already undefined, don't trigger handler for removeProp
845829
}
830+
} else if (path) {
831+
leaf[path] = value;
832+
}
833+
if (path) {
834+
this._trigger(leaf, {change: "set", path: path, value: value, oldValue: property, remove: removeProp});
846835
}
847836
}
848837
},
@@ -943,11 +932,13 @@ if (!$.observe) {
943932
newIndex = _data.length;
944933
}
945934
splice.apply(_data, [newIndex, 0].concat(items)); //re-insert
946-
this._trigger({change: "move", oldIndex: oldIndex, index: newIndex, items: items}, oldLength);
935+
if (newIndex !== oldIndex) {
936+
this._trigger({change: "move", oldIndex: oldIndex, index: newIndex, items: items}, oldLength);
937+
}
947938
}
948939
},
949940

950-
refresh: function(newItems, sort) {
941+
refresh: function(newItems) {
951942
function insertAdded() {
952943
if (k) {
953944
self.insert(j-k, addedItems); // Not found in original array - so insert
@@ -1005,7 +996,7 @@ if (!$.observe) {
1005996
$_data = $([_data]);
1006997
if (self._srt) {
1007998
eventArgs.refresh = true; // We are sorting during refresh
1008-
} else if (length !== oldLength) { // We have finished sort operations during refresh
999+
} else if (length !== oldLength) { // We have finished sort operations during refresh
10091000
$_data.triggerHandler(propertyChangeStr, {change: "set", path: "length", value: length, oldValue: oldLength});
10101001
}
10111002
$_data.triggerHandler(arrayChangeStr + (self._ns ? "." + /^\S+/.exec(self._ns)[0] : ""), eventArgs); // If white-space separated namespaces, use first one only
@@ -1061,7 +1052,7 @@ if (!$.observe) {
10611052
}
10621053
}, map.srcFlt);
10631054
}
1064-
if (mapDef.obsTgt ) {
1055+
if (mapDef.obsTgt) {
10651056
$observable(map.tgt).observeAll(map.obt = function(ev, eventArgs) {
10661057
if (!changing) {
10671058
changing = true;
@@ -1123,6 +1114,7 @@ if (!$.observe) {
11231114
}
11241115
: undefined; // In IE8 cannot do delete global._jsv
11251116
};
1117+
$sub._dp = dependsPaths;
11261118
}
11271119

11281120
return $;

0 commit comments

Comments
 (0)