/*global test, equal, module, ok, _jsv, viewsAndBindings*/ (function(jsv, $, undefined) { "use strict"; /* Setup */ var inputOrKeydownContentEditable, useInput = "oninput" in document, inputOrKeydown = useInput ? "input" : "keydown", inputOrKeydownContentEditable = "input"; function keydown(elem) { if (useInput) { elem.trigger("input"); } else { elem.keydown(); } } // =============== Model =============== function fullName(reverse, upper) { var name = reverse ? (this.lastName + " " + this.firstName()) : this.firstName() + " " + this.lastName; return upper ? name.toUpperCase() : name; } fullName.depends = function(object) { // object is also the this pointer, so we could write function(this) below return [ "firstName", function(object) { return "lastName"; } ]; }; fullName.set = function(val) { val = val.split(" "); jsv.observable(this).setProperty({ lastName: val.pop(), firstName: val.join(" ") }); }; var Person = function(first, last, home) { this._firstName = first; this.lastName = last; this.home = home; }, personProto = { firstName: function() { return settings.title + " " + this._firstName; }, fullName: fullName }; personProto.firstName.set = function(val) { this._firstName = val; }; Person.prototype = personProto; function updown(val, lower) { lower = this.tagCtx.props.lower !== undefined ? this.tagCtx.props.lower : lower; val = person1.firstName() + (val || ""); return (lower === true ? val.toLowerCase() : val.toUpperCase()) + settings.width + this.tagCtx.props.added; } function sort(array) { var ret = ""; if (this.tagCtx.props.reverse) { // Render in reverse order if (arguments.length > 1) { for (i = arguments.length; i; i--) { ret += sort.call(this, arguments[i - 1]); } } else for (var i = array.length; i; i--) { ret += this.tagCtx.render(array[i - 1]); } } else { // Render in original order ret += this.tagCtx.render(array); } return ret; } // =============== DATA =============== var address1 = {street: "StreetOne", ZIP: "111"}, address2 = {street: "StreetTwo", ZIP: "222"}, home1 = {address: address1}, home2 = {address: address2}, homeOfOwner = {address: {street: "OwnerStreet"}}, person1 = new Person("Jo", "One", home1), person2 = new Person("Xavier", "Two", home2), settings = { owner: new Person("Mr", "Owner", homeOfOwner), width: 30, reverse: true, upper: updown, title: "Mr" }, model = { person1: person1, person2: person2, things: [] }, people = [person1, person2]; personProto.firstName.depends = [settings, "title"]; updown.depends = function() { return [person1, "firstName", "~settings.width"]; }; // =============== RESOURCES =============== var cancelChange = false, noRenderOnUpdate = true, renders = false, cancelUpdate = false, eventData = ""; jsv.views .converters({ upper_: updown, cvtBack: function(val) { return val; } }) .helpers({ settings: settings, upper: updown }) .tags({ tmplTag: { template: "Name: {{:firstName()}}. Width: {{:~settings.width}}", depends: ["firstName", "~settings.width"] }, fnTag: { render: function() { return "Name: " + this.tagCtx.view.data.firstName() + ". Width: " + this.ctxPrm("settings").width; }, depends: ["firstName", "~settings.width"] }, fnTagElNoInit: function() { return "Name: " + this.tagCtx.view.data.firstName() + ". Width: " + this.ctxPrm("settings").width + ""; }, tmplTagEl: { template: "Name: {{:firstName()}}. Width: {{:~settings.width}}", depends: ["firstName", "~settings.width"] }, fnTagEl: { render: function() { return "Name: " + this.tagCtx.view.data.firstName() + ". Width: " + this.ctxPrm("settings").width + ""; }, depends: ["firstName", "~settings.width"] }, fnTagElCnt: { render: function() { return "
  • Name: " + this.tagCtx.view.data.firstName() + ". Width: " + this.ctxPrm("settings").width + "
  • "; }, depends: ["firstName", "~settings.width"] }, fnTagElCntNoInit: function() { return "
  • Name: " + this.tagCtx.view.data.firstName() + ". Width: " + this.ctxPrm("settings").width + "
  • "; }, fnTagWithProps: { render: function(data, val) { return "Name: " + this.tagCtx.view.data.firstName() + ". Width: " + this.ctxPrm("settings").width + ". Value: " + val + ". Prop theTitle: " + this.tagCtx.props.theTitle + ". Prop ~street: " + this.ctxPrm("street"); }, depends: ["firstName", "~settings.width"] }, tmplTagWithProps: { render: function(val) { this.ctxPrm("theTitle", this.tagCtx.props.theTitle); }, template: "Name: {{:firstName()}}. Width: {{:~settings.width}}. Value: {{:~settings.reverse}}. " + "Prop theTitle: {{dbg:~theTitle}}. Prop ~street: {{:~street}}", depends: ["firstName", "~settings.width"] }, twoWayTag: { init: function(tagCtx, linkCtx, ctx) { eventData += "init "; if (this.inline && !tagCtx.content) { this.template = ""; } }, render: function(val) { eventData += "render "; return renders ? (val + ' rendered') : undefined; }, onAfterLink: function(tagCtx, linkCtx, ctx, ev, eventArgs) { eventData += "onAfterLink "; this.value = tagCtx.args[0]; }, onBeforeUpdateVal: function(ev, eventArgs) { eventData += "onBeforeUpdateVal "; return !cancelUpdate; }, onUpdate: function(ev, eventArgs, newTagCtxs) { eventData += "onUpdate "; return !noRenderOnUpdate; }, onBind: function(tagCtx, linkCtx, ctx, ev, eventArgs) { eventData += "onBind "; this.linkedElem = this.linkedElem || (this.inline ? this.contents("input,div") : $(linkCtx.elem)); }, onUnbind: function(tagCtx, linkCtx, ctx, ev, eventArgs) { this.linkedElem = undefined; // remove, so newly rendered linkedElem gets created in onBind eventData += "onUnbind "; }, onBeforeChange: function(ev, eventArgs) { eventData += "onBeforeChange "; return !cancelChange; }, setValue: function() { eventData += "setValue "; }, onAfterChange: function(ev, eventArgs) { eventData += "onAfterChange "; }, onDispose: function() { eventData += "onDispose "; } } }); var topData = {people: people}; jsv.views.tags({ myWrap: { init: function(tc) { var test = tc.props.val; } }, myWrap2: {}, mySimpleWrap: function(val) { return this.tagCtx.render(val); }, myFlow: { flow: true }, myFlow2: { flow: true, template: "flow2" }, myWrapElCnt: { attr: "html", render: function(val) { return "" + this.tagCtx.render(val) + ""; }, onAfterLink: function() { //debugger; } }, myWrap2ElCnt: { attr: "html", render: function(val) { return "" + this.tagCtx.render(val) + ""; }, onAfterLink: function() { //debugger; } }, myFlowElCnt: { attr: "html", flow: true, render: function(val) { return "" + this.tagCtx.render(val) + ""; } } }); jsv.templates({ tmplHierarchy: '{{for people ~val=1}}' + '{{if true ~index=#index}}' + '{{myWrap val=1}}' + '' + 'a' + '{{myWrap2}}' + '{{if true}}xx{{/if}}' + '{{/myWrap2}}' + '{{if true}}' + '{{myWrap2}}' + '{{if true}}xx{{/if}}' + '{{/myWrap2}}' + '{{/if}}' + '{{if true}}' + 'yy' + '{{/if}}' + '{{myFlow}}' + 'zz' + '{{if true}}' + '{{myFlow2/}}' + '{{/if}}' + '{{/myFlow}}' + '' + '{{/myWrap}}' + '{{myWrap val=2/}}' + '{{/if}}' + 'www' + '{{/for}}', tmplHierarchyElCnt: '{{for people ~val=1}}' + '{{if true ~index=#index}}' + '{{myWrapElCnt val=1}}' + '' + '{{myWrap2ElCnt}}' + 'xx' + '{{/myWrap2ElCnt}}' + '{{if true}}' + '{{myWrap2ElCnt}}' + 'xx' + '{{/myWrap2ElCnt}}' + '{{/if}}' + '{{if true}}' + '' + '{{/if}}' + '{{myFlowElCnt}}' + 'xx' + '{{if true}}' + '{{myFlow2/}}' + '{{/if}}' + '{{/myFlowElCnt}}' + '' + '{{/myWrapElCnt}}' + '{{myWrapElCnt val=2/}}' + '33' + '{{/if}}' + '{{/for}}
    yy
    ', boundTmplHierarchy: '{{for people ~val=1}}' + '{{if true ~index=#index}}' + 'aa{^{myWrap val=1}}inside' + '' + 'a' + '{^{myWrap2}}' + '{{if true}}xx{{/if}}' + '{^{myFlow val=3}}xyz{{/myFlow}}' + '{{/myWrap2}}' + '{^{if true}}' + '{^{myWrap2}}' + '{{if true}}xx{{/if}}' + '{^{myFlow val=3}}xyz{{/myFlow}}' + '{{/myWrap2}}' + '{{/if}}' + '{{if true}}' + 'yy' + '{{/if}}' + '{^{myFlow val=4}}' + 'zz' + '{{/myFlow}}' + '' + '{{/myWrap}}' + 'bb{^{myWrap val=2/}}' + 'cc{{myWrap val=3 "this is unbound"/}}' + '{{/if}}' + '
    ' + '{^{mySimpleWrap val=5}}' + '{^{mySimpleWrap val=6}}' + '{^{myWrap2 val=7/}}' + '{{/mySimpleWrap}}' + '{{/mySimpleWrap}}' + '' + '{^{myWrap2 val=8/}}' + '
    ' + 'www' + '{{/for}}', boundTmplHierarchyElCnt: '{{for people ~val=1}}' + '{{if true ~index=#index}}' + '{^{myWrapElCnt val=1}}' + '' + '{^{myWrap2ElCnt val=11}}' + 'xx' + '{^{myFlow val=3}}xyz{{/myFlow}}{^{mySimpleWrap val=5/}}' + '{{/myWrap2ElCnt}}' + '{^{if true}}' + '{^{myWrap2ElCnt val=22}}' + 'xx' + '{^{myFlow val=3}}xyz{{/myFlow}}' + '{{/myWrap2ElCnt}}' + '{{/if}}' + '{{if true}}' + '' + '{{/if}}' + '{^{myFlowElCnt val=4}}' + 'xx' + '{{/myFlowElCnt}}' + '' + '{{/myWrapElCnt}}' + '{^{myWrapElCnt val=2/}}' + '{{myWrapElCnt "this is unbound"/}}' + '{{/if}}' + '' + '{^{mySimpleWrap val=5}}' + '{^{mySimpleWrap val=6}}' + '{^{myWrap2 val=7/}}' + '{{/mySimpleWrap}}' + '{{/mySimpleWrap}}' + '' + '{^{myWrap2ElCnt val=8/}}' + '' + '{{/for}}
    yy
    ', boundTmplHierarchyElCntWithDataLink: '
    ', wrapCnt: '', innerWrap: 'xx' }); // =============== INIT APP =============== var viewContent, before, after, lastEvData, lastEventArgs, listeners, handlersCount, elems, res = "", calls = 0; function reset() { res = ""; calls = 0; } // End Setup QUnit.module("Template structure"); QUnit.test("Template validation", function(assert) { jsv.views.settings.advanced({_jsv: true}); // For using window._jsv // =============================== Arrange =============================== jsv.templates('
    {{if 1}}X{{:a}}{{/if}}
    ') .link("#result", {a: "yes"}); // ............................... Assert ................................. assert.ok($("#result div").text() === "Xyes" && $("#result div").prop("title") === "yes", "Tag markup works within element content, or within element markup, for non element-only elements (
    )"); // =============================== Arrange =============================== jsv.templates('') .link("#result", {a: "yes"}); // ............................... Assert ................................. assert.ok($("#result ul").text() === "Xyes" && $("#result ul").prop("title") === "yes", "Tag markup works within element content, or within element markup, for element-only elements (
    '"), 0, "Validation - missing closing tag"); res = ""; // =============================== Arrange =============================== try { jsv.templates('
    {{:Thing}}
    ') // throws syntax error .link("#result", {thing: "Orig"}); } catch (e) { res = e.message; } // ............................... Assert ................................. assert.equal(res.indexOf("Syntax error\nMismatch: ''"), 0, "Validation - missing opening tag"); res = ""; // =============================== Arrange =============================== try { jsv.templates('{{:Thing}}') // throws syntax error .link("#result", {thing: "Orig"}); } catch (e) { res = e.message; } // ............................... Assert ................................. assert.equal(res.indexOf("Syntax error\nMismatch: ''"), 0, "Validation - extra closing tag"); res = ""; // =============================== Arrange =============================== try { jsv.templates('{{:Thing}}') .link("#result", {thing: "Orig"}); } catch (e) { res = e.message; } // ............................... Assert ................................. assert.equal(res.indexOf("Syntax error\nMismatch: ''"), 0, "Validation - extra closing tag"); res = ""; // =============================== Arrange =============================== try { jsv.templates('
    {{:Thing}}
    ') // throws syntax error .link("#result", {thing: "Orig"}); } catch (e) { res = e.message; } // ............................... Assert ................................. assert.equal(res.indexOf("Syntax error\n''"), 0, "Validation - self-closing tag is not a void element"); res = ""; // =============================== Arrange =============================== try { jsv.templates('
    {{:Thing}}') // throws syntax error .link("#result", {thing: "Orig"}); } catch (e) { res = e.message; } // ............................... Assert ................................. assert.equal(res.indexOf("Syntax error\nMismatched ''"), 0, "Validation - missing closing tag"); res = ""; // =============================== Arrange =============================== try { jsv.templates('
    {{:Thing}}') // throws syntax error .link("#result", {thing: "Orig"}); } catch (e) { res = e.message; } // ............................... Assert ................................. assert.equal(res.indexOf("Syntax error\nMismatch: ''"), 0, "Validation - missing opening tag"); res = ""; // =============================== Arrange =============================== try { jsv.templates('
    ') // throws syntax error .link("#result", {thing: "Orig"}); } catch (e) { res = e.message; } // ............................... Assert ................................. assert.equal(res.indexOf("Syntax error\n''"), 0, "Validation - self-closing tag is not a void element"); res = ""; // =============================== Arrange =============================== try { jsv.templates('
    {{:Thing}}
    ') // throws syntax error .link("#result", {thing: "Orig"}); } catch (e) { res = e.message; } // ............................... Assert ................................. assert.equal(res.indexOf("Syntax error\n''"), 0, "Validation - closing tag for a void element"); res = ""; // =============================== Arrange =============================== jsv.templates('prop:

    ' + '{{if true}}{{/if}}
    ') .link("#result", person1); // ................................ Act .................................. res = $("#result input")[0].value + $("#result input")[1].value; jsv.observable(person1).setProperty("lastName", "Two"); res += $("#result input")[0].value + $("#result input")[1].value; // ............................... Assert ................................. assert.equal(res, "OneOneTwoTwo", "Validation - void elements can have self-close slashes, or not..."); res = ""; // =============================== Arrange =============================== var markupData = {markup: ""}; jsv.templates('{^{:markup}}') .link("#result", markupData); // ................................ Act .................................. res = "" + ($("#result").html().indexOf("")>60); jsv.observable(markupData).setProperty("markup", "

    "); res += "|" + ($("#result").html().indexOf("

    ")>60); // ............................... Assert ................................. assert.equal(res, "true|true", "Validation - void elements inserted from data can have self-close slashes, or not..."); res = ""; // =============================== Arrange =============================== jsv.templates('') .link("#result", {lastName: "Blow"}); // ............................... Assert ................................. assert.equal($("#result #last").val(), "Blow", "{{if}} is supported within markup even when data-linking"); res = ""; // =============================== Arrange =============================== jsv.templates(' {{else}}/>{{/if}}') .link("#result", {lastName: "Blow"}); // ............................... Assert ................................. assert.equal($("#result #last").val(), "Blow", "{{if}} wrapping closing delimiter of markup is supported even when data-linking"); res = ""; // =============================== Arrange =============================== try { jsv.templates('') // throws syntax error .link("#result", {lastName: "Blow"}); } catch (e) { res = e.message; } // ............................... Assert ................................. assert.equal(res, "Syntax error\n{^{ within elem markup ( {{else}}/>{{/if}}') // throws syntax error .link("#result", {lastName: "Blow"}); } catch (e) { res = e.message; } // ............................... Assert ................................. assert.equal(res, "Syntax error\n{^{ within elem markup ( markup"); // =============================== Arrange =============================== try { jsv.templates('a') .link("#result", {thing: "Orig"}); } catch (e) { res = e.message; } // ............................... Assert ................................. assert.equal(res, "Syntax error\n{^{ within elem markup ( markup"); res = ""; // =============================== Arrange =============================== try { jsv.templates('
    a
    ') // throws syntax error .link("#result", {thing: "Orig"}); } catch (e) { res = e.message; } // ............................... Assert ................................. assert.equal(res, "Syntax error\n{^{ within elem markup (
    a
    ') // does not throw syntax error .link("#result", {thing: "Orig"}); } catch (e) { res = e.message; } // ............................... Assert ................................. assert.equal(res, "", "Validation - {{:...}} within element markup is OK"); res = ""; // =============================== Arrange =============================== try { jsv.templates('
    a
    ') // does not throw syntax error .link("#result", {thing: "Orig"}); } catch (e) { res = e.message; } // ............................... Assert ................................. assert.equal(res, "", "Validation - {{attr:...}} within element markup is OK"); res = ""; // =============================== Arrange =============================== try { jsv.templates('') // throws syntax error .link("#result", {thing: "Orig"}); } catch (e) { res = e.message; } // ............................... Assert ................................. assert.equal(res, "Syntax error\n{:foo:}- Remove target: value", "Validation - value{:foo:}"); res = ""; // ................................ Reset ................................ person1.lastName = "One"; // The syntax error exceptions thrown above meant some views were not fully linked. // We will 'force remove' them from the viewStore, the top view children, and the bindingStore. var v, viewstore = jsv.view().views; for (v in viewstore) { delete viewstore[v]; } viewstore = _jsv.views; for (v in viewstore) { if (v !== "0") { delete viewstore[v]; } } viewstore = _jsv.bindings; for (v in viewstore) { delete viewstore[v]; } jsv.views.settings.advanced({_jsv: false}); }); QUnit.module("data-link scenarios"); QUnit.test("jQuery cleanData integration", function(assert) { // ................................ Reset ................................ person1.lastName = "One"; // reset Prop // =============================== Arrange =============================== $("#result").html(''); jsv.link("lastName", "#inner", person1); $("#inner").on("click", function() {}); // ................................ Act .................................. res = $("#inner").html(); jsv.observable(person1).setProperty("lastName", "last2"); res += "|" + $("#inner").html(); $("#inner").off("click"); jsv.observable(person1).setProperty("lastName", "last3"); res += "|" + $("#inner").html(); // ............................... Assert ................................. assert.equal(res, 'One|last2|last3', 'Removing jQuery handlers does not remove views. (Issue https://github.com/BorisMoore/jsviews/issues/249)'); // ................................ Reset ................................ person1.lastName = "One"; // reset Prop $("#result").empty(); // =============================== Arrange =============================== $("#result").html(''); jsv.link("lastName", "#inner", person1); // ................................ Act .................................. res = $("#inner").html(); jsv.observable(person1).setProperty("lastName", "last2"); res += "|" + $("#inner").html(); $("#inner").data('foo', 'bar').removeData('foo'); jsv.observable(person1).setProperty("lastName", "last3"); res += "|" + $("#inner").html(); // ............................... Assert ................................. assert.equal(res, 'One|last2|last3', 'Adding and removing jQuery data does not remove views. (Issue https://github.com/BorisMoore/jsviews/issues/249)'); // ................................ Reset ................................ person1.lastName = "One"; // reset Prop $("#result").empty(); // =============================== Arrange =============================== $("#result").html(''); jsv.link("lastName", "#inner", person1); // ................................ Act .................................. res = $("#inner").html(); jsv.observable(person1).setProperty("lastName", "last2"); res += "|" + $("#inner").html(); jsv.observable(person1).setProperty("lastName", "last3"); res += "|" + $("#inner").html(); // ............................... Assert ................................. assert.equal(res, 'One|last2|last3', 'Calling dequeue does not remove views. (Issue https://github.com/BorisMoore/jsviews/issues/249)'); // =============================== Arrange =============================== jsv.views.settings.trigger(false); var tmpl1 = jsv.templates( '' + '
    • click me
    ' + '' + '
      {^{for things}}
    • inserted
    • {{/for}}
    ' ); var tmpl2 = jsv.templates( '
  • ' + '
  • click me
  • ' + '
  • ' + '{^{for things}}
  • inserted
  • {{/for}}' ), data = {name:"Jo", things:[]}, clicked = 0, helpers = { clicked: function(val) { clicked += 1; } }; $("#result").html( '
    ' + '' + '
    • click me
    ' + '' + '
      ' +'
      ' + '
        ' + '
        ' ); jsv.link(true, "#toclone .toplevel", data, helpers); tmpl1.link("#toclone .result", data, helpers); tmpl2.link("#toclone .result2", data, helpers); var inputs = $("#result input"), buttons = $("#result button"), lis = $("#result .clickLi"); // ................................ Act .................................. var cloned = $("#toclone").clone().removeAttr( 'id' ); cloned.empty(); var res = $("#result").text(); jsv.observable(data.things).insert(1); res += "|" + $("#result").text(); jsv.observable(data).setProperty("name", "Bob"); res += "|" + $("#result").text(); $(inputs[0]).val("n0").change(); res += "|" + $("#result").text(); $(inputs[1]).val("n1").change(); res += "|" + $("#result").text(); $(inputs[2]).val("n2").change(); res += "|" + $("#result").text(); // ............................... Assert ................................. assert.equal(res, 'JoJoJoJoJoJo|' + 'JoJoinsertedJoJoinsertedJoJoinserted|' + 'BobBobinsertedBobBobinsertedBobBobinserted|' + 'n0n0insertedn0n0insertedn0n0inserted|' + 'n1n1insertedn1n1insertedn1n1inserted|' + 'n2n2insertedn2n2insertedn2n2inserted', 'Cloning data-linked content then emptying clone does not remove original data bindings. (Issue https://github.com/BorisMoore/jsviews/issues/369)'); // ................................ Act .................................. res = "" + clicked; $(buttons[0]).click(); $(buttons[1]).click(); $(buttons[2]).click(); $(lis[0]).click(); $(lis[1]).click(); $(lis[2]).click(); // ............................... Assert ................................. res += "|" + clicked; assert.equal(res, '0|6', 'Cloning data-linked content then emptying clone does not remove click handlers. (Issue https://github.com/BorisMoore/jsviews/issues/369)'); jsv.views.settings.trigger(true); // ................................ Reset ................................ person1.lastName = "One"; // reset Prop jsv.unlink(); }); QUnit.module("API - data-link"); QUnit.test("Basic jsv.link(expression, container, data) and jsv.link(tmpl, container, data)", function(assert) { // ................................ Reset ................................ person1.lastName = "One"; // reset Prop home1.address = address1; // reset Prop jsv.views.settings.advanced({_jsv: true}); // For using viewsAndBindings($) // =============================== Arrange =============================== $("#result").html(''); jsv.link("lastName 44 a=3", "#inner", person1); // ................................ Act .................................. before = $("#inner").html(); jsv.observable(person1).setProperty("lastName", "newLast"); after = $("#inner").html(); // ............................... Assert ................................. assert.equal(before + "|" + after, 'One|newLast', 'jsv.link("fieldName", "#target", data) links field to content of target element (equivalent to data-link="fieldName")'); // =============================== Arrange =============================== // ................................ Act .................................. $("#result").empty(); // ............................... Assert ................................. assert.ok(!viewsAndBindings($) && !$._data(person1).events, "$(container).empty removes current listeners from that content"); // ................................ Reset ................................ person1.lastName = "One"; // reset Prop // =============================== Arrange =============================== $("#result").html(''); jsv.link("person1.lastName + ' ' + person1.home.address^street", "#inner", model); // ................................ Act .................................. before = $("#inner").html(); jsv.observable(person1).setProperty("lastName", "newLast"); jsv.observable(person1.home).setProperty("address", address2); // Using deep observability after = $("#inner").html(); // ............................... Assert ................................. assert.equal(before + "|" + after, 'One StreetOne|newLast StreetTwo', 'jsv.link(expression, "#target", data) links expression to target element (equivalent to data-link="expression")'); // =============================== Arrange =============================== // ................................ Act .................................. $("#result").empty(); // ............................... Assert ................................. assert.ok(!viewsAndBindings($) && !$._data(person1).events && !$._data(person1.home).events, "$(container).empty removes current listeners from that content"); // ................................ Reset ................................ person1.lastName = "One"; // reset Prop home1.address = address1; // reset Prop // =============================== Arrange =============================== var tmpl = jsv.templates("{^{:lastName}}"); jsv.link(tmpl, "#result", person1); // ................................ Act .................................. before = $("#result").text(); jsv.observable(person1).setProperty("lastName", "newLast"); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, 'One|newLast', 'jsv.link(template, "#container", data) links template to content of container (equivalent to template.link(container, data)'); // ................................ Reset ................................ $("#result").empty(); person1.lastName = "One"; // reset Prop // =============================== Arrange =============================== tmpl = jsv.templates("{^{for #data}}{^{:lastName}}{{/for}}"); jsv.link(tmpl, "#result", person1); // ................................ Act .................................. before = $("#result").text(); jsv.observable(person1).setProperty("lastName", "newLast"); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, 'One|newLast', 'jsv.link(template, "#container", data) links template to content of container (equivalent to template.link(container, data). Example 2.'); // =============================== Arrange =============================== // ................................ Act .................................. $("#result").empty(); // ............................... Assert ................................. assert.ok(!viewsAndBindings($) && !$._data(person1).events, "$(container).empty removes both views and current listeners from that content"); // ................................ Reset ................................ person1.lastName = "One"; // reset Prop jsv.unlink(); jsv.views.settings.advanced({_jsv: false}); }); QUnit.test("Top-level linking", function(assert) { jsv.views.settings.advanced({_jsv: true}); // For using cbBindings store jsv.view().ctx.root = jsv.view().data = undefined; // In some scenarios with nested templates, tmpl.render() // can leave topView.data and topView.ctx.root referencing data, causing tests below such as // tmpl.link(expression, container1, data1) works without setting data or root on top view to fail. So we 'start clean' here... // =============================== Arrange =============================== jsv.views.helpers("a", " A"); $("#result").html("
        "); // ............................... Act ................................. jsv.link(true, "#result", {name: "Jo"}, {b: " B"}); // ............................... Assert ................................. assert.equal($("#result").text(), "Jo A B", 'Passing in data to top-level linking'); // ............................... Act ................................. jsv.link(true, "#result div", {name: "Jo2"}, {b: " newB"}); // ............................... Assert ................................. assert.equal($("#result").text(), "Jo2 A newB", 'Top-level linking directly to the linked element'); // ................................ Reset ................................ $("#result").empty(); // ............................... Assert ................................. assert.equal(JSON.stringify(_jsv.cbBindings), "{}", "Top level bindings all removed when content removed from DOM"); // =============================== Arrange =============================== jsv.views.helpers("a", " A"); jsv.templates("inner", "{^{:name + ~a + ~b}}"); $("#result").html("
        "); // ............................... Act ................................. var data = {name: "Jo"}; jsv.link(true, "#result", data, {b: " B"}); // ............................... Assert ................................. assert.equal($("#result").text(), "Jo A B", 'Top-level data-link="{include tmpl=...}" passes in model and context'); // ............................... Act ................................. jsv.observable(data).setProperty("name", "JoChanged"); // ............................... Assert ................................. assert.equal($("#result").text(), "JoChanged A B", 'Top-level data-link="{include tmpl=...}" binds correctly within {{include}} template'); // ............................... Act ................................. data = {name: "Jo2"}; jsv.link(true, "#result", data, {b: " newB"}); // ............................... Assert ................................. assert.equal($("#result div").text(), "Jo2 A newB", 'Top-level linking directly to the linked element with data-link="{include... '); // ............................... Act ................................. jsv.observable(data).setProperty("name", "Jo2Changed"); // ............................... Assert ................................. assert.equal($("#result").text(), "Jo2Changed A newB", 'Top-level linking directly to the linked element with data-link="{include... binds correctly within {{include}} template'); // ................................ Reset ................................ $("#result").empty(); // ............................... Assert ................................. assert.equal(JSON.stringify(_jsv.cbBindings), "{}", "Top level bindings all removed when content removed from DOM"); // =============================== Arrange =============================== jsv.views.helpers("a", " A"); jsv.templates({ selectTmpl: "", listTmpl: "
      • {{>name}}
      • " }); $("#result").html("" + "
          "); var count = 0, model = { selected: "Jim", people: [ {name: "Bob"}, {name: "Jim"} ] }; // ............................... Act ................................. $("#result").link(true, model); res = $("#result select option:selected").text() + "-" + $("#result ul").text(); var newName = "new" + count++; jsv.observable(model.people).insert({ name: newName }); jsv.observable(model).setProperty("selected", newName); res += "|" + $("#result select option:selected").text() + "-" + $("#result ul").text(); $("#result").link(true, model); // ............................... Assert ................................. assert.equal(res, "Jim-BobJim|new0-BobJimnew0", "Top level bindings with multiple targets on the same element work correctly: html{for people tmpl='selectTmpl'} {:selected:}"); // ................................ Reset ................................ $("#result").empty(); // ............................... Assert ................................. assert.equal(JSON.stringify(_jsv.cbBindings), "{}", "Top level bindings all removed when content removed from DOM"); // =============================== Arrange =============================== jsv.templates({ myTmpl: "{{>name}} lead:{^{>~team.lead}} - " }); $("#result").html("
          "); model = { lead: "Jim", people: [ {name: "Bob"}, {name: "Jim"} ] }; // ............................... Act ................................. $("#result").link(true, model); res = $("#result").text(); jsv.observable(model.people).insert({ name: "newName" }); jsv.observable(model).setProperty("lead", "newName"); res += "|" + $("#result").text(); // ............................... Assert ................................. assert.equal(res, ("Bob lead:Jim - Jim lead:Jim - |Bob lead:newName - Jim lead:newName - newName lead:newName - "), "Top level bindings allow passing in new contextual parameters to template: data-link=\"{for people ~team=#data tmpl=..."); // ................................ Reset ................................ $("#result").empty(); // ............................... Assert ................................. assert.equal(JSON.stringify(_jsv.cbBindings), "{}", "Top level bindings all removed when content removed from DOM"); // =============================== Arrange =============================== $("#result").html('
          '); var tmpl = jsv.templates("{^{:name}} {{:~root.name}} {{:#parent && #parent.type}}"), person1 = {name: "Jo"}, person2 = {name: "Bob"}; // ............................... Act ................................. var html = tmpl.render(person1); // ............................... Assert ................................. assert.equal(html + JSON.stringify(jsv.view().views), "Jo Jo {}", "Render does not persist any view hierarchy"); // =============================== Arrange =============================== jsv.link("name + ' ' + ~root.name + ' ' + #parent.type", "#container1", person1); // ............................... Assert ................................. assert.ok( $("#container1").text() === "Jo Jo top" && jsv.view().get(true, "link").ctxPrm("root").name === "Jo" && jsv.view().get(true, "link").root === jsv.view().get(true, "link") && jsv.view().data === undefined && jsv.view().ctx.root === undefined, "tmpl.link(expression, container1, data1) works without setting data or root on top view"); // ................................ Reset ................................ jsv.unlink("#container1"); // =============================== Arrange =============================== jsv.link(true, "#container3", person1); // ............................... Assert ................................. assert.ok( $("#container3").text() === "Jo Jo top" && jsv.view().get(true, "link").ctxPrm("root").name === "Jo" && jsv.view().get(true, "link").root === jsv.view().get(true, "link") && jsv.view().data === undefined && jsv.view().ctx.root === undefined, "tmpl.link(true, container1, data1) works without setting data or root on top view"); // ................................ Reset ................................ jsv.unlink("#container3"); // =============================== Arrange =============================== tmpl.link("#container1", person1); tmpl.link("#container2", person2); var spans = $("#result span"); // ............................... Assert ................................. assert.ok( $("#container1").text() === "Jo Jo top" && $("#container2").text() === "Bob Bob top" && jsv.view(spans[0]).ctxPrm("root").name === "Jo" && jsv.view(spans[0]).root === jsv.view(spans[0]) && jsv.view(spans[1]).ctxPrm("root").name === "Bob" && jsv.view(spans[1]).root === jsv.view(spans[1]) && jsv.view().data === undefined && jsv.view().ctx.root === undefined, "tmpl.link(container1, data1) tmpl.link(container2, data2) - to different containers, works without setting data or root on top view"); // ............................... Act ................................. $("#container2").empty(); spans = $("#result span"); // ............................... Assert ................................. assert.ok( $("#container1").text() === "Jo Jo top" && $("#container2").text() === "" && jsv.view(spans[0]).ctxPrm("root").name === "Jo" && jsv.view(spans[0]).root === jsv.view(spans[0]) && spans[1] === undefined && jsv.view().data === undefined && jsv.view().ctx.root === undefined, "$(container2).empty() removes view hierarchy for that container, and leaves other container hierarchy intact"); // ............................... Act ................................. $("#container1").empty(); spans = $("#result span"); // ............................... Assert ................................. assert.ok( $("#container1").text() === "" && $("#container2").text() === "" && spans[0] === undefined && spans[0] === undefined && jsv.view().data === undefined && jsv.view().ctx.root === undefined, "$(container1).empty() removes other view hierarchy too"); // ................................ Reset ................................ $("#result").empty(); jsv.views.settings.advanced({_jsv: false}); }); QUnit.test("jsv.link() and $().link() variants", function(assert) { var done; if (assert.async) { done = assert.async() } else { stop() } // ................................ Reset ................................ person1.lastName = "One"; // reset Prop home1.address = address1; // reset Prop jsv.views.settings.advanced({_jsv: true}); // For using viewsAndBindings($) // =============================== Arrange =============================== $("#result").html(''); var help = { options: {bar: "BarA"} }; jsv.link("~root.person1.lastName + ' ' + person1.home.address^street + ' ' + ~options.bar", "#inner", model, help); // ................................ Act .................................. before = $("#inner").html(); jsv.observable(person1).setProperty("lastName", "newLast"); jsv.observable(person1.home).setProperty("address", address2); // Using deep observability jsv.observable(help.options).setProperty("bar", "BarB"); // Modify helper after = $("#inner").html(); // ............................... Assert ................................. assert.equal(before + "|" + after, 'One StreetOne BarA|newLast StreetTwo BarB', 'jsv.link(expression, "#target", data, helpers) links expression to target element (equivalent to data-link="expression")'); // =============================== Arrange =============================== // ................................ Act .................................. $("#result").empty(); // ............................... Assert ................................. assert.ok(!viewsAndBindings($) && !$._data(person1).events && !$._data(person1.home).events && !$._data(help.options).events, "$(container).empty removes current listeners from that content"); // ................................ Reset ................................ person1.lastName = "One"; // reset Prop home1.address = address1; // reset Prop help.options.bar = "BarA"; // reset Prop // =============================== Arrange =============================== $("#result").html(''); $("#inner").link("person1.lastName + ' ' + person1.home.address^street + ' ' + ~options.bar", model, help); // ................................ Act .................................. before = $("#inner").html(); jsv.observable(person1).setProperty("lastName", "newLast"); jsv.observable(person1.home).setProperty("address", address2); // Using deep observability jsv.observable(help.options).setProperty("bar", "BarB"); // Modify helper after = $("#inner").html(); // ............................... Assert ................................. assert.equal(before + "|" + after, 'One StreetOne BarA|newLast StreetTwo BarB', '$("#target").link(expression, data, helpers) links expression to target element (equivalent to data-link="expression")'); // ................................ Reset ................................ $("#result").empty(); person1.lastName = "One"; // reset Prop home1.address = address1; // reset Prop help.options.bar = "BarA"; // reset Prop // =============================== Arrange =============================== $("#result").html('
          '); // multiple targets, same class jsv.link("person1.lastName", ".inner", model, help); // ................................ Act .................................. before = $("#result").text() + $("#result input").val(); jsv.observable(person1).setProperty("lastName", "newLast"); after = $("#result").text() + $("#result input").val(); keydown($("#result input").val("modLast")); setTimeout(function() { after += "|" + $("#result").text() + $("#result input").val(); // ............................... Assert ................................. assert.equal(before + "|" + after, 'One One One|newLast newLast newLast|modLast modLast modLast', 'jsv.link(expression, ".target", data, helpers) links expression to multiple target elements, including two-way bindings (equivalent to data-link="expression" on each element)'); // ................................ Act .................................. $("#result").empty(); // ............................... Assert ................................. assert.ok(!viewsAndBindings($) && !$._data(person1).events, "$(container).empty removes current listeners and two-way bindings from that content"); // ................................ Reset ................................ person1.lastName = "One"; // reset Prop // =============================== Arrange =============================== $("#result").html('
          '); // multiple targets, same class $(".inner").link("person1.lastName", model, help); // ................................ Act .................................. before = $("#result").text() + $("#result input").val(); jsv.observable(person1).setProperty("lastName", "newLast"); after = $("#result").text() + $("#result input").val(); keydown($("#result input").val("modLast")); setTimeout(function() { after += "|" + $("#result").text() + $("#result input").val(); // ............................... Assert ................................. assert.equal(before + "|" + after, 'One One One|newLast newLast newLast|modLast modLast modLast', '$(".target").link(expression, data, helpers) links expression to multiple target elements, including two-way bindings (equivalent to data-link="expression" on each element)'); // ................................ Act .................................. $("#result").empty(); // ............................... Assert ................................. assert.ok(!viewsAndBindings($) && !$._data(person1).events, "$(container).empty removes current listeners and two-way bindings from that content"); // ................................ Reset ................................ person1.lastName = "One"; // reset Prop // =============================== Arrange =============================== $("#result").html('
          '); // multiple targets, same class help.options.tmpl = jsv.templates(" NAME: {^{:lastName}}"); jsv.link("title{:person1.lastName} {include person1 tmpl=~options.tmpl}", "div.inner, span.inner", model, help); jsv.link("{:person1.lastName:} title{:person1.lastName}", "input.inner", model, help); // ................................ Act .................................. function getTitles(selector) { var res = ""; $(selector).each(function() { res += " " + this.title; }); return res; } before = $("#result").text() + $("#result input").val() + getTitles(".inner"); jsv.observable(person1).setProperty("lastName", "newLast"); after = $("#result").text() + $("#result input").val() + getTitles(".inner"); keydown($("#result input").val("modLast")); setTimeout(function() { after += "|" + $("#result").text() + $("#result input").val() + getTitles(".inner"); // ............................... Assert ................................. assert.equal(before + "|" + after, ' NAME: One NAME: One One One One One|' + ' NAME: newLast NAME: newLast newLast newLast newLast newLast|' + ' NAME: modLast NAME: modLast modLast modLast modLast modLast', 'jsv.link(expression, selector, data, helpers) links expression to multiple targets on multiple target elements, including two-way bindings'); // ................................ Act .................................. $("#result").empty(); // ............................... Assert ................................. assert.ok(!viewsAndBindings($) && !$._data(person1).events, "$(container).empty removes current listeners and two-way bindings from that content"); // ................................ Reset ................................ person1.lastName = "One"; // reset Prop // =============================== Arrange =============================== $("#result").html('
          '); // multiple targets, same class $("div.inner, span.inner").link("title{:person1.lastName} {include person1 ^tmpl=~options.tmpl}", model, help); $("input.inner").link("title{:person1.lastName} {:person1.lastName:}", model, help); // ................................ Act .................................. before = $("#result").text() + $("#result input").val() + getTitles(".inner"); jsv.observable(person1).setProperty("lastName", "newLast"); after = $("#result").text() + $("#result input").val() + getTitles(".inner"); keydown($("#result input").val("modLast")); setTimeout(function() { after += "|" + $("#result").text() + $("#result input").val() + getTitles(".inner"); jsv.observable(help.options).setProperty("tmpl", jsv.templates(" NEWTMPLNAME: {^{:lastName}}")); // We dynamically change the template of {include ^tmpl=~tmpl} too after += "|" + $("#result").text() + $("#result input").val() + getTitles(".inner"); // ............................... Assert ................................. assert.equal(before + "|" + after, ' NAME: One NAME: One One One One One|' + ' NAME: newLast NAME: newLast newLast newLast newLast newLast|' + ' NAME: modLast NAME: modLast modLast modLast modLast modLast|' + ' NEWTMPLNAME: modLast NEWTMPLNAME: modLast modLast modLast modLast modLast', '$(selector).link(expression, data, helpers) links expression to multiple targets on multiple target elements, including binding to passed in templates'); // ................................ Act .................................. $("#result").empty(); // ............................... Assert ................................. assert.ok(!viewsAndBindings($) && !$._data(person1).events, "$(container).empty removes current listeners and two-way bindings from that content"); // ................................ Reset ................................ person1.lastName = "One"; // reset Prop help.options.tmpl = jsv.templates(" NAME: {^{:lastName}}"); // =============================== Arrange =============================== $("#result").html('' + '
          ' + ' '); // multiple targets, same class jsv.link(true, ".inner", model, help); // ................................ Act .................................. before = $("#result").text() + $("#result input").val() + getTitles(".inner"); jsv.observable(person1).setProperty("lastName", "newLast"); after = $("#result").text() + $("#result input").val() + getTitles(".inner"); keydown($("#result input").val("modLast")); setTimeout(function() { after += "|" + $("#result").text() + $("#result input").val() + getTitles(".inner"); // ............................... Assert ................................. assert.equal(before + "|" + after, ' NAME: One NAME: One One One One One|' + ' NAME: newLast NAME: newLast newLast newLast newLast newLast|' + ' NAME: modLast NAME: modLast modLast modLast modLast modLast', 'jsv.link(true, ".inner", data, helpers) links multiple targets on multiple target elements, including two-way bindings'); // ................................ Act .................................. $("#result").empty(); // ............................... Assert ................................. assert.ok(!viewsAndBindings($) && !$._data(person1).events, "$(container).empty removes current listeners and two-way bindings from that content"); // ................................ Reset ................................ person1.lastName = "One"; // reset Prop // =============================== Arrange =============================== $("#result").html('' + '
          ' + ' '); // multiple targets, same class $(".inner").link(true, model, help); // ................................ Act .................................. before = $("#result").text() + $("#result input").val() + getTitles(".inner"); jsv.observable(person1).setProperty("lastName", "newLast"); after = $("#result").text() + $("#result input").val() + getTitles(".inner"); keydown($("#result input").val("modLast")); setTimeout(function() { after += "|" + $("#result").text() + $("#result input").val() + getTitles(".inner"); // ............................... Assert ................................. assert.equal(before + "|" + after, ' NAME: One NAME: One One One One One|' + ' NAME: newLast NAME: newLast newLast newLast newLast newLast|' + ' NAME: modLast NAME: modLast modLast modLast modLast modLast', '$(".inner").link(true, data, helpers) links multiple targets on multiple target elements, including two-way bindings'); // ................................ Act .................................. $("#result").empty(); // ............................... Assert ................................. assert.ok(!viewsAndBindings($) && !$._data(person1).events, "$(container).empty removes current listeners and two-way bindings from that content"); // ................................ Reset ................................ person1.lastName = "One"; // reset Prop // =============================== Arrange =============================== $("#result").html('' + '
          ' + ' '); // multiple targets, same class jsv.link(true, "#result", model, help); // ................................ Act .................................. before = $("#result").text() + $("#result input").val() + getTitles("#result div, #result span, #result input"); jsv.observable(person1).setProperty("lastName", "newLast"); after = $("#result").text() + $("#result input").val() + getTitles("#result div, #result span, #result input"); keydown($("#result input").val("modLast")); setTimeout(function() { after += "|" + $("#result").text() + $("#result input").val() + getTitles("#result div, #result span, #result input"); // ............................... Assert ................................. assert.equal(before + "|" + after, ' NAME: One NAME: One One One One One|' + ' NAME: newLast NAME: newLast newLast newLast newLast newLast|' + ' NAME: modLast NAME: modLast modLast modLast modLast modLast', '$(container).link(true, data, helpers) links multiple targets on multiple target elements, including two-way bindings'); // ................................ Act .................................. $("#result").empty(); // ............................... Assert ................................. assert.ok(!viewsAndBindings($) && !$._data(person1).events, "$(container).empty removes current listeners and two-way bindings from that content"); // ................................ Reset ................................ person1.lastName = "One"; // reset Prop // =============================== Arrange =============================== $("#result").html('' + '
          ' + ' '); $("#result").link(true, model, help); // ................................ Act .................................. before = $("#result").text() + $("#result input").val() + getTitles("#result div, #result span, #result input"); jsv.observable(person1).setProperty("lastName", "newLast"); after = $("#result").text() + $("#result input").val() + getTitles("#result div, #result span, #result input"); keydown($("#result input").val("modLast")); setTimeout(function() { after += "|" + $("#result").text() + $("#result input").val() + getTitles("#result div, #result span, #result input"); // ............................... Assert ................................. assert.equal(before + "|" + after, ' NAME: One NAME: One One One One One|' + ' NAME: newLast NAME: newLast newLast newLast newLast newLast|' + ' NAME: modLast NAME: modLast modLast modLast modLast modLast', '$(container).link(true, data, helpers) links multiple targets on multiple target elements, including two-way bindings'); // ................................ Act .................................. $("#result").empty(); // ............................... Assert ................................. assert.ok(!viewsAndBindings($) && !$._data(person1).events, "$(container).empty removes current listeners and two-way bindings from that content"); // ................................ Reset ................................ person1.lastName = "One"; // reset Prop // =============================== Arrange =============================== jsv.templates('
          ').link("#result"); // Rendered by template: multiple targets, same class jsv.link("title{:person1.lastName} {include person1 tmpl=~options.tmpl}", "div.inner, span.inner", model, help); jsv.link("title{:person1.lastName} {:person1.lastName:}", "input.inner", model, help); // ................................ Act .................................. before = $("#result").text() + $("#result input").val() + getTitles(".inner"); jsv.observable(person1).setProperty("lastName", "newLast"); after = $("#result").text() + $("#result input").val() + getTitles(".inner"); keydown($("#result input").val("modLast")); setTimeout(function() { after += "|" + $("#result").text() + $("#result input").val() + getTitles(".inner"); // ............................... Assert ................................. assert.equal(before + "|" + after, ' NAME: One NAME: One One One One One|' + ' NAME: newLast NAME: newLast newLast newLast newLast newLast|' + ' NAME: modLast NAME: modLast modLast modLast modLast modLast', 'jsv.link(expression, selector, data, helpers) links correctly to multiple targets on multiple target elements within a linked rendered template'); // ................................ Act .................................. $("#result").empty(); // ............................... Assert ................................. assert.ok(!viewsAndBindings($) && !$._data(person1).events, "$(container).empty removes current listeners and two-way bindings from that content"); // ................................ Reset ................................ person1.lastName = "One"; // reset Prop // =============================== Arrange =============================== var data = {color: "green"}, outerHelp = { ob: { val: "outerVal", tmpl: jsv.templates("Inside {^{:~root.color}} {^{:~ob.val}} {^{:~foo}} NAME: {^{:lastName}}") } }; help.foo = "Foo"; jsv.templates('{^{:~root.color}} {^{:~ob.val}} ').link("#result", data, outerHelp); before = $("#result").text(); jsv.link("{include person1 tmpl=~ob.tmpl}", "span.inner", model, help); // ................................ Act .................................. before += "|" + $("#result").text(); jsv.observable(person1).setProperty("lastName", "newLast"); after = $("#result").text(); jsv.observable(data).setProperty("color", "red"); after += "|" + $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, 'green outerVal |' + 'green outerVal Inside green outerVal Foo NAME: One|' + 'green outerVal Inside green outerVal Foo NAME: newLast|' + 'red outerVal Inside red outerVal Foo NAME: newLast', 'jsv.link(expression, selector, data, helpers) links correctly to target elements within a linked rendered template - and extends the context of the target view'); // ................................ Act .................................. $("#result").empty(); // ............................... Assert ................................. assert.ok(!viewsAndBindings($) && !$._data(person1).events, "$(container).empty removes current listeners and two-way bindings from that content"); // ................................ Reset ................................ person1.lastName = "One"; // reset Prop // =============================== Arrange =============================== jsv.templates('' + '
          ' + ' ').link("#result", model, help); // Rendered and linked by template: multiple targets var model2 = { person1: { lastName: "lastModel2Name" } }, help2 = { options: { tmpl: jsv.templates(" NAMEModel2: {^{:lastName}}") } }; jsv.link(true, "#result", model2, help2); // ................................ Act .................................. before = $("#result").text() + $("#result input").val() + getTitles("#result div, #result span, #result input"); jsv.observable(person1).setProperty("lastName", "newLast"); after = $("#result").text() + $("#result input").val() + getTitles("#result div, #result span, #result input"); keydown($("#result input").val("modLast")); setTimeout(function() { after += "|" + $("#result").text() + $("#result input").val() + getTitles("#result div, #result span, #result input"); // ............................... Assert ................................. assert.equal(before + "|" + after + " - Model1: " + model.person1.lastName + "- Model2: " + model2.person1.lastName, ' NAME: One NAME: One One One One One|' + ' NAME: newLast NAME: newLast newLast newLast newLast newLast|' + ' NAME: modLast NAME: modLast modLast modLast modLast modLast' + ' - Model1: modLast- Model2: lastModel2Name', 'jsv.link(true, selector, data, helpers) is a no-op when targeting within a previously linked rendered template'); // ................................ Act .................................. jsv.observable(model2.person1).setProperty("lastName", "newModel2Last"); before = $("#result").text() + $("#result input").val() + getTitles("#result div, #result span, #result input"); jsv.observable(model.person1).setProperty("lastName", "new_ORIGMOD_Last"); after = $("#result").text() + $("#result input").val() + getTitles("#result div, #result span, #result input"); jsv.observable(help.options).setProperty("tmpl", jsv.templates(" NAME_ORIGMOD_NewTmpl: {^{:lastName}}")); after += "|" + $("#result").text() + $("#result input").val() + getTitles("#result div, #result span, #result input"); // ............................... Assert ................................. assert.equal(before + "|" + after, ' NAME: modLast NAME: modLast modLast modLast modLast modLast|' + ' NAME: new_ORIGMOD_Last NAME: new_ORIGMOD_Last new_ORIGMOD_Last new_ORIGMOD_Last new_ORIGMOD_Last new_ORIGMOD_Last|' + ' NAME: new_ORIGMOD_Last NAME_ORIGMOD_NewTmpl: new_ORIGMOD_Last new_ORIGMOD_Last new_ORIGMOD_Last new_ORIGMOD_Last new_ORIGMOD_Last', 'Continue: jsv.link(true, selector, data, helpers) is a no-op when targeting within a previously linked rendered template'); // ................................ Act .................................. $("#result").empty(); // ............................... Assert ................................. assert.ok(!viewsAndBindings($) && !$._data(person1).events, "$(container).empty removes current listeners and two-way bindings from that content"); // ................................ Reset ................................ person1.lastName = "One"; // reset Prop model2.person1.lastName = "lastModel2Name"; // reset Prop help2.options.tmpl = jsv.templates("Model2Tmpl: {^{:lastName}}"); // =============================== Arrange =============================== jsv.templates('
          ').link("#result", model, help); // ................................ Act .................................. before = $("#result").text(); jsv.link(true, "#inner", model2, help2); //NO-OP after = $("#result").text(); jsv.observable(model2.person1).setProperty("lastName", "newLast2"); after += "|" + $("#result").text(); keydown($("#result input").val("modLast")); setTimeout(function() { after += "|" + $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after + getTitles("#inner") + " - Model1: " + model.person1.lastName + "- Model2: " + model2.person1.lastName, "One|One|One|modLast title:modLast - Model1: modLast- Model2: newLast2", 'jsv.link(true, selector, data, helpers) is a no-op when targeting within a previously linked rendered template'); // ................................ Act .................................. before = $("#result").text() + getTitles("#inner"); jsv.link("{include person1 ^tmpl=~options.tmpl} title{:\'title2:\' + person1.lastName}", "#inner", model2, help2); after = $("#result").text() + getTitles("#inner"); jsv.observable(model2.person1).setProperty("lastName", "last2B"); after += "|" + $("#result").text() + getTitles("#inner"); jsv.observable(model.person1).setProperty("lastName", "last1A"); after += "|" + $("#result").text() + getTitles("#inner"); keydown($("#result input").val("modMoreLast")); setTimeout(function() { after += "|" + $("#result").text() + getTitles("#inner"); jsv.observable(help2.options).setProperty("tmpl", jsv.templates("Model2NEWTmpl: {^{:lastName}}")); after += "|" + $("#result").text() + getTitles("#inner"); // ............................... Assert ................................. assert.equal(before + "|" + after + " - Model1: " + model.person1.lastName + "- Model2: " + model2.person1.lastName, "modLast title:modLast|" + "Model2Tmpl: newLast2 title2:newLast2|" + "Model2Tmpl: last2B title2:last2B|" + "last1A title:last1A|" + "modMoreLast title:modMoreLast|" + "Model2NEWTmpl: last2B title:modMoreLast" + " - Model1: modMoreLast- Model2: last2B", 'jsv.link(expression, selector, data, helpers) links content correctly, including within a previously linked rendered template - leading to dual two-way binding to both models and contexts'); // ................................ Act .................................. $("#result").empty(); // ............................... Assert ................................. assert.ok(!viewsAndBindings($) && !$._data(person1).events, "$(container).empty removes current listeners and two-way bindings from that content"); // ................................ Reset ................................ person1.lastName = "One"; // reset Prop model2.person1.lastName = "lastModel2Name"; // reset Prop help2.options.tmpl = jsv.templates("Model2Tmpl: {^{:lastName}}"); // =============================== Arrange =============================== jsv.templates('
          ').link("#result", model, help); // ................................ Act .................................. before = $("#result").text() + getTitles("#inner") + " val:" + $("#result input").val(); jsv.observable(model.person1).setProperty("lastName", "newLast3"); after = $("#result").text() + getTitles("#inner") + " val:" + $("#result input").val(); keydown($("#result input").val("modLast4")); setTimeout(function() { after += "|" + $("#result").text() + getTitles("#inner") + " val:" + $("#result input").val() + " data:" + model.person1.lastName; $("#inner").unlink(); jsv.observable(model.person1).setProperty("lastName", "newLast5"); after += "|" + $("#result").text() + getTitles("#inner") + " val:" + $("#result input").val(); keydown($("#result input").val("modLast6")); setTimeout(function() { after += "|" + $("#result").text() + getTitles("#inner") + " val:" + $("#result input").val() + " data:" + model.person1.lastName; $("#result input").unlink(); jsv.observable(model.person1).setProperty("lastName", "newLast7"); after += "|" + $("#result").text() + getTitles("#inner") + " val:" + $("#result input").val(); setTimeout(function() { keydown($("#result input").val("modLast8")); after += "|" + $("#result").text() + getTitles("#inner") + " val:" + $("#result input").val() + " data:" + model.person1.lastName; // ............................... Assert ................................. assert.equal(before + "|" + after, "One title:One val:One|" + "newLast3 title:newLast3 val:newLast3|" + "modLast4 title:modLast4 val:modLast4 data:modLast4|" + "modLast4 title:modLast4 val:newLast5|" + "modLast4 title:modLast4 val:modLast6 data:modLast6|" + "modLast4 title:modLast4 val:modLast6|" + "modLast4 title:modLast4 val:modLast8 data:newLast7", '$(selector).unlink() removes data binding on target element, including both directions of binding on two-way data-linking'); // ............................... Assert ................................. assert.ok(viewsAndBindings($).split(" ").length === 7 && !$._data(model.person1).events, '$(selector).unlink() removes data binding, and the "link" views, for target elements'); // ................................ Act .................................. $("#result").empty(); // ............................... Assert ................................. assert.ok(!viewsAndBindings($) && !$._data(person1).events, "$(container).empty removes current listeners and two-way bindings from that content"); // ................................ Reset ................................ person1.lastName = "One"; // reset Prop model2.person1.lastName = "lastModel2Name"; // reset Prop help2.options.tmpl = jsv.templates("Model2Tmpl: {^{:lastName}}"); // =============================== Arrange =============================== jsv.templates('
          ').link("#result", model, help); // ................................ Act .................................. before = $("#result").text() + getTitles("#inner") + " val:" + $("#result input").val(); jsv.observable(model.person1).setProperty("lastName", "newLast3"); after = $("#result").text() + getTitles("#inner") + " val:" + $("#result input").val(); keydown($("#result input").val("modLast4")); setTimeout(function() { after += "|" + $("#result").text() + getTitles("#inner") + " val:" + $("#result input").val() + " data:" + model.person1.lastName; jsv.unlink("#inner"); jsv.observable(model.person1).setProperty("lastName", "newLast5"); after += "|" + $("#result").text() + getTitles("#inner") + " val:" + $("#result input").val(); keydown($("#result input").val("modLast6")); setTimeout(function() { after += "|" + $("#result").text() + getTitles("#inner") + " val:" + $("#result input").val() + " data:" + model.person1.lastName; jsv.unlink("#result input"); jsv.observable(model.person1).setProperty("lastName", "newLast7"); after += "|" + $("#result").text() + getTitles("#inner") + " val:" + $("#result input").val(); keydown($("#result input").val("modLast8")); setTimeout(function() { after += "|" + $("#result").text() + getTitles("#inner") + " val:" + $("#result input").val() + " data:" + model.person1.lastName; // ............................... Assert ................................. assert.equal(before + "|" + after, "One title:One val:One|" + "newLast3 title:newLast3 val:newLast3|" + "modLast4 title:modLast4 val:modLast4 data:modLast4|" + "modLast4 title:modLast4 val:newLast5|" + "modLast4 title:modLast4 val:modLast6 data:modLast6|" + "modLast4 title:modLast4 val:modLast6|" + "modLast4 title:modLast4 val:modLast8 data:newLast7", 'jsv.unlink(selector) removes data binding on target element, including both directions of binding on two-way data-linking'); // ............................... Assert ................................. assert.ok(viewsAndBindings($).split(" ").length === 7 && !$._data(model.person1).events, 'jsv.unlink(selector) removes data binding, and the "link" views, for target elements'); // ................................ Act .................................. $("#result").empty(); // ............................... Assert ................................. assert.ok(!viewsAndBindings($) && !$._data(person1).events, "$(container).empty removes current listeners and two-way bindings from that content"); // ................................ Reset ................................ jsv.views.settings.advanced({_jsv: false}); if (assert.async) { done() } else { start() } }, 0); }, 0); }, 0); }, 0); }, 0); }, 0); }, 0); }, 0); }, 0); }, 0); }, 0); }, 0); }, 0); }, 0); }, 0); }, 0); }, 0); }, 0); }); QUnit.module("template.link()"); QUnit.test("Helper overriding", function(assert) { var done; if (assert.async) { done = assert.async() } else { stop() } // ................................ Reset ................................ home1.address = address1; // reset Prop person1._firstName = "Jo"; // reset Prop person1.lastName = "One"; // reset Prop settings.title = "Mr"; // reset Prop // =============================== Arrange =============================== jsv.views.helpers("a", "globalHelper"); var tmpl = jsv.templates({ markup: "{{:~a}} {{:~b}} {{:~c}}", helpers: { b: "templateHelper" } }); tmpl.link("#result", {}, {c: "optionHelper"}); jsv.views.helpers("a", null); // ............................... Assert ................................. assert.equal($("#result").text(), "globalHelper templateHelper optionHelper", 'Passing in helpers - global, template or option'); // =============================== Arrange =============================== tmpl = jsv.templates({ markup: "{{:~a}}", helpers: { a: "templateHelper" } }); tmpl.link("#result", {}); // ............................... Assert ................................. assert.equal($("#result").text(), "templateHelper", 'template helper overrides global helper'); // =============================== Arrange =============================== tmpl = jsv.templates({ markup: "{{:~a}}" }); tmpl.link("#result", {}, {a: "optionHelper"}); // ............................... Assert ................................. assert.equal($("#result").text(), "optionHelper", 'option helper overrides global helper'); // =============================== Arrange =============================== tmpl = jsv.templates({ markup: "{{:~b}}", helpers: { b: "templateHelper" } }); tmpl.link("#result", {}, {b: "optionHelper"}); // ............................... Assert ................................. assert.equal($("#result").text(), "templateHelper", 'template helper overrides option helper'); // =============================== Arrange =============================== jsv.views.helpers("a", "globalHelper"); $("#result").html("
          "); jsv.link(true, "#result", {}, {b: "optionHelper"}); jsv.views.helpers("a", null); // ............................... Assert ................................. assert.equal($("#result").text(), "globalHelperoptionHelper", 'Passing in helpers to top-level linking - global or option'); // =============================== Arrange =============================== jsv.views.helpers("a", "globalHelper"); $("#result").html("
          "); jsv.link(true, "#result", {}, {a: "optionHelper"}); jsv.views.helpers("a", null); // ............................... Assert ................................. assert.equal($("#result").text(), "optionHelper", 'Passing in helpers to top-level linking - option overrides global'); // =============================== Arrange =============================== jsv.views.helpers({ onBeforeChange: function(ev, eventArgs) { res += "globalBeforeChange|" + eventArgs.change + "|" + (this.tagName || this.elem.tagName) + " "; }, onAfterChange: function(ev, eventArgs) { res += "globalAfterChange|" + eventArgs.change + "|" + (this.tagName || this.elem.tagName) + " "; }, onAfterCreate: function(view) { res += "globalAfterCreate "; } }); res = ''; var pson = {name: "Jo"}; tmpl = jsv.templates({ markup: " {^{:name}}" }); tmpl.link("#result", pson); jsv.observable(pson).setProperty("name", "name3"); // ............................... Assert ................................. assert.equal(res, "globalAfterCreate globalBeforeChange|set|INPUT globalAfterChange|set|INPUT globalBeforeChange|set|: globalAfterChange|set|: ", 'Global onAfterCreate, onBeforeChange, onAfterChange - setProperty'); res = ''; keydown($("#result input").val("editedName")); setTimeout(function() { // ............................... Assert ................................. assert.equal(res, "globalBeforeChange|set|INPUT globalAfterChange|set|INPUT globalBeforeChange|set|: globalAfterChange|set|: ", 'Global onAfterCreate, onBeforeChange, onAfterChange - elemChange'); res = ''; // =============================== Arrange =============================== tmpl.link("#result", pson, { onBeforeChange: function(ev, eventArgs) { res += "optionsBeforeChange|" + eventArgs.change + "|" + (this.tagName || this.elem.tagName) + " "; }, onAfterChange: function(ev, eventArgs) { res += "optionsAfterChange|" + eventArgs.change + "|" + (this.tagName || this.elem.tagName) + " "; }, onAfterCreate: function(view) { res += "optionsAfterCreate "; } }); jsv.observable(pson).setProperty("name", "name2"); // ............................... Assert ................................. assert.equal(res, "optionsAfterCreate optionsBeforeChange|set|INPUT optionsAfterChange|set|INPUT optionsBeforeChange|set|: optionsAfterChange|set|: ", 'options helper overrides global helper'); res = ''; keydown($("#result input").val("editedName")); setTimeout(function() { // ............................... Assert ................................. assert.equal(res, "optionsBeforeChange|set|INPUT optionsAfterChange|set|INPUT optionsBeforeChange|set|: optionsAfterChange|set|: ", 'options helper overrides global helper'); res = ''; // =============================== Arrange =============================== tmpl = jsv.templates({ markup: " {^{:name}}", helpers: { onBeforeChange: function(ev, eventArgs) { res += "templateBeforeChange|" + eventArgs.change + "|" + (this.tagName || this.elem.tagName) + " "; }, onAfterChange: function(ev, eventArgs) { res += "templateAfterChange|" + eventArgs.change + "|" + (this.tagName || this.elem.tagName) + " "; }, onAfterCreate: function(view) { res += "templateAfterCreate "; } } }); tmpl.link("#result", pson, { onBeforeChange: function(ev, eventArgs) { res += "optionsBeforeChange|" + eventArgs.change + "|" + (this.tagName || this.elem.tagName) + " "; }, onAfterChange: function(ev, eventArgs) { res += "optionsAfterChange|" + eventArgs.change + "|" + (this.tagName || this.elem.tagName) + " "; }, onAfterCreate: function(view) { res += "optionsAfterCreate "; } }); jsv.observable(pson).setProperty("name", "name4"); // ............................... Assert ................................. assert.equal(res, "templateAfterCreate templateBeforeChange|set|INPUT templateAfterChange|set|INPUT templateBeforeChange|set|: templateAfterChange|set|: ", 'template helper overrides options helper'); res = ''; keydown($("#result input").val("editedName")); setTimeout(function() { // ............................... Assert ................................. assert.equal(res, "templateBeforeChange|set|INPUT templateAfterChange|set|INPUT" + " templateBeforeChange|set|: templateAfterChange|set|: ", 'template helper overrides options helper'); res = ''; jsv.views.helpers({ onBeforeChange: null, onAfterChange: null, onAfterCreate: null }); $("#result").empty(); if (assert.async) { done() } else { start() } }, 0); }, 0); }, 0); }); QUnit.test('data-link="expression"', function(assert) { var done; if (assert.async) { done = assert.async() } else { stop() } // ................................ Reset ................................ home1.address = address1; // reset Prop person1._firstName = "Jo"; // reset Prop person1.lastName = "One"; // reset Prop settings.title = "Mr"; // reset Prop jsv.views.settings.advanced({_jsv: true}); // For using viewsAndBindings($) // =============================== Arrange =============================== jsv.templates('prop: ') .link("#result", person1); // ................................ Act .................................. before = $("#result span").html(); jsv.observable(person1).setProperty("lastName", "newLast"); after = $("#result span").html(); // ............................... Assert ................................. assert.equal(before + "|" + after, 'One|newLast', 'Data link using: '); // ................................ Reset ................................ $("#result").empty(); person1.lastName = "One"; // reset Prop // =============================== Arrange =============================== jsv.templates('prop: ') .link("#result", person1); // ................................ Act .................................. before = $("#last").val(); jsv.observable(person1).setProperty("lastName", "newLast"); after = $("#last").val(); // ............................... Assert ................................. assert.equal(before + "|" + after, 'One|newLast', 'Data link using: binds from data'); // ................................ Act .................................. keydown($("#result input").val("editedName")); setTimeout(function() { after = $("#result").html() + $("#last").val(); // ............................... Assert ................................. assert.equal(person1.lastName, "editedName", 'Data link using: does two-way binding'); // ................................ Reset ................................ $("#result").empty(); person1.lastName = "One"; // reset Prop // =============================== Arrange =============================== jsv.templates('prop: ') .link("#result", model); // ................................ Act .................................. before = $("#result span").html(); jsv.observable(person1).setProperty("lastName", "newLast"); jsv.observable(person1.home).setProperty("address", address2); // Using deep observability after = $("#result span").html(); // ............................... Assert ................................. assert.equal(before + "|" + after, 'One StreetOne|newLast StreetTwo', 'Data link using: '); // ................................ Act .................................. $("#result").empty(); before = $("#result").html(); // ............................... Assert ................................. assert.ok(!viewsAndBindings($) && !$._data(person1).events && !$._data(home1).events && !$._data(address2).events, "$(container).empty removes both views and current listeners from that content - including after swapping data on deep paths"); person1.lastName = "One"; // reset Prop home1.address = address1; // reset Prop // =============================== Arrange =============================== jsv.templates('') .link("#result", person1); // ................................ Act .................................. before = $("#result span").html(); jsv.observable(person1).setProperty({firstName: "newFirst", lastName: "newLast"}); jsv.observable(settings).setProperty("title", "Sir"); after = $("#result span").html(); // ............................... Assert ................................. assert.equal(before + "|" + after, 'Mr Jo One|Sir newFirst newLast', 'data-link="fullName()"'); // ................................ Reset ................................ person1._firstName = "Jo"; // reset Prop person1.lastName = "One"; // reset Prop settings.title = "Mr"; // reset Prop // =============================== Arrange =============================== jsv.templates('') .link("#result", { foo: function(val) { return {b: val}; } }); // ............................... Assert ................................. var html = $("#result span")[0].outerHTML; assert.equal(html, 'x\\x', 'Escaping of characters: data-link="foo(\'x\\x\').b"'); // ................................ Reset ................................ person1._firstName = "Jo"; // reset Prop person1.lastName = "One"; // reset Prop settings.title = "Mr"; // reset Prop // =============================== Arrange =============================== jsv.templates('') .link("#result", model); // ................................ Act .................................. elems = $("#result span"); before = elems[0].innerHTML + elems[1].innerHTML; jsv.observable(address1).setProperty("street", "newStreetOne"); jsv.observable(person1).setProperty("home", home2); elems = $("#result span"); after = elems[0].innerHTML + elems[1].innerHTML; // ............................... Assert ................................. assert.equal(before + "|" + after, 'StreetOneStreetOne|newStreetOneStreetTwo', 'person1.home.address.street binds only to the leaf, but person1.home^address.street does deep binding'); // ................................ Reset ................................ $("#result").empty(); address1.street = "StreetOne"; person1.home = home1; // =============================== Arrange =============================== jsv.templates('') .link("#result", 1, {model: model}); // ................................ Act .................................. before = $("#result span").html(); jsv.observable(address1).setProperty("street", "newStreetOne"); jsv.observable(person1).setProperty("home", home2); jsv.observable(address2).setProperty("street", address2.street + "+"); after = $("#result span").html(); // ............................... Assert ................................. assert.equal(before + "|" + after, 'StreetOne|StreetTwo+', '~a.b^c does deep binding'); // ................................ Reset ................................ $("#result").empty(); address1.street = "StreetOne"; person1.home = home1; address2.street = "StreetTwo"; // =============================== Arrange =============================== var util1 = {getVal: function(val) { return "getVal1 = " + val; }}; var util2 = {getVal: function(val) { return "getVal2 = " + val; }}; var appHelpers = {util: util1}; jsv.templates('') .link("#result", 22, {app: appHelpers}); // ................................ Act .................................. before = $("#result span").html(); jsv.observable(appHelpers).setProperty("util", util2); after = $("#result span").html(); jsv.observable(util2).setProperty("getVal", function(val) { return "getNewVal = " + val; }); after += "|" + $("#result span").html(); // ............................... Assert ................................. assert.equal(before + "|" + after, 'getVal1 = 22|getVal2 = 22|getNewVal = 22', '~a.b.helperFunction() does deep binding even for functions'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== var foobar = { foo: { bar: "initial " } }; jsv.views.converters({ noop: function(val) { return val; } }); jsv.templates('1 {^{:foo^bar}} 2 {^{:foo^bar}} 3 ' + ' 4 5 6 ' + ' INPUTS ' + '' + '') .link("#result", foobar); // ................................ Act .................................. jsv.observable(foobar).setProperty("foo", {bar: "new "}); res = $("#result").text(); $("input.linked").each(function(i, el) { res += el.value; }); // ............................... Assert ................................. assert.equal(res, "1 new 2 new 3 new 4 new 5 new 6 new INPUTS new new new new new new new new ", 'Duplicate paths bind correctly (https://github.com/BorisMoore/jsviews/issues/250)'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== var data = {name: "Jo"}; res = ""; // ................................ Act .................................. res = jsv.templates('{{:#data}}') .render(["aa", 22, 0, false, "", true]); // ............................... Assert ................................. assert.equal(res, "aa220falsetrue", '{{:#data}} renders correctly for different data types'); // ................................ Act .................................. res = jsv.templates('{{for items}}{{:#index}} {{:#data}} {{/for}}') .render({items: ["aa", 22, 0, false, "", true]}); // ............................... Assert ................................. assert.equal(res, "0 aa 1 22 2 0 3 false 4 5 true ", '{{:#data}} within {{for}} is correct for different data types'); // ................................ Act .................................. res = jsv.templates('{{:#data}}') .link("#result", ["aa", 22, 0, false, "", true]).text(); // ............................... Assert ................................. assert.equal(res, "aa220falsetrue", 'With link, {{:#data}} renders correctly for different data types'); // ................................ Act .................................. res = jsv.templates('{{for items}}{{:#index}} {{:#data}} {{/for}}') .link("#result", {items: ["aa", 22, 0, false, "", true]}).text(); // ............................... Assert ................................. assert.equal(res, "0 aa 1 22 2 0 3 false 4 5 true ", 'with link, {{:#data}} within {{for}} is correct for different data types'); // ................................ Act .................................. res = jsv.templates('{^{:#data}}') .link("#result", ["aa", 22, 0, false, "", true]).text(); // ............................... Assert ................................. assert.equal(res, "aa220falsetrue", 'With link, {^{:#data}} renders correctly for different data types'); // ................................ Act .................................. res = jsv.templates('{^{for items}}{{:#index}} {^{:#data}} {{/for}}') .link("#result", {items: ["aa", 22, 0, false, "", true]}).text(); // ............................... Assert ................................. assert.equal(res, "0 aa 1 22 2 0 3 false 4 5 true ", 'with link, {^{:#data}} within {^{for}} is correct for different data types'); // ................................ Act .................................. data = {name: "Jo"}; res = ""; jsv.templates( '{{for "some string" ~item=#data}}' + '' + '{{/for}}' + '{{for 22 ~item=#data}}' + '' + '{{/for}}' + '{{for 0 ~item=#data}}' + '' + '{{/for}}' + '{{for false ~item=#data}}' + '' + '{{/for}}' + '{{for "" ~item=#data}}' + '' + '{{/for}}' + '{{for true ~item=#data}}' + '' + '{{/for}}' ) .link("#result", data); $("#result input").each(function(i, el) { res += el.value + " | "; }); jsv.observable(data).setProperty("name", "new"); $("#result input").each(function(i, el) { res += el.value + " | "; }); // ............................... Assert ................................. assert.equal(res, "Josome string | Jo22 | Jo0 | Jofalse | Jo | Jotrue | newsome string | new22 | new0 | newfalse | new | newtrue | ", 'data-linking inside {{for sometype}} works correctly even when #data is not an object'); // ................................ Reset ................................ $("#result").empty(); // ................................ Act .................................. data = {name: "Jo"}; res = ""; jsv.templates( '{^{for "some string" ~item=#data}}' + '' + '{{/for}}' + '{^{for 22 ~item=#data}}' + '' + '{{/for}}' + '{^{for 0 ~item=#data}}' + '' + '{{/for}}' + '{^{for false ~item=#data}}' + '' + '{{/for}}' + '{^{for "" ~item=#data}}' + '' + '{{/for}}' + '{^{for true ~item=#data}}' + '' + '{{/for}}' ) .link("#result", data); $("#result input").each(function(i, el) { res += el.value + " | "; }); jsv.observable(data).setProperty("name", "new"); $("#result input").each(function(i, el) { res += el.value + " | "; }); // ............................... Assert ................................. assert.equal(res, "Josome string | Jo22 | Jo0 | Jofalse | Jo | Jotrue | newsome string | new22 | new0 | newfalse | new | newtrue | ", 'data-linking inside {^{for sometype}} works correctly even when #data is not an object'); // ................................ Reset ................................ $("#result").empty(); res = ""; jsv.views.settings.advanced({_jsv: false}); if (assert.async) { done() } else { start() } }, 0); }); QUnit.test('data-link="attr{:expression}"', function(assert) { // ................................ Reset ................................ person1.lastName = "One"; // reset Prop // =============================== Arrange =============================== jsv.templates('prop: ') .link("#result", person1); // ................................ Act .................................. before = $("#result span")[0].className; jsv.observable(person1).setProperty("lastName", "xxx"); after = $("#result span")[0].className; // ............................... Assert ................................. assert.equal(before + "|" + after, 'One|xxx', 'Data link using: , and setting lastName to "xxx" - sets className to "xxx"'); // ................................ Reset ................................ $("#result").empty(); person1.lastName = "One"; // reset Prop // =============================== Arrange =============================== jsv.templates('prop: ') .link("#result", person1); // ................................ Act .................................. before = $("#result span").attr("title"); jsv.observable(person1).setProperty("lastName", "xxx"); after = $("#result span").attr("title"); // ............................... Assert ................................. assert.equal(before + "|" + after, 'One|xxx', 'Data link using: , and setting lastName to "xxx" - sets title to "xxx"'); // ................................ Reset ................................ $("#result").empty(); person1.lastName = "One"; // reset Prop // =============================== Arrange =============================== jsv.templates('prop: ') .link("#result", person1); // ................................ Act .................................. before = $("#result span")[0].getAttribute("title"); jsv.observable(person1).setProperty("lastName", ""); after = $("#result span")[0].getAttribute("title"); // ............................... Assert ................................. assert.ok(before === 'One' && after === "", 'Data link using: , and setting lastName to "" - sets title to ""'); // ................................ Reset ................................ $("#result").empty(); person1.lastName = "One"; // reset Prop // =============================== Arrange =============================== jsv.templates('prop: ') .link("#result", person1); // ................................ Act .................................. before = $("#result span")[0].getAttribute("title"); jsv.observable(person1).setProperty("lastName", null); after = $("#result span")[0].getAttribute("title"); var html = $("#result span")[0].outerHTML; // ............................... Assert ................................. assert.ok(before === 'One' && after === null && html === "", 'Data link using: , and setting lastName to null - removes title attribute'); // ................................ Reset ................................ $("#result").empty(); person1.lastName = "One"; // reset Prop // =============================== Arrange =============================== jsv.templates('prop: ') .link("#result", person1); // ................................ Act .................................. before = $("#result span")[0].getAttribute("title"); jsv.observable(person1).setProperty("lastName", undefined); after = $("#result span")[0].getAttribute("title"); var html = $("#result span")[0].outerHTML; // ............................... Assert ................................. assert.ok(before === 'One' && after === null && html === "", 'Data link using: , and setting lastName to undefined - removes title attribute'); // ................................ Reset ................................ $("#result").empty(); person1.lastName = "One"; // reset Prop // =============================== Arrange =============================== jsv.templates('prop: ') .link("#result", person1); // ................................ Act .................................. before = $("#result span").html(); jsv.observable(person1).setProperty("lastName", null); after = $("#result span").html(); // ............................... Assert ................................. assert.ok(before === 'One' && after === "", 'Data link using: , and setting lastName to null - sets content to empty string'); // ................................ Reset ................................ $("#result").empty(); person1.lastName = "One"; // reset Prop // =============================== Arrange =============================== jsv.templates("prop: ") .link("#result", person1); // ................................ Act .................................. before = $("#result span").html(); jsv.observable(person1).setProperty("lastName", null); after = $("#result span").html(); // ............................... Assert ................................. assert.ok(before === 'One' && after === "", "Data link using: , and setting lastName to null - sets content to empty string"); // ................................ Reset ................................ $("#result").empty(); person1.lastName = "One"; // reset Prop // =============================== Arrange =============================== jsv.templates('prop: ') .link("#result", person1); // ................................ Act .................................. before = $("#result span")[0].getAttribute("title"); jsv.observable(person1).removeProperty("lastName"); after = $("#result span")[0].getAttribute("title"); var html = $("#result span")[0].outerHTML; // ............................... Assert ................................. assert.ok(before === 'One' && after === null && html === "", 'Data link using: , and removing lastName - removes title attribute'); // ................................ Reset ................................ $("#result").empty(); person1.lastName = "One"; // reset Prop // =============================== Arrange =============================== jsv.templates('prop: ') .link("#result", person1); // ................................ Act .................................. before = $("#result span").html(); jsv.observable(person1).removeProperty("lastName"); after = $("#result span").html(); // ............................... Assert ................................. assert.ok(before === 'One' && after === "", 'Data link using: , and removing lastName - sets content to empty string'); // ................................ Reset ................................ $("#result").empty(); person1.lastName = "One"; // reset Prop // =============================== Arrange =============================== jsv.templates("prop: ") .link("#result", person1); // ................................ Act .................................. before = $("#result span").html(); jsv.observable(person1).removeProperty("lastName"); after = $("#result span").html(); // ............................... Assert ................................. assert.ok(before === 'One' && after === "", "Data link using: , and removing lastName - sets content to empty string"); // ................................ Reset ................................ $("#result").empty(); person1.lastName = "One"; // reset Prop // =============================== Arrange =============================== person1.lastName = ""; // initialize jsv.templates('prop: ') .link("#result", person1); // ................................ Act .................................. before = $("#result span")[0].getAttribute("title"); // ............................... Assert ................................. assert.ok(before === "", 'Data link using: , with lastName "" - initializes with title attribute set to ""'); // ................................ Reset ................................ $("#result").empty(); person1.lastName = "One"; // reset Prop // =============================== Arrange =============================== person1.lastName = undefined; // initialize jsv.templates('prop: ') .link("#result", person1); // ................................ Act .................................. before = $("#result span")[0].getAttribute("title"); // ............................... Assert ................................. assert.ok(before === null, 'Data link using: , with lastName undefined - initializes with title attribute null'); // ................................ Reset ................................ $("#result").empty(); person1.lastName = "One"; // reset Prop // =============================== Arrange =============================== person1.lastName = null; // initialize jsv.templates('prop: ') .link("#result", person1); // ................................ Act .................................. before = $("#result span")[0].getAttribute("title"); // ............................... Assert ................................. assert.ok(before === null, 'Data link using: , with lastName null - initializes with title attribute null'); // ................................ Reset ................................ $("#result").empty(); person1.lastName = "One"; // reset Prop // =============================== Arrange =============================== jsv.templates('prop: ') .link("#result", person1); // ................................ Act .................................. before = $("#result span")[0].getAttribute("title"); jsv.observable(person1).setProperty("lastName", false); after = $("#result span")[0].getAttribute("title"); // ............................... Assert ................................. assert.ok(before === 'One' && after === "false", 'Data link using: , and string lastName to false - sets title to "false"'); // ................................ Reset ................................ $("#result").empty(); person1.lastName = "One"; // reset Prop // =============================== Arrange =============================== jsv.templates('prop: ') .link("#result", person1); // ................................ Act .................................. before = $("#result span")[0].getAttribute("data-foo"); var beforeVal = $("#result span").data("foo"); jsv.observable(person1).setProperty("lastName", "Two"); after = $("#result span")[0].getAttribute("data-foo"); var afterVal = $("#result span").data("foo"); // ............................... Assert ................................. assert.ok(before === 'One' && before === beforeVal && after === "Two" && after === afterVal, 'Data link using: sets data-foo attribute to lastName value and sets $(elem).data("foo") to the same value'); // ................................ Reset ................................ $("#result").empty(); person1.lastName = "One"; // reset Prop // =============================== Arrange =============================== var originalAddress = person1.home.address; jsv.templates('prop: ') .link("#result", person1); // ................................ Act .................................. before = $("#result span")[0].getAttribute("data-foo"); beforeVal = $("#result span").data("foo"); var crazyAddress = [1,2]; jsv.observable(person1.home).setProperty("address", crazyAddress); after = $("#result span")[0].getAttribute("data-foo"); afterVal = $("#result span").data("foo"); // ............................... Assert ................................. assert.ok(before === originalAddress.toString() && beforeVal === originalAddress && after === crazyAddress.toString() && afterVal === crazyAddress, 'Data link using: sets data-foo attribute to address.toString() value and sets $(elem).data("foo") to address'); // ................................ Reset ................................ $("#result").empty(); person1.lastName = "One"; // reset Prop person1.home.address = originalAddress; // reset Prop // =============================== Arrange =============================== jsv.templates('prop: ') .link("#result", person1); // ................................ Act .................................. var el = $("#result span")[0]; before = el.getAttribute("data-foo") + el.getAttribute("data-foo-bar") + el.getAttribute("a-b-c") + el.getAttribute("a_b_c-d-e"); jsv.observable(person1).setProperty("lastName", "Two"); after = el.getAttribute("data-foo") + el.getAttribute("data-foo-bar") + el.getAttribute("a-b-c") + el.getAttribute("a_b_c-d-e"); // ............................... Assert ................................. assert.ok(before === 'OneOneOneOne' && after === "TwoTwoTwoTwo", 'Data link using: data-link="data-foo{:lastName} data-foo-bar{:lastName} a-b-c{:lastName} a_b_c-d-e{:lastName}" sets attributes to lastName value'); // ................................ Reset ................................ $("#result").empty(); person1.lastName = "One"; // reset Prop // =============================== Arrange =============================== jsv.templates('prop: ') .link("#result", person1); // ................................ Act .................................. before = $("#result span")[0].getAttribute("a-b_foo"); jsv.observable(person1).setProperty("lastName", "Two"); after = $("#result span")[0].getAttribute("a-b_foo"); // ............................... Assert ................................. assert.ok(before === 'One' && after === "Two", 'Data link using: sets a-b_foo attribute to lastName value'); // ................................ Reset ................................ $("#result").empty(); person1.lastName = "One"; // reset Prop // =============================== Arrange =============================== jsv.templates('prop: ') .link("#result", person1); // ................................ Act .................................. before = $("#result span")[0].style.display; jsv.observable(person1).setProperty("lastName", ""); after = $("#result span")[0].style.display; // ............................... Assert ................................. assert.ok(before === "inline" && after === "none", 'Data link using: , and string lastName to "" - sets display to "none"'); // ................................ Reset ................................ $("#result").empty(); person1.lastName = "One"; // reset Prop // =============================== Arrange =============================== jsv.templates('prop: ') .link("#result", person1); // ................................ Act .................................. before = $("#result span")[0].style.display; jsv.observable(person1).setProperty("lastName", null); after = $("#result span")[0].style.display; // ............................... Assert ................................. assert.ok(before === "inline" && after === "none", 'Data link using: , and string lastName to null - sets display to "none"'); // ................................ Reset ................................ $("#result").empty(); person1.lastName = "One"; // reset Prop // =============================== Arrange =============================== jsv.templates('prop: ') .link("#result", person1); // ................................ Act .................................. before = $("#result span")[0].style.display; jsv.observable(person1).setProperty("lastName", undefined); after = $("#result span")[0].style.display; // ............................... Assert ................................. assert.ok(before === "inline" && after === "none", 'Data link using: , and string lastName to undefined - sets display to "none"'); // ................................ Reset ................................ $("#result").empty(); person1.lastName = "One"; // reset Prop // =============================== Arrange =============================== jsv.templates('prop: ') .link("#result", person1); // ................................ Act .................................. before = $("#result span")[0].style.display; jsv.observable(person1).setProperty("lastName", false); after = $("#result span")[0].style.display; // ............................... Assert ................................. assert.ok(before === "inline" && after === "none", 'Data link using: , and string lastName to false - sets display to "none"'); // ................................ Reset ................................ $("#result").empty(); person1.lastName = "One"; // reset Prop // =============================== Arrange =============================== jsv.templates('prop: ') .link("#result", person1); // ................................ Act .................................. before = $("#result span")[0].style.display; jsv.observable(person1).setProperty("lastName", ""); after = $("#result span")[0].style.display; jsv.observable(person1).setProperty("lastName", "One"); var reset = $("#result span")[0].style.display; // ............................... Assert ................................. assert.equal(before + "|" + after + "|" + reset, "inline|none|inline", 'Data link using: , and toggling string lastName to "" and back - sets display to "inline"'); // =============================== Arrange =============================== jsv.templates('prop: ') .link("#result", person1); // ................................ Act .................................. before = $("#result span")[0].style.display; jsv.observable(person1).setProperty("lastName", ""); after = $("#result span")[0].style.display; jsv.observable(person1).setProperty("lastName", "One"); reset = $("#result span")[0].style.display; // ............................... Assert ................................. assert.equal(before + "|" + after + "|" + reset, "inline-block|none|inline-block", 'Data link using: , and toggling lastName - sets display to "inline-block"'); // =============================== Arrange =============================== jsv.templates('prop: ') .link("#result", person1); // ................................ Act .................................. before = $("#result span")[0].style.display; jsv.observable(person1).setProperty("lastName", ""); after = $("#result span")[0].style.display; jsv.observable(person1).setProperty("lastName", "One"); reset = $("#result span")[0].style.display; // ............................... Assert ................................. assert.equal(before + "|" + after + "|" + reset, "inline|none|inline", 'Data link using: , and toggling lastName - sets display to "inline"'); // =============================== Arrange =============================== jsv.templates('prop:
          ') .link("#result", person1); // ................................ Act .................................. before = $("#result div")[0].style.display; jsv.observable(person1).setProperty("missing", "foo"); after = $("#result div")[0].style.display; jsv.observable(person1).setProperty("missing", ""); reset = $("#result div")[0].style.display; // ............................... Assert ................................. assert.equal(before + "|" + after + "|" + reset, "none|block|none", 'Data link using:
          , and toggling lastName - sets display to "block"'); // ................................ Reset ................................ delete person1.missing; // =============================== Arrange =============================== jsv.templates('prop:
          ') .link("#result", person1); // ................................ Act .................................. before = $("#result div")[0].style.display; jsv.observable(person1).setProperty("lastName", ""); after = $("#result div")[0].style.display; jsv.observable(person1).setProperty("lastName", "One"); reset = $("#result div")[0].style.display; // ............................... Assert ................................. assert.equal(before + "|" + after + "|" + reset, "block|none|block", 'Repeat (block style for div is now cached) data link using:' + '
          , and toggling lastName - sets display to "block"'); // =============================== Arrange =============================== jsv.templates('prop: ') .link("#result", person1); // ................................ Act .................................. before = $("#result [style]")[0].style.display; jsv.observable(person1).setProperty("missing", "foo"); after = $("#result [style]")[0].style.display; jsv.observable(person1).setProperty("missing", ""); reset = $("#result [style]")[0].style.display; // ............................... Assert ................................. assert.equal(before + "|" + after + "|" + reset, "none|block|none", 'Data link using: , and toggling lastName - sets display to "block"'); // ................................ Reset ................................ $("#result").empty(); person1.lastName = "One"; // reset Prop // =============================== Arrange =============================== jsv.views.converters({ not: function(val) { return !val; } }); jsv.templates('prop: No name') .link("#result", person1); // ................................ Act .................................. before = $("#result span")[0].style.display; jsv.observable(person1).setProperty("lastName", ""); after = $("#result span")[0].style.display; jsv.observable(person1).setProperty("lastName", "One"); reset = $("#result span")[0].style.display; // ............................... Assert ................................. assert.equal(before + "|" + after + "|" + reset, "none|inline|none", 'Data link using: , and toggling lastName - sets display to "inline" if lastName is ""'); // ................................ Reset ................................ $("#result").empty(); person1.lastName = "One"; // reset Prop // =============================== Arrange =============================== jsv.views.tags({ chooseAttr: { init: function(tagCtx, linkCtx, ctx) { if (this.tagCtx.props.tagAttr) { this.attr = this.tagCtx.props.tagAttr; } if (this.tagCtx.props.linkCtxAttr) { linkCtx.attr = this.tagCtx.props.linkCtxAttr; } }, render: function(val) { return val.name ? val.name + "
          " : ""; }, attr: "text", depends: "name" } }); var thing = {name: "box"}; function divProps() { var div = $("#result div")[0]; return "title: " + div.title + " - innerHTML: " + div.innerHTML.replace(/<\/script>/g, "") + " - display: " + div.style.display; } // ................................ Act .................................. jsv.templates('
          xx
          ') .link("#result", thing); // ............................... Assert ................................. assert.equal(divProps(), "title: - innerHTML: box<br/> - display: ", "{chooseAttr} has target 'text'"); // ................................ Act .................................. jsv.templates('
          xx
          ') .link("#result", thing); // ............................... Assert ................................. assert.equal(divProps(), "title: box
          - innerHTML: xx - display: ", "{chooseAttr tagAttr=\'title\'} overrides tag.attr, and has target 'title'"); // ................................ Act .................................. jsv.templates('
          xx
          ') .link("#result", thing); // ............................... Assert ................................. assert.equal(divProps(), "title: - innerHTML: " + "box
          " + " - display: ", "{chooseAttr tagAttr=\'html\'} overrides tag.attr, and has target 'html'"); // ................................ Act .................................. jsv.templates('
          xx
          ') .link("#result", thing); // ............................... Assert ................................. assert.equal(divProps(), "title: box
          - innerHTML: xx - display: ", "{chooseAttr linkCtxAttr=\'title\' tagAttr=\'html\'} overrides linkCtx.attr, and has target 'title'"); // ................................ Act .................................. jsv.templates('
          xx
          ') .link("#result", thing); // ............................... Assert ................................. assert.equal(divProps(), "title: - innerHTML: " + "box
          " + " - display: ", "html{chooseAttr} has target 'html'"); // ................................ Act .................................. jsv.templates('
          xx
          ') .link("#result", thing); // ............................... Assert ................................. assert.equal(divProps(), "title: - innerHTML: " + "box
          " + " - display: ", "html{chooseAttr tagAttr =\'title\'} overrides tag.attr, but still has target 'html'"); // ................................ Act .................................. jsv.templates('
          xx
          ') .link("#result", thing); // ............................... Assert ................................. assert.equal(divProps(), "title: box
          - innerHTML: xx - display: ", "html{chooseAttr tagAttr =\'html\' linkCtxAttr=\'title\'} overrides tag.attr and linkCtx.attr, and has target 'title'"); // ................................ Act .................................. jsv.templates('
          xx
          ') .link("#result", thing); // ............................... Assert ................................. assert.equal(divProps(), "title: - innerHTML: xx - display: block", "visible{chooseAttr} has display 'block'"); // ................................ Act .................................. jsv.observable(thing).removeProperty("name"); // ............................... Assert ................................. assert.equal(divProps(), "title: - innerHTML: xx - display: none", "visible{chooseAttr} has display 'none' if {chooseAttr} returns ''"); // ................................ Act .................................. thing.name = "box"; jsv.templates('
          xx
          ') .link("#result", thing); // ............................... Assert ................................. assert.equal(divProps(), "title: - innerHTML: xx - display: block", "visible{chooseAttr tagAttr=\'title\'} overrides tag.attr, but still has target 'visible' and has display 'block'"); // ................................ Act .................................. jsv.templates('
          xx
          ') .link("#result", thing); // ............................... Assert ................................. assert.equal(divProps(), "title: - innerHTML: " + "box
          " + " - display: ", "visible{chooseAttr tagAttr=\'title\' linkCtxAttr=\'html\'} overrides tag.attr and linkCtx.attr, and has target 'html'"); // ................................ Act .................................. jsv.templates('
          xx
          ') .link("#result", thing); // ............................... Assert ................................. assert.equal(divProps(), "title: box
          - innerHTML: xx - display: ", "visible{chooseAttr tagAttr=\'html\' linkCtxAttr=\'title\'} overrides tag.attr and linkCtx.attr, and has target 'title'"); // ................................ Reset ................................ $("#result").empty(); res = ""; }); QUnit.test('data-link="{cvt:expression:cvtBack}"', function(assert) { var done; if (assert.async) { done = assert.async() } else { stop() } // ................................ Reset ................................ $("#result").empty(); person1._firstName = "Jo"; // reset Prop person1.lastName = "One"; // reset Prop person2._firstName = "Xavier"; // reset Prop person2.lastName = "Two"; // reset Prop settings.title = "Mr"; // reset Prop settings.width = 30; // reset Prop // =============================== Arrange =============================== jsv.templates('prop: ') .link("#result", person2); // ................................ Act .................................. before = $("#result span").html(); jsv.observable(person1).setProperty({firstName: "newOneFirst", lastName: "newOneLast"}); jsv.observable(person2).setProperty({firstName: "newTwoFirst", lastName: "newTwoLast"}); jsv.observable(settings).setProperty({title: "Sir", width: 40}); after = $("#result span").html(); // ............................... Assert ................................. assert.equal(before + "|" + after, "mr jotwo30Mr Xavier|sir newonefirstnewtwolast40Sir newTwoFirst", 'Data link using: - with declared dependencies for converter'); // ................................ Reset ................................ $("#result").empty(); person1._firstName = "Jo"; // reset Prop person1.lastName = "One"; // reset Prop person2._firstName = "Xavier"; // reset Prop person2.lastName = "Two"; // reset Prop settings.title = "Mr"; // reset Prop settings.width = 30; // reset Prop // =============================== Arrange =============================== jsv.views .converters({ from: function(val) { return val + "from" + this.tagCtx.props.frst; }, to: function(val) { return val + "to" + this.tagCtx.props.frst; } }); // =============================== Arrange =============================== jsv.templates('prop: ') .link("#result", person1); // ................................ Act .................................. var value = $("#twoWay").val(); keydown($("#twoWay").val(value + "+")); setTimeout(function() { // ............................... Assert ................................. assert.equal(person1.lastName, "One+toMr Jo", 'Data link using: with no convert. - convertBack called with tag as this pointer.'); // ................................ Reset ................................ $("#result").empty(); person1.lastName = "One"; // reset Prop // =============================== Arrange =============================== jsv.templates('prop: ') .link("#result", person1); // ................................ Act .................................. value = $("#twoWay").val(); keydown($("#twoWay").val(value + "+")); setTimeout(function() { // ............................... Assert ................................. assert.equal(person1.lastName + $("#twoWay").data().foo, "One+23", 'Data link using: with no convert. - convertBack called with tag as this pointer.'); // ................................ Reset ................................ $("#result").empty(); person1.lastName = "One"; // reset Prop // =============================== Arrange =============================== jsv.templates('prop: ') .link("#result", person1); // ................................ Act .................................. value = $("#twoWay").val(); keydown($("#twoWay").val(value + "+")); setTimeout(function() { // ............................... Assert ................................. assert.equal(person1.lastName, "OnefromMr Jo+toMr Jo", 'Data link using: - convert and convertBack called with tag as this pointer.'); // ................................ Reset ................................ $("#result").empty(); person1.lastName = "One"; // reset Prop // =============================== Arrange =============================== var tmpl = jsv.templates('prop: ' + '{{for true}}prop: {{/for}}'); jsv.views .converters({ from: function(val) { return val + "from" + this.tagCtx.props.frst; }, to: function(val) { return val + "to" + this.tagCtx.props.frst; } }, tmpl); // =============================== Arrange =============================== tmpl.link("#result", person1); // ................................ Act .................................. value = $("#twoWay").val(); keydown($("#twoWay").val(value + "+")); setTimeout(function() { // ............................... Assert ................................. assert.equal(person1.lastName, "OnefromMr Jo+toMr Jo", 'Data link using: - with converters local to template: convert and convertBack called with tag as this pointer.'); // ................................ Act .................................. value = $("#twoWayInner").val(); keydown($("#twoWayInner").val(value + "+")); setTimeout(function() { // ............................... Assert ................................. assert.equal(person1.lastName, "OnefromMr Jo+toMr JofromMr Jo+toMr Jo", 'Data link using: in nested block - with converters local to template: convert and convertBack called with tag as this pointer.'); // ................................ Reset ................................ $("#result").empty(); person1.lastName = "One"; // reset Prop if (assert.async) { done() } else { start() } }, 0); }, 0); }, 0); }, 0); }, 0); }); QUnit.test('Two-way binding', function(assert) { var done; if (assert.async) { done = assert.async() } else { stop() } // =============================== Arrange =============================== var tmpl = jsv.templates('
          '); var model = { address: {street: "First St"} }; // ............................... Act ................................. tmpl.link("#result", model); res = $("#result").text() + " | "; keydown($("#result input").val('1st Ave')); setTimeout(function() { res += $("#result").text() + " | " + model.address.street; // ............................... Assert ................................. assert.equal(res, "First St | 1st Ave | 1st Ave", ''); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== function getSelection() { return model.selected + ":" + ($("#result select option:selected").text()||"-") + "(" + $("#result select")[0].selectedIndex + ")|"; } tmpl = jsv.templates(''); model = { selected: "Jim", people: [ {name: "Bob"}, {name: "Jim"} ] } var newName = "new"; // ............................... Act ................................. tmpl.link("#result", model); res = getSelection(); jsv.observable(model.people).insert({ name: newName }); jsv.observable(model).setProperty("selected", newName); res += getSelection(); jsv.observable(model.people).remove(2); res += getSelection(); $("#result select").val('Jim').change(); res += getSelection(); // ............................... Assert ................................. assert.equal(res, "Jim:Jim(1)|new:new(2)|new:-(-1)|Jim:Jim(1)|", '{^{for people}}{{/for}}'); model = { selected: "Jim", people: [ {name: "Bob"}, {name: "Jim"} ] }; // ............................... Act ................................. tmpl.link("#result", model); res = getSelection(); jsv.observable(model.people).insert({ name: newName }); jsv.observable(model).setProperty("selected", newName); res += getSelection(); jsv.observable(model.people).remove(2); res += getSelection(); $("#result select").val('Jim').change(); res += getSelection(); // ............................... Assert ................................. assert.equal(res, "Jim:JIM(1)|new:NEW(2)|new:-(-1)|Jim:JIM(1)|", '{^{for people}}{{/for}}'); model = { selected: "Jim", people: [ {name: "Bob"}, {name: "Jim"} ] }; // ............................... Act ................................. tmpl.link("#result", model); res = getSelection(); jsv.observable(model.people).insert({ name: newName }); jsv.observable(model).setProperty("selected", newName); res += getSelection(); jsv.observable(model.people).remove(2); res += getSelection(); $("#result select").val('Jim').change(); res += getSelection(); // ............................... Assert ................................. assert.equal(res, "Jim:Jim(1)|new:new(2)|new:-(-1)|Jim:Jim(1)|", '{^{for people}}{{/for}}'); model = { selected: "Jim", people: [ {name: "Bob"}, {name: "Jim"} ] }; // ............................... Act ................................. tmpl.link("#result", model); res = getSelection(); jsv.observable(model.people).insert({ name: newName }); jsv.observable(model).setProperty("selected", newName); res += getSelection(); jsv.observable(model.people).remove(2); res += getSelection(); $("#result select").val('Jim').change(); res += getSelection(); // ............................... Assert ................................. assert.equal(res, "Jim:JIM(1)|new:NEW(2)|new:-(-1)|Jim:JIM(1)|", '{^{for people}}{{/for}}'); model = { selected: "Jim", people: [ {name: "Bob"}, {name: "Jim"} ] }; // ............................... Act ................................. tmpl.link("#result", model); res = getSelection(); jsv.observable(model.people).insert({ name: newName }); jsv.observable(model).setProperty("selected", newName); res += getSelection(); jsv.observable(model.people).remove(2); res += getSelection(); $("#result select").val('Jim').change(); res += getSelection(); // ............................... Assert ................................. assert.equal(res, "Jim:JIM(1)|new:NEW(2)|new:-(-1)|Jim:JIM(1)|", '{^{for items}}{{/for}}'); model = { selected: 4, items: [] }; // ............................... Act ................................. tmpl.link("#result", model); res = getSelection(); jsv.observable(model.items).insert(0, [4,5,6]); res += getSelection(); // ............................... Assert ................................. assert.equal(res, "4:-(-1)|4:4(0)|", '{^{for items}}{{/for}}', converters: { cvt: function (value) { return value } } }); model = { selected: 4, items: [] }; // ............................... Act ................................. tmpl.link("#result", model); res = getSelection(); jsv.observable(model.items).insert(0, [4,5,6]); res += getSelection(); // ............................... Assert ................................. assert.equal(res, "4:-(-1)|4:4(0)|", '{^{for items}}{{/for}}', converters: { cvt: function (value) { return value } } }); model = { selected: 4, items: [] }; // ............................... Act ................................. tmpl.link("#result", model); res = getSelection(); jsv.observable(model.items).insert(0, [4,5,6]); res += getSelection(); // ............................... Assert ................................. assert.equal(res, "4:-(-1)|4:4(0)|", '{^{for items}}{{/for}}', converters: { cvt: function (value) { return value } } }); model = { selected: 4, items: [] }; // ............................... Act ................................. tmpl.link("#result", model); res = getSelection(); jsv.observable(model.items).insert(0, [4,5,6]); res += getSelection(); // ............................... Assert ................................. assert.equal(res, "4:-(-1)|4:4(0)|", '{^{for items}}{{/for}}'); model = { selected: 4, items: [1,2,3], items2: [4,5,6] }; // ............................... Act ................................. tmpl.link("#result", model); res = getSelection(); jsv.observable(model).setProperty('items', model.items2); res += getSelection(); // ............................... Assert ................................. assert.equal(res, "4:-(-1)|4:4(0)|", '{^{for items}}{{/for}}', converters: { lower: function(val) { return val.toLowerCase(); }, upper: function(val) { return val.toUpperCase(); } } }); model = { selected: "B", items: [] }; // ............................... Act ................................. tmpl.link("#result", model); // ............................... Assert ................................. assert.equal(testSelect(model), "B:-(-1)|B:b(1)|B:-(-1)|B:b(2)|x:x(1)|C:c(0)|", '{^{for items}}{{/for}}', converters: { lower: function(val) { return val.toLowerCase(); }, upper: function(val) { return val.toUpperCase(); } } }); model = { selected: "B", items: [] }; // ............................... Act ................................. tmpl.link("#result", model); // ............................... Assert ................................. assert.equal(testSelect(model), "B:-(-1)|B:b(1)|B:-(-1)|B:b(2)|x:x(1)|C:c(0)|", '{^{for items}}{{/for}}', helpers: { lower: function(val) { return val.toLowerCase(); }, upper: function(val) { return val.toUpperCase(); } } }); model = { selected: "B", items: [] }; // ............................... Act ................................. tmpl.link("#result", model); // ............................... Assert ................................. assert.equal(testSelect(model), "B:-(-1)|B:b(1)|B:-(-1)|B:b(2)|x:x(1)|C:c(0)|", '{^{for ~root.items}}{{/for}}", linkedElement: "select" } } }); model = { selected: "b", selected2: "c", items: [] }; // ............................... Act ................................. tmpl.link("#result", model); // ............................... Assert ................................. assert.equal(testSelect(model), "b:-(-1)|b:bc(1)|b:-(-1)|b:bc(2)|x:xc(1)|c:cc(0)|", 'Custom selTag with linkedElement:"select"... Dynamic subsequent insertion of selected option'); // =============================== Arrange =============================== tmpl = jsv.templates({ markup: '{^{selTag 1 selected options=items}}{{else 1 selected2 options=items}}{{/selTag}}', tags: { selTag: { template: "", linkedElement: [undefined, "select"], bindTo: [0, 1], convert: function(foo, val) { return [foo, val.toLowerCase()]; }, convertBack: function(foo, val) { return [foo, val.toUpperCase()]; } } } }); model = { selected: "B", selected2: "C", items: [] }; // ............................... Act ................................. tmpl.link("#result", model); // ............................... Assert ................................. assert.equal(testSelect(model), "B:-(-1)|B:bc(1)|B:-(-1)|B:bc(2)|x:xc(1)|C:cc(0)|", 'Custom selTag with linkedElement:[undefined, "select"] and converters... Dynamic subsequent insertion of selected option'); // =============================== Arrange =============================== tmpl = jsv.templates({ markup: '{^{selTag 1 selected options=items}}{{else 1 selected2 options=items}}{{/selTag}}', converters: { lower: function(foo, val) { return [foo, val.toLowerCase()]; }, upper: function(foo, val) { return [foo, val.toUpperCase()]; } }, tags: { selTag: { template: "", linkedElement: [undefined, "select"], bindTo: [0, 1], convert: "lower", convertBack: "upper" } } }); model = { selected: "B", selected2: "C", items: [] }; // ............................... Act ................................. tmpl.link("#result", model); // ............................... Assert ................................. assert.equal(testSelect(model), "B:-(-1)|B:bc(1)|B:-(-1)|B:bc(2)|x:xc(1)|C:cc(0)|", 'Custom selTag with linkedElement:[undefined, "select"] and registered converters... Dynamic subsequent insertion of selected option'); // =============================== Arrange =============================== tmpl = jsv.templates({ markup: '{^{selTag 1 selected options=items convert=~lower convertBack=\'upper\'}}{{else 1 selected2 options=items}}{{/selTag}}', converters: { upper: function(foo, val) { return [foo, val.toUpperCase()]; } }, helpers: { lower: function(foo, val) { return [foo, val.toLowerCase()]; } }, tags: { selTag: { template: "", linkedElement: [undefined, "select"], bindTo: [0, 1] } } }); model = { selected: "B", selected2: "C", items: [] }; // ............................... Act ................................. tmpl.link("#result", model); // ............................... Assert ................................. assert.equal(testSelect(model), "B:-(-1)|B:bc(1)|B:-(-1)|B:bc(2)|x:xc(1)|C:cc(0)|", 'Custom selTag with linkedElement:[undefined, "select"] and external converters... Dynamic subsequent insertion of selected option'); // =============================== Arrange =============================== tmpl = jsv.templates({ markup: '{^{selTag selected options=items convert=~lower convertBack=\'upper\'}}{{else selected2 options=items}}{{/selTag}}', converters: { upper: function(val) { return val.toUpperCase(); } }, helpers: { lower: function(val) { return val.toLowerCase(); } }, tags: { selTag: { template: "", linkedCtxParam: "sel" } } }); model = { selected: "B", selected2: "C", items: [] }; // ............................... Act ................................. tmpl.link("#result", model); // ............................... Assert ................................. assert.equal(testSelect(model), "B:-(-1)|B:bc(1)|B:-(-1)|B:bc(2)|x:xc(1)|C:cc(0)|", 'Custom selTag with linkedCtxParam:"sel" and external converters... Dynamic subsequent insertion of selected option'); // =============================== Arrange =============================== tmpl = jsv.templates({ markup: '{^{selTag 1 selected options=items}}{{else 1 selected2 options=items}}{{/selTag}}', converters: { lower: function(foo, val) { return [foo, val.toLowerCase()]; } }, tags: { selTag: { template: "", bindTo: [0, 1], linkedCtxParam: [undefined, "sel"], convert: "lower", convertBack: function(foo, val) { return [foo, val.toUpperCase()]; } } } }); model = { selected: "B", selected2: "C", items: [] }; // ............................... Act ................................. tmpl.link("#result", model); // ............................... Assert ................................. assert.equal(testSelect(model), "B:-(-1)|B:bc(1)|B:-(-1)|B:bc(2)|x:xc(1)|C:cc(0)|", 'Custom selTag with linkedCtxParam:[undefined, "sel"] and converters... Dynamic subsequent insertion of selected option'); // =============================== Arrange =============================== tmpl = jsv.templates({ markup: '{^{selTag 1 selected options=items convert=~lower convertBack=\'upper\'}}{{else 1 selected2 options=items}}{{/selTag}}', converters: { upper: function(foo, val) { return [foo, val.toUpperCase()]; } }, helpers: { lower: function(foo, val) { return [foo, val.toLowerCase()]; } }, tags: { selTag: { template: "", bindTo: [0, 1], linkedCtxParam: [undefined, "sel"], } } }); model = { selected: "B", selected2: "C", items: [] }; // ............................... Act ................................. tmpl.link("#result", model); // ............................... Assert ................................. assert.equal(testSelect(model), "B:-(-1)|B:bc(1)|B:-(-1)|B:bc(2)|x:xc(1)|C:cc(0)|", 'Custom selTag with linkedCtxParam:[undefined, "sel"] and external converters... Dynamic subsequent insertion of selected option'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== tmpl = jsv.templates(''); model = { selected: ["Jim","Bob"], people: [ {name: "Bob"}, {name: "Jim"} ] }; // ............................... Act ................................. tmpl.link("#result", model); res = $("#result select")[0].multiple + getSelection(); jsv.observable(model.people).insert({ name: newName }); jsv.observable(model).setProperty("selected", [newName, "Bob"]); res += getSelection(); jsv.observable(model.people).remove(2); res += getSelection(); jsv.observable(model.people).insert([ { name: "Pete" }, { name: "Jo" } ]); $("#result select").val(['Jo']).change(); res += getSelection(); $("#result select").val([]).change(); res += getSelection(); $("#result select").val(["Bob", "Pete", "Jim"]).change(); res += getSelection(); jsv.observable(model).setProperty("selected", "Bob"); res += getSelection(); // ............................... Assert ................................. assert.equal(res, "trueJim,Bob:BOBJIM(0)|new,Bob:BOBNEW(0)|new,Bob:BOB(0)|Jo:JO(3)|:-(-1)|Bob,Jim,Pete:BOBJIMPETE(0)|Bob:BOB(0)|", 'Multiselect with {^{for people}}{{/for}}'); model = { selected: "J", people: [ {name: "Bob", id: "B"}, {name: "Noone", id: ""}, {name: "Jim", id: "J"} ] }; // ............................... Act ................................. tmpl.link("#result", model); res = $("#result select")[0].multiple + $("#result select option:selected").text() + ":" + $("#result select")[0].selectedIndex + "|"; jsv.observable(model).setProperty("selected", ["", "J"]); res += $("#result select option:selected").text() + ":" + $("#result select")[0].selectedIndex + "|"; jsv.observable(model).setProperty("selected", "B"); res += $("#result select option:selected").text() + ":" + $("#result select")[0].selectedIndex + "|"; jsv.observable(model).setProperty("selected", []); res += $("#result select option:selected").text() + ":" + $("#result select")[0].selectedIndex + "|"; jsv.observable(model).setProperty("selected", ""); res += $("#result select option:selected").text() + ":" + $("#result select")[0].selectedIndex + "|"; jsv.observable(model).setProperty("selected", ["J"]); res += $("#result select option:selected").text() + ":" + $("#result select")[0].selectedIndex + "|"; jsv.observable(model).setProperty("selected", null); res += $("#result select")[0].multiple + $("#result select option:selected").text() + ":" + $("#result select")[0].selectedIndex + "|"; // ............................... Assert ................................. assert.equal(res, "trueJim:2|NooneJim:1|Bob:0|:-1|Noone:1|Jim:2|trueNoone:1|", 'Multiselect with \ \ \ \ {^{:foo}}\ {^{:#data.foo}}\ {^{:#view.data.foo}}'); // ............................... Act ................................. tmpl.link("#result", {foo:"F"}); var cnt = 0; res = ""; getContent(); $("#result input").each(function() { keydown($("#result input").val(cnt++)); }); setTimeout(function() { getContent(); assert.equal(res, "FFFFFF|222222|", 'Two-way binding to foo, #data.foo #view.data.foo'); // ................................ Reset ................................ $("#result").empty(); if (assert.async) { done() } else { start() } }, 0); }, 0); }); QUnit.test('data-link="{radiogroup}"', function(assert) { // =============================== Arrange =============================== var topLevel = '
          ' + '' + '' + '
          ' + '
          ' + '' + '' + '
          '; $("#result").html(topLevel); // ............................... Act ................................. var model = { selected: "Jim" }; jsv.link(true, "#result", model); res = $("#result input:checked").parent().text() + "|"; jsv.observable(model).setProperty("selected", "Bob"); res += $("#result input:checked").parent().text() + "|"; $("#result input").eq(1).prop("checked", true).change(); // Check second radio button res += $("#result input:checked").parent().text() + "|" + model.selected; // ............................... Assert ................................. assert.equal(res, ":Jim:Jim|:Bob:Bob|:Jim:Jim|Jim", 'data-link="{radiogroup selected}" top-level'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== topLevel = '
          ' + '
          '; var tmpl = jsv.templates(''), model = { selected: "Jim", people: [ {name: "Bob"}, {name: "Jim"} ] }, newName = "new"; $("#result").html(topLevel); // ............................... Act ................................. jsv.link(true, "#result", model, {itemTmpl: tmpl}); res = $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).insert({ name: newName }); jsv.observable(model).setProperty("selected", newName); res += $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).remove(2); res += $("#result input:checked").parent().text() + "|"; $("#result input").eq(1).prop("checked", true).change(); // Check first radio button res += model.selected + "-" + $("#result input:checked").parent().text() + "|"; // ............................... Assert ................................. assert.equal(res, ":Jim:Jim|:new:new||Jim-:Jim:Jim|", 'data-link="{radiogroup selected}" ... {^{for ...}}...'); // =============================== Arrange =============================== tmpl = jsv.templates( '
          ' + '{^{for people}}' + '' + '{{/for}}' + '
          ' ); model = { selected: "Jim", people: [ {name: "Bob"}, {name: "Jim"} ] }, newName = "new"; // ............................... Act ................................. tmpl.link("#result", model); res = $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).insert({ name: newName }); jsv.observable(model).setProperty("selected", newName); res += $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).remove(2); res += $("#result input:checked").parent().text() + "|"; $("#result input").first().prop("checked", true).change(); // Check first radio button res += model.selected + "-" + $("#result input:checked").parent().text() + "|"; // ............................... Assert ................................. assert.equal(res, ":Jim|:new||Bob-:Bob|", 'data-link="{radiogroup selected}" ... {^{for ...}}...'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== tmpl = jsv.templates( '
          ' + '' + '{^{for people}}' + '' + '{{/for}}' + '
          ' ); model = { selected: "Jim", people: [ {name: "Bob"}, {name: "Jim"} ] }; // ............................... Act ................................. tmpl.link("#result", model); res = $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).insert({ name: newName }); jsv.observable(model).setProperty("selected", newName); res += $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).remove(2); res += $("#result input:checked").parent().text() + "|"; $("#result input").first().prop("checked", true).change(); // Check first radio button res += model.selected + "-" + $("#result input:checked").parent().text() + "|"; // ............................... Assert ................................. assert.equal(res, ":JIM|:NEW||None-:NONE|", 'data-link="{radiogroup selected}" ... ...{^{for ...}}...'); // ............................... Act ................................. jsv.observable(model.people[1]).setProperty("name", "jimUpdated"); res = $("#result").text() + "|"; $("#result input").eq(2).prop("checked", true).change(); // Check third radio button res += model.selected + "-" + $("#result input:checked").parent().text() + "|"; assert.equal(res, ":NONE:BOB:JIMUPDATED|jimUpdated-:JIMUPDATED|", 'data-link="{radiogroup selected}" ... {^{for ...}}... - updated label and value'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== tmpl = jsv.templates({markup: '
          ' + '' + '{^{for people}}' + '' + '{{/for}}' + '
          ', converters: { upper: function(val) { return val.toUpperCase(); } } }); model = { selected: "JIM", people: [ {name: "bob"}, {name: "jim"} ] }; // ............................... Act ................................. tmpl.link("#result", model, { lower: function(val) { return val.toLowerCase(); } }); res = $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).insert({ name: newName }); jsv.observable(model).setProperty("selected", newName); res += $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).remove(2); res += $("#result input:checked").parent().text() + "|"; $("#result input").first().prop("checked", true).change(); // Check first radio button res += model.selected + "-" + model.selectedOut + "-" + $("#result input:checked").parent().text() + "|"; // ............................... Assert ................................. assert.equal(res, ":jim|:new||new-NONE-:none|", 'data-link="{radiogroup selected convert=... convertBack=... linkTo=...}"'); // ............................... Act ................................. jsv.observable(model.people[1]).setProperty("name", "jimUpdated"); res = $("#result").text() + "|"; $("#result input").eq(2).prop("checked", true).change(); // Check third radio button res += model.selected + "-" + model.selectedOut + "-" + $("#result input:checked").parent().text() + "|"; assert.equal(res, ":none:bob:jimUpdated|new-JIMUPDATED-:jimUpdated|", 'data-link="{radiogroup selected convert=... convertBack=... linkTo=...}" - updated label and value'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== tmpl = jsv.templates( '
          ' + ':' + '{^{for people}}' + ':' + '{{/for}}' + '
          ' ); model = { selected: "Jim", people: [ {name: "Bob"}, {name: "Jim"} ] }; // ............................... Act ................................. tmpl.link("#result", model); res = $("#" + $("#result input:checked").prop("id") + "Lbl").text() + "|"; jsv.observable(model.people).insert({ name: newName }); jsv.observable(model).setProperty("selected", newName); res += $("#" + $("#result input:checked").prop("id") + "Lbl").text() + "|"; jsv.observable(model.people).remove(2); res += $("#" + $("#result input:checked").prop("id") + "Lbl").text() + "|"; $("#result input").first().prop("checked", true).change(); // Check first radio button res += model.selected + "-" + $("#" + $("#result input:checked").prop("id") + "Lbl").text() + "|"; // ............................... Assert ................................. assert.equal(res, "JIM|NEW||None-NONE|", 'data-link="{radiogroup selected}" with labels by for/id'); // ............................... Act ................................. jsv.observable(model.people[1]).setProperty("name", "jimUpdated"); res = $("#result").text() + "|"; $("#result input").eq(2).prop("checked", true).change(); // Check third radio button res += model.selected + "-" + $("#" + $("#result input:checked").prop("id") + "Lbl").text() + "|"; assert.equal(res, ":NONE:BOB:JIMUPDATED|jimUpdated-JIMUPDATED|", 'data-link="{radiogroup selected}" with labels by for/id - updated label and value'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== tmpl = jsv.templates( '
          ' + '' + '{^{for people}}' + '' + '{{/for}}' + '
          ' + '
          ' + '' + '{^{for people}}' + '' + '{{/for}}' + '
          ' ); model = { selected: "Jim", people: [ {name: "Bob"}, {name: "Jim"} ] }; // ............................... Act ................................. tmpl.link("#result", model); res = $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).insert({ name: newName }); jsv.observable(model).setProperty("selected", newName); res += $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).remove(2); res += $("#result input:checked").parent().text() + "|"; $("#result input").first().prop("checked", true).change(); // Check first radio button res += model.selected + "-" + $("#result input:checked").parent().text() + "|"; // ............................... Assert ................................. assert.equal(res, ":JIM:JIM|:NEW:NEW||None-:NONE:NONE|", 'data-link="{radiogroup selected}" - two radiogroups with same selected bindings'); // ............................... Act ................................. jsv.observable(model.people[1]).setProperty("name", "jimUpdated"); res = $("#result").text() + "|"; $("#result input").eq(2).prop("checked", true).change(); // Check third radio button res += model.selected + "-" + $("#result input:checked").parent().text() + "|"; assert.equal(res, ":NONE:BOB:JIMUPDATED:NONE:BOB:JIMUPDATED|jimUpdated-:JIMUPDATED:JIMUPDATED|", 'data-link="{radiogroup selected}" - two radiogroups with same selected bindings - updated label and value'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== tmpl = jsv.templates( '
          ' + '{^{for people}}' + '' + '{{/for}}' + '
          ' + '
          ' + '' + '{^{for people}}' + '' + '{{/for}}' + '
          ' ); model = { selected: "Jim", people: [] }; // ............................... Act ................................. tmpl.link("#result", model); res = $("#result").text() + "|" + $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).insert([{name: "Bob"},{name: "Jim"},{name: "newName"}]); res += $("#result").text() + "|" + $("#result input:checked").parent().text() + "|"; jsv.observable(model).setProperty("selected", newName); res += $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).remove(2); res += $("#result input:checked").parent().text() + "|"; $("#result input").first().prop("checked", true).change(); // Check first radio button res += model.selected + "-" + $("#result input:checked").parent().text() + "|"; // ............................... Assert ................................. assert.equal(res, ":NONE||:BOB:JIM:NEWNAME:NONE:BOB:JIM:NEWNAME|:JIM:JIM|||Bob-:BOB:BOB|", 'data-link="{radiogroup selected}" - two radiogroups with same selected bindings - starting out with no items, so no radio buttons'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== tmpl = jsv.templates( '
          ' + '{^{for people}}' + '' + '{{/for}}' + '
          ' + '
          ' + '' + '{^{for people}}' + '' + '{{/for}}' + '
          ' ); model = { selected: "Jim", people: [] }; // ............................... Act ................................. tmpl.link("#result", model); res = $("#result").text() + "|" + $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).insert([{name: "Bob"},{name: "Jim"},{name: "newName"}]); res += $("#result").text() + "|" + $("#result input:checked").parent().text() + "|"; jsv.observable(model).setProperty("selected", newName); res += $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).remove(2); res += $("#result input:checked").parent().text() + "|"; $("#result input").first().prop("checked", true).change(); // Check first radio button res += model.selected + "-" + $("#result input:checked").parent().text() + "|" + $("#result input:checked")[0].name + "|" + $("#result input:checked")[1].name; // ............................... Assert ................................. assert.equal(res, ":NONE||:BOB:JIM:NEWNAME:NONE:BOB:JIM:NEWNAME|:JIM:JIM|||Bob-:BOB:BOB|rad1|rad2", 'data-link="{radiogroup selected}" - name for group can be specified rather than auto-generated - on item or on radiogroup tag'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== tmpl = jsv.templates( '{^{if rg1}}' + '
          ' + '{^{for people}}' + '' + '{{/for}}' + '
          ' + '{{/if}}' + '{^{if rg2}}' + '
          ' + '' + '{^{for people}}' + '' + '{{/for}}' + '
          ' + '{{/if}}' ); model = { rg1: true, rg2: true, selected: "Jim", people: [] }; newName = "newName"; // ............................... Act ................................. tmpl.link("#result", model); res = ">" + $("#result").text() + "|" + $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).insert([{name: "Bob"},{name: "Jim"},{name: "newName"}]); res += $("#result").text() + "|" + $("#result input:checked").parent().text() + "|"; jsv.observable(model).setProperty("selected", newName); res += $("#result input:checked").parent().text() + "|"; // ............................... Assert ................................. assert.equal(res, ">:NONE||:BOB:JIM:NEWNAME:NONE:BOB:JIM:NEWNAME|:JIM:JIM|:NEWNAME:NEWNAME|", 'data-link="{radiogroup selected}" - two radiogroups wrapped in {{if}} blocks - starting out with no items'); jsv.observable(model.people).refresh([]); jsv.observable(model).setProperty("rg1", false); res = ">" + $("#result").text() + "|" + $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).insert([{name: "Bob"},{name: "Jim"},{name: "newName"}]); res += $("#result").text() + "|" + $("#result input:checked").parent().text() + "|"; jsv.observable(model).setProperty("selected", newName); res += $("#result input:checked").parent().text() + "|"; // ............................... Assert ................................. assert.equal(res, ">:NONE||:NONE:BOB:JIM:NEWNAME|:NEWNAME|:NEWNAME|", 'data-link="{radiogroup selected}" - two radiogroups wrapped in {{if}} blocks - one set to false'); jsv.observable(model.people).refresh([]); jsv.observable(model).setProperty("selected", "Jim"); jsv.observable(model).setProperty("rg1", true); res = ">" + $("#result").text() + "|" + $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).insert([{name: "Bob"},{name: "Jim"},{name: "newName"}]); res += $("#result").text() + "|" + $("#result input:checked").parent().text() + "|"; jsv.observable(model).setProperty("selected", newName); res += $("#result input:checked").parent().text() + "|"; // ............................... Assert ................................. assert.equal(res, ">:NONE||:BOB:JIM:NEWNAME:NONE:BOB:JIM:NEWNAME|:JIM:JIM|:NEWNAME:NEWNAME|", 'data-link="{radiogroup selected}" - two radiogroups wrapped in {{if}} blocks - one set back to true'); // ................................ Reset ................................ $("#result").empty(); }); QUnit.test('data-link="{checkboxgroup}"', function(assert) { jsv.views.settings.advanced({_jsv: true}); // For using viewsAndBindings($) // =============================== Arrange =============================== var topLevel = '
          ' + '' + '' + '
          ' + '
          ' + '' + '' + '
          '; $("#result").html(topLevel); // ............................... Act ................................. var model = { selected: ["Jim"] }; jsv.link(true, "#result", model); res = $("#result input:checked").parent().text() + "|"; jsv.observable(model).setProperty("selected", ["Bob"]); res += $("#result input:checked").parent().text() + "|"; $("#result input").eq(1).prop("checked", true).change(); // Check second checkbox button res += $("#result input:checked").parent().text() + "|" + model.selected; // ............................... Assert ................................. assert.equal(res, ":Jim:Jim|:Bob:Bob|:Bob:Jim:Bob:Jim|Bob,Jim", 'data-link="{checkboxgroup selected}" top-level'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== topLevel = '
          ' + '
          '; var tmpl = jsv.templates(''), model = { selected: ["Jim"], people: [ {name: "Bob"}, {name: "Jim"} ] }, newName = "new"; $("#result").html(topLevel); // ............................... Act ................................. jsv.link(true, "#result", model, {itemTmpl: tmpl}); res = $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).insert({ name: newName }); jsv.observable(model).setProperty("selected", [newName]); res += $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).remove(2); res += $("#result input:checked").parent().text() + "|"; $("#result input").eq(1).prop("checked", true).change(); // Check first checkbox button res += model.selected + "-" + $("#result input:checked").parent().text() + "|"; // ............................... Assert ................................. assert.equal(res, ":Jim:Jim|:new:new||new,Jim-:Jim:Jim|", 'data-link="{checkboxgroup selected}" ... {^{for ...}}...'); // =============================== Arrange =============================== tmpl = jsv.templates( '
          ' + '{^{for people}}' + '' + '{{/for}}' + '
          ' ); model = { selected: ["Jim"], people: [ {name: "Bob"}, {name: "Jim"} ] }, newName = "new"; // ............................... Act ................................. tmpl.link("#result", model); res = $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).insert({ name: newName }); jsv.observable(model).setProperty("selected", [newName]); res += $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).remove(2); res += $("#result input:checked").parent().text() + "|"; $("#result input").first().prop("checked", true).change(); // Check first checkbox button res += model.selected + "-" + $("#result input:checked").parent().text() + "|"; // ............................... Assert ................................. assert.equal(res, ":Jim|:new||new,Bob-:Bob|", 'data-link="{checkboxgroup selected}" ... {^{for ...}}...'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== tmpl = jsv.templates( '
          ' + '' + '{^{for people}}' + '' + '{{/for}}' + '
          ' ); model = { selected: ["Jim"], people: [ {name: "Bob"}, {name: "Jim"} ] }; // ............................... Act ................................. tmpl.link("#result", model); res = $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).insert({ name: newName }); jsv.observable(model).setProperty("selected", [newName]); res += $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).remove(2); res += $("#result input:checked").parent().text() + "|"; $("#result input").first().prop("checked", true).change(); // Check first checkbox button res += model.selected + "-" + $("#result input:checked").parent().text() + "|"; // ............................... Assert ................................. assert.equal(res, ":JIM|:NEW||new,None-:NONE|", 'data-link="{checkboxgroup selected}" ... ...{^{for ...}}...'); // ............................... Act ................................. jsv.observable(model.people[1]).setProperty("name", "jimUpdated"); res = $("#result").text() + "|"; $("#result input").eq(2).prop("checked", true).change(); // Check third checkbox button res += model.selected + "-" + $("#result input:checked").parent().text() + "|"; assert.equal(res,":NONE:BOB:JIMUPDATED|new,None,jimUpdated-:NONE:JIMUPDATED|", 'data-link="{checkboxgroup selected}" ... {^{for ...}}... - updated label and value'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== tmpl = jsv.templates({markup: '
          ' + '' + '{^{for people}}' + '' + '{{/for}}' + '
          ', converters: { upper: function(val) { return val.map(function(x) {return x.toUpperCase();}) } } }); model = { selected: ["JIM"], people: [ {name: "bob"}, {name: "jim"} ] }; // ............................... Act ................................. tmpl.link("#result", model, { lower: function(val) { return val.map(function(x) {return x.toLowerCase();}) } }); res = $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).insert({ name: newName }); jsv.observable(model).setProperty("selected", [newName]); res += $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).remove(2); res += $("#result input:checked").parent().text() + "|"; $("#result input").first().prop("checked", true).change(); // Check first checkbox button res += model.selected + "-" + model.selectedOut + "-" + $("#result input:checked").parent().text() + "|"; // ............................... Assert ................................. assert.equal(res, ":jim|:new||new-NONE-:none|", 'data-link="{checkboxgroup selected convert=... convertBack=... linkTo=...}"'); // ............................... Act ................................. jsv.observable(model.people[1]).setProperty("name", "jimUpdated"); res = $("#result").text() + "|"; $("#result input").eq(2).prop("checked", true).change(); // Check third checkbox button res += model.selected + "-" + model.selectedOut + "-" + $("#result input:checked").parent().text() + "|"; assert.equal(res, ":none:bob:jimUpdated|new-NONE,JIMUPDATED-:none:jimUpdated|", 'data-link="{checkboxgroup selected convert=... convertBack=... linkTo=...}" - updated label and value'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== tmpl = jsv.templates( '
          ' + ':' + '{^{for people}}' + ':' + '{{/for}}' + '
          ' ); model = { selected: ["Jim"], people: [ {name: "Bob"}, {name: "Jim"} ] }; // ............................... Act ................................. tmpl.link("#result", model); res = $("#" + $("#result input:checked").prop("id") + "Lbl").text() + "|"; jsv.observable(model.people).insert({ name: newName }); jsv.observable(model).setProperty("selected", [newName]); res += $("#" + $("#result input:checked").prop("id") + "Lbl").text() + "|"; jsv.observable(model.people).remove(2); res += $("#" + $("#result input:checked").prop("id") + "Lbl").text() + "|"; $("#result input").first().prop("checked", true).change(); // Check first checkbox button res += model.selected + "-" + $("#" + $("#result input:checked").prop("id") + "Lbl").text() + "|"; // ............................... Assert ................................. assert.equal(res, "JIM|NEW||new,None-NONE|", 'data-link="{checkboxgroup selected}" with labels by for/id'); // ............................... Act ................................. jsv.observable(model.people[1]).setProperty("name", "jimUpdated"); res = $("#result").text() + "|"; $("#result input").eq(2).prop("checked", true).change(); // Check third checkbox button res += model.selected + "-" + $("#" + $("#result input:checked").prop("id") + "Lbl").text() + "|"; assert.equal(res, ":NONE:BOB:JIMUPDATED|new,None,jimUpdated-NONE|", 'data-link="{checkboxgroup selected}" with labels by for/id - updated label and value'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== tmpl = jsv.templates( '
          ' + '' + '{^{for people}}' + '' + '{{/for}}' + '
          ' + '
          ' + '' + '{^{for people}}' + '' + '{{/for}}' + '
          ' ); model = { selected: ["Jim"], people: [ {name: "Bob"}, {name: "Jim"} ] }; // ............................... Act ................................. tmpl.link("#result", model); res = $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).insert({ name: newName }); jsv.observable(model).setProperty("selected", [newName]); res += $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).remove(2); res += $("#result input:checked").parent().text() + "|"; $("#result input").first().prop("checked", true).change(); // Check first checkbox button res += model.selected + "-" + $("#result input:checked").parent().text() + "|"; // ............................... Assert ................................. assert.equal(res, ":JIM:JIM|:NEW:NEW||new,None-:NONE:NONE|", 'data-link="{checkboxgroup selected}" - two checkboxgroups with same selected bindings'); // ............................... Act ................................. jsv.observable(model.people[1]).setProperty("name", "jimUpdated"); res = $("#result").text() + "|"; $("#result input").eq(2).prop("checked", true).change(); // Check third checkbox button res += model.selected + "-" + $("#result input:checked").parent().text() + "|"; assert.equal(res, ":NONE:BOB:JIMUPDATED:NONE:BOB:JIMUPDATED|new,None,jimUpdated-:NONE:JIMUPDATED:NONE:JIMUPDATED|", 'data-link="{checkboxgroup selected}" - two checkboxgroups with same selected bindings - updated label and value'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== tmpl = jsv.templates( '
          ' + '{^{for people}}' + '' + '{{/for}}' + '
          ' + '
          ' + '' + '{^{for people}}' + '' + '{{/for}}' + '
          ' ); model = { selected: ["Jim"], people: [] }; // ............................... Act ................................. tmpl.link("#result", model); res = $("#result").text() + "|" + $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).insert([{name: "Bob"},{name: "Jim"},{name: newName}]); res += $("#result").text() + "|" + $("#result input:checked").parent().text() + "|"; jsv.observable(model).setProperty("selected", [newName]); res += $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).remove(2); res += $("#result input:checked").parent().text() + "|"; $("#result input").first().prop("checked", true).change(); // Check first checkbox button res += model.selected + "-" + $("#result input:checked").parent().text() + "|"; // ............................... Assert ................................. assert.equal(res, ":NONE||:BOB:JIM:NEW:NONE:BOB:JIM:NEW|:JIM:JIM|:NEW:NEW||new,Bob-:BOB:BOB|", 'data-link="{checkboxgroup selected}" - two checkboxgroups with same selected bindings - starting out with no items, so no checkbox buttons'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== tmpl = jsv.templates( '
          ' + '{^{for people}}' + '' + '{{/for}}' + '
          ' + '
          ' + '' + '{^{for people}}' + '' + '{{/for}}' + '
          ' ); model = { selected: ["Jim"], people: [] }; // ............................... Act ................................. tmpl.link("#result", model); res = $("#result").text() + "|" + $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).insert([{name: "Bob"},{name: "Jim"},{name: newName}]); res += $("#result").text() + "|" + $("#result input:checked").parent().text() + "|"; jsv.observable(model).setProperty("selected", [newName]); res += $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).remove(2); res += $("#result input:checked").parent().text() + "|"; $("#result input").first().prop("checked", true).change(); // Check first checkbox button res += model.selected + "-" + $("#result input:checked").parent().text() + "|" + $("#result input:checked")[0].name + "|" + $("#result input:checked")[1].name; // ............................... Assert ................................. assert.equal(res, ":NONE||:BOB:JIM:NEW:NONE:BOB:JIM:NEW|:JIM:JIM|:NEW:NEW||new,Bob-:BOB:BOB|rad1|rad2", 'data-link="{checkboxgroup selected}" - name for group can be specified rather than auto-generated - on item or on checkboxgroup tag'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== tmpl = jsv.templates( '{^{if rg1}}' + '
          ' + '{^{for people}}' + '' + '{{/for}}' + '
          ' + '{{/if}}' + '{^{if rg2}}' + '
          ' + '' + '{^{for people}}' + '' + '{{/for}}' + '
          ' + '{{/if}}' ); model = { rg1: true, rg2: true, selected: ["Jim"], people: [] }; newName = "newName"; // ............................... Act ................................. tmpl.link("#result", model); res = ">" + $("#result").text() + "|" + $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).insert([{name: "Bob"},{name: "Jim"},{name: newName}]); res += $("#result").text() + "|" + $("#result input:checked").parent().text() + "|"; jsv.observable(model).setProperty("selected", [newName]); res += $("#result input:checked").parent().text() + "|"; // ............................... Assert ................................. assert.equal(res, ">:NONE||:BOB:JIM:NEWNAME:NONE:BOB:JIM:NEWNAME|:JIM:JIM|:NEWNAME:NEWNAME|", 'data-link="{checkboxgroup selected}" - two checkboxgroups wrapped in {{if}} blocks - starting out with no items'); jsv.observable(model.people).refresh([]); jsv.observable(model).setProperty("rg1", false); res = ">" + $("#result").text() + "|" + $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).insert([{name: "Bob"},{name: "Jim"},{name: newName}]); res += $("#result").text() + "|" + $("#result input:checked").parent().text() + "|"; jsv.observable(model).setProperty("selected", [newName]); res += $("#result input:checked").parent().text() + "|"; // ............................... Assert ................................. assert.equal(res, ">:NONE||:NONE:BOB:JIM:NEWNAME|:NEWNAME|:NEWNAME|", 'data-link="{checkboxgroup selected}" - two checkboxgroups wrapped in {{if}} blocks - one set to false'); jsv.observable(model.people).refresh([]); jsv.observable(model).setProperty("selected", ["Jim"]); jsv.observable(model).setProperty("rg1", true); res = ">" + $("#result").text() + "|" + $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).insert([{name: "Bob"},{name: "Jim"},{name: newName}]); res += $("#result").text() + "|" + $("#result input:checked").parent().text() + "|"; jsv.observable(model).setProperty("selected", [newName]); res += $("#result input:checked").parent().text() + "|"; // ............................... Assert ................................. assert.equal(res, ">:NONE||:BOB:JIM:NEWNAME:NONE:BOB:JIM:NEWNAME|:JIM:JIM|:NEWNAME:NEWNAME|", 'data-link="{checkboxgroup selected}" - two checkboxgroups wrapped in {{if}} blocks - one set back to true'); // ................................ Reset ................................ $("#result").empty(); // ............................... Assert ................................. assert.ok(!viewsAndBindings($) && JSON.stringify(_jsv.cbBindings) === "{}" && !$._data(model).events, "$(container).empty removes current listeners and two-way bindings"); jsv.views.settings.advanced({_jsv: false}); // For using viewsAndBindings($) }); QUnit.test('data-link="{tag...}"', function(assert) { // ................................ Reset ................................ $("#result").empty(); person1._firstName = "Jo"; // reset Prop person1.lastName = "One"; // reset Prop settings.title = "Mr"; // reset Prop settings.width = 30; // reset Prop // =============================== Arrange =============================== jsv.views.tags({ norendernotemplate: {}, voidrender: function() {}, emptyrender: function() { return ""; }, emptytemplate: { template: "" }, templatereturnsempty: { template: "{{:a}}" } }); // ............................... Assert ................................. jsv.templates('ab').link("#result", 1); assert.equal($("#result").text(), "ab", "non-rendering tag (no template, no render function) renders empty string"); jsv.templates('ab').link("#result", 1); assert.equal($("#result").text(), "ab", "non-rendering tag (no template, no return from render function) renders empty string"); jsv.templates('ab').link("#result", 1); assert.equal($("#result").text(), "ab", "non-rendering tag (no template, empty string returned from render function) renders empty string"); jsv.templates('ab').link("#result", 1); assert.equal($("#result").text(), "ab", "non-rendering tag (template has no content, no render function) renders empty string"); jsv.templates('ab').link("#result", 1); assert.equal($("#result").text(), "ab", "non-rendering tag (template returns empty string, no render function) renders empty string"); // =============================== Arrange =============================== jsv.templates('') .link("#result", person1, {settings: settings}); // ................................ Act .................................. before = $("#result span").text(); jsv.observable(person1).setProperty({firstName: "newFirst", lastName: "newLast"}); jsv.observable(settings).setProperty({title: "Sir", width: 40}); jsv.observable(person1).setProperty({fullName: "compFirst compLast"}); after = $("#result span").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, 'Name: Mr Jo. Width: 30|Name: Sir compFirst. Width: 40', 'Data link using: '); // ................................ Reset ................................ $("#result").empty(); person1._firstName = "Jo"; // reset Prop person1.lastName = "One"; // reset Prop settings.title = "Mr"; // reset Prop settings.width = 30; // reset Prop // =============================== Arrange =============================== jsv.templates('') .link("#result", person1, {settings: settings}); // ................................ Act .................................. before = $("#result span").text(); jsv.observable(person1).setProperty({firstName: "newFirst", lastName: "newLast"}); jsv.observable(settings).setProperty({title: "Sir", width: 40}); jsv.observable(person1).setProperty({fullName: "compFirst compLast"}); after = $("#result span").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, 'Name: Mr Jo. Width: 30|Name: Sir compFirst. Width: 40', 'Data link using: '); // ................................ Reset ................................ $("#result").empty(); person1._firstName = "Jo"; // reset Prop person1.lastName = "One"; // reset Prop settings.title = "Mr"; // reset Prop settings.width = 30; // reset Prop // =============================== Arrange =============================== jsv.templates('
          ') .link("#result", person1, {settings: settings}); // ................................ Act .................................. before = $("#result div").html(); jsv.observable(person1).setProperty({firstName: "newFirst", lastName: "newLast"}); jsv.observable(settings).setProperty({title: "Sir", width: 40}); jsv.observable(person1).setProperty({fullName: "compFirst compLast"}); after = $("#result div").html(); // ............................... Assert ................................. assert.equal((before + "|" + after).replace(/<\/script>/g, ""), 'Name: Mr Jo. Width: 30|Name: Sir compFirst. Width: 40', 'Data link fnTagEl rendering , using:
          '); // ................................ Reset ................................ $("#result").empty(); person1._firstName = "Jo"; // reset Prop person1.lastName = "One"; // reset Prop settings.title = "Mr"; // reset Prop settings.width = 30; // reset Prop // =============================== Arrange =============================== jsv.templates('
            ') .link("#result", person1, {settings: settings}); // ................................ Act .................................. before = $("#result ul li").html(); // The innerHTML will be "
          • Name: Mr Jo. Width: 30
          • " jsv.observable(person1).setProperty({firstName: "newFirst", lastName: "newLast"}); jsv.observable(settings).setProperty({title: "Sir", width: 40}); jsv.observable(person1).setProperty({fullName: "compFirst compLast"}); after = $("#result ul li").html(); // ............................... Assert ................................. assert.equal(before + "|" + after, 'Name: Mr Jo. Width: 30|Name: Sir compFirst. Width: 40', 'Data link fnTagElCnt rendering
          • , using:
              '); // ................................ Reset ................................ $("#result").empty(); person1._firstName = "Jo"; // reset Prop person1.lastName = "One"; // reset Prop settings.title = "Mr"; // reset Prop settings.width = 30; // reset Prop // =============================== Arrange =============================== jsv.templates('
                ') .link("#result", person1, {settings: settings}); // ................................ Act .................................. before = $("#result ul li").html(); // The innerHTML will be "
              • Name: Mr Jo. Width: 30
              • " jsv.observable(person1).setProperty({firstName: "newFirst", lastName: "newLast"}); jsv.observable(settings).setProperty({title: "Sir", width: 40}); jsv.observable(person1).setProperty({fullName: "compFirst compLast"}); after = $("#result ul li").html(); // ............................... Assert ................................. assert.equal(before + "|" + after, 'Name: Mr Jo. Width: 30|Name: Sir compFirst. Width: 40', 'Data link fnTagElCntNoInit rendering
              • , using:
                  '); // ................................ Reset ................................ $("#result").empty(); person1._firstName = "Jo"; // reset Prop person1.lastName = "One"; // reset Prop settings.title = "Mr"; // reset Prop settings.width = 30; // reset Prop // =============================== Arrange =============================== jsv.templates('') .link("#result", person1, {settings: settings}); // ................................ Act .................................. before = $("#result span").text(); jsv.observable(person1).setProperty({firstName: "newFirst", lastName: "newLast", "home.address.street": "newStreet"}); jsv.observable(settings).setProperty({title: "Sir", width: 40, reverse: false}); jsv.observable(person1).setProperty({fullName: "compFirst compLast"}); after = $("#result span").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, 'Name: Mr Jo. Width: 30. Value: true. Prop theTitle: Mr. Prop ~street: StreetOne|Name: Sir compFirst. Width: 40. Value: false.' + ' Prop theTitle: Sir. Prop ~street: newStreet', 'Data link using: updates correctly when data changes'); // ................................ Reset ................................ $("#result").empty(); person1._firstName = "Jo"; // reset Prop person1.lastName = "One"; // reset Prop settings.title = "Mr"; // reset Prop settings.width = 30; // reset Prop settings.reverse = true; // reset Prop address1.street = "StreetOne"; // reset Prop // =============================== Arrange =============================== jsv.templates('') .link("#result", person1, {settings: settings}); // ................................ Act .................................. before = $("#result span").text(); jsv.observable(person1).setProperty({firstName: "newFirst", lastName: "newLast", "home.address.street": "newStreet"}); jsv.observable(settings).setProperty({title: "Sir", width: 40, reverse: false}); jsv.observable(person1).setProperty({fullName: "compFirst compLast"}); after = $("#result span").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, 'Name: Mr Jo. Width: 30. Value: true. Prop theTitle: Mr. Prop ~street: StreetOne|Name: Sir compFirst. Width: 40. Value: false.' + ' Prop theTitle: Sir. Prop ~street: newStreet', 'Data link using: updates correctly when data changes'); // ................................ Reset ................................ $("#result").empty(); person1._firstName = "Jo"; // reset Prop person1.lastName = "One"; // reset Prop settings.title = "Mr"; // reset Prop settings.width = 30; // reset Prop settings.reverse = true; // reset Prop address1.street = "StreetOne"; // reset Prop }); QUnit.test("Computed observables in paths", function(assert) { jsv.views.settings.advanced({_jsv: true}); // For using cbBindings store // =============================== Arrange =============================== var app = { items: [ { name: "one", row: "1", expanded: false }, { name: "two", row: "2", expanded: false }, { name: "three", row: "3", expanded: false } ] }; function testTemplate(message, template) { jsv.templates(template) .link("#result", app, { getItems: function(exp) { return exp ? ["a", "b"] : []; } }); // ................................ Act .................................. var ret = $("#result").text() + "|"; jsv.view("#result .groupdata").refresh(); jsv.observable(app.items[0]).setProperty("expanded", true); ret += $("#result").text() + "|"; jsv.observable(app.items[0]).setProperty("expanded", false); ret += $("#result").text() + "|"; jsv.observable(app.items[1]).setProperty("expanded", true); ret += $("#result").text() + "|"; jsv.observable(app.items[1]).setProperty("expanded", false); ret += $("#result").text() + "|"; jsv.observable(app.items).insert(0, { name: "added", row: "+", expanded: false }); ret += $("#result").text() + "|"; jsv.observable(app.items).remove(0); ret += $("#result").text() + "|"; $("#result").empty(); // ............................... Assert ................................. assert.equal(ret, "onetwothree|one1a1btwothree|onetwothree|onetwo2a2bthree|onetwothree|addedonetwothree|onetwothree|", "Interplay of view and tag refresh in deep content: " + message); } // ............................... Assert ................................. testTemplate("div", "{^{for items}}" + "{{:name}}" + "{^{for ~getItems(expanded) ~row=row}}" + "
                  {{:~row}}{{:#data}}
                  " + "{{/for}}" + "{{/for}}"); // ............................... Assert ................................. testTemplate("deep div", "{^{for items}}" + "{{:name}}" + "
                  {^{for ~getItems(expanded) ~row=row}}" + "" + "{{:~row}}{{:#data}}" + "{{/for}}
                  " + "{{/for}}"); // ............................... Assert ................................. testTemplate("deep div2", "{^{for items}}" + "{{:name}}" + "
                  {^{for ~getItems(expanded) ~row=row}}" + "" + "{{:~row}}{{:#data}}" + "{{/for}}
                  " + "{{/for}}"); // ............................... Assert ................................. testTemplate("li", "
                    {^{for items}}" + "
                  • {{:name}}
                  • " + "
                    • {^{for ~getItems(expanded) ~row=row ~item=#data}}" + "
                    • " + "
                    • {{:~row}}{{:#data}}
                    • " + "{{/for}}
                  • " + "{{/for}}
                  "); // ............................... Assert ................................. testTemplate("table", "{^{for items}}" + "" + "{^{for ~getItems(expanded) ~row=row}}" + "" + "" + "" + "{{/for}}" + " {{/for}}
                  {{:name}}
                  {{:~row}}{{:#data}}
                  "); // ............................... Assert ................................. testTemplate("deep div2 with comments", // See https://github.com/BorisMoore/jsviews/issues/452 "{^{for items}}" + "{{:name}}" + "
                  {^{for ~getItems(expanded) ~row=row}}" + "" + "{{:~row}}{{:#data}}" + "{{/for}}
                  " + "{{/for}}"); // ............................... Assert ................................. testTemplate("deep table with comments", // See https://github.com/BorisMoore/jsviews/issues/452 "{^{for items}}" + "" + "" + "{^{for ~getItems(expanded) ~row=row}}" + "" + "" + "" + "{{/for}}" + "" + " {{/for}}
                  {{:name}}
                  {{:~row}}{^{:#data}}
                  "); // =============================== Arrange =============================== var ret = "", people1 = [{address: {street: "1 first street"}}], people2 = [{address: {street: "1 second street"}}, {address: {street: "2 second street"}}], data1 = {value: "data1", people: people1}, data2 = {value: "data2", people: people2}; app = { alt: false, index: 1, getPeople: getPeople, getData: getData, options: { getWidth: function() { return "33"; } } }; function getPeople(type) { return this.alt ? people2 : people1; } getPeople.depends = function() { return [this, "alt"]; // this is app }; function getData(type) { return this.alt ? data2 : data1; } getData.depends = function(data) { return [data, "alt"]; // data === this === app }; // ................................ Act .................................. jsv.templates("{^{for (getPeople()[index]||{})^address}}{^{:street}}{{/for}}").link("#result", app); ret = "|" + $("#result").text(); // ................................ Act .................................. jsv.observable(app).setProperty("index", 0); ret += "|" + $("#result").text(); // ................................ Act .................................. jsv.observable(people1[0].address).setProperty("street", "1 first streetB"); ret += "|" + $("#result").text(); // ................................ Act .................................. jsv.observable(people1[0]).setProperty("address", {street: "1 first swappedstreet"}); ret += "|" + $("#result").text(); // ................................ Act .................................. jsv.observable(app).setProperty("alt", true); ret += "|" + $("#result").text(); // ................................ Act .................................. jsv.observable(app).setProperty("index", 1); ret += "|" + $("#result").text(); // ................................ Act .................................. jsv.observable(people2[1]).setProperty("address", {street: "2 second swappedstreet"}); ret += "|" + $("#result").text(); // ................................ Act .................................. jsv.observable(people2[1].address).setProperty("street", "2 second swappedstreetB"); ret += "|" + $("#result").text(); // ................................ Assert .................................. assert.equal(ret, "||1 first street|1 first streetB|1 first swappedstreet|1 second street|2 second street|2 second swappedstreet|2 second swappedstreetB", "deep paths with computed observables bind correctly to rest of path after computed returns new array"); $("#result").empty(); app.alt = false; app.index = 0; people1 = [{address: {street: "1 first street"}}, {address: {street: "2 first street"}}]; people2 = [{address: {street: "1 second street"}}, {address: {street: "2 second street"}}]; data1 = {value: "data1", people: people1}; data2 = {value: "data2", people: people2}; // ................................ Act .................................. jsv.templates("{^{:(getData()^people[index]).address^street}}").link("#result", app); ret = $("#result").text(); jsv.observable(app).setProperty("index", 1); ret += "|" + $("#result").text(); jsv.observable(app).setProperty("index", 0); ret += "|" + $("#result").text(); jsv.observable(people1[0].address).setProperty("street", "1 first streetB"); ret += "|" + $("#result").text(); // ................................ Act .................................. jsv.observable(people1[0]).setProperty("address", {street: "1 first swappedstreet"}); ret += "|" + $("#result").text(); // ................................ Act .................................. jsv.observable(app).setProperty("alt", true); ret += "|" + $("#result").text(); jsv.observable(app).setProperty("index", 1); ret += "|" + $("#result").text(); jsv.observable(people2[1]).setProperty("address", {street: "2 second swappedstreet"}); ret += "|" + $("#result").text(); jsv.observable(people2[1].address).setProperty("street", "2 second swappedstreetB"); ret += "|" + $("#result").text(); // ................................ Assert .................................. assert.equal(ret, "1 first street|2 first street|1 first street|1 first streetB|1 first swappedstreet|1 second street|2 second street|2 second swappedstreet|2 second swappedstreetB", "deep paths with computed observables bind correctly to rest of path after computed returns new object"); // ................................ Reset ................................ $("#result").empty(); //TODO allow the following to work by declaring getPeople as depending on collection change of app.alt ? people2 : people; //jsv.observable(people2).insert(1, {address:{street: "99 new street"}}) //ret += "|" + $("#result").text(); // =============================== Arrange =============================== function getValue(a) { return this.value + "_" + a; } function switchAlt() { jsv.observable(app).setProperty("alt", !app.alt); } app.alt = false; app.index = 0; people1 = [{address: {street: "1 first street"}}]; people2 = [{address: {street: "1 second street"}}, {address: {street: "2 second street"}}]; data1 = {value: "val1", people: people1, getValue: getValue}; data2 = {value: "val2", people: people2, getValue: getValue}; // ................................ Act .................................. jsv.templates("{^{:getData()^getValue(22)}}").link("#result", app); // ................................ Act .................................. jsv.observable(app).setProperty("alt", true); ret = "|A: " + $("#result").text(); switchAlt(); ret += "--" + $("#result").text(); // |A: val2_22--val1_22 $("#result").empty(); // ................................ Act .................................. jsv.templates("{^{for (getPeople())}}{^{:address.street}}{{/for}}").link("#result", app); ret += "|B: " + $("#result").text(); switchAlt(); ret += "--" + $("#result").text(); // |B: 1 first street--1 second street2 second street $("#result").empty(); // ................................ Act .................................. jsv.templates("{^{for getPeople()}}{^{:address.street}}{{/for}}").link("#result", app); ret += "|C: " + $("#result").text(); switchAlt(); ret += "--" + $("#result").text(); // |C: 1 second street2 second street--1 first street $("#result").empty(); // ................................ Act .................................. jsv.templates("{^{:(getData()^getValue(22))}}").link("#result", app); ret += "|D: " + $("#result").text(); switchAlt(); ret += "--" + $("#result").text(); // |D: val1_22--val2_22 $("#result").empty(); // ................................ Act .................................. jsv.templates("{^{:getData()^getValue((getData()^getValue(33)))}}").link("#result", app); ret += "|E: " + $("#result").text(); switchAlt(); ret += "--" + $("#result").text(); // E: val2_val2_33--val1_val1_33 $("#result").empty(); // ................................ Act .................................. jsv.templates("{^{:getData(getPeople(getData(alt || 2)^getValue())^length)^value}}").link("#result", app); ret += "|F: " + $("#result").text(); switchAlt(); ret += "--" + $("#result").text(); // |F: val1--val2 $("#result").empty(); // ................................ Act .................................. jsv.templates("{^{for (getPeople()[index]||{})^address}}{^{:street}}{{/for}}").link("#result", app); ret += "|G: " + $("#result").text(); switchAlt(); ret += "--" + $("#result").text(); // |G: 1 second street--1 first street $("#result").empty(); // ................................ Act .................................. jsv.templates("{^{:(((getData())^people[0])^address.street)}}").link("#result", app); ret += "|H: " + $("#result").text(); switchAlt(); ret += "--" + $("#result").text(); // |H: 1 first street--1 second street $("#result").empty(); // ................................ Act .................................. jsv.templates("{^{:'b'+((getData()^value) + ('a'+getData()^value)) + getData()^getValue(55)}}").link("#result", app); ret += "|I: " + $("#result").text(); switchAlt(); ret += "--" + $("#result").text(); // |I: bval2aval2val2_55--bval1aval1val1_55 $("#result").empty(); // ................................ Act .................................. jsv.templates("{^{:'a' + getData()^value}}").link("#result", app); ret += "|J: " + $("#result").text(); switchAlt(); ret += "--" + $("#result").text(); // |J: aval1--aval2 $("#result").empty(); // =============================== Arrange =============================== getValue.depends = "value"; // ................................ Act .................................. jsv.templates("{^{:getData()^getValue((getData()^getValue(33)))}}").link("#result", app); ret += "|K: " + $("#result").text(); jsv.observable(data2).setProperty("value", "newVal1"); ret += "--" + $("#result").text(); switchAlt(); ret += "--" + $("#result").text(); jsv.observable(data1).setProperty("value", "newVal2"); ret += "--" + $("#result").text(); // |K: val2_val2_33--newVal1_newVal1_33--val1_val1_33--newVal2_newVal2_33 // ................................ Act .................................. $("#result").empty(); ret += "|L: " + !!$._data(data1).events + " " + !!$._data(data1).events + " " + (JSON.stringify(_jsv.cbBindings) === "{}"); // |L: false false true // ................................ Assert .................................. assert.equal(ret, "|A: val2_22--val1_22|" + "B: 1 first street--1 second street2 second street|" + "C: 1 second street2 second street--1 first street|" + "D: val1_22--val2_22|" + "E: val2_val2_33--val1_val1_33|" + "F: val1--val2|" + "G: 1 second street--1 first street|" + "H: 1 first street--1 second street|" + "I: bval2aval2val2_55--bval1aval1val1_55|" + "J: aval1--aval2|" + "K: val2_val2_33--newVal1_newVal1_33--val1_val1_33--newVal2_newVal2_33|" + "L: false false true", "deep paths with computed observables bind correctly to rest of path after computed returns new object or array, including complex expressions, wrapped in parens etc."); // =============================== Arrange =============================== ret = ""; var model = { enabled: function(val) { if (!arguments.length) { return this._enabled + this._selected + this._colored; } this._enabled = val; }, selected: function(val) { if (!arguments.length) { return this._selected; } this._selected = val; }, colored: function(val) { if (!arguments.length) { return this._colored; } this._colored = val; }, _colored: "C", _enabled: "E", _selected: "S" }; model.selected.set = model.colored.set = model.enabled.set = true; model.enabled.depends = [,"selected"]; jsv.templates('
                  ').link("#result", model); // ................................ Act .................................. ret = $._data(model).events.propertyChange.length + "_"; jsv.observable(model).setProperty("selected", "s2"); ret += $("#result").text(); assert.equal(ret, "1_ESC", 'Computed function with depends = [undefined,"selected"] - no listener added'); // ................................ Reset ................................ model.selected("S"); $("#result").empty(); // =============================== Arrange =============================== model.enabled.depends = [,"selected", model, "colored"]; jsv.templates('
                  ').link("#result", model); // ................................ Act .................................. ret = $._data(model).events.propertyChange.length + "_"; jsv.observable(model).setProperty("selected", "s2"); ret += $("#result").text() + "_"; jsv.observable(model).setProperty("colored", "c3"); ret += $("#result").text(); assert.equal(ret, "2_ESC_Es2c3", 'Computed function with depends = [undefined,"selected", model, "colored"] - has listener for "colored" only'); // ................................ Reset ................................ model.selected("S"); model.colored("C"); $("#result").empty(); // =============================== Arrange =============================== model.enabled.depends = ["enabled"]; jsv.templates('
                  ').link("#result", model); // ................................ Act .................................. ret = $._data(model).events.propertyChange.length + "_"; jsv.observable(model).setProperty("enabled", "e2"); ret += $("#result").text(); assert.equal(ret, "1_e2SC", "Computed function with circular depends - no stack overflow - handler skipped"); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== function name() { return this._name; } name.set = function(val) { this._name = val; } var data = { _name: "Bob", name: name, a: function() { return this; } }, hlprs = { util: { myFn: function() { return "myFn"; }, _name: "Fred", myname: name } }; var tmpl = jsv.templates(''); tmpl.link("#result", data, hlprs); var inputs = $("#result input"), i, j; // ................................ Act .................................. res = ""; for (i=0; i<5; i++) { $(inputs[i]).val(inputs[i].value + "X").trigger("input"); res += "|"; for (j=0; j<5; j++) { res += inputs[j].value + " "; } } assert.equal(res, "|BobX BobX myFn Fred Fred |BobXX BobXX myFn Fred Fred |BobXX BobXX myFnX Fred Fred |BobXX BobXX myFnX FredX FredX |BobXX BobXX myFnX FredXX FredXX ", "computed functions with setters"); // =============================== Arrange =============================== // See https://github.com/BorisMoore/jsviews/issues/451 name.set = undefined; data._name = "Bob"; hlprs.util._name = "Fred"; tmpl.link("#result", data, hlprs); res = ""; // ................................ Act .................................. for (i=0; i<5; i++) { $(inputs[i]).val(inputs[i].value + "X").trigger("input"); res += "|"; for (j=0; j<5; j++) { res += inputs[j].value + " "; } } assert.equal(res, "|BobXXX BobXX myFnX FredXX FredXX |BobXXX BobXXX myFnX FredXX FredXX |BobXXX BobXXX myFnXX FredXX FredXX |BobXXX BobXXX myFnXX FredXXX FredXX |BobXXX BobXXX myFnXX FredXXX FredXXX ", "Computed functions with no setters (2way binding is now a noop)"); // ................................ Reset ................................ $("#result").empty(); res = ""; // =============================== Arrange =============================== res = ""; function name() { return this._name; } name.set = function(val) { this._name = val; } data = { name: name, _name: "Bob" }; jsv.views.tags({ tag1: { template: '
                  {^{:~arg1}}
                  ', bindTo: [0], linkedCtxParam: ["arg1"], onBind: function() { res += ("tag1.onBind "); }, onUnbind: function() { res += ("tag1.onUnbind "); }, onUpdate: true } }); tmpl = jsv.templates("{^{tag1 name()/}}"); tmpl.link("#result", data); // ................................ Act .................................. var $input = $("#result input"); $input.val($input[0].value + "X").trigger("input"); res += $input[0].value + " " + $("#result").text() + " "; assert.equal(res, "tag1.onBind BobX BobX ", "computed functions, with linkedCtxParam & setters"); // =============================== Arrange =============================== res = ""; // See https://github.com/BorisMoore/jsviews/issues/451 name.set = undefined; data._name = "Bob"; tmpl.link("#result", data); // ................................ Act .................................. $input = $("#result input"), $input.val($input[0].value + "X").trigger("input"); res += $input[0].value + " " + $("#result").text() + " "; assert.equal(res, "tag1.onUnbind tag1.onBind BobX Bob ", "computed functions with linkedCtxParam & without setters (2way binding is now a noop)"); // ................................ Reset ................................ $("#result").empty(); jsv.views.settings.advanced({_jsv: false}); }); QUnit.test("Computed observables in jsv.link() expressions", function(assert) { (function() { // =============================== Arrange =============================== var res = ""; $("#result").html(''); function ob() { return helpers.alt ? this._obB : this._obA; } ob.depends = "~helpers.alt"; function address() { return this._address; } function setAddress(val) { this._address = val; } address.set = setAddress; var helpers = {alt: false}; var person = { changeAlt: changeAlt, changeHome: changeHome, changeObtype: changeObtype, changeAddress: changeAddress, changeStreet: changeStreet, ob: ob, _obA: { home: { _address: { street: "A" }, address: address }, obtype: "a" }, _obB: { home: { _address: { street: "B" }, address: address }, obtype: "b" } }; function changeHome(label) { jsv.observable(person.ob()).setProperty("home", { _address: { street: person.ob().home.address().street + "$" }, address: address }); } function changeAlt(label) { jsv.observable(helpers).setProperty("alt", !helpers.alt); res += " |" + label + ": " + $("#inner").text(); } function changeObtype(label) { jsv.observable(person.ob()).setProperty("obtype", person.ob().obtype + "@"); res += " |" + label + ": " + $("#inner").text(); } function changeAddress(label) { jsv.observable(person.ob().home).setProperty("address", {street: person.ob().home.address().street + "+"}); res += " |" + label + ": " + $("#inner").text(); } function changeStreet(label) { jsv.observable(person.ob().home.address()).setProperty("street", person.ob().home.address().street + ">"); res += " |" + label + ": " + $("#inner").text(); } jsv.link("ob()^obtype", "#inner", person, {helpers: helpers}); // ................................ Act .................................. changeAlt(1); changeObtype(2); changeAlt(3); changeObtype(4); changeAlt(5); // ............................... Assert ................................. assert.equal(res, " |1: b |2: b@ |3: a |4: a@ |5: b@", "complex"); // ................................ Reset ................................ $("#result").empty(); })(); (function() { // =============================== Arrange =============================== var res = ""; $("#result").html(''); function ob(alt) { return alt ? this._obB : this._obA; } function address() { return this._address; } function setAddress(val) { this._address = val; } address.set = setAddress; var helpers = {alt: false}; var person = { changeAlt: changeAlt, changeHome: changeHome, changeObtype: changeObtype, changeAddress: changeAddress, changeStreet: changeStreet, ob: ob, _obA: { home: { _address: { street: "A" }, address: address }, obtype: "a" }, _obB: { home: { _address: { street: "B" }, address: address }, obtype: "b" } }; function changeHome(label) { jsv.observable(person.ob(helpers.alt)).setProperty("home", { _address: { street: person.ob(helpers.alt).home.address().street + "$" }, address: address }); } function changeAlt(label) { jsv.observable(helpers).setProperty("alt", !helpers.alt); res += " |" + label + ": " + $("#inner").text(); } function changeObtype(label) { jsv.observable(person.ob(helpers.alt)).setProperty("obtype", person.ob(helpers.alt).obtype + "@"); res += " |" + label + ": " + $("#inner").text(); } function changeAddress(label) { jsv.observable(person.ob(helpers.alt).home).setProperty("address", {street: person.ob(helpers.alt).home.address().street + "+"}); res += " |" + label + ": " + $("#inner").text(); } function changeStreet(label) { jsv.observable(person.ob(helpers.alt).home.address()).setProperty("street", person.ob(helpers.alt).home.address().street + ">"); res += " |" + label + ": " + $("#inner").text(); } jsv.link("ob(~helpers.alt)^home.address().street", "#inner", person, {helpers: helpers}); // ................................ Act .................................. changeAddress(1); changeStreet(2); changeAddress(3); changeStreet(4); changeAlt(5); changeAddress(6); changeStreet(7); changeAddress(8); changeStreet(9); changeAlt(10); changeStreet(11); changeAddress(12); changeStreet(13); changeAddress(14); changeAlt(15); // ............................... Assert ................................. assert.equal(res, " |1: A+ |2: A+> |3: A+>+ |4: A+>+> |5: B |6: B+ |7: B+> |8: B+>+ |9: B+>+> |10: A+>+> |11: A+>+>> |12: A+>+>>+ |13: A+>+>>+> |14: A+>+>>+>+ |15: B+>+>", "complex"); // ................................ Reset ................................ $("#result").empty(); })(); (function() { // =============================== Arrange =============================== var res = ""; $("#result").html(''); function ob() { return helpers.alt ? this._obB : this._obA; } ob.depends = "~helpers.alt"; function address(val) { if (!arguments.length) { return helpers.alt ? this._address2 : this._address; } if (helpers.alt) { this._address2 = val; } else { this._address = val; } } address.set = true; address.depends = "~helpers.alt"; var helpers = {alt: false}; var person = { changeAlt: changeAlt, changeHome: changeHome, changeObtype: changeObtype, changeAddress: changeAddress, changeStreet: changeStreet, ob: ob, _obA: { home: { _address: { street: "A" }, _address2: { street: "A2" }, address: address }, obtype: "a" }, _obB: { home: { _address: { street: "B" }, _address2: { street: "B2" }, address: address }, obtype: "b" } }; function changeHome(label) { jsv.observable(person.ob()).setProperty("home", { _address: { street: person.ob().home.address().street + "$" }, address: address }); } function changeAlt(label) { jsv.observable(helpers).setProperty("alt", !helpers.alt); res += " |" + label + ": " + $("#inner").text(); } function changeObtype(label) { jsv.observable(person.ob()).setProperty("obtype", person.ob().obtype + "@"); res += " |" + label + ": " + $("#inner").text(); } function changeAddress(label) { jsv.observable(person.ob().home).setProperty("address", {street: person.ob().home.address().street + "+"}); res += " |" + label + ": " + $("#inner").text(); } function changeStreet(label) { jsv.observable(person.ob().home.address()).setProperty("street", person.ob().home.address().street + ">"); res += " |" + label + ": " + $("#inner").text(); } jsv.link("ob()^home.address().street", "#inner", person, {helpers: helpers}); // ................................ Act .................................. changeAddress(1); changeStreet(2); changeAddress(3); changeStreet(4); changeAlt(5); changeAddress(6); changeStreet(7); changeAddress(8); changeStreet(9); changeAlt(10); changeStreet(11); changeAddress(12); changeStreet(13); changeAddress(14); changeAlt(15); // ............................... Assert ................................. assert.equal(res, " |1: A+ |2: A+> |3: A+>+ |4: A+>+> |5: B2 |6: B2+ |7: B2+> |8: B2+>+ |9: B2+>+> |10: A+>+> |11: A+>+>> |12: A+>+>>+ |13: A+>+>>+> |14: A+>+>>+>+ |15: B2+>+>", "complex"); // ................................ Reset ................................ $("#result").empty(); })(); }); QUnit.test("Computed observables in two-way binding", function(assert) { var done; if (assert.async) { done = assert.async() } else { stop() } // =============================== Arrange =============================== var fullName = function(reversed) { return reversed ? this.lastName + " " + this.firstName : this.firstName + " " + this.lastName; }; var person = { firstName: "Jeff", lastName: "Smith", fullName: fullName }; fullName.depends = "*"; fullName.set = function(val) { val = val.split(" "); jsv.observable(this).setProperty({ lastName: val.pop(), firstName: val.join(" ") }); }; jsv.templates('{^{:firstName}} {^{:lastName}} {^{:fullName()}} {^{:fullName(true)}} ') .link("#result", person); // ................................ Act .................................. var res = $("#result").text() + $("#full").val(); jsv.observable(person).setProperty({firstName: "newFirst", lastName: "newLast"}); res += "|" + $("#result").text() + $("#full").val(); jsv.observable(person).setProperty({fullName: "compFirst compLast"}); res += "|" + $("#result").text() + $("#full").val(); keydown($("#full").val("2wayFirst 2wayLast")); setTimeout(function() { res += "|" + $("#result").text() + $("#full").val(); // ............................... Assert ................................. assert.equal(res, "Jeff Smith Jeff Smith Smith Jeff Jeff Smith|newFirst newLast newFirst newLast newLast newFirst newFirst newLast|compFirst compLast compFirst" + " compLast compLast compFirst compFirst compLast|2wayFirst 2wayLast 2wayFirst 2wayLast 2wayLast 2wayFirst 2wayFirst 2wayLast", 'Two-way binding to a computed observable data property correctly calls the setter'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== fullName = function(reversed) { return reversed ? this.lastName + " " + this.firstName() : this.firstName() + " " + this.lastName; }; fullName.depends = ["firstName", "lastName"]; fullName.set = function(val) { val = val.split(" "); jsv.observable(this).setProperty({ lastName: val.pop(), firstName: val.join(" ") }); }; // Constructor var Person = function(first, last) { this._firstName = first; this.lastName = last; }, // Prototype personProto = { // Computed firstName firstName: function() { return this._firstName; }, // Computed fullName fullName: fullName }; personProto.firstName.set = function(val) { this._firstName = val; }; Person.prototype = personProto; var person = new Person("Jeff", "Smith"); jsv.templates('{^{:firstName()}} {^{:lastName}} {^{:fullName()}} {^{:fullName(true)}} ') .link("#result", person); // ................................ Act .................................. res = $("#result").text() + $("#full").val(); jsv.observable(person).setProperty({firstName: "newFirst", lastName: "newLast"}); res += "|" + $("#result").text() + $("#full").val(); jsv.observable(person).setProperty({fullName: "compFirst compLast"}); res += "|" + $("#result").text() + $("#full").val(); keydown($("#full").val("2wayFirst 2wayLast")); setTimeout(function() { res += "|" + $("#result").text() + $("#full").val(); // ............................... Assert ................................. assert.equal(res, "Jeff Smith Jeff Smith Smith Jeff Jeff Smith|newFirst newLast newFirst newLast newLast newFirst newFirst newLast|compFirst compLast compFirst" + " compLast compLast compFirst compFirst compLast|2wayFirst 2wayLast 2wayFirst 2wayLast 2wayLast 2wayFirst 2wayFirst 2wayLast", 'Two-way binding to a computed observable data property defined on the prototype correctly calls the setter'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== fullName = function(reverse) { var person = this.data; // this pointer is the view return reverse ? person.lastName + " " + person.firstName : person.firstName + " " + person.lastName; } fullName.depends = function(data) { return [this, "firstName", data, "lastName"]; // this and data are both contextual data object }; fullName.set = function(val) { val = val.split(" "); var person = this.data; // this pointer is the view jsv.observable(person).setProperty({ lastName: val.pop(), firstName: val.join(" ") }); }; var people = [ { firstName: "Jeff", lastName: "Friedman" }, { firstName: "Rose", lastName: "Lee" } ]; jsv.templates('{^{:firstName}} {^{:lastName}} {^{:~fullName()}} {^{:~fullName(true)}} ') .link("#result", people, {fullName: fullName}); // ................................ Act .................................. res = $("#result").text() + ":" + $("#full0").val(); jsv.observable(people[0]).setProperty({firstName: "newFirst", lastName: "newLast"}); res += "|" + $("#result").text() + ":" + $("#full0").val(); keydown($("#full0").val("2wayFirst 2wayLast")); setTimeout(function() { res += "|" + $("#result").text() + ":" + $("#full0").val(); // ............................... Assert ................................. assert.equal(res, "Jeff Friedman Jeff Friedman Friedman Jeff Rose Lee Rose Lee Lee Rose :Jeff Friedman|" + "newFirst newLast newFirst newLast newLast newFirst Rose Lee Rose Lee Lee Rose :newFirst newLast|" + "2wayFirst 2wayLast 2wayFirst 2wayLast 2wayLast 2wayFirst Rose Lee Rose Lee Lee Rose :2wayFirst 2wayLast", 'Two-way binding to a computed observable data property passed in as helper calls the setter'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== // See https://github.com/BorisMoore/jsviews/issues/287 // From Paul Martin pull request: https://github.com/Paul-Martin/jsviews.com/commit/10d716ccd0d6478dea042faeeca64bf44e4642ed function getsetA(val) { if (!arguments.length) { return this._a; } this._a = val; } getsetA.set = true; function getsetB(val) { if (!arguments.length) { return this._b; } this._b = val; } getsetB.set = true; function Root(a) { this._a = a; this.a = getsetA; } function A(b) { this._b = b; this.b = getsetB; } var o1 = new Root(new A('one')), o2 = new Root(new A('two')), tmpl = jsv.templates(''); $("#result").html("
                  "); tmpl.link('#one', o1); tmpl.link('#two', o2); // ................................ Act .................................. res = ""; var input1 = $("#one input"), input2 = $("#two input"), span1 = $("#one span"), span2 = $("#two span"), getResult = function() { res += input1.val() + " " + span1.text() + " " + input2.val() + " " + span2.text() + "|"; }; getResult(); keydown(input1.val('onechange')); setTimeout(function() { getResult(); keydown(input2.val('twochange')); setTimeout(function() { getResult(); jsv.observable(o1.a()).setProperty('b', 'oneupdate'); getResult(); jsv.observable(o2.a()).setProperty('b', 'twoupdate'); getResult(); // ............................... Assert ................................. assert.equal(res, "one one two two|" + "onechange onechange two two|" + "onechange onechange twochange twochange|" + "oneupdate oneupdate twochange twochange|" + "oneupdate oneupdate twoupdate twoupdate|", 'Two-way bindings with chained computed observables remain independent when same template links to multiple target elements'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== people = [ { name: "n0", _address: { street: "s0" }, ob: function() { return this; }, address: function() { return this._address; } }, { name: "n1", _address: { street: "s1" }, ob: function() { return this; }, address: function() { return this._address; } } ]; tmpl = jsv.templates( "
                  " + " {^{:ob().name}}" + " {^{:ob().address().street}}" + "
                  "); tmpl.link("#result", people); // ................................ Act .................................. res = ""; var nameInput0 = $("#result .person0 input.name"), nameSpan0 = $("#result .person0 span.name"), streetInput0 = $("#result .person0 input.street"), streetSpan0 = $("#result .person0 span.street"), nameInput1 = $("#result .person1 input.name"), nameSpan1 = $("#result .person1 span.name"), streetInput1 = $("#result .person1 input.street"), streetSpan1 = $("#result .person1 span.street"); getResult = function() { res += nameInput0.val() + " " + nameSpan0.text() + " " + streetInput0.val() + " " + streetSpan0.text() + " " + nameInput1.val() + " " + nameSpan1.text() + " " + streetInput1.val() + " " + streetSpan1.text() + "|"; } getResult(); keydown(nameInput0.val('n0new')); setTimeout(function() { getResult(); keydown(nameInput1.val('n1new')); setTimeout(function() { getResult(); keydown(streetInput0.val('s0new')); setTimeout(function() { getResult(); keydown(streetInput1.val('s1new')); setTimeout(function() { getResult(); jsv.observable(people[0]).setProperty('name', 'n0update'); getResult(); jsv.observable(people[0].address()).setProperty('street', 's0update'); getResult(); jsv.observable(people[1]).setProperty('name', 'n1update'); getResult(); jsv.observable(people[1].address()).setProperty('street', 's1update'); getResult(); jsv.observable(people).remove(1); jsv.observable(people).insert({ name: "n1inserted", _address: { street: "s1inserted", address: function() { return this; } }, ob: function() { return this; }, address: function() { return this._address; } }); nameInput1 = $("#result .person1 input.name"); nameSpan1 = $("#result .person1 span.name"); streetInput1 = $("#result .person1 input.street"); streetSpan1 = $("#result .person1 span.street"); getResult(); // ............................... Assert ................................. assert.equal(res, "n0 n0 s0 s0 n1 n1 s1 s1|" + "n0new n0new s0 s0 n1 n1 s1 s1|" + "n0new n0new s0 s0 n1new n1new s1 s1|" + "n0new n0new s0new s0new n1new n1new s1 s1|" + "n0new n0new s0new s0new n1new n1new s1new s1new|" + "n0update n0update s0new s0new n1new n1new s1new s1new|" + "n0update n0update s0update s0update n1new n1new s1new s1new|" + "n0update n0update s0update s0update n1update n1update s1new s1new|" + "n0update n0update s0update s0update n1update n1update s1update s1update|" + "n0update n0update s0update s0update n1inserted n1inserted s1inserted s1inserted|", 'Two-way bindings with chained computed observables remain independent when same template links different elements of an array'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== people = [ { name: "n0", _address: { street: "s0" }, ob: function() { return this; }, address: function() { return this._address; } }, { name: "n1", _address: { street: "s1" }, ob: function() { return this; }, address: function() { return this._address; } } ]; tmpl = jsv.templates( "{^{for people}}
                  " + " {^{:ob().name}}" + " {^{:ob().address().street}}" + "
                  {{/for}}"); tmpl.link("#result", {people: people}); // ................................ Act .................................. res = ""; nameInput0 = $("#result .person0 input.name"); nameSpan0 = $("#result .person0 span.name"); streetInput0 = $("#result .person0 input.street"); streetSpan0 = $("#result .person0 span.street"); nameInput1 = $("#result .person1 input.name"); nameSpan1 = $("#result .person1 span.name"); streetInput1 = $("#result .person1 input.street"); streetSpan1 = $("#result .person1 span.street"); getResult = function() { res += nameInput0.val() + " " + nameSpan0.text() + " " + streetInput0.val() + " " + streetSpan0.text() + " " + nameInput1.val() + " " + nameSpan1.text() + " " + streetInput1.val() + " " + streetSpan1.text() + "|"; }; getResult(); keydown(nameInput0.val('n0new')); setTimeout(function() { getResult(); keydown(nameInput1.val('n1new')); setTimeout(function() { getResult(); keydown(streetInput0.val('s0new')); setTimeout(function() { getResult(); keydown(streetInput1.val('s1new')); setTimeout(function() { getResult(); jsv.observable(people[0]).setProperty('name', 'n0update'); getResult(); jsv.observable(people[0].address()).setProperty('street', 's0update'); getResult(); jsv.observable(people[1]).setProperty('name', 'n1update'); getResult(); jsv.observable(people[1].address()).setProperty('street', 's1update'); getResult(); jsv.observable(people).remove(1); jsv.observable(people).insert({ name: "n1inserted", _address: { street: "s1inserted" }, ob: function() {return this; }, address: function() { return this._address; } }); nameInput1 = $("#result .person1 input.name"); nameSpan1 = $("#result .person1 span.name"); streetInput1 = $("#result .person1 input.street"); streetSpan1 = $("#result .person1 span.street"); getResult(); // ............................... Assert ................................. assert.equal(res, "n0 n0 s0 s0 n1 n1 s1 s1|" + "n0new n0new s0 s0 n1 n1 s1 s1|" + "n0new n0new s0 s0 n1new n1new s1 s1|" + "n0new n0new s0new s0new n1new n1new s1 s1|" + "n0new n0new s0new s0new n1new n1new s1new s1new|" + "n0update n0update s0new s0new n1new n1new s1new s1new|" + "n0update n0update s0update s0update n1new n1new s1new s1new|" + "n0update n0update s0update s0update n1update n1update s1new s1new|" + "n0update n0update s0update s0update n1update n1update s1update s1update|" + "n0update n0update s0update s0update n1inserted n1inserted s1inserted s1inserted|", 'Two-way bindings with chained computed observables remain independent when same {{for}} block links different elements of an array'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== people = [ { name: "n0", _address: { street: "s0" }, ob: function() { return this; }, address: function() { return this._address; } }, { name: "n1", _address: { street: "s1" }, ob: function() { return this; }, address: function() { return this._address; } } ]; tmpl = jsv.templates( "
                  " + " {^{:ob().name}}" + " {^{:ob().address().street}}" + "
                  "); tmpl.link("#result", people); // ................................ Act .................................. res = ""; nameInput0 = $("#result .person0 input.name"); nameSpan0 = $("#result .person0 span.name"); streetInput0 = $("#result .person0 input.street"); streetSpan0 = $("#result .person0 span.street"); nameInput1 = $("#result .person1 input.name"); nameSpan1 = $("#result .person1 span.name"); streetInput1 = $("#result .person1 input.street"); streetSpan1 = $("#result .person1 span.street"); getResult = function() { res += nameInput0.val() + " " + nameSpan0.text() + " " + streetInput0.val() + " " + streetSpan0.text() + " " + nameInput1.val() + " " + nameSpan1.text() + " " + streetInput1.val() + " " + streetSpan1.text() + "|"; }; getResult(); keydown(nameInput0.val('n0new')); setTimeout(function() { getResult(); keydown(nameInput1.val('n1new')); setTimeout(function() { getResult(); keydown(streetInput0.val('s0new')); setTimeout(function() { getResult(); keydown(streetInput1.val('s1new')); setTimeout(function() { getResult(); // ............................... Assert ................................. assert.equal(res, "n0 n0 s0 s0 n1 n1 s1 s1|" + "n0new n0 n0new n0new n1 n1 s1 s1|" + "n0new n0 n0new n0new n1new n1 n1new n1new|" + "s0new s0new s0new n0new n1new n1 n1new n1new|" + "s0new s0new s0new n0new s1new s1new s1new n1new|", 'Two-way bindings with chained computed observables using linkTo remain independent when same template links different elements of an array'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== function ob() { return this.alt ? this._obB : this._obA; } ob.depends = "alt"; function change() { jsv.observable(this._address).setProperty("street", this._address.street + "+"); } function switchAlt() { jsv.observable(this).setProperty("alt", !this.alt); } people = [ { alt: false, ob: ob, switchAlt: switchAlt, _obA: { change: change, _address: { street: "A0" }, address: function() { return this._address; } }, _obB: { change: change, _address: { street: "B0" }, address: function() { return this._address; } } }, { alt: false, ob: ob, switchAlt: switchAlt, _obA: { change: change, _address: { street: "A1" }, address: function() { return this._address; } }, _obB: { change: change, _address: { street: "B1" }, address: function() { return this._address; } } } ]; tmpl = jsv.templates( "
                  " + "" + "" + "" + "" + " " + " " + "" + "" + "" + "
                  "); tmpl.link("#result", people); // ................................ Act .................................. res = ""; var toStreet1_0 = $("#result .person0 input.toStreet1"), toStreet2_0 = $("#result .person0 input.toStreet2"), toStreetA_0 = $("#result .person0 input.toStreetA"), toStreetB_0 = $("#result .person0 input.toStreetB"), spanStreet1_0 = $("#result .person0 span.street1"), spanStreet2_0 = $("#result .person0 span.street2"), street1_0 = $("#result .person0 input.street1"), street2_0 = $("#result .person0 input.street2"), streetA_0 = $("#result .person0 input.streetA"), streetB_0 = $("#result .person0 input.streetB"), toStreet1_1 = $("#result .person1 input.toStreet1"), toStreet2_1 = $("#result .person1 input.toStreet2"), toStreetA_1 = $("#result .person1 input.toStreetA"), toStreetB_1 = $("#result .person1 input.toStreetB"), spanStreet1_1 = $("#result .person1 span.street1"), spanStreet2_1 = $("#result .person1 span.street2"), street1_1 = $("#result .person1 input.street1"), street2_1 = $("#result .person1 input.street2"), streetA_1 = $("#result .person1 input.streetA"), streetB_1 = $("#result .person1 input.streetB"); getResult = function(name) { res += name + ":" + spanStreet1_0.text() + " " + spanStreet2_0.text() + " " + street1_0.val() + " " + street2_0.val() + " " + streetA_0.val() + " " + streetB_0.val() + " " + spanStreet1_1.text() + " " + spanStreet2_1.text() + " " + street1_1.val() + " " + street2_1.val() + " " + streetA_1.val() + " " + streetB_1.val() + "|"; }; getResult(1); people[0]._obA.change(); people[0]._obB.change(); getResult(2); people[0].switchAlt(); getResult(3); people[0]._obA.change(); people[0]._obB.change(); getResult(4); people[0].switchAlt(); getResult(5); people[1].ob().change(); getResult(6); people[1].switchAlt(); getResult(7); people[1].ob().change(); getResult(8); keydown(toStreet1_0.val("new1")); setTimeout(function() { getResult(9); keydown(toStreet2_0.val("new2")); setTimeout(function() { getResult(10); keydown(toStreetA_0.val("new3")); setTimeout(function() { getResult(11); keydown(toStreetB_0.val("new4")); setTimeout(function() { getResult(12); keydown(toStreet1_1.val("new5")); setTimeout(function() { getResult(13); keydown(toStreet2_1.val("new6")); setTimeout(function() { getResult(14); keydown(toStreetA_1.val("new7")); setTimeout(function() { getResult(15); keydown(toStreetB_1.val("new8")); setTimeout(function() { getResult(16); // ............................... Assert ................................. assert.equal(res, "1:A0 A0 A0 A0 A0 B0 A1 A1 A1 A1 A1 B1|" + "2:A0+ A0+ A0+ A0+ A0+ B0+ A1 A1 A1 A1 A1 B1|" + "3:B0+ B0+ B0+ B0+ A0+ B0+ A1 A1 A1 A1 A1 B1|" + "4:B0++ B0++ B0++ B0++ A0++ B0++ A1 A1 A1 A1 A1 B1|" + "5:A0++ A0++ A0++ A0++ A0++ B0++ A1 A1 A1 A1 A1 B1|" + "6:A0++ A0++ A0++ A0++ A0++ B0++ A1+ A1+ A1+ A1+ A1+ B1|" + "7:A0++ A0++ A0++ A0++ A0++ B0++ B1 B1 B1 B1 A1+ B1|" + "8:A0++ A0++ A0++ A0++ A0++ B0++ B1+ B1+ B1+ B1+ A1+ B1+|" + "9:new1 new1 new1 new1 new1 B0++ B1+ B1+ B1+ B1+ A1+ B1+|" + "10:new2 new2 new2 new2 new2 B0++ B1+ B1+ B1+ B1+ A1+ B1+|" + "11:new3 new3 new3 new3 new3 B0++ B1+ B1+ B1+ B1+ A1+ B1+|" + "12:new3 new3 new3 new3 new3 new4 B1+ B1+ B1+ B1+ A1+ B1+|" + "13:new3 new3 new3 new3 new3 new4 new5 new5 new5 new5 A1+ new5|" + "14:new3 new3 new3 new3 new3 new4 new6 new6 new6 new6 A1+ new6|" + "15:new3 new3 new3 new3 new3 new4 new6 new6 new6 new6 new7 new6|" + "16:new3 new3 new3 new3 new3 new4 new8 new8 new8 new8 new7 new8|", 'Two-way bindings with chained computed observables with or without linkTo remain independent when multiple bindings in same tag block use same path expression'); // ................................ Reset ................................ $("#result").empty(); if (assert.async) { done() } else { start() } }, 0); }, 0); }, 0); }, 0); }, 0); }, 0); }, 0); }, 0); }, 0); }, 0); }, 0); }, 0); }, 0); }, 0); }, 0); }, 0); }, 0); }, 0); }, 0); }, 0); }, 0); }, 0); }, 0); }, 0); }, 0); }); QUnit.test("Chained computed observables in template expressions", function(assert) { (function() { // =============================== Arrange =============================== function ob() { return helpers.alt ? this._obB : this._obA; } ob.depends = "~helpers.alt"; ob.set = function(val) { if (helpers.alt) { this._obB = val; } else { this._obA = val; } }; function address() { return this._address; } function setAddress(val) { this._address = val; } address.set = setAddress; var res, resCount, helpers, ch, person, person2, data; function setData() { res = ""; resCount = 1; ch = 0; helpers = {alt: false}; person = { ob: ob, _obA: { home: { _address: { street: "A" }, _address2: { street: "A2" }, address: address } }, _obB: { home: { _address: { street: "B" }, _address2: { street: "B2" }, address: address } } }; person2 = { ob: ob, _obA: { home: { _address: { street: "xA" }, _address2: { street: "xA2" }, address: address } }, _obB: { home: { _address: { street: "xB" }, _address2: { street: "xB2" }, address: address } } }; data = {person: person}; } function swapPerson() { jsv.observable(data).setProperty("person", data.person === person ? person2 : person); } function changeAlt() { jsv.observable(helpers).setProperty("alt", !helpers.alt); } function changeOb() { jsv.observable(data.person).setProperty("ob", { home: { _address: { street: data.person.ob().home._address.street + ch++ }, _address2: { street: data.person.ob().home._address2.street + ch }, address: address } }); } function changeHome() { jsv.observable(data.person.ob()).setProperty("home", { _address: { street: data.person.ob().home._address.street + "$" }, _address2: { street: data.person.ob().home._address2.street + "$" }, address: address }); } function changeAddress() { jsv.observable(data.person.ob().home).setProperty("address", {street: data.person.ob().home.address().street + "+"}); } function changeStreet() { jsv.observable(data.person.ob().home.address()).setProperty("street", data.person.ob().home.address().street + ">"); } function getResult(name) { res += (name||resCount++) + ": " + $("#result").text() + " |"; } var tmpl = jsv.templates(""); setData(); tmpl.link("#result", data, {helpers: helpers}); // ................................ Act .................................. getResult("None"); changeStreet(); getResult("Street"); changeAddress(); getResult("Address"); changeHome(); getResult("Home"); changeOb(); getResult("Ob"); changeAlt(); getResult("Alt"); changeHome(); getResult("Home"); changeAddress(); getResult("Address"); changeStreet(); getResult("Street"); swapPerson(); getResult("Person"); changeHome(); getResult("Home"); changeStreet(); getResult("Street"); changeAddress(); getResult("Address"); changeOb(); getResult("Ob"); changeHome(); getResult("Home"); changeStreet(); getResult("Street"); changeAddress(); getResult("Address"); // ............................... Assert ................................. assert.equal(res, "None: A |Street: A> |Address: A>+ |Home: A>+$ |Ob: A>+$0 |Alt: B |Home: B$ |Address: B$+ |Street: B$+> |Person: xB |" + "Home: xB$ |Street: xB$> |Address: xB$>+ |Ob: xB$>+1 |Home: xB$>+1$ |Street: xB$>+1$> |Address: xB$>+1$>+ |", "Deep path with chained observables, binding to full depth: person^ob().home.address().street"); // ................................ Reset ................................ $("#result").empty(); setData(); // =============================== Arrange =============================== tmpl = jsv.templates(""); tmpl.link("#result", data, {helpers: helpers}); // ................................ Act .................................. getResult("None"); changeStreet(); getResult("Street"); changeAddress(); getResult("Address"); changeHome(); getResult("Home"); changeOb(); getResult("Ob"); changeAlt(); getResult("Alt"); changeHome(); getResult("Home"); changeAddress(); getResult("Address"); changeStreet(); getResult("Street"); swapPerson(); getResult("Person"); changeStreet(); getResult("Street"); changeAddress(); getResult("Address"); changeHome(); getResult("Home"); changeOb(); getResult("Ob"); changeAlt(); getResult("Alt"); // ............................... Assert ................................. assert.equal(res, "None: A |Street: A> |Address: A>+ |Home: A>+$ |Ob: A>+$0 |Alt: B |Home: B$ |Address: B$+ |Street: B$+> |Person: B$+> |Street: B$+> |Address: B$+> |Home: B$+> |Ob: B$+> |Alt: xA |", "Deep path with chained observables, binding to full depth - 1: person.ob()^home.address().street"); // ................................ Reset ................................ $("#result").empty(); setData(); // =============================== Arrange =============================== tmpl = jsv.templates(""); tmpl.link("#result", data, {helpers: helpers}); // ................................ Act .................................. getResult("None"); changeStreet(); getResult("Street"); changeAddress(); getResult("Address"); changeHome(); getResult("Home"); changeOb(); getResult("Ob"); changeStreet(); getResult("Street"); changeAddress(); getResult("Address"); changeHome(); getResult("Home"); changeAlt(); getResult("Alt"); // ............................... Assert ................................. assert.equal(res, "None: A |Street: A> |Address: A>+ |Home: A>+$ |Ob: A>+$ |Street: A>+$ |Address: A>+$ |Home: A>+$ |Alt: A>+$ |", "Deep path with chained observables, binding to leaf depth + 2: person.ob().home^address().street"); // ................................ Reset ................................ $("#result").empty(); setData(); // =============================== Arrange =============================== tmpl = jsv.templates(""); tmpl.link("#result", data, {helpers: helpers}); // ................................ Act .................................. getResult("None"); changeStreet(); getResult("Street"); changeAddress(); getResult("Address"); changeHome(); getResult("Home"); changeStreet(); getResult("Street"); changeAddress(); getResult("Address"); changeAlt(); getResult("Alt"); // ............................... Assert ................................. assert.equal(res, "None: A |Street: A> |Address: A>+ |Home: A>+ |Street: A>+ |Address: A>+ |Alt: A>+ |", "Deep path with chained observables, binding to leaf depth plus 1: person.ob().home.address()^street"); // ................................ Reset ................................ $("#result").empty(); setData(); // =============================== Arrange =============================== tmpl = jsv.templates(""); tmpl.link("#result", data, {helpers: helpers}); // ................................ Act .................................. getResult("None"); changeStreet(); getResult("Street"); changeAddress(); getResult("Address"); changeStreet(); getResult("Street"); changeAlt(); getResult("Alt"); // ............................... Assert ................................. assert.equal(res, "None: A |Street: A> |Address: A> |Street: A> |Alt: A> |", "Deep path with chained observables, binding to leaf only: person.ob().home.address().street"); })(); (function() { // =============================== Arrange =============================== function ob() { return this._ob; } ob.set = function(val) { this._ob = val; }; var res, resCount, helpers, ch, person, person2, data; function setData() { res = ""; resCount = 1; ch = 0; person = { ob: { street: "A", type: "T" } }; person2 = { ob: { street: "A2", type: "T2" } }; data = {person: person}; } function swapPerson() { jsv.observable(data).setProperty("person", data.person === person ? person2 : person); } function changeStreet() { jsv.observable(data.person.ob).setProperty("street", data.person.ob.street + ">"); } function changeType() { jsv.observable(data.person.ob).setProperty("type", data.person.ob.type + "$"); } function getResult(name) { res += (name||resCount++) + ": " + $("#result").text() + " |"; } var tmpl = jsv.templates(""); setData(); tmpl.link("#result", data); // ................................ Act .................................. getResult("None"); changeStreet(); getResult("Street"); changeType(); getResult("Type"); swapPerson(); getResult("Person"); changeType(); getResult("Type"); changeStreet(); getResult("Street"); // ............................... Assert ................................. assert.equal(res, "None: TA |Street: TA> |Type: T$A> |Person: T2A2 |Type: T2$A2 |Street: T2$A2> |", "Adjacent deep paths in expression: person^ob.type + person^ob.street"); // ................................ Reset ................................ $("#result").empty(); })(); (function() { // =============================== Arrange =============================== function ob() { return this._ob; } ob.set = function(val) { this._ob = val; }; var res, person, data; function setData() { res = ""; person = { ob: ob, _ob: { street: "A", type: "T" } }; data = {person: person}; } function changeOb() { jsv.observable(data.person).setProperty("ob", { street: data.person._ob.street + "+", type: data.person._ob.type + "+" }); } function changeStreet() { jsv.observable(data.person.ob()).setProperty("street", data.person.ob().street + ">"); } function changeType() { jsv.observable(data.person.ob()).setProperty("type", data.person.ob().type + "$"); } function getResult(name) { res += name + ": " + $("#result").text() + " |"; } var tmpl = jsv.templates(""); setData(); tmpl.link("#result", data); // ................................ Act .................................. getResult("None"); changeStreet(); getResult("Street"); changeType(); getResult("Type"); changeOb(); getResult("Ob"); changeType(); getResult("Type"); changeStreet(); getResult("Street"); // ............................... Assert ................................. assert.equal(res, "None: AT |Street: A>T |Type: A>T$ |Ob: A>+T$+ |Type: A>+T$+$ |Street: A>+>T$+$ |", "Adjacent terms in expression with paths with observables: person^ob().street + person^ob().type"); // ................................ Reset ................................ $("#result").empty(); })(); (function() { // =============================== Arrange =============================== function ob(alt, a, b) { var _ob = alt ? this._obB : this._obA; if (a) { _ob.t = [a,b,a+b+2]; } return _ob; } ob.set = function(val) { if (helpers.alt) { this._obB = val; } else { this._obA = val; } }; function address(c, d) { if (c) { this._address.t = c + d; } return this._address; } function setAddress(val) { this._address = val; } address.set = setAddress; var res, resCount, helpers, ch, person, person2, data; function setData() { res = ""; resCount = 1; ch = 0; helpers = { index: 1, a: 2, b: 1, c: 3, d: 4, alt: false }; person = { ob: ob, _obA: { home: { _address: { street: "A" }, _address2: { street: "A2" }, address: address } }, _obB: { home: { _address: { street: "B" }, _address2: { street: "B2" }, address: address } } }; person2 = { ob: ob, _obA: { home: { _address: { street: "xA" }, _address2: { street: "xA2" }, address: address } }, _obB: { home: { _address: { street: "xB" }, _address2: { street: "xB2" }, address: address } } }; data = {person: person}; } function swapPerson() { jsv.observable(data).setProperty("person", data.person === person ? person2 : person); } function changeAlt() { jsv.observable(helpers).setProperty("alt", !helpers.alt); } function changeA() { jsv.observable(helpers).setProperty("a", helpers.a + 1); } function changeD() { jsv.observable(helpers).setProperty("d", helpers.d + 1); } function changeIndex(val) { jsv.observable(helpers).setProperty("index", val); } function changeOb() { jsv.observable(data.person).setProperty("ob", { home: { _address: { street: data.person.ob(helpers.alt).home._address.street + ch++ }, _address2: { street: data.person.ob(helpers.alt).home._address2.street + ch }, address: address } }); } function changeHome() { jsv.observable(data.person.ob(helpers.alt)).setProperty("home", { _address: { street: data.person.ob(helpers.alt).home._address.street + "$" }, _address2: { street: data.person.ob(helpers.alt).home._address2.street + "$" }, address: address }); } function changeAddress() { jsv.observable(data.person.ob(helpers.alt).home).setProperty("address", {street: data.person.ob(helpers.alt).home.address().street + "+"}); } function changeStreet() { jsv.observable(data.person.ob(helpers.alt).home.address()).setProperty("street", data.person.ob(helpers.alt).home.address().street + ">"); } function getResult(name) { res += (name||resCount++) + ": " + $("#result").text() + " |"; } var tmpl = jsv.templates(""); setData(); tmpl.link("#result", data, {helpers: helpers}); // ................................ Act .................................. getResult("None"); changeStreet(); getResult("Street"); changeAddress(); getResult("Address"); changeHome(); getResult("Home"); changeD(); getResult("D"); changeIndex(3); getResult("Index3"); changeA(); getResult("A"); changeIndex(2); getResult("Index2"); changeOb(); getResult("Ob"); changeAlt(); getResult("Alt"); changeHome(); getResult("Home"); changeAddress(); getResult("Address"); changeStreet(); getResult("Street"); swapPerson(); getResult("Person"); changeHome(); getResult("Home"); changeStreet(); getResult("Street"); changeAddress(); getResult("Address"); changeOb(); getResult("Ob"); changeHome(); getResult("Home"); changeIndex(1); getResult("Index1"); changeA(); getResult("A"); changeD(); getResult("D"); changeStreet(); getResult("Street"); changeAddress(); getResult("Address"); // ............................... Assert ................................. assert.equal(res, "None: 46A |Street: 46A> |Address: 46A>+ |Home: 46A>+$ |D: 52A>+$ |Index3: 58A>+$ |A: 60A>+$ |Index2: 50A>+$ |Ob: 50A>+$0 |Alt: 50B |Home: 50B$ |Address: 50B$+ |Street: 50B$+> |Person: 50xB |Home: 50xB$ |Street: 50xB$> |Address: 50xB$>+ |Ob: 50xB$>+1 |Home: 50xB$>+1$ |Index1: 54xB$>+1$ |A: 56xB$>+1$ |D: 62xB$>+1$ |Street: 62xB$>+1$> |Address: 62xB$>+1$>+ |", "Complex expression with multiple adjacent paths, with nested () and [] paren expressions, chained observables, arithmetic expressions etc."); // ................................ Reset ................................ $("#result").empty(); })(); (function() { // =============================== Arrange =============================== function getResult(name) { res += (name||resCount++) + ": " + $("#result").text() + " |"; } var res = "", resCount = 1, data = { type: 't', sectionTypes: { t: { types: [22, 33] }, n: { types: [66, 77] } } }, helpers = { mode: "m" }, tmpl = jsv.templates({ markup: "{^{section 'A' ~mode ~sectionTypes=~root.sectionTypes[type].types/}}", tags: { section: function(setting, mode) { return setting + mode + this.ctxPrm("sectionTypes")[0]; } } }); // ................................ Act .................................. tmpl.link("#result", data, helpers); getResult("None"); jsv.observable(data).setProperty("type", "n"); getResult("type"); // ............................... Assert ................................. assert.equal(res, "None: Am22 |type: Am22 |", "Complex unbound expression with [] paren expressions etc."); // ................................ Reset ................................ $("#result").empty(); })(); (function() { // =============================== Arrange =============================== function getResult(name) { res += (name||resCount++) + ": " + $("#result").text() + " |"; } var res = "", resCount = 1, data = { type: 't', sectionTypes: { t: { types: [22, 33] }, n: { types: [66, 77] } } }, helpers = { mode: "m" }, tmpl = jsv.templates({ markup: "{^{section 'A' ~mode ^~sectionTypes=~root.sectionTypes[type].types/}}", tags: { section: function(setting, mode) { return setting + mode + this.ctxPrm("sectionTypes")[0]; } } }); // ................................ Act .................................. tmpl.link("#result", data, helpers); getResult("None"); jsv.observable(data).setProperty("type", "n"); getResult("type"); // ............................... Assert ................................. assert.equal(res, "None: Am22 |type: Am66 |", "Complex bound expression with [] paren expressions etc."); // ................................ Reset ................................ $("#result").empty(); })(); (function() { // =============================== Arrange =============================== function ob(alt, a, b) { var _ob = alt ? this._obB : this._obA; if (a) { _ob.t = [a,b,a+b+2]; } return _ob; } ob.set = function(val) { if (helpers.alt) { this._obB = val; } else { this._obA = val; } }; function address(c, d) { if (c) { this._address.t = c + d; } return this._address; } function setAddress(val) { this._address = val; } address.set = setAddress; var res, resCount, helpers, ch, person, person2, data; function setData() { res = ""; resCount = 1; ch = 0; helpers = { index: 1, a: 2, b: 1, c: 3, d: 4, alt: false }; person = { ob: ob, _obA: { home: { _address: { street: "A" }, _address2: { street: "A2" }, address: address } }, _obB: { home: { _address: { street: "B" }, _address2: { street: "B2" }, address: address } } }; person2 = { ob: ob, _obA: { home: { _address: { street: "xA" }, _address2: { street: "xA2" }, address: address } }, _obB: { home: { _address: { street: "xB" }, _address2: { street: "xB2" }, address: address } } }; data = {person: person}; } function swapPerson() { jsv.observable(data).setProperty("person", data.person === person ? person2 : person); } function changeAlt() { jsv.observable(helpers).setProperty("alt", !helpers.alt); } function changeA() { jsv.observable(helpers).setProperty("a", helpers.a + 1); } function changeD() { jsv.observable(helpers).setProperty("d", helpers.d + 1); } function changeIndex(val) { jsv.observable(helpers).setProperty("index", val); } function changeOb() { jsv.observable(data.person).setProperty("ob", { home: { _address: { street: data.person.ob(helpers.alt).home._address.street + ch++ }, _address2: { street: data.person.ob(helpers.alt).home._address2.street + ch }, address: address } }); } function changeHome() { jsv.observable(data.person.ob(helpers.alt)).setProperty("home", { _address: { street: data.person.ob(helpers.alt).home._address.street + "$" }, _address2: { street: data.person.ob(helpers.alt).home._address2.street + "$" }, address: address }); } function changeAddress() { jsv.observable(data.person.ob(helpers.alt).home).setProperty("address", {street: data.person.ob(helpers.alt).home.address().street + "+"}); } function changeStreet() { jsv.observable(data.person.ob(helpers.alt).home.address()).setProperty("street", data.person.ob(helpers.alt).home.address().street + ">"); } function getResult(name) { res += (name||resCount++) + ": " + $("#result").text() + " |"; } var tmpl = jsv.templates({ markup: "{^{mytag ^myprop=((person^ob(~helpers.alt).home.address(~helpers.c, ~helpers.d).t*3) + person^ob(~helpers.alt, ~helpers.a, ~helpers.b).t[(~helpers.index + 1) - 2])*2 + person^ob(~helpers.alt).home.address().street}}{{/mytag}}", tags: { mytag: function() { return this.tagCtx.props.myprop; } } }); setData(); tmpl.link("#result", data, {helpers: helpers}); // ................................ Act .................................. getResult("None"); changeStreet(); getResult("Street"); changeAddress(); getResult("Address"); changeHome(); getResult("Home"); changeD(); getResult("D"); changeIndex(3); getResult("Index3"); changeA(); getResult("A"); changeIndex(2); getResult("Index2"); changeOb(); getResult("Ob"); changeAlt(); getResult("Alt"); changeHome(); getResult("Home"); changeAddress(); getResult("Address"); changeStreet(); getResult("Street"); swapPerson(); getResult("Person"); changeHome(); getResult("Home"); changeStreet(); getResult("Street"); changeAddress(); getResult("Address"); changeOb(); getResult("Ob"); changeHome(); getResult("Home"); changeIndex(1); getResult("Index1"); changeA(); getResult("A"); changeD(); getResult("D"); changeStreet(); getResult("Street"); changeAddress(); getResult("Address"); // ............................... Assert ................................. assert.equal(res, "None: 46A |Street: 46A> |Address: 46A>+ |Home: 46A>+$ |D: 52A>+$ |Index3: 58A>+$ |A: 60A>+$ |Index2: 50A>+$ |Ob: 50A>+$0 |Alt: 50B |Home: 50B$ |Address: 50B$+ |Street: 50B$+> |Person: 50xB |Home: 50xB$ |Street: 50xB$> |Address: 50xB$>+ |Ob: 50xB$>+1 |Home: 50xB$>+1$ |Index1: 54xB$>+1$ |A: 56xB$>+1$ |D: 62xB$>+1$ |Street: 62xB$>+1$> |Address: 62xB$>+1$>+ |", "Complex expression on bound tag property, with multiple adjacent paths, with nested () and [] paren expressions, chained observables, arithmetic expressions etc."); // ................................ Reset ................................ $("#result").empty(); })(); (function() { // =============================== Arrange =============================== function setData() { res = ""; theB = { a: "a" }; theC = { _b: theB, b: function() { return this._b; } }; theD = { _c: theC, c: function() { return this._c; } }; theE = { _d: theD, d: function() { return this._d; } }; theF = { _e: theE, e: function() { return this._e; } }; theC.b.set = function(val) { this._b = val; }; theD.c.set = function(val) { this._c = val; }; theE.d.set = function(val) { this._d = val; }; theF.e.set = function(val) { this._e = val; }; data = { f: theF, e: theE, d: theD, c: theC, b: theB }; } function getResult(name) { res += (name||resCount++) + ": " + $("#result").text() + " |"; } var theB, theC, theD, theE, theF, data, res, resCount = 1, tmpl = jsv.templates('{^{:f.e().d().c().b().a}} {^{:f.e().d().c().b()^a}} {^{:f.e().d().c()^b().a}} {^{:f.e().d()^c().b().a}} {^{:f.e()^d().c().b().a}} {^{:f^e().d().c().b().a}}'); setData(); tmpl.link("#result", data); getResult("None"); // ................................ Act .................................. jsv.observable(theB).setProperty("a", "A"); getResult("Change a"); jsv.observable(theC).setProperty("b", {a: "B"}); getResult("Change b"); jsv.observable(theD).setProperty("c", { _b: {a: "C"}, b: function() { return this._b; } }); getResult("Change c"); jsv.observable(theE).setProperty("d", { _c: { _b: {a: "D"}, b: function() { return this._b; } }, c: function() { return this._c; } }); getResult("Change d"); jsv.observable(theF).setProperty("e", { _d: { _c: { _b: {a: "E"}, b: function() { return this._b; } }, c: function() { return this._c; } }, d: function() { return this._d; } }); getResult("Change e"); // ............................... Assert ................................. assert.equal(res, "None: a a a a a a |Change a: A A A A A A |Change b: A B B B B B |Change c: A B C C C C |Change d: A B C D D D |Change e: A B C D E E |", "{{: ...}} expressions with deeply chained computed observables"); // =============================== Arrange =============================== function setData2() { res = ""; theB = { a1: { a: "a" } }; theC = { b1: { _b: theB, b: function() { return this._b; } } }; theD = { c1: { _c: theC, c: function() { return this._c; } } }; theE = { d1: { _d: theD, d: function() { return this._d; } } }; theF = { e1: { _e: theE, e: function() { return this._e; } } }; theC.b1.b.set = function(val) { this._b = val; }; theD.c1.c.set = function(val) { this._c = val; }; theE.d1.d.set = function(val) { this._d = val; }; theF.e1.e.set = function(val) { this._e = val; }; } tmpl = jsv.templates('{^{:e1.e().d1.d().c1.c().b1.b().a1.a}} {^{:e1.e().d1.d().c1.c().b1.b().a1^a}} {^{:e1.e().d1.d().c1.c().b1.b()^a1.a}} {^{:e1.e().d1.d().c1.c().b1^b().a1.a}} {^{:e1.e().d1.d().c1.c()^b1.b().a1.a}} {^{:e1.e().d1.d().c1^c().b1.b().a1.a}} {^{:e1.e().d1.d()^c1.c().b1.b().a1.a}} {^{:e1.e().d1^d().c1.c().b1.b().a1.a}} {^{:e1.e()^d1.d().c1.c().b1.b().a1.a}} {^{:e1^e().d1.d().c1.c().b1.b().a1.a}}'); setData2(); tmpl.link("#result", theF); getResult("None"); // ................................ Act .................................. jsv.observable(theB.a1).setProperty("a", "A"); getResult("Change a"); jsv.observable(theB).setProperty("a1", { a: "A1" }); getResult("Change a1"); jsv.observable(theC.b1).setProperty("b", { a1: { a: "B" } }); getResult("Change b"); jsv.observable(theC).setProperty("b1", { _b: { a1: { a: "B1" } }, b: function() { return this._b; } }); getResult("Change b1"); jsv.observable(theD.c1).setProperty("c", { b1: { _b: { a1: { a: "C" } }, b: function() { return this._b; } } }); getResult("Change c"); jsv.observable(theD).setProperty("c1", { _c: { b1: { _b: { a1: { a: "C1" } }, b: function() { return this._b; } } }, c: function() { return this._c; } }); getResult("Change c1"); jsv.observable(theE.d1).setProperty("d", { c1: { _c: { b1: { _b: { a1: { a: "D" } }, b: function() { return this._b; } } }, c: function() { return this._c; } } }); getResult("Change d"); jsv.observable(theE).setProperty("d1", { _d: { c1: { _c: { b1: { _b: { a1: { a: "D1" } }, b: function() { return this._b; } } }, c: function() { return this._c; } } }, d: function() { return this._d; } }); getResult("Change d1"); jsv.observable(theF.e1).setProperty("e", { d1: { _d: { c1: { _c: { b1: { _b: { a1: { a: "E" } }, b: function() { return this._b; } } }, c: function() { return this._c; } } }, d: function() { return this._d; } } }); getResult("Change e"); jsv.observable(theF).setProperty("e1", { _e: { d1: { _d: { c1: { _c: { b1: { _b: { a1: { a: "E1" } }, b: function() { return this._b; } } }, c: function() { return this._c; } } }, d: function() { return this._d; } } }, e: function() { return this._e; } }); getResult("Change e1"); // ............................... Assert ................................. assert.equal(res, "None: a a a a a a a a a a |Change a: A A A A A A A A A A |Change a1: A A1 A1 A1 A1 A1 A1 A1 A1 A1 |Change b: A A1 B B B B B B B B |Change b1: A A1 B B1 B1 B1 B1 B1 B1 B1 |Change c: A A1 B B1 C C C C C C |Change c1: A A1 B B1 C C1 C1 C1 C1 C1 |Change d: A A1 B B1 C C1 D D D D |Change d1: A A1 B B1 C C1 D D1 D1 D1 |Change e: A A1 B B1 C C1 D D1 E E |Change e1: A A1 B B1 C C1 D D1 E E1 |", "{{: ...}} expressions with deeply chained computed observables (variant)"); // =============================== Arrange =============================== tmpl = jsv.templates('{^{for f.e().d().c().b()}}{^{:a}}{{/for}} {^{for f.e().d().c()^b()}}{^{:a}}{{/for}} {^{for f.e().d()^c().b()}}{^{:a}}{{/for}} {^{for f.e()^d().c().b()}}{^{:a}}{{/for}} {^{for f^e().d().c().b()}}{^{:a}}{{/for}}'); setData(); tmpl.link("#result", data); getResult("None"); // ................................ Act .................................. jsv.observable(theB).setProperty("a", "A"); getResult("Change a"); jsv.observable(theC).setProperty("b", {a: "B"}); getResult("Change b"); jsv.observable(theD).setProperty("c", { _b: {a: "C"}, b: function() { return this._b; } }); getResult("Change c"); jsv.observable(theE).setProperty("d", { _c: { _b: {a: "D"}, b: function() { return this._b; } }, c: function() { return this._c; } }); getResult("Change d"); jsv.observable(theF).setProperty("e", { _d: { _c: { _b: {a: "E"}, b: function() { return this._b; } }, c: function() { return this._c; } }, d: function() { return this._d; } }); getResult("Change e"); // ............................... Assert ................................. assert.equal(res, "None: a a a a a |Change a: A A A A A |Change b: B B B B B |Change c: B C C C C |Change d: B C D D D |Change e: B C D E E |", "{{for ...}} expressions with deeply chained computed observables"); // =============================== Arrange =============================== tmpl = jsv.templates('{^{:f.e().d().c().b().a + " " + e.d().c().b().a + " " + d.c().b().a + " " + c.b().a + " " + b.a}} | {^{:f.e()^d().c().b().a + " " + e.d()^c().b().a + " " + d.c()^b().a + " " + c.b()^a + " " + b.a}}'); setData(); tmpl.link("#result", data); getResult("None"); // ................................ Act .................................. jsv.observable(theB).setProperty("a", "A"); getResult("Change a"); jsv.observable(theC).setProperty("b", {a: "B"}); getResult("Change b"); jsv.observable(theD).setProperty("c", { _b: {a: "C"}, b: function() { return this._b; } }); getResult("Change c"); jsv.observable(theE).setProperty("d", { _c: { _b: {a: "D"}, b: function() { return this._b; } }, c: function() { return this._c; } }); getResult("Change d"); jsv.observable(theF).setProperty("e", { _d: { _c: { _b: {a: "E"}, b: function() { return this._b; } }, c: function() { return this._c; } }, d: function() { return this._d; } }); getResult("Change e"); // ............................... Assert ................................. assert.equal(res, "None: a a a a a | a a a a a |Change a: A A A A A | A A A A A |Change b: A A A A A | B B B B A |Change c: A A A A A | C C C B A |Change d: A A A A A | D D C B A |Change e: A A A A A | E D C B A |", "Sibling {{: ...}} expressions with deeply chained computed observables"); // =============================== Arrange =============================== var VM = {Places: ["dd", "ee"]}, vmCollection = jsv.views.viewModels; jsv.views.viewModels({Invitation: {getters: [{ getter: "Places", defaultVal: []}]}}); data = vmCollection.Invitation.map(VM); tmpl = jsv.templates('{^{include ~rowsLength=Places().length}}{{/include}}{^{:Places()^length}}'); tmpl.link("#result", data); res = ""; try { let newList = data.Places()[0] == "aa" ? ["bb", "cc"] : ["aa"]; jsv.observable(data).setProperty("Places", newList); } catch(e) { res = e.message; } // ............................... Assert ................................. assert.equal(res, "", "Complex nested parens with data-link expressions"); //https://github.com/BorisMoore/jsviews/issues/463#issuecomment-2651496030 // ................................ Reset ................................ $("#result").empty(); })(); }); QUnit.test("replace mode and link=false mode", function(assert) { (function() { jsv.views.settings.advanced({_jsv: true}); // For using viewsAndBindings($) // =============================== Arrange =============================== $("#result").html("
                  "); var tmpl = jsv.templates('{^{include}}{^{for people}}{^{:name}}{{/for}}{{/include}}'), data = {people: [ {name: "Jo"}, {name: "Pete"} ]}, result = ""; // ................................ Act .................................. tmpl.link("#result .content", data); res = $("#result").text() + "(" + $("#result .content script").length + ") "; jsv.observable(data.people[0]).setProperty("name", "Bob"); jsv.observable(data.people).insert({name: "Jane"}); res += $("#result").text(); // ............................... Assert ................................. assert.equal(res, "JoPete(18) BobPeteJane", 'tmpl.link(container, data) renders and data-links'); // ................................ Act .................................. $("#result .content").empty(); // ............................... Assert ................................. assert.ok(!viewsAndBindings($) && JSON.stringify(_jsv.cbBindings) === "{}", "$(container).empty removes both views and current listeners from that content"); // ................................ Act .................................. data = {people: [ {name: "Jo"}, {name: "Pete"} ]}; tmpl.link("#result .content", data, {link: false}); res = $("#result").text() + "(" + $("#result .content script").length + ") "; jsv.observable(data.people[0]).setProperty("name", "Bob"); jsv.observable(data.people).insert({name: "Jane"}); res += $("#result").text(); // ............................... Assert ................................. assert.equal(res, "JoPete(4) JoPete", 'tmpl.link(container, data, {link: false}) renders but does not data-link, and does not insert script node markers'); // ................................ Act .................................. $("#result .content").empty(); // ............................... Assert ................................. assert.ok(!viewsAndBindings($) && JSON.stringify(_jsv.cbBindings) === "{}", "$(container).empty removes both views and current listeners from that content"); // ................................ Act .................................. data = {people: [ {name: "Jo"}, {name: "Pete"} ]}; tmpl.link("#result .content", data, {target: "replace"}); res = $("#result .content").length + " - " + $("#result").text() + "(" + $("#result script").length + ") "; jsv.observable(data.people[0]).setProperty("name", "Bob"); jsv.observable(data.people).insert({name: "Jane"}); res += $("#result").text(); // ............................... Assert ................................. assert.equal(res, "0 - JoPete(18) BobPeteJane", 'tmpl.link(container, data, {target: "replace"}) replaces the target "container" by rendered content, and data-links'); // ................................ Act .................................. $("#result").empty(); // ............................... Assert ................................. assert.ok(!viewsAndBindings($) && JSON.stringify(_jsv.cbBindings) === "{}", "$(container).empty removes both views and current listeners from that content"); // =============================== Arrange =============================== $("#result").html("
                  "); // ................................ Act .................................. data = {people: [ {name: "Jo"}, {name: "Pete"} ]}; tmpl.link("#result .content", data, {target: "replace", link: false}); res = $("#result .content").length + " - " + $("#result").text() + "(" + $("#result script").length + ") "; jsv.observable(data.people[0]).setProperty("name", "Bob"); jsv.observable(data.people).insert({name: "Jane"}); res += $("#result").text(); // ............................... Assert ................................. assert.equal(res, "0 - JoPete(4) JoPete", 'tmpl.link(container, data, {target: "replace", link: false}) replaces the target "container" by rendered content, and does not data-link'); // ................................ Act .................................. $("#result").empty(); // ............................... Assert ................................. assert.ok(!viewsAndBindings($) && JSON.stringify(_jsv.cbBindings) === "{}", "$(container).empty removes both views and current listeners from that content"); // =============================== Arrange =============================== $("#result").html("
                  "); tmpl = jsv.templates('{^{include link=false}}{^{for people}}{^{:name}}{{/for}}{{/include}}'); // ................................ Act .................................. data = {people: [ {name: "Jo"}, {name: "Pete"} ]}; tmpl.link("#result .content", data); res = $("#result").text() + "(" + $("#result .content script").length + ") "; jsv.observable(data.people[0]).setProperty("name", "Bob"); jsv.observable(data.people).insert({name: "Jane"}); res += $("#result").text(); // ............................... Assert ................................. assert.equal(res, "JoPete(8) JoPete", 'link=false on a block tag (e.g. {^{include link=false}}...) renders, but does not data-link, the content'); // ................................ Act .................................. $("#result .content").empty(); // ............................... Assert ................................. assert.ok(!viewsAndBindings($) && JSON.stringify(_jsv.cbBindings) === "{}", "$(container).empty removes both views and current listeners from that content"); jsv.views.settings.advanced({_jsv: false}); // For using viewsAndBindings($) })(); }); QUnit.module("API - data-bound tags"); QUnit.test("{^{:expression}}", function(assert) { // ................................ Reset ................................ person1.lastName = "One"; // reset Prop jsv.views.settings.advanced({_jsv: true}); // For using viewsAndBindings($) // =============================== Arrange =============================== jsv.templates("{^{:\"'\" + 1 + '\"' + 2 + '\\' + 3}}") .link("#result"); // ................................ Act .................................. before = $("#result").text(); // ............................... Assert ................................. assert.equal(before, "'1\"2\\3", "Data link using: {^{:'1\"2\\3}}"); // =============================== Arrange =============================== jsv.templates('prop:{^{:lastName}}') .link("#result", person1); // ................................ Act .................................. before = $("#result").text(); jsv.observable(person1).setProperty("lastName", "newLast"); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, 'prop:One|prop:newLast', 'Data link using: {^{:lastName}}'); // =============================== Arrange =============================== // ................................ Act .................................. $("#result").empty(); // ............................... Assert ................................. assert.ok(!viewsAndBindings($) && !$._data(person1).events, "$(container).empty removes both views and current listeners from that content"); // ................................ Reset ................................ person1.lastName = "One"; // reset Prop // =============================== Arrange =============================== jsv.templates('prop:{^{:wasUndefined}}') .link("#result", person1); // ................................ Act .................................. before = $("#result").text(); jsv.observable(person1).setProperty("wasUndefined", "newLast"); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, 'prop:|prop:newLast', 'Data link using: {^{:wasUndefined}} - renders to empty string when undefined, and still binds correctly for subsequent modifications'); // ................................ Reset ................................ $("#result").empty(); person1.lastName = "One"; // reset Prop // =============================== Arrange =============================== var tmpl = jsv.templates("{^{:#data.person1.home.address.street}}{^{:person1.home^address.street}}"); jsv.link(tmpl, "#result", model); // ................................ Act .................................. before = $("#result").text(); jsv.observable(address1).setProperty("street", "newStreetOne"); jsv.observable(person1).setProperty("home", home2); // Deep change after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, "StreetOneStreetOne|newStreetOneStreetTwo", '#data.person1.home.address.street binds only to the leaf, but person1.home^address.street does deep binding'); // ................................ Reset ................................ $("#result").empty(); address1.street = "StreetOne"; person1.home = home1; // =============================== Arrange =============================== var count = 0, data = { last: "Smith", other: "Other", a: function() { return this; } }; jsv.views.tags({ textbox: { onAfterLink: function() { // Find input in contents, if not already found this.linkedElem = this.linkedElem || this.contents("input"); }, onUpdate: false, template: "{^{:~tagCtx.props.label}}
                  " } }); tmpl = jsv.templates( '' + '' + '' + '' + '{^{:convert=~upper last a().other}}' + '{^{:a().other last convert=~upper}}' + '{^{textbox a().other last convert=~upper convertBack=~lower/}}' + '{^{textbox convert=~upper convertBack=~lower last a().other/}}'); jsv.views.settings.trigger(false); jsv.link(tmpl, "#result", data, { upper: function(val) { return val.toUpperCase(); }, lower: function(val) { return val.toLowerCase(); } }); // ................................ Act .................................. before = $("#result").text(); $("#result input").each(function() { before += this.value; }); jsv.observable(data).setProperty("last", "newLast"); jsv.observable(data).setProperty("other", "newOther"); $("#result input").each(function() { var last = data.last; this.value += count++; $(this).change(); }); after = $("#result").text(); $("#result input").each(function() { after += this.value; }); jsv.views.settings.trigger(true); // ............................... Assert ................................. assert.equal(before + "|" + after, "SMITHOTHEROTHERSMITHOtherSmithOTHERSMITH|NEWLAST135NEWOTHER024NEWOTHER024NEWLAST135newother024newlast135NEWOTHER024NEWLAST135", 'Binding correctly to and from first argument, even with multiple args and props and with objects in paths, and with converters'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== var ob = {text: "aBc"}; jsv.link("~upper(~ob.text) + ~lower(~ob.text)", "#result", undefined, { upper: function(val) { return val.toUpperCase(); }, lower: function(val) { return val.toLowerCase(); }, ob: ob }); // ................................ Act .................................. before = $("#result").text(); jsv.observable(ob).setProperty("text", "DeF"); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, "ABCabc|DEFdef", 'jsv.link(expression, selector, undefined, helpers) - without passing data, data-links correctly to helpers'); // ................................ Reset ................................ $("#result").empty(); jsv.unlink("#result"); // =============================== Arrange =============================== ob = {text: "aBc"}; jsv.views.helpers({ upper: function(val) { return val.toUpperCase(); }, lower: function(val) { return val.toLowerCase(); }, ob: ob }); jsv.link("~upper(~ob.text) + ~lower(~ob.text)", "#result"); // ................................ Act .................................. before = $("#result").text(); jsv.observable(ob).setProperty("text", "DeF"); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, "ABCabc|DEFdef", 'jsv.link(expression, selector) - without passing data, data-links correctly to helpers'); // ................................ Reset ................................ jsv.unlink("#result"); $("#result").empty(); // =============================== Arrange =============================== tmpl = jsv.templates("{^{:~upper(~ob.text)}}{^{:~lower(~ob.text)}}"); jsv.views.helpers.ob = ob = {text: "aBc"}; jsv.link(tmpl, "#result"); // ................................ Act .................................. before = $("#result").text(); jsv.observable(ob).setProperty("text", "DeF"); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, "ABCabc|DEFdef", 'jsv.link(template, selector) - without passing data, data-links correctly to helpers'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== jsv.views.helpers.ob = ob = {text: "aBc"}; tmpl.link("#result"); // ................................ Act .................................. before = $("#result").text(); jsv.observable(ob).setProperty("text", "DeF"); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, "ABCabc|DEFdef", 'template.link(selector) - without passing data, data-links correctly to helpers'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== jsv.views.helpers.ob = ob = {text: "aBc"}; $("#result").link(tmpl); // ................................ Act .................................. before = $("#result").text(); jsv.observable(ob).setProperty("text", "DeF"); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, "ABCabc|DEFdef", '$(selector).link(template) - without passing data, data-links correctly to helpers'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== jsv.views.helpers.ob = ob = {text: "aBc"}; $("#result").link("~upper(~ob.text) + ~lower(~ob.text)"); // ................................ Act .................................. before = $("#result").text(); jsv.observable(ob).setProperty("text", "DeF"); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, "ABCabc|DEFdef", '$(selector).link(expression) - without passing data, data-links correctly to helpers'); // ................................ Reset ................................ jsv.unlink("#result"); $("#result").empty(); jsv.views.settings.advanced({_jsv: false}); }); QUnit.test("{^{>expression}}", function(assert) { jsv.views.settings.advanced({_jsv: true}); // For using viewsAndBindings($) // =============================== Arrange =============================== jsv.templates('prop:{^{>lastName + "
                  "}}') .link("#result", person1); // ................................ Act .................................. before = $("#result").text(); jsv.observable(person1).setProperty("lastName", "newLast"); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, 'prop:One
                  |prop:newLast
                  ', 'Data link using: {^{:lastName}}'); // =============================== Arrange =============================== // ................................ Act .................................. $("#result").empty(); // ............................... Assert ................................. assert.ok(!viewsAndBindings($) && !$._data(person1).events, "$(container).empty removes both views and current listeners from that content"); // ................................ Reset ................................ person1.lastName = "One"; // reset Prop // =============================== Arrange =============================== var tmpl = jsv.templates("{^{>#data.person1.home.address.street}}{^{>person1.home^address.street}}"); jsv.link(tmpl, "#result", model); // ................................ Act .................................. before = $("#result").text(); jsv.observable(address1).setProperty("street", "newStreetOne"); jsv.observable(person1).setProperty("home", home2); // Deep change after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, "StreetOneStreetOne|newStreetOneStreetTwo", '#data.person1.home.address.street binds only to the leaf, but person1.home^address.street does deep binding'); // ................................ Reset ................................ $("#result").empty(); address1.street = "StreetOne"; person1.home = home1; jsv.views.settings.advanced({_jsv: false}); }); QUnit.test("{^{tag}}", function(assert) { // ................................ Reset ................................ person1._firstName = "Jo"; // reset Prop person1.lastName = "One"; // reset Prop settings.title = "Mr"; // reset Prop settings.width = 30; // reset Prop // =============================== Arrange =============================== jsv.views.tags({ norendernotemplate: {}, voidrender: function() {}, emptyrender: function() { return ""; }, emptytemplate: { template: "" }, templatereturnsempty: { template: "{{:a}}" } }); // ............................... Assert ................................. jsv.templates("a{{norendernotemplate/}}b{^{norendernotemplate/}}c{{norendernotemplate}}{{/norendernotemplate}}d{^{norendernotemplate}}{{/norendernotemplate}}e").link("#result", 1); assert.equal($("#result").text(), "abcde", "non-rendering tag (no template, no render function) renders empty string"); jsv.templates("a{{voidrender/}}b{^{voidrender/}}c{{voidrender}}{{/voidrender}}d{^{voidrender}}{{/voidrender}}e").link("#result", 1); assert.equal($("#result").text(), "abcde", "non-rendering tag (no template, no return from render function) renders empty string"); jsv.templates("a{{emptyrender/}}b{^{emptyrender/}}c{{emptyrender}}{{/emptyrender}}d{^{emptyrender}}{{/emptyrender}}e").link("#result", 1); assert.equal($("#result").text(), "abcde", "non-rendering tag (no template, empty string returned from render function) renders empty string"); jsv.templates("a{{emptytemplate/}}b{^{emptytemplate/}}c{{emptytemplate}}{{/emptytemplate}}d{^{emptytemplate}}{{/emptytemplate}}e").link("#result", 1); assert.equal($("#result").text(), "abcde", "non-rendering tag (template has no content, no render function) renders empty string"); jsv.templates("a{{templatereturnsempty/}}b{^{templatereturnsempty/}}c{{templatereturnsempty}}{{/templatereturnsempty}}d{^{templatereturnsempty}}{{/templatereturnsempty}}e").link("#result", 1); assert.equal($("#result").text(), "abcde", "non-rendering tag (template returns empty string, no render function) renders empty string"); // =============================== Arrange =============================== jsv.templates('{^{tmplTag/}}') .link("#result", person1, {settings: settings}); // ................................ Act .................................. before = $("#result").text(); jsv.observable(person1).setProperty({firstName: "newFirst", lastName: "newLast"}); jsv.observable(settings).setProperty({title: "Sir", width: 40}); jsv.observable(person1).setProperty({fullName: "compFirst compLast"}); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, 'Name: Mr Jo. Width: 30|Name: Sir compFirst. Width: 40', 'Data link with: {^{tmplTag/}} updates when dependant object paths change'); // ................................ Reset ................................ $("#result").empty(); person1._firstName = "Jo"; // reset Prop person1.lastName = "One"; // reset Prop settings.title = "Mr"; // reset Prop settings.width = 30; // reset Prop // =============================== Arrange =============================== jsv.templates('{{tmplTag/}}') .link("#result", person1, {settings: settings}); // ................................ Act .................................. before = $("#result").text(); jsv.observable(person1).setProperty({firstName: "newFirst", lastName: "newLast"}); jsv.observable(settings).setProperty({title: "Sir", width: 40}); jsv.observable(person1).setProperty({fullName: "compFirst compLast"}); after = $("#result").text(); // ............................... Assert ................................. assert.ok(before === 'Name: Mr Jo. Width: 30' && before === after && !$._data(person1).events && !$._data(settings).events, 'Data link with: {{tmplTag/}} does nothing'); // ................................ Reset ................................ $("#result").empty(); person1._firstName = "Jo"; // reset Prop person1.lastName = "One"; // reset Prop settings.title = "Mr"; // reset Prop settings.width = 30; // reset Prop // =============================== Arrange =============================== jsv.templates('{^{fnTag/}}') .link("#result", person1, {settings: settings}); // ................................ Act .................................. before = $("#result").text(); jsv.observable(person1).setProperty({firstName: "newFirst", lastName: "newLast"}); jsv.observable(settings).setProperty({title: "Sir", width: 40}); jsv.observable(person1).setProperty({fullName: "compFirst compLast"}); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, 'Name: Mr Jo. Width: 30|Name: Sir compFirst. Width: 40', 'Data link with: {^{fnTag/}} updates when dependant object paths change'); // ................................ Reset ................................ $("#result").empty(); person1._firstName = "Jo"; // reset Prop person1.lastName = "One"; // reset Prop settings.title = "Mr"; // reset Prop settings.width = 30; // reset Prop // =============================== Arrange =============================== jsv.templates('{{fnTag/}}') .link("#result", person1, {settings: settings}); // ................................ Act .................................. before = $("#result").text(); jsv.observable(person1).setProperty({firstName: "newFirst", lastName: "newLast"}); jsv.observable(settings).setProperty({title: "Sir", width: 40}); jsv.observable(person1).setProperty({fullName: "compFirst compLast"}); after = $("#result").text(); // ............................... Assert ................................. assert.ok(before === 'Name: Mr Jo. Width: 30' && before === after && !$._data(person1).events && !$._data(settings).events, 'Data link with: {{fnTag/}} does nothing'); // ................................ Reset ................................ $("#result").empty(); person1._firstName = "Jo"; // reset Prop person1.lastName = "One"; // reset Prop settings.title = "Mr"; // reset Prop settings.width = 30; // reset Prop // =============================== Arrange =============================== jsv.templates('
                  {^{fnTagEl/}}
                  ') .link("#result", person1, {settings: settings}); // ................................ Act .................................. before = $("#result div span")[0].outerHTML; // The innerHTML will be Name: Sir compFirst. Width: 40 jsv.observable(person1).setProperty({firstName: "newFirst", lastName: "newLast"}); jsv.observable(settings).setProperty({title: "Sir", width: 40}); jsv.observable(person1).setProperty({fullName: "compFirst compLast"}); after = $("#result div span")[0].outerHTML; // ............................... Assert ................................. assert.equal(before + "|" + after, 'Name: Mr Jo. Width: 30|Name: Sir compFirst. Width: 40', 'Data link with: {^{fnTagEl/}} rendering , updates when dependant object paths change'); // ................................ Reset ................................ $("#result").empty(); person1._firstName = "Jo"; // reset Prop person1.lastName = "One"; // reset Prop settings.title = "Mr"; // reset Prop settings.width = 30; // reset Prop // =============================== Arrange =============================== jsv.templates('
                  {^{fnTagElNoInit firstName ~settings.width/}}
                  ') .link("#result", person1, {settings: settings}); // ................................ Act .................................. before = $("#result div span").html(); // The innerHTML will be ""
                • Name: Mr Jo. Width: 30
                • " jsv.observable(person1).setProperty({firstName: "newFirst", lastName: "newLast"}); jsv.observable(settings).setProperty({title: "Sir", width: 40}); jsv.observable(person1).setProperty({fullName: "compFirst compLast"}); after = $("#result div span").html(); // ............................... Assert ................................. assert.equal(before + "|" + after, 'Name: Mr Jo. Width: 30|Name: Sir compFirst. Width: 40', 'Data link with {^{fnTagElNoInit}} rendering , updates when dependant object paths change'); // ................................ Reset ................................ $("#result").empty(); person1._firstName = "Jo"; // reset Prop person1.lastName = "One"; // reset Prop settings.title = "Mr"; // reset Prop settings.width = 30; // reset Prop // =============================== Arrange =============================== jsv.templates('
                    {^{fnTagElCnt/}}
                  ') .link("#result", person1, {settings: settings}); // ................................ Act .................................. before = $("#result ul li").html(); // The innerHTML will be ""
                • Name: Mr Jo. Width: 30
                • " jsv.observable(person1).setProperty({firstName: "newFirst", lastName: "newLast"}); jsv.observable(settings).setProperty({title: "Sir", width: 40}); jsv.observable(person1).setProperty({fullName: "compFirst compLast"}); after = $("#result ul li").html(); // ............................... Assert ................................. assert.equal(before + "|" + after, 'Name: Mr Jo. Width: 30|Name: Sir compFirst. Width: 40', 'Data link with {^{fnTagElCnt}} rendering
                • , updates when dependant object paths change'); // ................................ Reset ................................ $("#result").empty(); person1._firstName = "Jo"; // reset Prop person1.lastName = "One"; // reset Prop settings.title = "Mr"; // reset Prop settings.width = 30; // reset Prop // =============================== Arrange =============================== jsv.templates('
                    {^{fnTagElCntNoInit firstName ~settings.width/}}
                  ') .link("#result", person1, {settings: settings}); // ................................ Act .................................. before = $("#result ul li").html(); // The innerHTML will be ""
                • Name: Mr Jo. Width: 30
                • " jsv.observable(person1).setProperty({firstName: "newFirst", lastName: "newLast"}); jsv.observable(settings).setProperty({title: "Sir", width: 40}); jsv.observable(person1).setProperty({fullName: "compFirst compLast"}); after = $("#result ul li").html(); // ............................... Assert ................................. assert.equal(before + "|" + after, 'Name: Mr Jo. Width: 30|Name: Sir compFirst. Width: 40', 'Data link with {^{fnTagElCntNoInit}} rendering
                • , updates when dependant object paths change'); // ................................ Reset ................................ $("#result").empty(); person1._firstName = "Jo"; // reset Prop person1.lastName = "One"; // reset Prop settings.title = "Mr"; // reset Prop settings.width = 30; // reset Prop // =============================== Arrange =============================== jsv.templates('{^{tmplTagWithProps #data ~settings.reverse theTitle=~settings.title ~street=home.address.street/}}') .link("#result", person1, {settings: settings}); // ................................ Act .................................. before = $("#result").text(); jsv.observable(person1).setProperty({firstName: "newFirst", lastName: "newLast", "home.address.street": "newStreet"}); jsv.observable(settings).setProperty({title: "Sir", width: 40, reverse: false}); jsv.observable(person1).setProperty({fullName: "compFirst compLast"}); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, 'Name: Mr Jo. Width: 30. Value: true. Prop theTitle: Mr. Prop ~street: StreetOne|Name: Sir compFirst. Width: 40. Value: false. Prop theTitle: Sir. Prop ~street: newStreet', 'Data link with: {^{tmplTagWithProps ~some.path foo=~other.path ~bar=another.path/}} updates when dependant object paths change'); // ................................ Reset ................................ $("#result").empty(); person1._firstName = "Jo"; // reset Prop person1.lastName = "One"; // reset Prop settings.title = "Mr"; // reset Prop settings.width = 30; // reset Prop settings.reverse = true; // reset Prop address1.street = "StreetOne"; // reset Prop // =============================== Arrange =============================== jsv.templates('{{tmplTagWithProps #data ~settings.reverse theTitle=~settings.title ~street=home.address.street/}}') .link("#result", person1, {settings: settings}); // ................................ Act .................................. before = $("#result").text(); jsv.observable(person1).setProperty({firstName: "newFirst", lastName: "newLast", "home.address.street": "newStreet"}); jsv.observable(settings).setProperty({title: "Sir", width: 40, reverse: false}); jsv.observable(person1).setProperty({fullName: "compFirst compLast"}); after = $("#result").text(); // ............................... Assert ................................. assert.ok(before === 'Name: Mr Jo. Width: 30. Value: true. Prop theTitle: Mr. Prop ~street: StreetOne' && before === after && !$._data(person1).events && !$._data(settings).events, 'Data link with: {{tmplTagWithProps ~some.path foo=~other.path ~bar=another.path/}} does nothing'); // ................................ Reset ................................ $("#result").empty(); person1._firstName = "Jo"; // reset Prop person1.lastName = "One"; // reset Prop settings.title = "Mr"; // reset Prop settings.width = 30; // reset Prop settings.reverse = true; // reset Prop address1.street = "StreetOne"; // reset Prop // =============================== Arrange =============================== jsv.templates('{^{fnTagWithProps #data ~settings.reverse theTitle=~settings.title ~street=home.address.street/}}') .link("#result", person1, {settings: settings}); // ................................ Act .................................. before = $("#result").text(); jsv.observable(person1).setProperty({firstName: "newFirst", lastName: "newLast", "home.address.street": "newStreet"}); jsv.observable(settings).setProperty({title: "Sir", width: 40, reverse: false}); jsv.observable(person1).setProperty({fullName: "compFirst compLast"}); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, 'Name: Mr Jo. Width: 30. Value: true. Prop theTitle: Mr. Prop ~street: StreetOne|Name: Sir compFirst. Width: 40. Value: false. Prop theTitle: Sir. Prop ~street: newStreet', 'Data link with: {^{fnTagWithProps ~some.path foo=~other.path ~bar=another.path/}} updates when dependant object paths change'); // ................................ Reset ................................ $("#result").empty(); person1._firstName = "Jo"; // reset Prop person1.lastName = "One"; // reset Prop settings.title = "Mr"; // reset Prop settings.width = 30; // reset Prop settings.reverse = true; // reset Prop address1.street = "StreetOne"; // reset Prop // =============================== Arrange =============================== jsv.templates('{{fnTagWithProps #data ~settings.reverse theTitle=~settings.title ~street=home.address.street/}}') .link("#result", person1, {settings: settings}); // ................................ Act .................................. before = $("#result").text(); jsv.observable(person1).setProperty({firstName: "newFirst", lastName: "newLast", "home.address.street": "newStreet"}); jsv.observable(settings).setProperty({title: "Sir", width: 40, reverse: false}); jsv.observable(person1).setProperty({fullName: "compFirst compLast"}); after = $("#result").text(); // ............................... Assert ................................. assert.ok(before === 'Name: Mr Jo. Width: 30. Value: true. Prop theTitle: Mr. Prop ~street: StreetOne' && before === after && !$._data(person1).events && !$._data(settings).events, 'Data link with: {{fnTagWithProps ~some.path foo=~other.path ~bar=another.path/}} does nothing'); // ................................ Reset ................................ $("#result").empty(); person1._firstName = "Jo"; // reset Prop person1.lastName = "One"; // reset Prop settings.title = "Mr"; // reset Prop settings.width = 30; // reset Prop settings.reverse = true; // reset Prop address1.street = "StreetOne"; // reset Prop // =============================== Arrange =============================== jsv.views.tags({ mytag: { template: "{{:~tagCtx.args[0]}}", attr: "html" } }); jsv.templates("{^{mytag foo(\"w\\x\'y\").b/}}
                  ") .link("#result", { foo: function(val) { return { b: val}; } }); // ............................... Assert ................................. assert.equal($("#result span")[0].outerHTML, "w\\x\'y", "{^{mytag foo(\"w\\x\'y\").b/}} - correct compilation and output of quotes and backslash, with object returned in path (so nested compilation)"); assert.equal($("#result span")[1].outerHTML, "w\\x", "
                  - correct compilation and output of quotes and backslash, with object returned in path (so nested compilation)"); // ................................ Reset ................................ $("#result").empty(); res = ""; }); QUnit.test("{^{for}}", function(assert) { // ................................ Reset ................................ person1._firstName = "Jo"; // reset Prop person1.lastName = "One"; // reset Prop settings.title = "Mr"; // reset Prop settings.width = 30; // reset Prop model.things = []; // reset Prop jsv.views.settings.advanced({_jsv: true}); // For using viewsAndBindings($) // =============================== Arrange =============================== model.things = [{thing: "box"}]; // reset Prop jsv.templates('{^{for things}}{{:thing}}{{/for}}') .link("#result", model); // ................................ Act .................................. before = $("#result").text(); jsv.observable(model.things).insert(0, {thing: "tree"}); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, 'box|treebox', '{^{for things}} binds to array changes on leaf array'); // ................................ Act .................................. jsv.observable(model).setProperty({things: [{thing: "triangle"}, {thing: "circle"}]}); after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, 'trianglecircle', '{^{for things}} binds to property change on path'); // ................................ Act .................................. jsv.observable(model).setProperty({things: {thing: "square"}}); after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, 'square', '{^{for things}} binds to property change on path - swapping from array to singleton object'); // ................................ Act .................................. jsv.observable(model).setProperty({things: [{thing: "triangle2"}, {thing: "circle2"}]}); after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, 'triangle2circle2', '{^{for things}} binds to property change on path - swapping from singleton back to array'); // ................................ Act .................................. jsv.observable(model.things).insert([{thing: "oblong"}, {thing: "pentagon"}]); after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, 'triangle2circle2oblongpentagon', '{^{for things}} binds to array change on array after swapping from singleton back to array'); // ................................ Reset ................................ $("#result").empty(); model.things = []; // reset Prop assert.equal(JSON.stringify(_jsv.cbBindings), "{}", "Bindings all removed when content removed from DOM"); // =============================== Arrange =============================== var things1 = [{thing: "box"}], things2 = [{thing: "triangle"}, {thing: "circle"}], square = {thing: "square"}; model.things = things1; // reset Prop jsv.templates('{^{for things}}{{:thing}}{{/for}}') .link("#result", model); // ................................ Act .................................. before = $("#result").text(); jsv.observable(things1).insert(0, {thing: "tree"}); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, 'box|treebox', '{^{for things}} binds to array changes on leaf array'); // ................................ Act .................................. jsv.observable(model).setProperty({things: things2}); after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, 'trianglecircle', '{^{for things}} binds to property change on path'); // ................................ Act .................................. jsv.observable(model).setProperty({things: square}); after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, 'square', '{^{for things}} binds to property change on path - swapping from array to singleton object'); // ................................ Act .................................. jsv.observable(model).setProperty({things: things2}); after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, 'trianglecircle', '{^{for things}} binds to property change on path - swapping from singleton back to previous array'); // ................................ Act .................................. jsv.observable(things2).insert([{thing: "oblong"}, {thing: "pentagon"}]); after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, 'trianglecircleoblongpentagon', '{^{for things}} binds to array change on array after swapping from singleton back to array'); // ................................ Reset ................................ $("#result").empty(); model.things = []; // reset Prop // =============================== Arrange =============================== things1 = [{thing: "box"}]; things2 = [{thing: "triangle"}, {thing: "circle"}]; square = {thing: "square"}; model.things = things1; // reset Prop jsv.templates('
                    {^{for things}}
                  • {{:thing}}
                  • {{/for}}
                  ') .link("#result", model); // ................................ Act .................................. before = $("#result").text(); jsv.observable(things1).insert(0, {thing: "tree"}); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, 'box|treebox', '{^{for things}} in element content binds to array changes on leaf array'); // ................................ Act .................................. jsv.observable(model).setProperty({things: things2}); after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, 'trianglecircle', '{^{for things}} binds to property change on path'); // ................................ Act .................................. jsv.observable(model).setProperty({things: square}); after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, 'square', '{^{for things}} binds to property change on path - swapping from array to singleton object'); // ................................ Act .................................. jsv.observable(model).setProperty({things: things2}); after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, 'trianglecircle', '{^{for things}} binds to property change on path - swapping from singleton back to previous array'); // ................................ Act .................................. jsv.observable(things2).insert([{thing: "oblong"}, {thing: "pentagon"}]); after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, 'trianglecircleoblongpentagon', '{^{for things}} binds to array change on array after swapping from singleton back to array'); // ................................ Reset ................................ $("#result").empty(); model.things = []; // reset Prop // =============================== Arrange =============================== model.things = [{thing: "box"}, {thing: "table"}]; // reset Prop jsv.templates('{^{:length}} {^{for #data}}{{:thing}}{{/for}}') .link("#result", model.things, null, true); // ................................ Act .................................. before = $("#result").text(); jsv.observable(model.things).insert(0, {thing: "tree"}); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, "2 boxtable|3 treeboxtable", '{^{for #data}} when #data is an array binds to array changes on #data'); // ................................ Reset ................................ $("#result").empty(); model.things = []; // reset Prop // =============================== Arrange =============================== model.things = [{thing: "box"}, {thing: "table"}]; // reset Prop jsv.templates('{^{:length}} {^{for}}{{:thing}}{{/for}}') .link("#result", model.things, null, true); // ................................ Act .................................. before = $("#result").text(); jsv.observable(model.things).insert(0, {thing: "tree"}); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, "2 boxtable|3 treeboxtable", '{^{for}} when #data is an array binds to array changes on #data'); // ................................ Reset ................................ $("#result").empty(); model.things = []; // reset Prop assert.equal(JSON.stringify(_jsv.cbBindings), "{}", "Bindings all removed when content removed from DOM"); // =============================== Arrange =============================== var ret = ""; var listData = {nodes: {list: ["A "]}}; jsv.templates('Plain: {^{for nodes^list}}{{:}}{{/for}}\ |SpanWithNext: {^{for nodes^list}}{{:}}{{/for}}nextElem\ |UlWithNext:
                    {^{for nodes^list}}
                  • {{:}}
                  • {{/for}}
                  • nextElem
                  \ |Ul:
                    {^{for nodes^list}}
                  • {{:}}
                  • {{/for}}
                  ') .link('#result', listData); //"Plain: A |SpanWithNext: A nextElem|UlWithNext: A nextElem|Ul: A " // ................................ Act .................................. ret += "|1 " + $("#result").text() + " " + $._data(listData.nodes.list).events.arrayChange.length; jsv.observable(listData).setProperty("nodes", {list: []}); ret += "|2 " + $("#result").text() + " " + $._data(listData.nodes.list).events.arrayChange.length; jsv.observable(listData.nodes.list).insert("C "); ret += "|3 " + $("#result").text() + " " + $._data(listData.nodes.list).events.arrayChange.length; jsv.observable(listData.nodes).setProperty("list", []); ret += "|4 " + $("#result").text() + " " + $._data(listData.nodes.list).events.arrayChange.length; jsv.observable(listData.nodes.list).insert("C2 "); ret += "|5 " + $("#result").text() + " " + $._data(listData.nodes.list).events.arrayChange.length; // ............................... Assert ................................. assert.equal(ret, "|1 Plain: A |SpanWithNext: A nextElem|UlWithNext: AnextElem|Ul: A 4" + "|2 Plain: |SpanWithNext: nextElem|UlWithNext: nextElem|Ul: 4" + "|3 Plain: C |SpanWithNext: C nextElem|UlWithNext: CnextElem|Ul: C 4" + "|4 Plain: |SpanWithNext: nextElem|UlWithNext: nextElem|Ul: 4" + "|5 Plain: C2 |SpanWithNext: C2 nextElem|UlWithNext: C2nextElem|Ul: C2 4", "Deep observable updates of array path on {^{for}} does not create additional array bindings"); // =============================== Arrange =============================== model.things = []; // reset Prop jsv.templates('{^{if things.length}}X {^{for things}}{{:thing}}{{/for}}{{/if}}') .link("#result", model); // ................................ Act .................................. after = $("#result").text(); jsv.observable(model.things).insert({thing: "box "}); after += "|" + $("#result").text(); jsv.observable(model.things).insert({thing: "table "}); after += "|" + $("#result").text(); jsv.observable(model.things).insert(0, {thing: "tree "}); after += "|" + $("#result").text(); jsv.observable(model.things).remove(); after += "|" + $("#result").text(); jsv.observable(model.things).remove(0); after += "|" + $("#result").text(); jsv.observable(model.things).remove(); after += "|" + $("#result").text(); jsv.observable(model.things).refresh([{thing: "pen "},{thing: "lamp "}]); after += "|" + $("#result").text(); jsv.observable(model.things).move(0, 1); after += "|" + $("#result").text(); jsv.observable(model.things).refresh([]); after += "|" + $("#result").text(); // ............................... Assert ................................. assert.equal(after, "|X box |X box table |X tree box table |X tree box |X box ||X pen lamp |X lamp pen |", '{^{if things.length}}{^{for things}} content block bound to both array and array.length responds correctly to observable array changes'); // ................................ Reset ................................ $("#result").empty(); model.things = []; // reset Prop // =============================== Arrange =============================== model.things = []; // reset Prop jsv.templates('{^{for things.length && things}}{{:thing}}{{/for}}') .link("#result", model); // ................................ Act .................................. after = $("#result").text(); jsv.observable(model.things).insert({thing: "box "}); after += "|" + $("#result").text(); jsv.observable(model.things).insert({thing: "table "}); after += "|" + $("#result").text(); jsv.observable(model.things).insert(0, {thing: "tree "}); after += "|" + $("#result").text(); jsv.observable(model.things).remove(); after += "|" + $("#result").text(); jsv.observable(model.things).remove(0); after += "|" + $("#result").text(); jsv.observable(model.things).remove(); after += "|" + $("#result").text(); jsv.observable(model.things).refresh([{thing: "pen "},{thing: "lamp "}]); after += "|" + $("#result").text(); jsv.observable(model.things).move(0, 1); after += "|" + $("#result").text(); jsv.observable(model.things).refresh([]); after += "|" + $("#result").text(); // ............................... Assert ................................. assert.equal(after, "|box |box table |tree box table |tree box |box ||pen lamp |lamp pen |", '{^{for things.length && things}} content block bound to both array and array.length responds correctly to observable array changes'); // ................................ Reset ................................ $("#result").empty(); model.things = []; // reset Prop // =============================== Arrange =============================== model.things = []; // reset Prop jsv.templates('{^{for things.length}}{^{for ~root.things}}{{:thing}}{{/for}}{{/for}}') .link("#result", model); // ................................ Act .................................. after = $("#result").text(); jsv.observable(model.things).insert({thing: "box "}); after += "|" + $("#result").text(); jsv.observable(model.things).insert({thing: "table "}); after += "|" + $("#result").text(); jsv.observable(model.things).insert(0, {thing: "tree "}); after += "|" + $("#result").text(); jsv.observable(model.things).remove(); after += "|" + $("#result").text(); jsv.observable(model.things).remove(0); after += "|" + $("#result").text(); jsv.observable(model.things).remove(); after += "|" + $("#result").text(); jsv.observable(model.things).refresh([{thing: "pen "},{thing: "lamp "}]); after += "|" + $("#result").text(); jsv.observable(model.things).move(0, 1); after += "|" + $("#result").text(); jsv.observable(model.things).refresh([]); after += "|" + $("#result").text(); // ............................... Assert ................................. assert.equal(after, "|box |box table |tree box table |tree box |box ||pen lamp |lamp pen |", '{^{for things.length}}{^{for ~root.things}} content bound to both array and array.length responds correctly to observable array changes'); // ................................ Reset ................................ $("#result").empty(); model.things = []; // reset Prop // =============================== Arrange =============================== model.things = [{thing: "box"}, {thing: "table"}]; // reset Prop jsv.templates('{{include things}}{^{:length}} {^{for}}{{:thing}}{{/for}}{{/include}}') .link("#result", model); // ................................ Act .................................. before = $("#result").text(); jsv.observable(model.things).insert(0, {thing: "tree"}); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, "2 boxtable|3 treeboxtable", '{{include things}} moves context to things array, and {^{for}} then iterates and binds to array'); // ................................ Reset ................................ $("#result").empty(); model.things = []; // reset Prop // =============================== Arrange =============================== model.things = [{thing: "box"}]; jsv.templates('{^{for things}}{{:thing}}{{else}}None{{/for}}') .link("#result", model); // ................................ Act .................................. before = $("#result").text(); jsv.observable(model.things).insert(0, {thing: "tree"}); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, 'box|treebox', '{^{for things}}{{else}}{{/for}} binds to array changes on leaf array'); // ................................ Act .................................. before = $("#result").text(); jsv.observable(model.things).remove(0, 2); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, 'treebox|None', '{^{for things}}{{else}}{{/for}} renders {{else}} block when array is emptied'); // ................................ Act .................................. jsv.observable(model.things).insert(0, {thing: "tree"}); after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, 'tree', '{^{for things}}{{else}}{{/for}} removes {{else}} block when item is added again'); // ................................ Act .................................. jsv.observable(model).setProperty({things: [{thing: "triangle"}, {thing: "circle"}]}); after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, 'trianglecircle', '{^{for things}}{{else}}{{/for}} binds to property change on path'); // ................................ Act .................................. jsv.observable(model).setProperty({things: {thing: "square"}}); after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, 'square', '{^{for things}}{{else}}{{/for}} binds to property change on path - swapping from array to singleton object'); // ................................ Act .................................. jsv.observable(model).removeProperty("things"); after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, 'None', '{^{for things}}{{else}}{{/for}} binds to removeProperty change on path - and renders {{else}} block'); // ................................ Reset ................................ $("#result").empty(); model.things = []; // reset Prop // =============================== Arrange =============================== model.things = [{thing: "box"}]; jsv.templates('
                    {^{for things}}
                  • {{:thing}}
                  • {{else}}
                  • None
                  • {{/for}}
                  ') .link("#result", model); // ................................ Act .................................. before = $("#result").text(); jsv.observable(model.things).insert(0, {thing: "tree"}); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, 'box|treebox', '{^{for things}}{{else}}{{/for}} binds to array changes on leaf array'); // ................................ Act .................................. before = $("#result").text(); jsv.observable(model.things).remove(0, 2); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, 'treebox|None', '{^{for things}}{{else}}{{/for}} renders {{else}} block when array is emptied'); // ................................ Act .................................. jsv.observable(model.things).insert(0, {thing: "tree"}); after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, 'tree', '{^{for things}}{{else}}{{/for}} removes {{else}} block when item is added again'); // ................................ Act .................................. jsv.observable(model).setProperty({things: [{thing: "triangle"}, {thing: "circle"}]}); after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, 'trianglecircle', '{^{for things}}{{else}}{{/for}} binds to property change on path'); // ................................ Act .................................. jsv.observable(model).setProperty({things: {thing: "square"}}); after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, 'square', '{^{for things}}{{else}}{{/for}} binds to property change on path - swapping from array to singleton object'); // ................................ Act .................................. jsv.observable(model).removeProperty("things"); after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, 'None', '{^{for things}}{{else}}{{/for}} binds to removeProperty change on path - and renders {{else}} block'); // ................................ Reset ................................ $("#result").empty(); model.things = []; // reset Prop // =============================== Arrange =============================== jsv.templates('{^{for things}}{{:thing}}{{else}}None{{/for}}{^{if true}}_yes{{/if}}') .link("#result", model); // ................................ Act .................................. before = $("#result").text(); jsv.observable(model.things).insert(0, {thing: "tree"}); jsv.observable(model.things).insert(0, {thing: "box"}); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, 'None_yes|boxtree_yes', '{^{for things}}{{else}}{{/for}}{^{if ...}} starting with empty array binds to array inserts'); // See https://github.com/BorisMoore/jsviews/issues/326 // ................................ Reset ................................ $("#result").empty(); model.things = []; // reset Prop // =============================== Arrange =============================== // ................................ Act .................................. jsv.templates("testTmpl", '{{if ~things.length}}{{for ~things}}{{:thing}}{{/for}}{{/if}}'); jsv.templates('{^{for things ~things=things tmpl="testTmpl"/}}
                  top
                  ') .link("#result", model); before = $("#result td").text(); jsv.observable(model.things).insert(0, {thing: "tree"}); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, 'top|toptree', 'Complex template, with empty placeholder for tbody after thead, and subsequent data-linked insertion of tbody'); // ................................ Act .................................. jsv.view("#result", true).refresh(); res = "" + (after === $("#result").text()); jsv.view("#result", true).views._2.refresh(); res += " " + (after === $("#result").text()); jsv.view("#result", true).views._2.views[0].refresh(); res += " " + (after === $("#result").text()); jsv.view("#result", true).views._2.views[0].views._2.refresh(); res += " " + (after === $("#result").text()); jsv.view("#result", true).views._2.views[0].views._2.views._2.refresh(); res += " " + (after === $("#result").text()); jsv.view("#result", true).views._2.views[0].views._2.views._2.views[0].refresh(); res += " " + (after === $("#result").text()); // ............................... Assert ................................. assert.equal(res, 'true true true true true true', 'view refresh at all levels correctly maintains content'); // ................................ Reset ................................ $("#result").empty(); model.things = []; // reset Prop assert.equal(JSON.stringify(_jsv.cbBindings), "{}", "Bindings all removed when content removed from DOM"); // =============================== Arrange =============================== // ................................ Act .................................. jsv.templates("testTmpl", '{{if ~things.length}}
                  {{for ~things}}{{:thing}}{{/for}}
                  {{/if}}'); jsv.templates('
                  top{^{for things ~things=things tmpl="testTmpl"/}}
                  ') .link("#result", model); before = $("#result div").text(); jsv.observable(model.things).insert(0, {thing: "tree"}); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, 'top|toptree', 'Complex template, with empty placeholder for span, and subsequent data-linked insertion of in div'); // ................................ Act .................................. jsv.view("#result", true).refresh(); res = "" + (after === $("#result").text()); jsv.view("#result", true).views._2.refresh(); res += " " + (after === $("#result").text()); jsv.view("#result", true).views._2.views[0].refresh(); res += " " + (after === $("#result").text()); jsv.view("#result", true).views._2.views[0].views._2.refresh(); res += " " + (after === $("#result").text()); jsv.view("#result", true).views._2.views[0].views._2.views._2.refresh(); res += " " + (after === $("#result").text()); jsv.view("#result", true).views._2.views[0].views._2.views._2.views[0].refresh(); res += " " + (after === $("#result").text()); // ............................... Assert ................................. assert.equal(res, 'true true true true true true', 'view refresh at all levels correctly maintains content'); // ................................ Reset ................................ $("#result").empty(); model.things = []; // reset Prop assert.equal(JSON.stringify(_jsv.cbBindings), "{}", "Bindings all removed when content removed from DOM"); // =============================== Arrange =============================== // ................................ Act .................................. jsv.templates('{^{for things}}{^{if expanded}}{{/if}}{{/for}}
                  {{:thing}}
                  ') .link("#result", model); jsv.observable(model.things).insert(0, [{thing: "tree", expanded: false}]); res = $._data(model.things[0]).events.propertyChange.length; jsv.view("#result", true).views._1.views[0].refresh(); res += "|" + $._data(model.things[0]).events.propertyChange.length; $("#result").empty(); res += "|" + $._data(model.things[0]).events; // ............................... Assert ................................. assert.equal(res, '1|1|undefined', 'Refreshing a view containing a tag which is bound to dependant data, and has no _prv node, removes the original binding and replaces it with a new one'); // ................................ Reset ................................ $("#result").empty(); model.things = []; // reset Prop assert.equal(JSON.stringify(_jsv.cbBindings), "{}", "Bindings all removed when content removed from DOM"); // =============================== Arrange =============================== // ................................ Act .................................. jsv.templates('
                  {^{for things}}{^{if expanded}}{{:thing}}{{/if}}{{/for}}
                  ') .link("#result", model); jsv.observable(model.things).insert(0, [{thing: "tree", expanded: false}]); res = $._data(model.things[0]).events.propertyChange.length; jsv.view("#result", true).views._1.views[0].refresh(); res += "|" + $._data(model.things[0]).events.propertyChange.length; $("#result").empty(); res += "|" + $._data(model.things[0]).events; // ............................... Assert ................................. assert.equal(res, '1|1|undefined', 'Refreshing a view containing a tag which is bound to dependant data, and has no _prv node, removes the original binding and replaces it with a new one'); // ................................ Reset ................................ $("#result").empty(); model.things = []; // reset Prop assert.equal(JSON.stringify(_jsv.cbBindings), "{}", "Bindings all removed when content removed from DOM"); // =============================== Arrange =============================== // ................................ Act .................................. jsv.templates('
                  {{if true}}{^{:things.length||""}}{{/if}}
                  ') .link("#result", model); before = $("#result div *").length; jsv.view("#result div", true).refresh(); after = $("#result div *").length; // ............................... Assert ................................. assert.equal(after, before, 'Refreshing a view containing non-elOnly content, with a data-bound tag with no rendered content removes the original script node markers for the tag and replace with the new ones'); // ................................ Reset ................................ $("#result").empty(); model.things = []; // reset Prop assert.equal(JSON.stringify(_jsv.cbBindings), "{}", "Bindings all removed when content removed from DOM"); // =============================== Arrange =============================== // ................................ Act .................................. jsv.templates("testTmpl", '{^{if expanded}}{{:thing}}{{/if}}'); jsv.templates('{^{for things tmpl="testTmpl"/}}
                  ') .link("#result", model); res = $("#result td").text(); jsv.observable(model.things).insert(0, [{thing: "tree", expanded: false}, {thing: "bush", expanded: true}]); res += "|" + $("#result td").text(); jsv.observable(model.things[0]).setProperty("expanded", true); jsv.observable(model.things[1]).setProperty("expanded", false); res += "|" + $("#result td").text(); // ............................... Assert ................................. assert.equal(res, '|bush|tree', 'Changing dependant data on bindings with deferred correctly triggers refreshTag and refreshes content with updated data binding'); // ................................ Act .................................. jsv.view("#result tr").parent.refresh(); res = $("#result td").text(); jsv.view("#result tr").parent.parent.views[1].refresh(); res += "|" + $("#result td").text(); // ............................... Assert ................................. assert.equal(res, 'tree|tree', 'view refresh with deferred correctly refreshes content'); // ................................ Act .................................. jsv.observable(model.things[1]).setProperty("expanded", true); res = $("#result td").text(); jsv.observable(model.things[0]).setProperty("expanded", false); res += "|" + $("#result td").text(); // ............................... Assert ................................. assert.equal(res, 'treebush|bush', 'Changing dependant data on bindings with deferred, after view refresh correctly triggers refreshTag and refreshes content with updated data binding'); // ................................ Reset ................................ $("#result").empty(); model.things = []; // reset Prop assert.equal(JSON.stringify(_jsv.cbBindings), "{}", "Bindings all removed when content removed from DOM"); // =============================== Arrange =============================== // ................................ Act .................................. jsv.templates("testTmpl", '{^{if expanded}}{{:thing}}{{/if}}'); jsv.templates('{^{for things tmpl="testTmpl"/}}
                  ') .link("#result", model); res = $("#result td").text(); jsv.observable(model.things).insert(0, [{thing: "tree", expanded: false}, {thing: "bush", expanded: true}]); res += "|" + $("#result").text(); jsv.observable(model.things[0]).setProperty("expanded", true); jsv.observable(model.things[1]).setProperty("expanded", false); res += "|" + $("#result").text(); // ............................... Assert ................................. assert.equal(res, '|bush|tree', 'Changing dependant data on bindings with deferred correctly triggers refreshTag and refreshes content with updated data binding'); // ................................ Act .................................. jsv.view("#result tr").refresh(); res = $("#result").text(); jsv.view("#result tr").parent.views[1].refresh(); res += "|" + $("#result").text(); // ............................... Assert ................................. assert.equal(res, 'tree|tree', 'view refresh with deferred correctly refreshes content'); // ................................ Act .................................. jsv.observable(model.things[1]).setProperty("expanded", true); res = $("#result").text(); jsv.observable(model.things[0]).setProperty("expanded", false); res += "|" + $("#result").text(); // ............................... Assert ................................. assert.equal(res, 'treebush|bush', 'Changing dependant data on bindings with deferred, after view refresh correctly triggers refreshTag and refreshes content with updated data binding'); // ................................ Reset ................................ $("#result").empty(); model.things = []; // reset Prop assert.equal(JSON.stringify(_jsv.cbBindings), "{}", "Bindings all removed when content removed from DOM"); // =============================== Arrange =============================== jsv.templates("
                    {{for}}
                  • Name: {{:firstName()}}. Width: {{:~settings.width}}
                  • {{/for}}
                  ") .link("#result", person1, {settings: settings}); // ................................ Act .................................. before = $("#result ul li").html(); // The innerHTML will be Name: Sir compFirst. Width: 40 person1.fullName.set.call(person1, "compFirst compLast"); settings.title = "Sir"; settings.width = 40; jsv.view("li").refresh(); after = $("#result ul li").html(); // ............................... Assert ................................. assert.equal(before + "|" + after, 'Name: Mr Jo. Width: 30|Name: Sir compFirst. Width: 40', 'Calling view("li").refresh() for a view in element-only content (elCnt true) updates correctly: "
                    {{for}}
                  • ...
                  • {{/for}}
                  "'); // ................................ Reset ................................ $("#result").empty(); person1._firstName = "Jo"; // reset Prop person1.lastName = "One"; // reset Prop settings.title = "Mr"; // reset Prop settings.width = 30; // reset Prop // =============================== Arrange =============================== var data = {}; jsv.templates("
                    {^{for items}}
                  • insertBefore
                  • {{/for}}
                  • next
                  ") .link("#result", data); // ................................ Act .................................. before = $("#result ul").text(); // The innerHTML will be Name: Sir compFirst. Width: 40 jsv.observable(data).setProperty("items", []); var deferredString = $("#result ul li")[0]._df || ""; jsv.observable(data.items).insert("X"); after = $("#result ul").text(); // ............................... Assert ................................. assert.equal(before + "|" + deferredString + "|" + after, (' next||insertBefore next'), 'Inserting content before a next sibling element in element-only context does not set ._df, and subsequent insertion is correctly placed before the next sibling.'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== jsv.templates('{^{for things}}
                  #index: #view.index: {{:thing}} Nested:{{for true}}{{for true}} #get(\'item\').index: #parent.parent.index:|{{/for}}{{/for}}
                  {{/for}}') .link("#result", model); // ................................ Act .................................. jsv.observable(model.things).insert(0, {thing: "tree"}); jsv.observable(model.things).insert(0, {thing: "bush"}); // ............................... Assert ................................. assert.equal($("#result").text(), "#index:0 #view.index:0 bush Nested: #get('item').index:0 #parent.parent.index:0|#index:1 #view.index:1 tree Nested: #get('item').index:1 #parent.parent.index:1|", 'Data-link to "#index" and "#get(\'item\').index" work correctly'); // ................................ Reset ................................ $("#result").empty(); model.things = []; // reset Prop // =============================== Arrange =============================== jsv.templates('{^{for things}}
                  {{:thing}} Nested:{{for}}{{for}}{{/for}}{{/for}}
                  |{{/for}}') .link("#result", model); // ................................ Act .................................. jsv.observable(model.things).insert(0, {thing: "tree"}); jsv.observable(model.things).insert(0, {thing: "bush"}); // ............................... Assert ................................. assert.equal($("#result").text(), "bush Nested:0|tree Nested:1|", 'Data-link to "#getIndex()" works correctly'); // ................................ Reset ................................ $("#result").empty(); model.things = []; // reset Prop // =============================== Arrange =============================== jsv.templates('
                    {^{for things}}
                  • xxx
                  • {{/for}}
                  ') .link("#result", model); // ................................ Act .................................. $("#result div").empty(); // ............................... Assert ................................. assert.ok(viewsAndBindings($).split(" ").length === 7 // We removed view inside div, but still have the view for the outer template. && !$._data(model.things).events, '$(container).empty removes listeners for empty tags in element-only content (_df="#n_/n_")'); // =============================== Arrange =============================== data = { list: [], q: true }; jsv.templates('
                    {^{if q}}{^{for list}}
                  • {{:#data}}
                  • {{/for}}{{/if}}
                  ') .link("#result", data); // ................................ Act .................................. jsv.observable(data).setProperty("q", false); jsv.observable(data).setProperty("q", true); jsv.observable(data.list).insert("added"); // ............................... Assert ................................. assert.ok(viewsAndBindings($).split(" ").length === 13 // We removed view inside div, but still have the view for the outer template. && $._data(data.list).events.arrayChange.length === 1 && $("#result ul").text() === "added", 'In element-only content, updateContent calls disposeTokens on _df inner bindings'); // ................................ Reset ................................ $("#result").empty(); assert.equal(JSON.stringify(_jsv.cbBindings), "{}", "Bindings all removed when content removed from DOM"); jsv.views.settings.advanced({_jsv: false}); }); QUnit.test("{^{for start end sort filter reverse}}", function(assert) { jsv.views.settings.advanced({_jsv: true}); // For using cbBindings store assert.equal(jsv.templates("{{for start=0 end=10}}{{:}} {{/for}}").render(), "0 1 2 3 4 5 6 7 8 9 ", "{{for start=0 end=10}}: Auto-create array"); assert.equal(jsv.templates("{{for start=5 end=9 reverse=1}}{{:}} {{/for}}").render(), "8 7 6 5 ", "{{for start=5 end=9 reverse=1}}: Auto-create array"); assert.equal(jsv.templates("{{for start=8 end=4 step=-1}}{{:}} {{/for}}").render(), "8 7 6 5 ", "{{for start=8 end=4 step=-1}}: Auto-create array"); assert.equal(jsv.templates("{{for start=8 end=4 step=-1 reverse=true}}{{:}} {{/for}}").render(), "5 6 7 8 ", "{{for start=8 end=4 step=-1 reverse=true}}: Auto-create array"); assert.equal(jsv.templates("{{for start=20 end='10' step=-2}}{{:}} {{/for}}").render(), "20 18 16 14 12 ", "{{for start=20 end='10' step=-2}}: Auto-create array"); assert.equal(jsv.templates("{{for start=20 end='10' step=2}}{{:}} {{/for}}").render(), "", "{{for start=20 end='10' step=2}}: Auto-create array (outputs nothing)"); assert.equal(jsv.templates("{{for start=2 end=-1.5 step=-.5}}{{:}} {{/for}}").render(), "2 1.5 1 0.5 0 -0.5 -1 ", "{{for start=0 end='10' step=-1}}: Auto-create array"); assert.equal(jsv.templates("{{for start=2}}{{:}} {{/for}}").render(), "", "{{for start=2}}: (outputs nothing)"); assert.equal(jsv.templates("{{for end=4}}{{:}} {{/for}}").render(), "0 1 2 3 ", "{{for end=4}}: (start defaults to 0)"); var myarray = [1, 9, 2, 8, 3, 7, 4, 6, 5, -100, 20, 100, -1]; var mypeople = [ {name: "Jo", details: {age: 22}}, {name: "Bob", details: {age: 2}}, {name: "Emma", details: {age: 12}}, {name: "Jeff", details: {age: 13.5}}, {name: "Julia", details: {age: 0.6}}, {name: "Xavier", details: {age: 0}} ]; var oddValue = function(item, index, items) { return item%2; }; var oddIndex = function(item, index, items) { return index%2; }; var under20 = function(item, index, items) { return item.details.age < 20; }; assert.equal(jsv.templates("{{for #data}}{{:}} {{/for}}").render(myarray, true), "1 9 2 8 3 7 4 6 5 -100 20 100 -1 ", "{{for #data}}"); assert.equal(jsv.templates("{{for #data sort=true}}{{:}} {{/for}}").render(myarray, true), "-100 -1 1 2 3 4 5 6 7 8 9 20 100 ", "{{for #data sort=true}}"); assert.equal(jsv.templates("{{for myarray reverse=true}}{{:}} {{/for}}").render({myarray: myarray}), "-1 100 20 -100 5 6 4 7 3 8 2 9 1 ", "{{for myarray reverse=true}}"); assert.equal(jsv.templates("{{for myarray start=1 end=-1}}{{:}} {{/for}}").render({myarray: myarray}, {oddIndex: oddIndex}), "9 2 8 3 7 4 6 5 -100 20 100 ", "{{for myarray start=1 end=-1}}"); assert.equal(jsv.templates("{{for myarray start=1}}{{:}} {{/for}}").render({myarray: myarray}, {oddIndex: oddIndex}), "9 2 8 3 7 4 6 5 -100 20 100 -1 ", "{{for myarray start=1}}"); assert.equal(jsv.templates("{{for myarray end=-1}}{{:}} {{/for}}").render({myarray: myarray}, {oddIndex: oddIndex}), "1 9 2 8 3 7 4 6 5 -100 20 100 ", "{{for myarray end=-1}}"); assert.equal(jsv.templates("{{for myarray}}{{:}} {{/for}}").render({myarray: myarray}), "1 9 2 8 3 7 4 6 5 -100 20 100 -1 ", "{{for myarray}}"); assert.equal(jsv.templates("{{for myarray reverse=true}}{{:}} {{/for}}").render({myarray: myarray}), "-1 100 20 -100 5 6 4 7 3 8 2 9 1 ", "{{for myarray reverse=true}}"); assert.equal(jsv.templates("{{for myarray sort=true}}{{:}} {{/for}}").render({myarray: myarray}), "-100 -1 1 2 3 4 5 6 7 8 9 20 100 ", "{{for myarray sort=true}}"); assert.equal(jsv.templates("{{for myarray sort=true reverse=true}}{{:}} {{/for}}").render({myarray: myarray}), "100 20 9 8 7 6 5 4 3 2 1 -1 -100 ", "{{for myarray sort=true reverse=true}}"); assert.equal(jsv.templates("{{for myarray filter=~oddValue}}{{:}} {{/for}}").render({myarray: myarray}, {oddValue: oddValue}), "1 9 3 7 5 -1 ", "{{for myarray filter=~oddValue}}!!!"); assert.equal(jsv.templates("{{for myarray filter=~oddIndex}}{{:}} {{/for}}").render({myarray: myarray}, {oddIndex: oddIndex}), "9 8 7 6 -100 100 ", "{{for myarray filter=~oddIndex}}"); assert.equal(jsv.templates("{{for myarray filter=~oddValue}}{{:}} {{/for}}").render({myarray: myarray}, {oddValue: oddValue}), "1 9 3 7 5 -1 ", "{{for myarray filter=~oddValue}}"); assert.equal(jsv.templates("{{for myarray sort=true filter=~oddValue}}{{:}} {{/for}}").render({myarray: myarray}, {oddValue: oddValue}), "-1 1 3 5 7 9 ", "{{for myarray sort=true filter=~oddValue}}"); assert.equal(jsv.templates("{{for myarray sort=true filter=~oddIndex}}{{:}} {{/for}}").render({myarray: myarray}, {oddIndex: oddIndex}), "-1 2 4 6 8 20 ", "{{for myarray sort=true filter=~oddIndex}}"); assert.equal(jsv.templates("{{for myarray sort=true filter=~oddIndex start=1 end=3}}{{:}} {{/for}}").render({myarray: myarray}, {oddIndex: oddIndex}), "2 4 ", "{{for myarray sort=true filter=~oddIndex start=1 end=3}}"); assert.equal(jsv.templates("{{for myarray sort=true filter=~oddIndex start=-3 end=-1}}{{:}} {{/for}}").render({myarray: myarray}, {oddIndex: oddIndex}), "6 8 ", "{{for myarray sort=true filter=~oddIndex start=-3 end=-1}} Negative start or end count from the end"); assert.equal(jsv.templates("{{for myarray sort=true filter=~oddIndex start=3 end=3}}{{:}} {{/for}}").render({myarray: myarray}, {oddIndex: oddIndex}), "", "{{for myarray sort=true filter=~oddIndex start=3 end=3}} (outputs nothing)"); assert.equal(jsv.templates("{{for mypeople sort='name'}}{{:name}}: age {{:details.age}} - {{/for}}").render({mypeople: mypeople}), "Bob: age 2 - Emma: age 12 - Jeff: age 13.5 - Jo: age 22 - Julia: age 0.6 - Xavier: age 0 - ", "{{for mypeople sort='name'}}"); assert.equal(jsv.templates("{{for mypeople sort='details.age'}}{{:name}}: age {{:details.age}} - {{/for}}").render({mypeople: mypeople}), "Xavier: age 0 - Julia: age 0.6 - Bob: age 2 - Emma: age 12 - Jeff: age 13.5 - Jo: age 22 - ", "{{for mypeople sort='details.age'}}"); assert.equal(jsv.templates("{{for mypeople sort='details.age' reverse=true filter=~under20}}{{:name}}: age {{:details.age}} - {{/for}}").render({mypeople: mypeople}, {under20: under20}), "Jeff: age 13.5 - Emma: age 12 - Bob: age 2 - Julia: age 0.6 - Xavier: age 0 - ", "{{for mypeople sort='details.age' reverse=true filter=~under20}}"); assert.equal(jsv.templates("{{for mypeople sort='details.age' reverse=true filter=~under20 start=1 end=-1}}{{:name}}: age {{:details.age}} - {{/for}}").render({mypeople: mypeople}, {under20: under20}), "Emma: age 12 - Bob: age 2 - Julia: age 0.6 - ", "{{for mypeople sort='details.age' reverse=true filter=~under20 start=1 end=-1}}"); // =============================== Arrange =============================== model.things = [{ob: {thing: "box"}}]; var ctx = {}; jsv.templates('|All: {^{for things}}{{:ob.thing}} {{else}}None{{/for}}
                  \ |Sort: {^{for this=~ctx.sorted things sort="ob.thing"}}{{:ob.thing}} {{else}}None{{/for}}
                  \ |NotTreeReverse: {^{for things filter=~notTree reverse=true}}{{:ob.thing}} {{else}}None{{/for}}
                  \ |Start1: {^{for things start=1}}{{:ob.thing}} {{else}}None{{/for}}') .link("#result", model, { ctx: ctx, notTree: function(item) { return item.ob.thing !== "tree"; } }); var after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, "|All: box |Sort: box |NotTreeReverse: box |Start1: None", '{^{for things}}{{else}}{{/for}} plus sorting and filtering'); // ................................ Act .................................. jsv.observable(model.things).insert(0, {ob: {thing: "tree"}}); jsv.observable(model.things).insert([{ob: {thing: "apple"}}, {ob: {thing: "tree"}}]); after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, "|All: tree box apple tree |Sort: apple box tree tree |NotTreeReverse: apple box |Start1: box apple tree ", '{^{for things}}{{else}}{{/for}} plus sorting and filtering binds to array changes on leaf array'); // ................................ Act .................................. jsv.observable(model.things).remove(0, 3); after = $("#result").text(); // ............................... Assert .................................6 assert.equal(after, "|All: tree |Sort: tree |NotTreeReverse: None|Start1: None", '{^{for things}}{{else}}{{/for}} plus sorting and filtering renders {{else}} block when array is emptied'); // ................................ Act .................................. jsv.observable(model.things).insert(0, {ob: {thing: "tree"}}); after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, "|All: tree tree |Sort: tree tree |NotTreeReverse: None|Start1: tree ", '{^{for things}}{{else}}{{/for}} plus sorting and filtering removes {{else}} block when item is added again'); // ................................ Act .................................. jsv.observable(model).setProperty({things: [{ob: {thing: "triangle"}}, {ob: {thing: "circle"}}]}); after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, "|All: triangle circle |Sort: circle triangle |NotTreeReverse: circle triangle |Start1: circle ", '{^{for things}}{{else}}{{/for}} plus sorting and filtering binds to property change on path'); // ................................ Act .................................. jsv.observable(model.things).insert([{ob: {thing: "square"}}, {ob: {thing: "tree"}}]); after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, "|All: triangle circle square tree |Sort: circle square tree triangle |NotTreeReverse: square circle triangle |Start1: circle square tree ", '{^{for things}}{{else}}{{/for}} plus sorting and filtering inserts new items with sorting/filtering'); // ................................ Act .................................. jsv.observable(model).setProperty({things: {ob: {thing: "tree"}}}); after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, "|All: tree |Sort: tree |NotTreeReverse: tree |Start1: tree ", '{^{for things}}{{else}}{{/for}} plus sorting and filtering binds to property change on path - swapping from array to singleton object'); // ................................ Act .................................. jsv.observable(model).setProperty({things: [{ob: {thing: "square"}}, {ob: {thing: "apple"}}, {ob: {thing: "tree"}}]}); after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, "|All: square apple tree |Sort: apple square tree |NotTreeReverse: apple square |Start1: apple tree ", '{^{for things}}{{else}}{{/for}} plus sorting and filtering binds to property change on path - swapping from singleton object back to array'); // ................................ Act .................................. jsv.observable(model).removeProperty("things"); after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, "|All: None|Sort: None|NotTreeReverse: None|Start1: None", '{^{for things}}{{else}}{{/for}} plus sorting and filtering binds to removeProperty change on path - and renders {{else}} block'); // ................................ Act .................................. jsv.observable(model).setProperty("things", [{ob: {thing: "circle"}}, {ob: {thing: "tree"}}, {ob: {thing: "square"}}]); after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, "|All: circle tree square |Sort: circle square tree |NotTreeReverse: square circle |Start1: tree square ", '{^{for things}}{{else}}{{/for}} plus sorting and filtering binds to setProperty change on path - and renders {{for}} block again'); // =============================== Arrange =============================== var tgt = ctx.sorted.tagCtx.map.tgt; // ................................ Act .................................. jsv.observable(tgt).insert([{ob: {thing: "red"}}, {ob: {thing: "green"}}, {ob: {thing: "blue"}}]); after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, "|All: circle tree square red green blue |Sort: circle square tree red green blue |NotTreeReverse: blue green red square circle |Start1: tree square red green blue ", '{^{for things}} plus sorting and filtering support observable changes to target array tagCtx.map.tgt'); // ................................ Reset ................................ $("#result").empty(); model.things = []; // reset Prop assert.equal(JSON.stringify(_jsv.cbBindings), "{}", "Bindings all removed when content removed from DOM"); // =============================== Arrange =============================== var movies = [{title: "a0"}, {title: "x0"}, {title: "b0"}, {title: "y0"}, {title: "c0"}, {title: "z0"}]; ctx = {}; var cnt = 0; jsv.templates( '|All:--- {^{for movies}}{{:title}} {{/for}}
                  \ |Sort:-- {^{for movies sort="title" reverse=true}}{{:title}} {{/for}}
                  \ |Filter: {^{for movies sort="title" reverse=true filter=~odd}}{{:title}} {{/for}}
                  \ |Slice:- {^{for movies sort="title" reverse=true filter=~odd start=1 end=-1 this=~ctx.target}}{{:title}} {{/for}}') .link("#result", {movies: movies}, { ctx: ctx, odd: function(item, index, items) { return index%2; } }); tgt = ctx.target.tagCtx.map.tgt; // This is the target array for the fourth (and last) {^{for}} tag above - Slice: {^{for ...}} // ................................ Act .................................. after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, "|All:--- a0 x0 b0 y0 c0 z0 |Sort:-- z0 y0 x0 c0 b0 a0 |Filter: y0 c0 a0 |Slice:- c0 ", '{{for}} with sorting, filtering, reverse, start and end settings'); // ................................ Act .................................. jsv.observable(tgt).insert({title: "t" + cnt++}); // Append item to fourth {^{for}} tag instance above after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, "|All:--- a0 x0 b0 y0 c0 z0 t0 |Sort:-- z0 y0 x0 t0 c0 b0 a0 |Filter: y0 t0 b0 |Slice:- c0 t0 ", 'Appending of item in target array (sorted, filtered etc) - item is rendered without refreshing sort, filter etc.'); // But note that in our scenario above this will append an item to the source array movies, which will trigger refreshed // rendering of the first three {^{for}} instance above ctx.target.refresh(); after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, "|All:--- a0 x0 b0 y0 c0 z0 t0 |Sort:-- z0 y0 x0 t0 c0 b0 a0 |Filter: y0 t0 b0 |Slice:- t0 ", 'To refresh sort etc with new item included, call tag.refresh() '); // ................................ Act .................................. jsv.observable(tgt).insert(0, {title: "t" + cnt++}); after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, "|All:--- a0 x0 b0 y0 c0 z0 t0 t1 |Sort:-- z0 y0 x0 t1 t0 c0 b0 a0 |Filter: y0 t1 c0 a0 |Slice:- t1 t0 ", 'Insertion of item in target array (sorted, filtered etc) - item is rendered without refreshing sort, filter etc.'); // ................................ Act .................................. jsv.observable(movies).insert(1, {title: "m" + cnt++}); after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, "|All:--- a0 m2 x0 b0 y0 c0 z0 t0 t1 |Sort:-- z0 y0 x0 t1 t0 m2 c0 b0 a0 |Filter: y0 t1 m2 b0 |Slice:- t1 m2 ", 'Insertion of item in source array will also refresh sort, filter etc.'); // ................................ Act .................................. jsv.observable(tgt).insert(1, [{title: "t" + cnt++}, {title: "t" + cnt++}]); after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, "|All:--- a0 m2 x0 b0 y0 c0 z0 t0 t1 t3 t4 |Sort:-- z0 y0 x0 t4 t3 t1 t0 m2 c0 b0 a0 |Filter: y0 t4 t1 m2 b0 |Slice:- t1 t3 t4 m2 ", 'Insertion of multiple items in target array (sorted, filtered etc) - items are rendered without refreshing sort, filter etc.'); // ................................ Act .................................. jsv.observable(tgt).refresh([tgt[1], {title: "t" + cnt++}, tgt[0]]); after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, "|All:--- a0 x0 b0 y0 c0 z0 t0 t1 t3 t5 |Sort:-- z0 y0 x0 t5 t3 t1 t0 c0 b0 a0 |Filter: y0 t5 t1 c0 a0 |Slice:- t3 t5 t1 ", 'Calling refresh() on target array will insert and remove items appropriately from source array and target array (and move items in target array) without refreshing sort, filter etc.'); // ................................ Act .................................. jsv.observable(tgt).remove(); after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, "|All:--- a0 x0 b0 y0 c0 z0 t0 t3 t5 |Sort:-- z0 y0 x0 t5 t3 t0 c0 b0 a0 |Filter: y0 t5 t0 b0 |Slice:- t3 t5 ", 'Removing item in target array (sorted, filtered etc) - items are rendered without refreshing sort, filter etc.'); // ................................ Act .................................. jsv.observable(movies).remove(0); after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, "|All:--- x0 b0 y0 c0 z0 t0 t3 t5 |Sort:-- z0 y0 x0 t5 t3 t0 c0 b0 |Filter: y0 t5 t0 b0 |Slice:- t5 t0 ", 'Removal of item in source array will also refresh sort, filter etc.'); // ................................ Act .................................. jsv.observable(tgt).move(0, 1); after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, "|All:--- x0 b0 y0 c0 z0 t0 t3 t5 |Sort:-- z0 y0 x0 t5 t3 t0 c0 b0 |Filter: y0 t5 t0 b0 |Slice:- t0 t5 ", 'Moving items in target array (sorted, filtered etc) - items are moved in target but not in source, and this is without refreshing sort, filter etc.'); // ................................ Act .................................. jsv.observable(tgt).refresh([{title: "t" + cnt++}]); after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, "|All:--- x0 b0 y0 c0 z0 t3 t6 |Sort:-- z0 y0 x0 t6 t3 c0 b0 |Filter: y0 t6 c0 |Slice:- t6 ", 'Calling refresh() on target array will insert and remove items appropriately from source array and target array (and move items in target array) without refreshing sort, filter etc.'); // ................................ Act .................................. jsv.observable(movies).insert(1, {title: "m" + cnt++}); after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, "|All:--- x0 m7 b0 y0 c0 z0 t3 t6 |Sort:-- z0 y0 x0 t6 t3 m7 c0 b0 |Filter: y0 t6 m7 b0 |Slice:- t6 m7 ", 'Insertion of item in source array will also refresh sort, filter etc.'); // ................................ Act .................................. jsv.observable(tgt).insert(1, [{title: "t" + cnt++}, {title: "t" + cnt++}]); after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, "|All:--- x0 m7 b0 y0 c0 z0 t3 t6 t8 t9 |Sort:-- z0 y0 x0 t9 t8 t6 t3 m7 c0 b0 |Filter: y0 t9 t6 m7 b0 |Slice:- t6 t8 t9 m7 ", 'Insertion of multiple items in target array (sorted, filtered etc) - items are rendered without refreshing sort, filter etc.'); // ................................ Act .................................. jsv.observable(movies).move(1, 3, 2); after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, "|All:--- x0 y0 c0 m7 b0 z0 t3 t6 t8 t9 |Sort:-- z0 y0 x0 t9 t8 t6 t3 m7 c0 b0 |Filter: y0 t9 t6 m7 b0 |Slice:- t9 t6 m7 ", 'Moving of items in source array will also refresh sort, filter etc.'); // ................................ Reset ................................ $("#result").empty(); assert.equal(JSON.stringify(_jsv.cbBindings), "{}", "Bindings all removed when content removed from DOM"); // =============================== Arrange =============================== movies = [{title: "a0"}, {title: "x0"}, {title: "b0"}, {title: "y0"}, {title: "c0"}, {title: "z0"}]; ctx = {}; cnt = 0; jsv.templates( '|All:--- {^{for movies}}{{:title}} {{/for}}
                  \ |Slice:- {^{for movies start=1 end=-1 this=~ctx.target}}{{:title}} {{/for}}') .link("#result", {movies: movies}, { ctx: ctx, odd: function(item, index, items) { return index%2; } }); tgt = ctx.target.tagCtx.map.tgt; // ................................ Act .................................. after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, "|All:--- a0 x0 b0 y0 c0 z0 |Slice:- x0 b0 y0 c0 ", '{{for}} with start and end settings ("sliced")'); // ................................ Act .................................. jsv.observable(tgt).insert({title: "t" + cnt++}); after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, "|All:--- a0 x0 b0 y0 c0 z0 t0 |Slice:- x0 b0 y0 c0 t0 ", 'Appending of item in target array ("sliced") - item is rendered without refreshing sort, filter etc.'); ctx.target.refresh(); after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, "|All:--- a0 x0 b0 y0 c0 z0 t0 |Slice:- x0 b0 y0 c0 z0 ", 'To refresh correct start and end with new item included, call tag.refresh() '); // ................................ Act .................................. jsv.observable(tgt).insert(0, {title: "t" + cnt++}); after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, "|All:--- a0 x0 b0 y0 c0 z0 t0 t1 |Slice:- t1 x0 b0 y0 c0 z0 ", 'Insertion of item at specific position in target array ("sliced") - item is rendered at insert location, but item is simply appended to source array'); // ................................ Act .................................. jsv.observable(movies).insert(1, {title: "m" + cnt++}); after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, "|All:--- a0 m2 x0 b0 y0 c0 z0 t0 t1 |Slice:- m2 x0 b0 y0 c0 z0 t0 ", 'Insertion of item in source array will also refresh "slicing"'); // ................................ Act .................................. jsv.observable(tgt).insert(1, [{title: "t" + cnt++}, {title: "t" + cnt++}]); after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, "|All:--- a0 m2 x0 b0 y0 c0 z0 t0 t1 t3 t4 |Slice:- m2 t3 t4 x0 b0 y0 c0 z0 t0 ", 'Insertion of items at specific position in target array ("sliced") - items are rendered at insert location, but simply appended to source array'); // ................................ Act .................................. jsv.observable(tgt).refresh([tgt[1], {title: "t" + cnt++}, tgt[0], tgt[2]]); after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, "|All:--- a0 m2 t1 t3 t4 t5 |Slice:- t3 t5 m2 t4 ", 'Calling refresh() on target array will append and remove items appropriately from source array and target array (and move items in target array)'); // ................................ Act .................................. jsv.observable(tgt).remove(); after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, "|All:--- a0 m2 t1 t3 t5 |Slice:- t3 t5 m2 ", 'Removing item in target array ("sliced") - items are rendered without refreshing "slicing".'); // ................................ Act .................................. jsv.observable(movies).remove(0); after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, "|All:--- m2 t1 t3 t5 |Slice:- t1 t3 ", 'Removal of item in source array will also refresh "slicing".'); // ................................ Act .................................. jsv.observable(tgt).move(0, 1); after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, "|All:--- m2 t1 t3 t5 |Slice:- t3 t1 ", 'Moving items in target array ("slice") - items are moved in target but not in source, and this is without refreshing "slicing".'); // ................................ Act .................................. jsv.observable(movies).move(1, 3, 2); after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, "|All:--- m2 t5 t1 t3 |Slice:- t5 t1 ", 'Moving of items in source array will also refresh "slicing".'); // ................................ Reset ................................ $("#result").empty(); assert.equal(JSON.stringify(_jsv.cbBindings), "{}", "Bindings all removed when content removed from DOM"); // =============================== Arrange =============================== var team = { members: [ {name: "one", phones:[]}, {name: "two", phones:[21, 22]} ] }; jsv.templates( '
                    {^{for start=0 members}}{^{for start=0 phones}}
                  • |{{:}}
                  • {{else}}
                  • |NoPhones
                  • {{/for}}
                  • |{{:name}}
                  • {{else}}
                  • |NoMembers
                  • {{/for}}
                  ') .link("#result", team); var firstLi = $("li")[0]; // ............................... Assert ................................. assert.equal( jsv.view("ul").views._1._prv === firstLi && jsv.view("ul").views._1.tag._prv === firstLi && jsv.view("ul").views._1.views["0"].views._1._prv === firstLi && jsv.view("ul").views._1.views["0"].views._1.tag._prv === firstLi && $("#result").text(), "|NoPhones|one|21|22|two", "First li is _prv for {{for}} tags"); // ................................ Act .................................. jsv.observable(team.members).move(1, 0); // ............................... Assert ................................. firstLi = $("li")[0]; assert.equal( jsv.view("ul").views._1._prv === firstLi && jsv.view("ul").views._1.tag._prv === firstLi && jsv.view("ul").views._1.views["0"].views._1._prv === firstLi && jsv.view("ul").views._1.views["0"].views._1.tag._prv === firstLi && $("#result").text(), "|21|22|two|NoPhones|one", "After observable move, first li is _prv for {{for}} tags"); // ................................ Act .................................. jsv.observable(team.members).remove(0); // ............................... Assert ................................. firstLi = $("li")[0]; assert.equal( jsv.view("ul").views._1._prv === firstLi && jsv.view("ul").views._1.tag._prv === firstLi && jsv.view("ul").views._1.views["0"].views._1._prv === firstLi && jsv.view("ul").views._1.views["0"].views._1.tag._prv === firstLi && $("#result").text(), "|NoPhones|one", "After remove, first li is _prv for {{for}} tags"); // ................................ Act .................................. jsv.observable(team.members).remove(0); // ............................... Assert ................................. firstLi = $("li")[0]; assert.equal( jsv.view("ul").views._2._prv === firstLi && jsv.view("ul").views._2.tag._prv === firstLi && jsv.view("ul").views._3._prv === firstLi && jsv.view("ul").views._3.tag._prv === firstLi && $("#result").text(), "|NoMembers", "After removing all, first li is _prv for {{for}} tags"); //................................ Reset ................................ $("#result").empty(); // ............................... Assert ................................. assert.equal(JSON.stringify(_jsv.cbBindings), "{}", "dataMap bindings all removed when tag disposed (content removed from DOM)"); jsv.views.settings.advanced({_jsv: false}); }); QUnit.test("{^{for}} with start end sort filter reverse: Incremental rendering", function(assert) { var data = { p_st: 2, p_en: 4, p_rev: false, t_st: 0, t_en: 4, t_stp: undefined, t_mapdeps: "flt", flt: "l", people: [ "Jo", "Bob", "Jane", "Jeff", "May", "Alice" ], things: [ {is: "table"}, {is: "porcelain"}, {is: "lamp"}, {is: "hat"} ] }, out = "", content = "", people = data.people; jsv.templates( '{^{for skip}}{{:~rndr(#data)}}' + '{{else people ~foo="test" start=p_st end=p_en reverse=p_rev}}|{{:~rndr(#data)}}' + '{{else things sort=t_srt filter=t_flt start=t_st end=t_en step=t_stp mapDepends=t_mapdeps}}|{{:~rndr(is)}}' + '{{else}}None{{/for}}' ) .link("#result", data, { rndr: function(value) { out += "|" + value; return value; } }); // ............................... Assert ................................. assert.equal($("#result").text() + "::" + out, "|Jane|Jeff::|Jane|Jeff", 'initial render, first {{else}} block'); // ................................ Act .................................. out = ""; jsv.observable(data).setProperty("skip", "SkipTheList"); // ............................... Assert ................................. assert.equal($("#result").text() + "::" + out, "SkipTheList::|SkipTheList", 'move to initial block (no mapped list'); // ................................ Act .................................. out = ""; jsv.observable(data).setProperty("skip", undefined); // ............................... Assert ................................. assert.equal($("#result").text() + "::" + out, "|Jane|Jeff::|Jane|Jeff", 'move back to {{else people}} block'); // ................................ Act .................................. out = ""; jsv.observable(data).setProperty("p_st", 1); // ............................... Assert ................................. assert.equal($("#result").text() + "::" + out, "|Bob|Jane|Jeff::|Bob", 'incremental, on reducing start'); // ................................ Act .................................. out = ""; jsv.observable(data).setProperty("p_en", 5); // ............................... Assert ................................. assert.equal($("#result").text() + "::" + out, "|Bob|Jane|Jeff|May::|May", 'incremental, on increasing end'); // ................................ Act .................................. out = ""; jsv.observable(data).setProperty("p_en", 3); // ............................... Assert ................................. assert.equal($("#result").text() + "::" + out, "|Bob|Jane::", 'incremental, on reducing end'); // ................................ Act .................................. out = ""; jsv.observable(data).setProperty("p_st", 4); // ............................... Assert ................................. assert.equal($("#result").text() + "::" + out, "|table|porcelain|lamp|hat::|table|porcelain|lamp|hat", 'incremental, on increasing start, no items, moves to {{else}} block'); // ................................ Act .................................. out = ""; jsv.observable(data).setProperty("t_st", -1); // ............................... Assert ................................. assert.equal($("#result").text() + "::" + out, "|hat::", 'incremental, on changing start - integer from end'); // ................................ Act .................................. out = ""; jsv.observable(data).setProperty("t_en", -2); // ............................... Assert ................................. assert.equal($("#result").text() + "::" + out, "None::", 'incremental, on changing end - integer from end, no items, moves to final {{else}}'); // ................................ Act .................................. out = ""; jsv.observable(data).setProperty({p_st: 1, p_en:40}); // ............................... Assert ................................. assert.equal($("#result").text() + "::" + out, "|Bob|Jane|Jeff|May|Alice::|Bob|Jane|Jeff|May|Alice", 'incremental, on changing start/end - moves to first block'); // ................................ Act .................................. out = ""; jsv.observable(data).setProperty("p_rev", true); // ............................... Assert ................................. assert.equal($("#result").text() + "::" + out, "|May|Jeff|Jane|Bob|Jo::|Jo", 'incremental, set reverse=true'); // ................................ Act .................................. out = ""; jsv.observable(data).setProperty({p_en: 0, t_st: 0, t_en: 10}); // ............................... Assert ................................. assert.equal($("#result").text() + "::" + out, "|table|porcelain|lamp|hat::|table|porcelain|lamp|hat", 'incremental, moves to second block'); // ................................ Act .................................. out = ""; jsv.observable(data).setProperty("t_srt", "is"); // ............................... Assert ................................. assert.equal($("#result").text() + "::" + out, "|hat|lamp|porcelain|table::", 'incremental, on changing start'); // ................................ Act .................................. out = ""; jsv.observable(data).setProperty("t_flt", function(item, index, items) { return index%2 === 1; // Include only odd index items }); // ............................... Assert ................................. assert.equal($("#result").text() + "::" + out, "|lamp|table::", 'incremental, on setting filter'); // ................................ Act .................................. out = ""; jsv.observable(data).setProperty({t_st: 0, t_en: 10, t_flt: false, t_srt: function(a, b) { return a.is.length> b.is.length? 1 : a.is.length< b.is.length? -1 : 0; // Sort by string length of items }}); // ............................... Assert ................................. assert.equal($("#result").text() + "::" + out, "|hat|lamp|table|porcelain::|hat|porcelain", 'incremental, on setting sort function'); // ................................ Act .................................. out = ""; jsv.observable(data).setProperty("t_flt", function(item, index, items) { var flt = this.view.data.flt; return flt ? item.is.toLowerCase().indexOf(flt.toLowerCase()) !== -1 : true; }); // ............................... Assert ................................. assert.equal($("#result").text() + "::" + out, "|lamp|table|porcelain::", 'incremental, on setting filter, with flt="l"'); // ................................ Act .................................. out = ""; jsv.observable(data).setProperty("flt", "t"); // ............................... Assert ................................. assert.equal($("#result").text() + "::" + out, "|hat|table::|hat", 'incremental, on setting flt to "t"'); // ................................ Act .................................. out = ""; jsv.observable(data).setProperty("flt", "e"); // ............................... Assert ................................. assert.equal($("#result").text() + "::" + out, "|table|porcelain::|porcelain", 'incremental, on setting flt to "e"'); // ................................ Act .................................. out = ""; jsv.observable(data.things).insert(2, [{is: "cupboard"}, {is: "window"}]); // ............................... Assert ................................. assert.equal($("#result").text() + "::" + out, "|table|porcelain::", 'incremental, on inserting items'); // ................................ Act .................................. out = ""; jsv.observable(data).setProperty("flt", ""); // ............................... Assert ................................. assert.equal($("#result").text() + "::" + out, "|hat|lamp|table|window|cupboard|porcelain::|hat|lamp|window|cupboard", 'incremental, on setting flt to ""'); // ................................ Act .................................. out = ""; jsv.observable(data).setProperty("t_srt", false); // ............................... Assert ................................. assert.equal($("#result").text() + "::" + out, "|table|porcelain|cupboard|window|lamp|hat::", 'incremental, on setting sort to false'); // ................................ Act .................................. out = ""; jsv.observable(data).setProperty("t_stp", 3); // ............................... Assert ................................. assert.equal($("#result").text() + "::" + out, "|table|window::", 'incremental, on setting step to 3'); // ................................ Act .................................. out = ""; jsv.observable(data).setProperty("t_stp", 2); // ............................... Assert ................................. assert.equal($("#result").text() + "::" + out, "|table|cupboard|lamp::|cupboard|lamp", 'incremental, on setting step to 2'); // ................................ Act .................................. out = ""; jsv.observable(data.things).move(1, 4, 2); // ............................... Assert ................................. assert.equal($("#result").text() + "::" + out, "|table|lamp|porcelain::|porcelain", 'incremental, on using move(1, 4, 2)'); // ................................ Act .................................. out = ""; jsv.observable(data).setProperty("t_stp", false); // ............................... Assert ................................. assert.equal($("#result").text() + "::" + out, "|table|window|lamp|hat|porcelain|cupboard::|window|hat|cupboard", 'incremental, on setting step to false'); // ................................ Act .................................. out = ""; jsv.observable(data.things).refresh([data.things[4], data.things[2], data.things[0], data.things[3]]); // ............................... Assert ................................. assert.equal($("#result").text() + "::" + out, "|porcelain|lamp|table|hat::", 'incremental, on using refresh(...)'); // ................................ Act .................................. out = ""; jsv.observable(data).setProperty("t_st", 10); // ............................... Assert ................................. assert.equal($("#result").text() + "::" + out, "None::", 'incremental, move to final {{else}} block'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== var data = { p_rev: false, people: [ "Jo", "Bob", "Jane" ] }, out = "", content = "", people = data.people; jsv.templates( '{^{for people sort=p_srt reverse=p_rev}}|{{:~rndr(#data)}}{{/for}}' ) .link("#result", data, { rndr: function(value) { out += "|" + value; return value; } }); // ............................... Assert ................................. assert.equal($("#result").text() + "::" + out, "|Jo|Bob|Jane::|Jo|Bob|Jane", 'initial render, (no initial DataMap use)'); // ................................ Act .................................. out = ""; jsv.observable(data).setProperty("p_rev", true); // ............................... Assert ................................. assert.equal($("#result").text() + "::" + out, "|Jane|Bob|Jo::", 'reverse order (DataMap used - incremental re-order)'); // ................................ Act .................................. out = ""; jsv.observable(data).setProperty("p_srt", true); // ............................... Assert ................................. assert.equal($("#result").text() + "::" + out, "|Jo|Jane|Bob::", 'reverse sort (DataMap used - incremental re-order)'); // ................................ Reset ................................ $("#result").empty(); }); QUnit.test("{^{props}} with start end sort filter reverse: Incremental rendering", function(assert) { var data = { p_st: 2, p_en: 4, p_rev: false, t_st: 0, t_en: 4, t_stp: undefined, t_mapdeps: "flt", flt: "l", people: { one: "Jo", b: "Bob", x: "Jane", two: "Jeff", m: "May", last: "Alice" }, things: { a: {is: "table"}, b: {is: "porcelain"}, c: {is: "lamp"}, d: {is: "hat"} } }, out = "", content = "", people = data.people; jsv.templates( '{^{props skip}}{{:~rndr(prop)}}' + '{{else people start=p_st end=p_en reverse=p_rev}}|{{:~rndr(prop)}}' + '{{else things sort=t_srt filter=t_flt start=t_st end=t_en step=t_stp mapDepends=t_mapdeps}}|{{:~rndr(prop.is)}}' + '{{else}}None{{/props}}' ) .link("#result", data, { rndr: function(value) { out += "|" + value; return value; } }); // ............................... Assert ................................. assert.equal($("#result").text() + "::" + out, "|Jane|Jeff::|Jane|Jeff", 'initial render, first {{else}} block'); // ................................ Act .................................. out = ""; jsv.observable(data).setProperty("skip", {is: "SkipTheList"}); // ............................... Assert ................................. assert.equal($("#result").text() + "::" + out, "SkipTheList::|SkipTheList", 'move to initial block (no mapped list'); // ................................ Act .................................. out = ""; jsv.observable(data).setProperty("skip", undefined); // ............................... Assert ................................. assert.equal($("#result").text() + "::" + out, "|Jane|Jeff::|Jane|Jeff", 'move back to {{else people}} block'); // ................................ Act .................................. out = ""; jsv.observable(data).setProperty("p_st", 1); // ............................... Assert ................................. assert.equal($("#result").text() + "::" + out, "|Bob|Jane|Jeff::|Bob", 'incremental, on reducing start'); // ................................ Act .................................. out = ""; jsv.observable(data).setProperty("p_en", 5); // ............................... Assert ................................. assert.equal($("#result").text() + "::" + out, "|Bob|Jane|Jeff|May::|May", 'incremental, on increasing end'); // ................................ Act .................................. out = ""; jsv.observable(data).setProperty("p_en", 3); // ............................... Assert ................................. assert.equal($("#result").text() + "::" + out, "|Bob|Jane::", 'incremental, on reducing end'); // ................................ Act .................................. out = ""; jsv.observable(data).setProperty("p_st", 4); // ............................... Assert ................................. assert.equal($("#result").text() + "::" + out, "|table|porcelain|lamp|hat::|table|porcelain|lamp|hat", 'incremental, on increasing start, no items, moves to {{else}} block'); // ................................ Act .................................. out = ""; jsv.observable(data).setProperty("t_st", -1); // ............................... Assert ................................. assert.equal($("#result").text() + "::" + out, "|hat::", 'incremental, on changing start - integer from end'); // ................................ Act .................................. out = ""; jsv.observable(data).setProperty("t_en", -2); // ............................... Assert ................................. assert.equal($("#result").text() + "::" + out, "None::", 'incremental, on changing end - integer from end, no items, moves to final {{else}}'); // ................................ Act .................................. out = ""; jsv.observable(data).setProperty({p_st: 1, p_en:40}); // ............................... Assert ................................. assert.equal($("#result").text() + "::" + out, "|Bob|Jane|Jeff|May|Alice::|Bob|Jane|Jeff|May|Alice", 'incremental, on changing start/end - moves to first block'); // ................................ Act .................................. out = ""; jsv.observable(data).setProperty("p_rev", true); // ............................... Assert ................................. assert.equal($("#result").text() + "::" + out, "|May|Jeff|Jane|Bob|Jo::|Jo", 'incremental, set reverse=true'); // ................................ Act .................................. out = ""; jsv.observable(data).setProperty({p_en: 0, t_st: 0, t_en: 10}); // ............................... Assert ................................. assert.equal($("#result").text() + "::" + out, "|table|porcelain|lamp|hat::|table|porcelain|lamp|hat", 'incremental, moves to second block'); // ................................ Act .................................. out = ""; jsv.observable(data).setProperty("t_srt", "prop.is"); // ............................... Assert ................................. assert.equal($("#result").text() + "::" + out, "|hat|lamp|porcelain|table::", 'incremental, on changing start'); // ................................ Act .................................. out = ""; jsv.observable(data).setProperty("t_flt", function(item, index, items) { return index%2 === 1; // Include only odd index items }); // ............................... Assert ................................. assert.equal($("#result").text() + "::" + out, "|lamp|table::", 'incremental, on setting filter'); // ................................ Act .................................. out = ""; jsv.observable(data).setProperty({t_st: 0, t_en: 10, t_flt: false, t_srt: function(a, b) { return a.prop.is.length> b.prop.is.length? 1 : a.prop.is.length< b.prop.is.length? -1 : 0; // Sort by string length of items }}); // ............................... Assert ................................. assert.equal($("#result").text() + "::" + out, "|hat|lamp|table|porcelain::|hat|porcelain", 'incremental, on setting sort function'); // ................................ Act .................................. out = ""; jsv.observable(data).setProperty("t_flt", function(item, index, items) { var flt = this.view.data.flt; return flt ? item.prop.is.toLowerCase().indexOf(flt.toLowerCase()) !== -1 : true; }); // ............................... Assert ................................. assert.equal($("#result").text() + "::" + out, "|lamp|table|porcelain::", 'incremental, on setting filter, with flt="l"'); // ................................ Act .................................. out = ""; jsv.observable(data).setProperty("flt", "t"); // ............................... Assert ................................. assert.equal($("#result").text() + "::" + out, "|hat|table::|hat", 'incremental, on setting flt to "t"'); // ................................ Act .................................. out = ""; jsv.observable(data).setProperty("flt", "e"); // ............................... Assert ................................. assert.equal($("#result").text() + "::" + out, "|table|porcelain::|porcelain", 'incremental, on setting flt to "e"'); // ................................ Act .................................. out = ""; jsv.observable(data.things).setProperty({e: {is: "cupboard"}, f: {is: "window"}}); // ............................... Assert ................................. assert.equal($("#result").text() + "::" + out, "|table|porcelain::", 'incremental, on inserting items'); // ................................ Act .................................. out = ""; jsv.observable(data).setProperty("flt", ""); // ............................... Assert ................................. assert.equal($("#result").text() + "::" + out, "|hat|lamp|table|window|cupboard|porcelain::|hat|lamp|window|cupboard", 'incremental, on setting flt to ""'); // ................................ Act .................................. out = ""; jsv.observable(data).setProperty("t_srt", "prop.is"); // ............................... Assert ................................. assert.equal($("#result").text() + "::" + out, "|cupboard|hat|lamp|porcelain|table|window::", 'incremental, on setting sort to "prop.is"'); // ................................ Act .................................. out = ""; jsv.observable(data).setProperty("t_srt", false); // ............................... Assert ................................. assert.equal($("#result").text() + "::" + out, "|hat|lamp|table|window|cupboard|porcelain::", 'incremental, on setting sort to false'); // ................................ Act .................................. out = ""; jsv.observable(data).setProperty("t_stp", 3); // ............................... Assert ................................. assert.equal($("#result").text() + "::" + out, "|hat|window::", 'incremental, on setting step to 3'); // ................................ Act .................................. out = ""; jsv.observable(data).setProperty("t_stp", 2); // ............................... Assert ................................. assert.equal($("#result").text() + "::" + out, "|hat|table|cupboard::|table|cupboard", 'incremental, on setting step to 2'); // ................................ Act .................................. out = ""; jsv.observable(data).setProperty("t_stp", false); // ............................... Assert ................................. assert.equal($("#result").text() + "::" + out, "|hat|lamp|table|window|cupboard|porcelain::|lamp|window|porcelain", 'incremental, on setting step to false'); // ................................ Act .................................. out = ""; jsv.observable(data).setProperty("t_st", 10); // ............................... Assert ................................. assert.equal($("#result").text() + "::" + out, "None::", 'incremental, move to final {{else}} block'); // ................................ Reset ................................ $("#result").empty(); }); QUnit.test("{^{if}}...{{else}}...{{/if}}", function(assert) { jsv.views.settings.advanced({_jsv: true}); // For using cbBindings store // =============================== Arrange =============================== var data = {one: true, two: false, three: true}, boundIfElseTmpl = jsv.templates( '{^{if one pane=0}}' + '{^{if two pane=0}}' + '{^{if three pane=0}}ONE TWO THREE {{else}}ONE TWO notThree {{/if}}' + '{{else}}ONE notTwo {^{if three}}THREE {{/if}}{^{if !three}}notThree {{/if}}{{/if}}' + '{{else three pane=1}}' + '{^{if two pane=0}}notOne TWO THREE{{else}}notOne notTwo THREE {{/if}}' + '{{else}}' + '{^{if two pane=0}}notOne TWO notThree {{else}}notOne TWO notThree {{/if}}' + '{{/if}}'); // ................................ Act .................................. boundIfElseTmpl.link("#result", data); // ............................... Assert ................................. after = $("#result").text(); assert.equal(after, boundIfElseTmpl.render(data), 'Bound if and else with link render the same as unbound, when using the JsRender render() method'); // ............................... Assert ................................. assert.equal(after, "ONE notTwo THREE ", 'Bound if and else render correct blocks based on boolean expressions'); // ................................ Act .................................. jsv.observable(data).setProperty({one: false, two: false, three: true}); after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, "notOne notTwo THREE ", 'Bound if and else render correct blocks based on boolean expressions'); // ................................ Act .................................. jsv.observable(data).setProperty({one: false, two: true, three: false}); after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, "notOne TWO notThree ", 'Bound if and else render correct blocks based on boolean expressions'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== data = {expanded: true}; var deepIfTmpl = jsv.templates( '' + '{^{if expanded}}' + '' + '{{/if}}' + '' + '
                  DeepContent
                  afterDeep
                  '); // ................................ Act .................................. deepIfTmpl.link("#result", data); jsv.observable(data).setProperty("expanded", false); jsv.observable(data).setProperty("expanded", true); // ............................... Assert ................................. after = $("#result").text(); var deferredString = $("#result tr")[0]._df; // "/226_/322^" // With deep version, the tokens for the {^{if}} binding had to be deferred - we test the format: deferredString = /\/\d+\_\/\d+\^/.test(deferredString); assert.equal(deferredString && after, 'DeepContentafterDeep', 'With deep bound {^{if}} tag, there is deferred binding and binding behaves correctly after removing and inserting'); // ................................ Act .................................. jsv.observable(data).setProperty("expanded", false); // ............................... Assert ................................. after = $("#result").text(); deferredString = $("#result tr")[0]._df; // "#322^/322^" // With deep version, the tokens for the {^{if}} binding had to be deferred - we test the format: deferredString = /#(\d+\^)\/\1/.test(deferredString); assert.equal(deferredString && after, 'afterDeep', 'With deep bound {^{if}} tag, there is deferred binding and binding behaves correctly after further remove'); // =============================== Arrange =============================== var shallowIfTmpl = jsv.templates( '' + '{^{if expanded}}' + '' + '{{/if}}' + '' + '
                  ShallowContent
                  afterShallow
                  '); // ................................ Act .................................. shallowIfTmpl.link("#result", data); jsv.observable(data).setProperty("expanded", false); jsv.observable(data).setProperty("expanded", true); // ............................... Assert ................................. after = $("#result").text(); deferredString = $("#result tr")[0]._df; // "" // With shallow version, no deferred binding assert.equal(!deferredString && after, 'ShallowContentafterShallow', 'With shallow bound {^{if}} tag, there is no deferred binding, and binding behaves correctly after removing and inserting'); // ................................ Act .................................. jsv.observable(data).setProperty("expanded", false); // ............................... Assert ................................. after = $("#result").text(); deferredString = $("#result tr")[0]._df; // "" // With shallow version, no deferred binding assert.equal(!deferredString && after, 'afterShallow', 'With shallow bound {^{if}} tag, there is no deferred binding and binding behaves correctly after further remove'); // ................................ Reset ................................ $("#result").empty(); res = ""; jsv.views.settings.advanced({_jsv: false}); }); QUnit.test("{^{props}} basic", function(assert) { jsv.views.settings.advanced({_jsv: true}); // For using cbBindings store // =============================== Arrange =============================== var root = { objA: {propA1: "valA1a"}, objB: {propB1: "valB1a"} }; jsv.templates('{^{props objA}}{^{:key}}:{^{:prop}},{{/props}}') .link("#result", root); // ................................ Act .................................. before = $("#result").text(); jsv.observable(root.objA).setProperty({propA1: "valA1b"}); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, 'propA1:valA1a,|propA1:valA1b,', '{^{props}} - set existing property'); // ................................ Act .................................. before = $("#result").text(); jsv.observable(root.objA).setProperty({propA1: "valA1c", propA2: "valA2a"}); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, 'propA1:valA1b,|propA1:valA1c,propA2:valA2a,', '{^{props}} - set new property'); // ................................ Act .................................. before = $("#result").text(); jsv.observable(root.objA).setProperty({propA1: "", propA2: null}); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, 'propA1:valA1c,propA2:valA2a,|propA1:,propA2:,', '{^{props}} - set property to empty string or null'); // ................................ Act .................................. before = $("#result").text(); jsv.observable(root.objA).setProperty({propA1: null}); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, 'propA1:,propA2:,|propA1:,propA2:,', '{^{props}} - all properties null'); // ................................ Act .................................. before = $("#result").text(); jsv.observable(root.objA).removeProperty("propA1").removeProperty("propA2"); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, 'propA1:,propA2:,|', '{^{props}} - all properties removed'); // ................................ Act .................................. before = $("#result").text(); jsv.observable(root.objA).setProperty({propA1: "valA1b"}); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, "|propA1:valA1b,", '{^{props}} - set property where there were none'); // ................................ Act .................................. before = $("#result").text(); jsv.observable(root).setProperty({objA: {}}); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, "propA1:valA1b,|", '{^{props}} - set whole object to empty object'); // ................................ Act .................................. before = $("#result").text(); jsv.observable(root).setProperty({objA: {propX: "XX"}}); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, "|propX:XX,", '{^{props}} - set whole object to different object'); //................................ Reset ................................ $("#result").empty(); // ............................... Assert ................................. assert.equal(JSON.stringify(_jsv.cbBindings), "{}", "{^{props}} dataMap bindings all removed when tag disposed (content removed from DOM)"); jsv.views.settings.advanced({_jsv: false}); }); QUnit.test("{^{props}} modifying content, through arrayChange/propertyChange on target array", function(assert) { var done; if (assert.async) { done = assert.async() } else { stop() } jsv.views.settings.advanced({_jsv: true}); // For using cbBindings store // =============================== Arrange =============================== var root = { objA: {propA1: "valA1a"} }; jsv.templates( '{^{props objA}}' + '{^{:key}}:{^{:prop}},' + '' + ',' + ',' + '' + '' + '{{/props}}') .link("#result", root, { add: function(ev, eventArgs) { var view = eventArgs.view, arr = view.get("array").data; jsv.observable(arr).insert({key: "addkey", prop: "addprop"}); }, remove: function(ev, eventArgs) { var view = eventArgs.view, arr = view.get("array").data, index = view.index; jsv.observable(arr).remove(index); }, change: function(ev, eventArgs) { var view = eventArgs.view, item = view.data; jsv.observable(item).setProperty({key: "changed", prop: "changedValue"}); } }); // ................................ Act .................................. before = $("#result").text(); $(".addProp").click(); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, "propA1:valA1a,removeadd,change,|propA1:valA1a,removeadd,change,addkey:addprop,removeadd,change,", '{^{props}} - add properties to props target array'); // ................................ Act .................................. before = $("#result").text(); $(".removeProp:first()").click(); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, "propA1:valA1a,removeadd,change,addkey:addprop,removeadd,change,|addkey:addprop,removeadd,change,", '{^{props}} - remove properties from props target array'); // ................................ Act .................................. before = $("#result").text(); $(".changeProp").click(); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, "addkey:addprop,removeadd,change,|changed:changedValue,removeadd,change,", '{^{props}} - change value of key and prop in props target array'); // ................................ Act .................................. before = $("#result").text(); keydown($(".changePropInput").val("newValue")); setTimeout(function() { after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after + "|" + JSON.stringify(root.objA), "changed:changedValue,removeadd,change,|changed:newValue,removeadd,change,|{\"changed\":\"newValue\"}", '{^{props}} - change value of input bound to prop in props target array'); // ................................ Act .................................. before = $("#result").text(); keydown($(".changeKeyInput").val("newKey")); setTimeout(function() { after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after + "|" + JSON.stringify(root.objA), "changed:newValue,removeadd,change,|newKey:newValue,removeadd,change,|{\"newKey\":\"newValue\"}", '{^{props}} - change value of input bound to key in props target array'); // ................................ Reset ................................ before = "" + $._data(root).events.propertyChange.length + "-" + $._data(root.objA).events.propertyChange.length; $("#result").empty(); after = "" + ($._data(root).events === undefined) + "-" + ($._data(root.objA).events === undefined) + " -" + JSON.stringify(_jsv.cbBindings); // ............................... Assert ................................. assert.equal(before + "|" + after, "1-1|true-true -{}", '{^{props}} dataMap bindings all removed when tag disposed (content removed from DOM)'); jsv.views.settings.advanced({_jsv: false}); if (assert.async) { done() } else { start() } }, 0); }, 0); }); QUnit.test("{^{props}}...{{else}} ...", function(assert) { jsv.views.settings.advanced({_jsv: true}); // For using cbBindings store // =============================== Arrange =============================== var root = { objA: {propA1: "valA1"}, objB: {propB1: "valb1", propB2: "valb2"} }; jsv.templates('{^{props objA}}{^{:key}}:{^{:prop}},' + '{{else objB}}{^{:key}}:{^{:prop}},' + '{{else}}' + 'NONE' + '{{/props}}') .link("#result", root); // ................................ Act .................................. before = $("#result").text(); jsv.observable(root.objA).setProperty("propA2", "valA2"); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, "propA1:valA1,|propA1:valA1,propA2:valA2,", '{^{props}} - set new property on objA - shows additional property'); // ................................ Act .................................. before = $("#result").text(); jsv.observable(root.objA).removeProperty("propA1").removeProperty("propA2"); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, "propA1:valA1,propA2:valA2,|propB1:valb1,propB2:valb2,", '{^{props}} - remove properties from objA - switches to {{else objB}}'); // ................................ Act .................................. before = $("#result").text(); jsv.observable(root.objB).removeProperty("propB1").removeProperty("propB2"); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, "propB1:valb1,propB2:valb2,|NONE", '{^{props}} - remove properties from objB - switches to {{else}}'); // ................................ Act .................................. before = $("#result").text(); jsv.observable(root.objB).removeProperty("NotAProperty"); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, "NONE|NONE", '{^{props}} - remove inexistant property from objB - remains on {{else}}'); // ................................ Act .................................. before = $("#result").text(); jsv.observable(root.objB).setProperty("newProp", ""); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, "NONE|newProp:,", '{^{props}} - set property on objB to undefined - render {{else objB}}'); // ................................ Reset ................................ $("#result").empty(); // ............................... Assert ................................. assert.equal(JSON.stringify(_jsv.cbBindings), "{}", "{^{props}} dataMap bindings all removed when tag disposed (content removed from DOM)"); // =============================== Arrange =============================== root = { objA: {propA1: "valA1"}, objB: {propB1: "valb1", propB2: "valb2"} }; jsv.templates('{^{props objA}}{^{:key}}:{^{:prop}},' + ',' + '{{else objB}}{^{:key}}:{^{:prop}},' + ',' + '{{else}}' + 'NONE' + '{{/props}}') .link("#result", root, { remove: function(ev, eventArgs) { var view = eventArgs.view, arr = view.get("array").data, index = view.index; jsv.observable(arr).remove(index); } }); // ................................ Act .................................. before = $("#result").text(); $(".removePropA").click(); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, "propA1:valA1,remove,|propB1:valb1,remove,propB2:valb2,remove,", '{^{props}} - remove properties from objA target array - switches to {{else objB}}'); // ................................ Act .................................. before = $("#result").text(); $(".removePropB").click(); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, "propB1:valb1,remove,propB2:valb2,remove,|NONE", '{^{props}} - remove properties from objB target array - switches to {{else}}'); // ................................ Reset ................................ $("#result").empty(); // ............................... Assert ................................. assert.equal(JSON.stringify(_jsv.cbBindings), "{}", "{^{props}} dataMap bindings all removed when tag disposed (content removed from DOM)"); jsv.views.settings.advanced({_jsv: false}); }); QUnit.test("{^{props start end sort filter reverse}}...{{else}} ...", function(assert) { jsv.views.settings.advanced({_jsv: true}); // For using _jsv // =============================== Arrange =============================== var movies = {keyf: {title: "a0"}, keye: {title: "x0"}, keyd: {title: "b0"}, keyc: {title: "y0"}, keyb: {title: "c0"}, keya: {title: "z0"}}, ctx = {}, cnt = 0; jsv.templates( '|All:--- {^{props movies}}{{:key}}:{{:prop.title}} {{/props}}
                  \ |Sort:-- {^{props movies sort="prop.title" reverse=true}}{{:prop.title}} {{/props}}
                  \ |Filter: {^{props movies sort="prop.title" reverse=true filter=~odd}}{{:prop.title}} {{/props}}
                  \ |Slice:- {^{props movies sort="prop.title" reverse=true filter=~odd start=1 end=-1 this=~ctx.target}}{{:prop.title}} {{/props}}') .link("#result", {movies: movies}, { ctx: ctx, odd: function(item, index, items) { return index%2; } }); var tgt = ctx.target.tagCtx.contentView.data function newProp(title) { return {key: "key"+cnt++, prop: {title: title}} } // ................................ Act .................................. after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, "|All:--- keyf:a0 keye:x0 keyd:b0 keyc:y0 keyb:c0 keya:z0 |Sort:-- z0 y0 x0 c0 b0 a0 |Filter: y0 c0 a0 |Slice:- c0 ", '{{props}} with Sorting, filtering, reverse, start and end settings'); // ................................ Act .................................. jsv.observable(tgt).insert(newProp("t" + cnt)); after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, "|All:--- keyf:a0 keye:x0 keyd:b0 keyc:y0 keyb:c0 keya:z0 key0:t0 |Sort:-- z0 y0 x0 t0 c0 b0 a0 |Filter: y0 t0 b0 |Slice:- c0 t0 ", 'Appending of item in target array (sorted, filtered etc) - item is rendered without refreshing sort, filter etc.'); ctx.target.refresh(); after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, "|All:--- keyf:a0 keye:x0 keyd:b0 keyc:y0 keyb:c0 keya:z0 key0:t0 |Sort:-- z0 y0 x0 t0 c0 b0 a0 |Filter: y0 t0 b0 |Slice:- t0 ", 'To refresh, sort, slice etc with new item included, call tag.refresh() '); // ................................ Act .................................. jsv.observable(tgt).insert(0, newProp("t" + cnt)); after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, "|All:--- keyf:a0 keye:x0 keyd:b0 keyc:y0 keyb:c0 keya:z0 key0:t0 key1:t1 |Sort:-- z0 y0 x0 t1 t0 c0 b0 a0 |Filter: y0 t1 c0 a0 |Slice:- t1 t0 ", 'Insertion of item in target array (sorted, filtered etc) - item is rendered without refreshing sort, filter etc.'); // ................................ Act .................................. jsv.observable(movies).setProperty("key" + cnt++, {title: "m" + cnt}); after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, "|All:--- keyf:a0 keye:x0 keyd:b0 keyc:y0 keyb:c0 keya:z0 key0:t0 key1:t1 key2:m3 |Sort:-- z0 y0 x0 t1 t0 m3 c0 b0 a0 |Filter: y0 t1 m3 b0 |Slice:- t1 m3 ", 'Insertion of item in source array will also refresh sort, filter etc.'); // ................................ Act .................................. jsv.observable(tgt).insert(1, [newProp("t" + cnt), newProp("t" + cnt)]); after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, "|All:--- keyf:a0 keye:x0 keyd:b0 keyc:y0 keyb:c0 keya:z0 key0:t0 key1:t1 key2:m3 key4:t4 key3:t3 |Sort:-- z0 y0 x0 t4 t3 t1 t0 m3 c0 b0 a0 |Filter: y0 t4 t1 m3 b0 |Slice:- t1 t3 t4 m3 ", 'Insertion of multiple items in target array (sorted, filtered etc) - items are rendered without refreshing sort, filter etc.'); // ................................ Act .................................. jsv.observable(tgt).refresh([tgt[1], newProp("t" + cnt), tgt[0]]); after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, "|All:--- keyf:a0 keye:x0 keyd:b0 keyc:y0 keyb:c0 keya:z0 key0:t0 key1:t1 key3:t3 key5:t5 |Sort:-- z0 y0 x0 t5 t3 t1 t0 c0 b0 a0 |Filter: y0 t5 t1 c0 a0 |Slice:- t3 t5 t1 ", 'Calling refresh() on target array will insert and remove items appropriately from source array and target array (and move items in target array) without refreshing sort, filter etc.'); // ................................ Act .................................. jsv.observable(tgt).remove(); after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, "|All:--- keyf:a0 keye:x0 keyd:b0 keyc:y0 keyb:c0 keya:z0 key0:t0 key3:t3 key5:t5 |Sort:-- z0 y0 x0 t5 t3 t0 c0 b0 a0 |Filter: y0 t5 t0 b0 |Slice:- t3 t5 ", 'Removing item in target array (sorted, filtered etc) - items are rendered without refreshing sort, filter etc.'); // ................................ Act .................................. jsv.observable(movies).removeProperty("keyf"); after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, "|All:--- keye:x0 keyd:b0 keyc:y0 keyb:c0 keya:z0 key0:t0 key3:t3 key5:t5 |Sort:-- z0 y0 x0 t5 t3 t0 c0 b0 |Filter: y0 t5 t0 b0 |Slice:- t5 t0 ", 'Removal of item in source array will also refresh sort, filter etc.'); // ................................ Act .................................. jsv.observable(tgt).move(0, 1); after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, "|All:--- keye:x0 keyd:b0 keyc:y0 keyb:c0 keya:z0 key0:t0 key3:t3 key5:t5 |Sort:-- z0 y0 x0 t5 t3 t0 c0 b0 |Filter: y0 t5 t0 b0 |Slice:- t0 t5 ", 'Moving items in target array (sorted, filtered etc) - items are moved in target but not in source, and this is without refreshing sort, filter etc.'); // ................................ Act .................................. jsv.observable(tgt).refresh([newProp("t" + cnt)]); after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, "|All:--- keye:x0 keyd:b0 keyc:y0 keyb:c0 keya:z0 key3:t3 key6:t6 |Sort:-- z0 y0 x0 t6 t3 c0 b0 |Filter: y0 t6 c0 |Slice:- t6 ", 'Calling refresh() on target array will insert and remove items appropriately from source array and target array (and move items in target array) without refreshing sort, filter etc.'); // ................................ Act .................................. jsv.observable(movies).setProperty("key" + cnt++, {title: "m" + cnt}); after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, "|All:--- keye:x0 keyd:b0 keyc:y0 keyb:c0 keya:z0 key3:t3 key6:t6 key7:m8 |Sort:-- z0 y0 x0 t6 t3 m8 c0 b0 |Filter: y0 t6 m8 b0 |Slice:- t6 m8 ", 'Insertion of item in source array will also refresh sort, filter etc.'); // ................................ Act .................................. jsv.observable(tgt).insert(1, [newProp("t" + cnt), newProp("t" + cnt)]); after = $("#result").text(); // ............................... Assert ................................. assert.equal(after, "|All:--- keye:x0 keyd:b0 keyc:y0 keyb:c0 keya:z0 key3:t3 key6:t6 key7:m8 key9:t9 key8:t8 |Sort:-- z0 y0 x0 t9 t8 t6 t3 m8 c0 b0 |Filter: y0 t9 t6 m8 b0 |Slice:- t6 t8 t9 m8 ", 'Insertion of multiple items in target array (sorted, filtered etc) - items are rendered without refreshing sort, filter etc.'); // ................................ Reset ................................ $("#result").empty(); assert.equal(JSON.stringify(_jsv.cbBindings), "{}", "Bindings all removed when content removed from DOM"); jsv.views.settings.advanced({_jsv: false}); }); QUnit.test('data-link="{on ...', function(assert) { jsv.views.settings.advanced({_jsv: true}); // For using _jsv // =============================== Arrange =============================== function swap(ev, eventArgs) { jsv.observable(this).setProperty("type", this.type === "shape" ? "line" : "shape"); } var thing = { type: "shape", swap: swap }; jsv.templates('
                  {^{:type}}
                  ') .link("#result", thing); // ................................ Act .................................. before = $("#result").text(); $("#result div").click(); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, "shape|line", '{on swap} calls swap method on click, with "this" pointer context on data object'); // ................................ Reset ................................ $("#result").empty(); thing.type = "shape"; // =============================== Arrange =============================== jsv.templates('
                  {^{:type}}
                  ') .link("#result", thing, {swap: swap}); // ................................ Act .................................. before = $("#result").text(); $("#result div").click(); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, "shape|line", '{on ~swap} calls swap helper method on click, with "this" pointer context defaulting to current data object'); // ................................ Reset ................................ $("#result").empty(); thing.type = "shape"; // =============================== Arrange =============================== jsv.templates('
                  {^{:type}} {^{:check}}
                  ') .link("#result", thing, { util: { swap: function(ev, eventArgs) { jsv.observable(this.data).setProperty({ type: this.data.type === "shape" ? "line" : "shape", check: this.data === eventArgs.view.data }); }, data: thing } }); // ................................ Act .................................. before = $("#result").text(); $("#result div").click(); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, "shape |line true", '{on ~util.swap} calls util.swap helper method on click, with ~util as this pointer'); // ................................ Reset ................................ $("#result").empty(); thing.type = "shape"; delete thing.check; // =============================== Arrange =============================== jsv.templates('
                  {^{:type}}
                  ') .link("#result", thing, { util: { swap: swap } }); // ................................ Act .................................. before = $("#result").text(); $("#result div").click(); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, "shape|line", '{on ~util.swap context=#data} calls util.swap helper method on click, with current data object as this pointer'); // ................................ Reset ................................ $("#result").empty(); thing.type = "shape"; // =============================== Arrange =============================== jsv.templates('
                  {^{:type}} {^{:check}}
                  ') .link("#result", thing, { util: { swap: function(ev, eventArgs) { jsv.observable(this.data).setProperty({ type: this.data.type === "shape" ? "line" : "shape", check: this.data === eventArgs.view.data }); }, data: thing, swapCtx: { data: thing } } }); // ................................ Act .................................. before = $("#result").text(); $("#result div").click(); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, "shape |line true", '{on ~util.swap context=~util.swapCtx} calls util.swap helper method on click, with util.swapCtx as this pointer'); // ................................ Reset ................................ $("#result").empty(); thing.type = "shape"; delete thing.check; // =============================== Arrange =============================== jsv.templates('
                  {^{:type}} {^{:check}}
                  ') .link("#result", thing, { util: { swap: function(ev, eventArgs) { jsv.observable(ev.data).setProperty({ type: ev.data.type === "shape" ? "line" : "shape", check: ev.data === eventArgs.view.data }); }, data: thing, swapCtx: { data: thing } } }); // ................................ Act .................................. before = $("#result").text(); $("#result div").click(); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, "shape |line true", '{on ~util.swap data=#data} calls util.swap helper method on click, and passes current data #data as ev.data'); // ................................ Reset ................................ $("#result").empty(); thing.type = "shape"; delete thing.check; // =============================== Arrange =============================== jsv.templates('
                  {^{:type}}
                  ') .link("#result", thing); // ................................ Act .................................. before = $("#result").text(); $("#result div").mouseup(); after = $("#result").text(); $("#result div").mousedown(); after += $("#result").text(); $("#result div").blur(); after += $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, "shape|lineshapeline", "{on 'mouseup mousedown blur' swap} calls util method on mouseup, mousedown and blur"); // ................................ Reset ................................ $("#result").empty(); thing.type = "shape"; delete thing.check; // =============================== Arrange =============================== jsv.templates('
                  {^{:type}}
                  ') .link("#result", thing, { util: [{ swap: swap, data: thing }] }); // ................................ Act .................................. before = $("#result").text(); $("#result div").click(); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, "shape |line ", '{on ~util[0].swap} calls util[0].swap helper method on click'); // ................................ Reset ................................ $("#result").empty(); thing.type = "shape"; // =============================== Arrange =============================== var res = "1: "; jsv.templates( "
                  " + "" + "" + "
                  ") .link("#result", { unbind: function(ev, eventArgs) { res += "unbind "; eventArgs.linkCtx.tag.onUnbind(); }, refresh: function(ev, eventArgs) { res += "refresh "; eventArgs.linkCtx.tag.refresh(); }, test: function() { res += "test "; } }); // ................................ Act .................................. var events = $._data($("#divForOn")[0]).events, eventBindings = "before: " + events.keydown.length + events.keyup.length + events.mouseup.length + events.mousedown.length; $("#divForOn #inputB").mouseup(); res += "2: "; $("#divForOn .inputA").mouseup(); res += "3: "; $("#divForOn #inputB").keyup(); res += "4: "; $("#divForOn #inputB").keyup(); res += "5: "; $("#divForOn .inputA").keydown(); res += "6: "; $("#divForOn .inputA").keyup(); res += "7: "; $("#divForOn #inputB").mouseup(); res += "8: "; $("#divForOn #inputB").mousedown(); eventBindings += " | after: " + events.keydown + events.keyup + events.mouseup.length + events.mousedown.length; // ............................... Assert ................................. assert.equal(res, "1: test 2: test 3: unbind 4: 5: unbind 6: 7: test 8: refresh ", "multiple {on events selector method} bindings on container attach events on delegated elements. Also, tag.onDispose on tag instances removes specific handlers for corresponding elements/selectors"); // ............................... Assert ................................. assert.equal(eventBindings, "before: 1211 | after: undefinedundefined11", "onDispose removes specific delegated events"); // ................................ Act .................................. res = "1: "; $("#divForOn").html(""); $("#divForOn #newlyAdded").mouseup(); res += "2: "; $("#divForOn #newlyAdded").keyup(); // ............................... Assert ................................. assert.equal(res, "1: test 2: ", "delegated {on events selector method} binding allows additional elements added to content to bind correctly"); // ................................ Act .................................. $("#result").empty(); eventBindings = "" + events.keydown + events.keyup + events.mouseup + JSON.stringify([_jsv.cbBindings, _jsv.bindings]); // ............................... Assert ................................. assert.equal(eventBindings, "undefinedundefinedundefined[{},{}]", "Removing the element removes all associated attached {on } handlers"); // =============================== Arrange =============================== var tmpl = jsv.templates("
                  \ \ \
                  "), data = { name: "Jo", role: "Advisor", option: { allow: true }, thisIsTheMethod: function(role, text, isFoo, compile, amount, root, ev, eventArgs) { if (compile) { compile.call(root, role, text, isFoo, amount, ev.data.allow, eventArgs.linkCtx.tag.tagCtx.args[2]); } }, process: function(role, text, isFoo, amount, allow, extraParam) { jsv.observable(this).setProperty("res", this.res + role + text + isFoo + amount + " allow:" + allow + " extraParam: " + extraParam + "|"); }, res: "" }; tmpl.link("#result", data); // ................................ Act .................................. $("#doIt").click(); data.option.allow = false; jsv.observable(data).setProperty("role", "Follower"); $("#doIt").click(); // ............................... Assert ................................. assert.equal(data.res, "Advisorheytrue33 allow:true extraParam: 754|Followerheytrue33 allow:false extraParam: 754|", "{on 'click' selector otherParams... method params...} : supports passing params to method, of any type, as well as setting data and context for the function call"); // =============================== Arrange =============================== res = "1: "; $("#result").html("
                  " + "" + "" + "
                  "); jsv.link(true, "#result", { unbind: function(ev, eventArgs) { res += "unbind "; eventArgs.linkCtx.tag.onUnbind(); }, refresh: function(ev, eventArgs) { res += "refresh "; eventArgs.linkCtx.tag.refresh(); }, test: function() { res += "test "; } }); // ................................ Act .................................. events = $._data($("#divForOn")[0]).events; eventBindings = "before: " + events.keydown.length + events.keyup.length + events.mouseup.length + events.mousedown.length; $("#divForOn #inputB").mouseup(); res += "2: "; $("#divForOn .inputA").mouseup(); res += "3: "; $("#divForOn #inputB").keyup(); res += "4: "; $("#divForOn #inputB").keyup(); res += "5: "; $("#divForOn .inputA").keydown(); res += "6: "; $("#divForOn .inputA").keyup(); res += "7: "; $("#divForOn #inputB").mouseup(); res += "8: "; $("#divForOn #inputB").mousedown(); eventBindings += " | after: " + events.keydown + events.keyup + events.mouseup.length + events.mousedown.length; // ............................... Assert ................................. assert.equal(res, "1: test 2: test 3: unbind 4: 5: unbind 6: 7: test 8: refresh ", "Top-level {on }: multiple {on events selector method} top-level bindings on container attach events on delegated elements. Also, tag.onDispose on tag instances removes specific handlers for corresponding elements/selectors"); // ............................... Assert ................................. assert.equal(eventBindings, "before: 1211 | after: undefinedundefined11", "Top-level {on }: onDispose removes specific delegated events"); // ................................ Act .................................. res = "1: "; $("#divForOn").html(""); $("#divForOn #newlyAdded").mouseup(); res += "2: "; $("#divForOn #newlyAdded").keyup(); // ............................... Assert ................................. assert.equal(res, "1: test 2: ", "Top-level {on }: delegated {on events selector method} binding allows additional elements added to content to bind correctly"); // ................................ Act .................................. $("#result").empty(); eventBindings = "" + events.keydown + events.keyup + events.mouseup + JSON.stringify([_jsv.cbBindings, _jsv.bindings]); // ............................... Assert ................................. assert.equal(eventBindings, "undefinedundefinedundefined[{},{}]", "Top-level {on }: Removing the element removes all associated attached {on } handlers"); // =============================== Arrange =============================== $("#result").html("
                  \ \ \
                  "); data = { name: "Jo", role: "Advisor", option: { allow: true }, thisIsTheMethod: function(role, text, isFoo, compile, amount, root, ev, eventArgs) { if (compile) { compile.call(root, role, text, isFoo, amount, ev.data.allow, eventArgs.linkCtx.tag.tagCtx.args[2]); } }, process: function(role, text, isFoo, amount, allow, extraParam) { jsv.observable(this).setProperty("res", this.res + role + text + isFoo + amount + " allow:" + allow + " extraParam: " + extraParam + "|"); }, res: "" }; jsv.link(true, "#result", data); // ................................ Act .................................. $("#doIt").click(); data.option.allow = false; jsv.observable(data).setProperty("role", "Follower"); $("#doIt").click(); // ............................... Assert ................................. assert.equal(data.res, "Advisorheytrue33 allow:true extraParam: 754|Followerheytrue33 allow:false extraParam: 754|", "Top-level {on 'click' selector method params...} : supports passing params to method, of any type, as well as setting data and context for the function call"); // =============================== Arrange =============================== res = "1: "; data = { unbind: function(ev, eventArgs) { res += "unbind "; eventArgs.linkCtx.tag.onUnbind(); }, refresh: function(ev, eventArgs) { res += "refresh "; eventArgs.linkCtx.tag.refresh(); }, test: function() { res += "test "; } }; $("#result").html("
                  oldcontent
                  "); jsv.link(true, "#linkTgt", data); events = $._data($("#linkTgt")[0]).events; // ................................ Act .................................. $("#linkTgt").mousedown(); res += "2: "; $("#linkTgt").mouseup(); res += "3: "; $("#linkTgt").click(); res += "4: "; $("#linkTgt").mousedown(); res += "5: "; $("#linkTgt").mouseup(); res += "6: "; $("#linkTgt").click(); // ............................... Assert ................................. assert.equal(res, "1: test 2: test 3: refresh 4: test 5: test 6: refresh ", 'jsv.link(true, "#linkTgt", data): top-level linking to element (not container) links correctly, including \'{on }\' bindings'); // ............................... Assert ................................. eventBindings = "" + events.mouseup.length + events.mousedown.length + events.click.length; assert.equal(eventBindings, "111", 'jsv.link(true, "#linkTgt", data): top-level linking to element (not container) adds {on } binding handlers correctly - including calling refresh() on {on } tag'); // ................................ Act .................................. jsv.unlink("#linkTgt"); // ............................... Assert ................................. eventBindings = "" + events.mouseup + events.mousedown + events.click + JSON.stringify([_jsv.cbBindings, _jsv.bindings]); assert.equal(eventBindings, "undefinedundefinedundefined[{},{}]", 'jsv.unlink("#linkTgt"): directly on top-level data-linked element (not through container) removes all \'{on }\' handlers'); jsv.views.settings.advanced({_jsv: false}); }); QUnit.test('{^{radiogroup}}', function(assert) { // =============================== Arrange =============================== var tmpl = jsv.templates( '{^{radiogroup selected}}' + '{^{for people}}' + '' + '{{/for}}' +'{{/radiogroup}}' ); var model = { selected: "Jim", people: [ {name: "Bob"}, {name: "Jim"} ] }, newName = "new"; // ............................... Act ................................. tmpl.link("#result", model); res = $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).insert({ name: newName }); jsv.observable(model).setProperty("selected", newName); res += $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).remove(2); res += $("#result input:checked").parent().text() + "|"; $("#result input").first().prop("checked", true).change(); // Check first radio button res += model.selected + "-" + $("#result input:checked").parent().text() + "|"; // ............................... Assert ................................. assert.equal(res, ":Jim|:new||Bob-:Bob|", '{^{radiogroup selected}}{^{for ...}}...'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== var tmpl = jsv.templates( '{^{radiogroup selected}}' + '' + '{^{for people}}' + '' + '{{/for}}' +'{{/radiogroup}}' ); model = { selected: "Jim", people: [ {name: "Bob"}, {name: "Jim"} ] }; // ............................... Act ................................. tmpl.link("#result", model); res = $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).insert({ name: newName }); jsv.observable(model).setProperty("selected", newName); res += $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).remove(2); res += $("#result input:checked").parent().text() + "|"; $("#result input").first().prop("checked", true).change(); // Check first radio button res += model.selected + "-" + $("#result input:checked").parent().text() + "|"; // ............................... Assert ................................. assert.equal(res, ":JIM|:NEW||None-:NONE|", '{^{radiogroup selected}}......{^{for ...}}...'); // ............................... Act ................................. jsv.observable(model.people[1]).setProperty("name", "jimUpdated"); res = $("#result").text() + "|"; $("#result input").eq(2).prop("checked", true).change(); // Check third radio button res += model.selected + "-" + $("#result input:checked").parent().text() + "|"; assert.equal(res, ":NONE:BOB:JIMUPDATED|jimUpdated-:JIMUPDATED|", '{^{radiogroup selected}}...{^{for ...}}... - updated label and value'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== tmpl = jsv.templates({markup: '{^{radiogroup selected convert=~lower convertBack="upper" linkTo=selectedOut}}' + '' + '{^{for people}}' + '' + '{{/for}}' +'{{/radiogroup}}', converters: { upper: function(val) { return val.toUpperCase(); } } }); model = { selected: "JIM", people: [ {name: "bob"}, {name: "jim"} ] }; // ............................... Act ................................. tmpl.link("#result", model, { lower: function(val) { return val.toLowerCase(); } }); res = $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).insert({ name: newName }); jsv.observable(model).setProperty("selected", newName); res += $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).remove(2); res += $("#result input:checked").parent().text() + "|"; $("#result input").first().prop("checked", true).change(); // Check first radio button res += model.selected + "-" + model.selectedOut + "-" + $("#result input:checked").parent().text() + "|"; // ............................... Assert ................................. assert.equal(res, ":jim|:new||new-NONE-:none|", '{^{radiogroup selected convert=... convertBack=... linkTo=...}}'); // ............................... Act ................................. jsv.observable(model.people[1]).setProperty("name", "jimUpdated"); res = $("#result").text() + "|"; $("#result input").eq(2).prop("checked", true).change(); // Check third radio button res += model.selected + "-" + model.selectedOut + "-" + $("#result input:checked").parent().text() + "|"; assert.equal(res, ":none:bob:jimUpdated|new-JIMUPDATED-:jimUpdated|", '{^{radiogroup selected convert=... convertBack=... linkTo=...}} - updated label and value'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== tmpl = jsv.templates( '{^{radiogroup selected}}' + ':' + '{^{for people}}' + ':' + '{{/for}}' +'{{/radiogroup}}' ); model = { selected: "Jim", people: [ {name: "Bob"}, {name: "Jim"} ] }; // ............................... Act ................................. tmpl.link("#result", model); res = $("#" + $("#result input:checked").prop("id") + "Lbl").text() + "|"; jsv.observable(model.people).insert({ name: newName }); jsv.observable(model).setProperty("selected", newName); res += $("#" + $("#result input:checked").prop("id") + "Lbl").text() + "|"; jsv.observable(model.people).remove(2); res += $("#" + $("#result input:checked").prop("id") + "Lbl").text() + "|"; $("#result input").first().prop("checked", true).change(); // Check first radio button res += model.selected + "-" + $("#" + $("#result input:checked").prop("id") + "Lbl").text() + "|"; // ............................... Assert ................................. assert.equal(res, "JIM|NEW||None-NONE|", '{^{radiogroup selected}} with labels by for/id'); // ............................... Act ................................. jsv.observable(model.people[1]).setProperty("name", "jimUpdated"); res = $("#result").text() + "|"; $("#result input").eq(2).prop("checked", true).change(); // Check third radio button res += model.selected + "-" + $("#" + $("#result input:checked").prop("id") + "Lbl").text() + "|"; assert.equal(res, ":NONE:BOB:JIMUPDATED|jimUpdated-JIMUPDATED|", '{^{radiogroup selected}} with labels by for/id - updated label and value'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== tmpl = jsv.templates( '{^{radiogroup selected}}' + '' + '{^{for people}}' + '' + '{{/for}}' +'{{/radiogroup}}' + '{^{radiogroup selected}}' + '' + '{^{for people}}' + '' + '{{/for}}' +'{{/radiogroup}}' ); model = { selected: "Jim", people: [ {name: "Bob"}, {name: "Jim"} ] }; // ............................... Act ................................. tmpl.link("#result", model); res = $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).insert({ name: newName }); jsv.observable(model).setProperty("selected", newName); res += $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).remove(2); res += $("#result input:checked").parent().text() + "|"; $("#result input").first().prop("checked", true).change(); // Check first radio button res += model.selected + "-" + $("#result input:checked").parent().text() + "|"; // ............................... Assert ................................. assert.equal(res, ":JIM:JIM|:NEW:NEW||None-:NONE:NONE|", '{^{radiogroup selected}} - two radiogroups with same selected bindings'); // ............................... Act ................................. jsv.observable(model.people[1]).setProperty("name", "jimUpdated"); res = $("#result").text() + "|"; $("#result input").eq(2).prop("checked", true).change(); // Check third radio button res += model.selected + "-" + $("#result input:checked").parent().text() + "|"; assert.equal(res, ":NONE:BOB:JIMUPDATED:NONE:BOB:JIMUPDATED|jimUpdated-:JIMUPDATED:JIMUPDATED|", '{^{radiogroup selected}} - two radiogroups with same selected bindings - updated label and value'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== tmpl = jsv.templates( '{^{radiogroup selected}}' + '{^{for people}}' + '' + '{{/for}}' +'{{/radiogroup}}' + '{^{radiogroup selected}}' + '' + '{^{for people}}' + '' + '{{/for}}' +'{{/radiogroup}}' ); model = { selected: "Jim", people: [] }; // ............................... Act ................................. tmpl.link("#result", model); res = $("#result").text() + "|" + $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).insert([{name: "Bob"},{name: "Jim"},{name: newName}]); res += $("#result").text() + "|" + $("#result input:checked").parent().text() + "|"; jsv.observable(model).setProperty("selected", newName); res += $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).remove(2); res += $("#result input:checked").parent().text() + "|"; $("#result input").first().prop("checked", true).change(); // Check first radio button res += model.selected + "-" + $("#result input:checked").parent().text() + "|"; // ............................... Assert ................................. assert.equal(res, ":NONE||:BOB:JIM:NEW:NONE:BOB:JIM:NEW|:JIM:JIM|:NEW:NEW||Bob-:BOB:BOB|", '{^{radiogroup selected}} - two radiogroups with same selected bindings - starting out with no items, so no radio buttons'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== tmpl = jsv.templates( '{^{radiogroup selected name="rad1"}}' + '{^{for people}}' + '' + '{{/for}}' +'{{/radiogroup}}' + '{^{radiogroup selected name="rad1"}}' + '' + '{^{for people}}' + '' + '{{/for}}' +'{{/radiogroup}}' ); model = { selected: "Jim", people: [] }; // ............................... Act ................................. tmpl.link("#result", model); res = $("#result").text() + "|" + $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).insert([{name: "Bob"},{name: "Jim"},{name: newName}]); res += $("#result").text() + "|" + $("#result input:checked").parent().text() + "|"; jsv.observable(model).setProperty("selected", newName); res += $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).remove(2); res += $("#result input:checked").parent().text() + "|"; $("#result input").first().prop("checked", true).change(); // Check first radio button res += model.selected + "-" + $("#result input:checked").parent().text() + "|" + $("#result input:checked")[0].name + "|" + $("#result input:checked")[1].name; // ............................... Assert ................................. assert.equal(res, ":NONE||:BOB:JIM:NEW:NONE:BOB:JIM:NEW|:JIM:JIM|:NEW:NEW||Bob-:BOB:BOB|rad1|rad2", '{^{radiogroup selected}} - name for group can be specified rather than auto-generated - on item or on radiogroup tag'); // =============================== Arrange =============================== tmpl = jsv.templates( '{^{if rg1}}' + '{^{radiogroup selected name="rad1"}}' + '{^{for people}}' + '' + '{{/for}}' +'{{/radiogroup}}' + '{{/if}}' + '{^{if rg2}}' + '{^{radiogroup selected name="rad1"}}' + '' + '{^{for people}}' + '' + '{{/for}}' +'{{/radiogroup}}' + '{{/if}}' ); model = { rg1: true, rg2: true, selected: "Jim", people: [] }; newName = "newName"; // ............................... Act ................................. tmpl.link("#result", model); res = ">" + $("#result").text() + "|" + $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).insert([{name: "Bob"},{name: "Jim"},{name: newName}]); res += $("#result").text() + "|" + $("#result input:checked").parent().text() + "|"; jsv.observable(model).setProperty("selected", newName); res += $("#result input:checked").parent().text() + "|"; // ............................... Assert ................................. assert.equal(res, ">:NONE||:BOB:JIM:NEWNAME:NONE:BOB:JIM:NEWNAME|:JIM:JIM|:NEWNAME:NEWNAME|", '{^{radiogroup selected}} - two radiogroups wrapped in {{if}} blocks - starting out with no items'); jsv.observable(model.people).refresh([]); jsv.observable(model).setProperty("rg1", false); res = ">" + $("#result").text() + "|" + $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).insert([{name: "Bob"},{name: "Jim"},{name: newName}]); res += $("#result").text() + "|" + $("#result input:checked").parent().text() + "|"; jsv.observable(model).setProperty("selected", newName); res += $("#result input:checked").parent().text() + "|"; // ............................... Assert ................................. assert.equal(res, ">:NONE||:NONE:BOB:JIM:NEWNAME|:NEWNAME|:NEWNAME|", '{^{radiogroup selected}} - two radiogroups wrapped in {{if}} blocks - one set to false'); jsv.observable(model.people).refresh([]); jsv.observable(model).setProperty("selected", "Jim"); jsv.observable(model).setProperty("rg1", true); res = ">" + $("#result").text() + "|" + $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).insert([{name: "Bob"},{name: "Jim"},{name: newName}]); res += $("#result").text() + "|" + $("#result input:checked").parent().text() + "|"; jsv.observable(model).setProperty("selected", newName); res += $("#result input:checked").parent().text() + "|"; // ............................... Assert ................................. assert.equal(res, ">:NONE||:BOB:JIM:NEWNAME:NONE:BOB:JIM:NEWNAME|:JIM:JIM|:NEWNAME:NEWNAME|", '{^{radiogroup selected}} - two radiogroups wrapped in {{if}} blocks - one set back to true'); // ................................ Reset ................................ $("#result").empty(); }); QUnit.test('{^{checkboxgroup}}', function(assert) { jsv.views.settings.advanced({_jsv: true}); // =============================== Arrange =============================== var tmpl = jsv.templates( '{^{checkboxgroup selectedPeople}}' + '{^{for people}}' + '' + '{{/for}}' +'{{/checkboxgroup}}' ); var model = { selectedPeople: ["Jim"], people: [ {name: "Bob"}, {name: "Jim"} ] }, newName = "new"; // ............................... Act ................................. tmpl.link("#result", model); res = $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).insert({ name: newName }); jsv.observable(model).setProperty("selectedPeople", [newName, "Jim"]); res += $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).remove(2); res += $("#result input:checked").parent().text() + "|"; $("#result input").first().prop("checked", true).change(); // Check first checkbox res += model.selectedPeople + "-" + $("#result input:checked").parent().text() + "|"; jsv.observable(model.selectedPeople).remove(); res += model.selectedPeople + "-" + $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).insert({name: "new"}); res += model.selectedPeople + "-" + $("#result input:checked").parent().text() + "|"; // ............................... Assert ................................. assert.equal(res, ":Jim|:Jim:new|:Jim|new,Jim,Bob-:Bob:Jim|new,Jim-:Jim|new,Jim-:Jim:new|", '{^{checkboxgroup selectedPeople}}{^{for ...}}...'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== var tmpl = jsv.templates( '{^{checkboxgroup selectedPeople}}' + '' + '{^{for people}}' + '' + '{{/for}}' +'{{/checkboxgroup}}' ); model = { selectedPeople: ["Jim"], people: [ {name: "Bob"}, {name: "Jim"} ] }; // ............................... Act ................................. tmpl.link("#result", model); res = $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).insert({ name: newName }); jsv.observable(model).setProperty("selectedPeople", [newName, "Bob"]); res += $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).remove(2); res += $("#result input:checked").parent().text() + "|"; $("#result input").first().prop("checked", true).change(); // Check first checkbox res += model.selectedPeople + "-" + $("#result input:checked").parent().text() + "|"; // ............................... Assert ................................. assert.equal(res, ":JIM|:BOB:NEW|:BOB|new,Bob,None-:NONE:BOB|", '{^{checkboxgroup selectedPeople}}......{^{for ...}}...'); // ............................... Act ................................. jsv.observable(model.people[1]).setProperty("name", "jimUpdated"); res = $("#result").text() + "|"; $("#result input").eq(2).prop("checked", true).change(); // Check third checkbox res += model.selectedPeople + "-" + $("#result input:checked").parent().text() + "|"; assert.equal(res, ":NONE:BOB:JIMUPDATED|new,Bob,None,jimUpdated-:NONE:BOB:JIMUPDATED|", '{^{checkboxgroup selectedPeople}}...{^{for ...}}... - updated label and value'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== tmpl = jsv.templates({markup: '{^{checkboxgroup selectedPeople convert=~lower convertBack="upper" linkTo=selectedOut}}' + '' + '{^{for people}}' + '' + '{{/for}}' +'{{/checkboxgroup}}', converters: { upper: function(val) { return val.map(function(x) {return x.toUpperCase();}) } } }); model = { selectedPeople: ["Jim"], people: [ {name: "bob"}, {name: "jim"} ] }; // ............................... Act ................................. tmpl.link("#result", model, { lower: function(val) { return val.map(function(x) {return x.toLowerCase();}) } }); res = $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).insert({ name: newName }); jsv.observable(model).setProperty("selectedPeople", [newName]); res += $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).remove(2); res += $("#result input:checked").parent().text() + "|"; $("#result input").first().prop("checked", true).change(); // Check first checkbox res += model.selectedPeople + "-" + model.selectedOut + "-" + $("#result input:checked").parent().text() + "|"; // ............................... Assert ................................. assert.equal(res, ":jim|:new||new-NONE-:none|", '{^{checkboxgroup selectedPeople convert=... convertBack=... linkTo=...}}'); // ............................... Act ................................. jsv.observable(model.people[1]).setProperty("name", "jimUpdated"); res = $("#result").text() + "|"; $("#result input").eq(2).prop("checked", true).change(); // Check third checkbox res += model.selectedPeople + "-" + model.selectedOut + "-" + $("#result input:checked").parent().text() + "|"; assert.equal(res, ":none:bob:jimUpdated|new-NONE,JIMUPDATED-:none:jimUpdated|", '{^{checkboxgroup selectedPeople convert=... convertBack=... linkTo=...}} - updated label and value'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== tmpl = jsv.templates( '{^{checkboxgroup selectedPeople}}' + ':' + '{^{for people}}' + ':' + '{{/for}}' +'{{/checkboxgroup}}' ); model = { selectedPeople: ["Jim"], people: [ {name: "Bob"}, {name: "Jim"} ] }; // ............................... Act ................................. tmpl.link("#result", model); res = $("#" + $("#result input:checked").prop("id") + "Lbl").text() + "|"; jsv.observable(model.people).insert({ name: newName }); jsv.observable(model).setProperty("selectedPeople", [newName]); res += $("#" + $("#result input:checked").prop("id") + "Lbl").text() + "|"; jsv.observable(model.people).remove(2); res += $("#" + $("#result input:checked").prop("id") + "Lbl").text() + "|"; $("#result input").first().prop("checked", true).change(); // Check first checkbox res += model.selectedPeople + "-" + $("#" + $("#result input:checked").prop("id") + "Lbl").text() + "|"; // ............................... Assert ................................. assert.equal(res, "JIM|NEW||new,None-NONE|", '{^{checkboxgroup selectedPeople}} with labels by for/id'); // ............................... Act ................................. jsv.observable(model.people[1]).setProperty("name", "jimUpdated"); res = $("#result").text() + "|"; $("#result input").eq(2).prop("checked", true).change(); // Check third checkbox res += model.selectedPeople + "-" + $("#" + $("#result input:checked").prop("id") + "Lbl").text() + "|"; assert.equal(res, ":NONE:BOB:JIMUPDATED|new,None,jimUpdated-NONE|", '{^{checkboxgroup selectedPeople}} with labels by for/id - updated label and value'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== tmpl = jsv.templates( '{^{checkboxgroup selectedPeople}}' + '' + '{^{for people}}' + '' + '{{/for}}' +'{{/checkboxgroup}}' + '{^{checkboxgroup selectedPeople}}' + '' + '{^{for people}}' + '' + '{{/for}}' +'{{/checkboxgroup}}' ); model = { selectedPeople: ["Jim"], people: [ {name: "Bob"}, {name: "Jim"} ] }; // ............................... Act ................................. tmpl.link("#result", model); res = $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).insert({ name: newName }); jsv.observable(model).setProperty("selectedPeople", [newName]); res += $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).remove(2); res += $("#result input:checked").parent().text() + "|"; $("#result input").first().prop("checked", true).change(); // Check first checkbox res += model.selectedPeople + "-" + $("#result input:checked").parent().text() + "|"; // ............................... Assert ................................. assert.equal(res, ":JIM:JIM|:NEW:NEW||new,None-:NONE:NONE|", '{^{checkboxgroup selectedPeople}} - two checkboxgroups with same selectedPeople bindings'); // ............................... Act ................................. jsv.observable(model.people[1]).setProperty("name", "jimUpdated"); res = $("#result").text() + "|"; $("#result input").eq(2).prop("checked", true).change(); // Check third checkbox res += model.selectedPeople + "-" + $("#result input:checked").parent().text() + "|"; assert.equal(res, ":NONE:BOB:JIMUPDATED:NONE:BOB:JIMUPDATED|new,None,jimUpdated-:NONE:JIMUPDATED:NONE:JIMUPDATED|", '{^{checkboxgroup selectedPeople}} - two checkboxgroups with same selected bindings - updated label and value'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== tmpl = jsv.templates( '{^{checkboxgroup selectedPeople}}' + '{^{for people}}' + '' + '{{/for}}' +'{{/checkboxgroup}}' + '{^{checkboxgroup selectedPeople}}' + '' + '{^{for people}}' + '' + '{{/for}}' +'{{/checkboxgroup}}' ); model = { selectedPeople: ["Jim"], people: [] }; // ............................... Act ................................. tmpl.link("#result", model); res = $("#result").text() + "|" + $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).insert([{name: "Bob"},{name: "Jim"},{name: newName}]); res += $("#result").text() + "|" + $("#result input:checked").parent().text() + "|"; jsv.observable(model).setProperty("selectedPeople", [newName]); res += $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).remove(2); res += $("#result input:checked").parent().text() + "|"; $("#result input").first().prop("checked", true).change(); // Check first checkbox res += model.selectedPeople + "-" + $("#result input:checked").parent().text() + "|"; // ............................... Assert ................................. assert.equal(res, ":NONE||:BOB:JIM:NEW:NONE:BOB:JIM:NEW|:JIM:JIM|:NEW:NEW||new,Bob-:BOB:BOB|", '{^{checkboxgroup selectedPeople}} - two checkboxgroups with same selected bindings - starting out with no items, so no checkboxes'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== tmpl = jsv.templates( '{^{checkboxgroup selectedPeople name="rad1"}}' + '{^{for people}}' + '' + '{{/for}}' +'{{/checkboxgroup}}' + '{^{checkboxgroup selectedPeople name="rad1"}}' + '' + '{^{for people}}' + '' + '{{/for}}' +'{{/checkboxgroup}}' ); model = { selectedPeople: ["Jim"], people: [] }; // ............................... Act ................................. tmpl.link("#result", model); res = $("#result").text() + "|" + $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).insert([{name: "Bob"},{name: "Jim"},{name: newName}]); res += $("#result").text() + "|" + $("#result input:checked").parent().text() + "|"; jsv.observable(model).setProperty("selectedPeople", [newName]); res += $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).remove(2); res += $("#result input:checked").parent().text() + "|"; $("#result input").first().prop("checked", true).change(); // Check first checkbox res += model.selectedPeople + "-" + $("#result input:checked").parent().text() + "|" + $("#result input:checked")[0].name + "|" + $("#result input:checked")[1].name; // ............................... Assert ................................. assert.equal(res, ":NONE||:BOB:JIM:NEW:NONE:BOB:JIM:NEW|:JIM:JIM|:NEW:NEW||new,Bob-:BOB:BOB|rad1|rad2", '{^{checkboxgroup selectedPeople}} - name for group can be specified rather than auto-generated - on item or on checkboxgroup tag'); // =============================== Arrange =============================== tmpl = jsv.templates( '{^{if rg1}}' + '{^{checkboxgroup selectedPeople name="rad1"}}' + '{^{for people}}' + '' + '{{/for}}' +'{{/checkboxgroup}}' + '{{/if}}' + '{^{if rg2}}' + '{^{checkboxgroup selectedPeople name="rad1"}}' + '' + '{^{for people}}' + '' + '{{/for}}' +'{{/checkboxgroup}}' + '{{/if}}' ); model = { rg1: true, rg2: true, selectedPeople: ["Jim"], people: [] }; newName = "new"; // ............................... Act ................................. tmpl.link("#result", model); res = ">" + $("#result").text() + "|" + $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).insert([{name: "Bob"},{name: "Jim"},{name: newName}]); res += $("#result").text() + "|" + $("#result input:checked").parent().text() + "|"; jsv.observable(model).setProperty("selectedPeople", [newName]); res += $("#result input:checked").parent().text() + "|"; // ............................... Assert ................................. assert.equal(res, ">:NONE||:BOB:JIM:NEW:NONE:BOB:JIM:NEW|:JIM:JIM|:NEW:NEW|", '{^{checkboxgroup selectedPeople}} - two checkboxgroups wrapped in {{if}} blocks - starting out with no items'); jsv.observable(model.people).refresh([]); jsv.observable(model).setProperty("rg1", false); res = ">" + $("#result").text() + "|" + $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).insert([{name: "Bob"},{name: "Jim"},{name: newName}]); res += $("#result").text() + "|" + $("#result input:checked").parent().text() + "|"; jsv.observable(model).setProperty("selectedPeople", [newName]); res += $("#result input:checked").parent().text() + "|"; // ............................... Assert ................................. assert.equal(res, ">:NONE||:NONE:BOB:JIM:NEW|:NEW|:NEW|", '{^{checkboxgroup selectedPeople}} - two checkboxgroups wrapped in {{if}} blocks - one set to false'); jsv.observable(model.people).refresh([]); jsv.observable(model).setProperty("selectedPeople", ["Jim"]); jsv.observable(model).setProperty("rg1", true); res = ">" + $("#result").text() + "|" + $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).insert([{name: "Bob"},{name: "Jim"},{name: newName}]); res += $("#result").text() + "|" + $("#result input:checked").parent().text() + "|"; jsv.observable(model).setProperty("selectedPeople", [newName]); res += $("#result input:checked").parent().text() + "|"; // ............................... Assert ................................. assert.equal(res, ">:NONE||:BOB:JIM:NEW:NONE:BOB:JIM:NEW|:JIM:JIM|:NEW:NEW|", '{^{checkboxgroup selectedPeople}} - two checkboxgroups wrapped in {{if}} blocks - one set back to true'); // ................................ Reset ................................ $("#result").empty(); assert.equal(JSON.stringify(_jsv.cbBindings), "{}", "All bindings removed when content removed from DOM"); jsv.views.settings.advanced({_jsv: false}); }); QUnit.test('radio buttons without {{radiogroup}}', function(assert) { // =============================== Arrange =============================== var tmpl = jsv.templates( '{^{for people}}' + '' + '{{/for}}' ); var model = { selected: "Jim", people: [ {name: "Bob"}, {name: "Jim"} ] }, newName = "new"; // ............................... Act ................................. tmpl.link("#result", model); res = $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).insert({ name: newName }); jsv.observable(model).setProperty("selected", newName); res += $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).remove(2); res += $("#result input:checked").parent().text() + "|"; $("#result input").first().prop("checked", true).change(); // Check first radio button res += model.selected + "-" + $("#result input:checked").parent().text() + "|"; // ............................... Assert ................................. assert.equal(res, ":Jim|:new||Bob-:Bob|", '{^{for ...}}...'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== tmpl = jsv.templates( '' + '{^{for people}}' + '' + '{{/for}}' ); model = { selected: "Jim", people: [ {name: "Bob"}, {name: "Jim"} ] }; // ............................... Act ................................. tmpl.link("#result", model); res = $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).insert({ name: newName }); jsv.observable(model).setProperty("selected", newName); res += $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).remove(2); res += $("#result input:checked").parent().text() + "|"; $("#result input").first().prop("checked", true).change(); // Check first radio button res += model.selected + "-" + $("#result input:checked").parent().text() + "|"; // ............................... Assert ................................. assert.equal(res, ":JIM|:NEW||None-:NONE|", '...{^{for ...}}...'); // ............................... Act ................................. jsv.observable(model.people[1]).setProperty("name", "jimUpdated"); res = $("#result").text() + "|"; $("#result input").eq(2).prop("checked", true).change(); // Check third radio button res += model.selected + "-" + $("#result input:checked").parent().text() + "|"; assert.equal(res, ":NONE:BOB:JIMUPDATED|jimUpdated-:JIMUPDATED|", '{^{for ...}}... - updated label and value'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== tmpl = jsv.templates({markup: '' + '{^{for people}}' + '' + '{{/for}}', converters: { upper: function(val) { return val.toUpperCase(); } } }); model = { selected: "JIM", people: [ {name: "bob"}, {name: "jim"} ] }; // ............................... Act ................................. tmpl.link("#result", model, { lower: function(val) { return val.toLowerCase(); } }); res = $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).insert({ name: newName }); jsv.observable(model).setProperty("selected", newName); res += $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).remove(2); res += $("#result input:checked").parent().text() + "|"; $("#result input").first().prop("checked", true).change(); // Check first radio button res += model.selected + "-" + model.selectedOut + "-" + $("#result input:checked").parent().text() + "|"; // ............................... Assert ................................. assert.equal(res, ":jim|:new||new-NONE-:none|", 'data-link="{:select convert=... convertBack=... linkTo=...:}"'); // ............................... Act ................................. jsv.observable(model.people[1]).setProperty("name", "jimUpdated"); res = $("#result").text() + "|"; $("#result input").eq(2).prop("checked", true).change(); // Check third radio button res += model.selected + "-" + model.selectedOut + "-" + $("#result input:checked").parent().text() + "|"; assert.equal(res, ":none:bob:jimUpdated|new-JIMUPDATED-:jimUpdated|", 'data-link="{:select convert=... convertBack=... linkTo=...:}" - updated label and value'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== tmpl = jsv.templates( ':' + '{^{for people}}' + ':' + '{{/for}}' ); model = { selected: "Jim", people: [ {name: "Bob"}, {name: "Jim"} ] }; // ............................... Act ................................. tmpl.link("#result", model); res = $("#" + $("#result input:checked").prop("id") + "Lbl").text() + "|"; jsv.observable(model.people).insert({ name: newName }); jsv.observable(model).setProperty("selected", newName); res += $("#" + $("#result input:checked").prop("id") + "Lbl").text() + "|"; jsv.observable(model.people).remove(2); res += $("#" + $("#result input:checked").prop("id") + "Lbl").text() + "|"; $("#result input").first().prop("checked", true).change(); // Check first radio button res += model.selected + "-" + $("#" + $("#result input:checked").prop("id") + "Lbl").text() + "|"; // ............................... Assert ................................. assert.equal(res, "JIM|NEW||None-NONE|", 'data-link="selected" with labels by for/id'); // ............................... Act ................................. jsv.observable(model.people[1]).setProperty("name", "jimUpdated"); res = $("#result").text() + "|"; $("#result input").eq(2).prop("checked", true).change(); // Check third radio button res += model.selected + "-" + $("#" + $("#result input:checked").prop("id") + "Lbl").text() + "|"; assert.equal(res, ":NONE:BOB:JIMUPDATED|jimUpdated-JIMUPDATED|", 'data-link="selected" with labels by for/id - updated label and value'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== tmpl = jsv.templates( '' + '{^{for people}}' + '' + '{{/for}}' + '' + '{^{for people}}' + '' + '{{/for}}' ); model = { selected: "Jim", people: [ {name: "Bob"}, {name: "Jim"} ] }; // ............................... Act ................................. tmpl.link("#result", model); res = $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).insert({ name: newName }); jsv.observable(model).setProperty("selected", newName); res += $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).remove(2); res += $("#result input:checked").parent().text() + "|"; $("#result input").first().prop("checked", true).change(); // Check first radio button res += model.selected + "-" + $("#result input:checked").parent().text() + "|"; // ............................... Assert ................................. assert.equal(res, ":JIM:JIM|:NEW:NEW||None-:NONE:NONE|", 'data-link="selected" - two radiogroups with same selected bindings'); // ............................... Act ................................. jsv.observable(model.people[1]).setProperty("name", "jimUpdated"); res = $("#result").text() + "|"; $("#result input").eq(2).prop("checked", true).change(); // Check third radio button res += model.selected + "-" + $("#result input:checked").parent().text() + "|"; assert.equal(res, ":NONE:BOB:JIMUPDATED:NONE:BOB:JIMUPDATED|jimUpdated-:JIMUPDATED:JIMUPDATED|", 'data-link="selected" - two radiogroups with same selected bindings - updated label and value'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== tmpl = jsv.templates( '{^{for people}}' + '' + '{{/for}}' + '' + '{^{for people}}' + '' + '{{/for}}' ); model = { selected: "Jim", people: [] }; // ............................... Act ................................. tmpl.link("#result", model); res = $("#result").text() + "|" + $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).insert([{name: "Bob"},{name: "Jim"},{name: "newName"}]); res += $("#result").text() + "|" + $("#result input:checked").parent().text() + "|"; jsv.observable(model).setProperty("selected", newName); res += $("#result input:checked").parent().text() + "|"; jsv.observable(model.people).remove(2); res += $("#result input:checked").parent().text() + "|"; $("#result input").first().prop("checked", true).change(); // Check first radio button res += model.selected + "-" + $("#result input:checked").parent().text() + "|"; // ............................... Assert ................................. assert.equal(res, ":NONE||:BOB:JIM:NEWNAME:NONE:BOB:JIM:NEWNAME|:JIM:JIM|||Bob-:BOB:BOB|", 'data-link="selected" - two radiogroups with same selected bindings - starting out with no items, so no radio buttons'); // ................................ Reset ................................ $("#result").empty(); }); QUnit.test('{^{on}}', function(assert) { jsv.views.settings.advanced({_jsv: true}); // For using _jsv // =============================== Arrange =============================== function swap(ev, eventArgs) { jsv.observable(this).setProperty("type", this.type === "shape" ? "line" : "shape"); } var thing = { type: "shape", swap: swap }; jsv.templates('{^{on swap/}} {^{:type}}') .link("#result", thing); // ................................ Act .................................. before = $("#result").text(); $("#result button").click(); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, "swap shape|swap line", '{^{on swap/}} renders as button with label "swap", and calls swap method on click, with "this" pointer context on data object'); // ................................ Reset ................................ $("#result").empty(); thing.type = "shape"; // =============================== Arrange =============================== jsv.templates('{^{on missingMethod/}} {^{:type}}') .link("#result", thing); // ................................ Act .................................. before = $("#result").text(); $("#result button").click(); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, "noop shape|noop shape", '{^{on missingMethod/}} renders as button with label "noop", and is noop on click'); // ................................ Reset ................................ $("#result").empty(); thing.type = "shape"; // =============================== Arrange =============================== var tmpla = jsv.templates('{^{on swap}} clickme {{/on}} {^{:type}}'); tmpla.link("#result", thing); // ................................ Act .................................. before = $("#result").text(); $("#result button").click(); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, "clickme shape|clickme line", '{^{on swap}} clickme {{/on}} renders as button with label "clickme", and calls swap method on click'); // ................................ Reset ................................ $("#result").empty(); thing.type = "shape"; // =============================== Arrange =============================== jsv.templates('{^{on swap tmpl=~label}} clickme {{/on}} {^{:type}}') .link("#result", thing, {label: "clickagain"}); // ................................ Act .................................. before = $("#result").text(); $("#result button").click(); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, "clickagain shape|clickagain line", '{^{on swap tmpl=stringValue renders as button with label stringValue, and calls swap method on click'); // ................................ Reset ................................ $("#result").empty(); thing.type = "shape"; // =============================== Arrange =============================== jsv.templates('{^{on swap}}clickme{{/on}} {^{:type}}') .link("#result", thing); // ................................ Act .................................. before = $("#result").text(); $("#result span").click(); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, "clickme shape|clickme line", '{^{on swap}}clickme{{/on}} renders as span with label clickme, and calls swap method on click'); // ................................ Reset ................................ $("#result").empty(); thing.type = "shape"; // =============================== Arrange =============================== jsv.templates('{^{on ~swap/}} {^{:type}}') .link("#result", thing, {swap: swap}); // ................................ Act .................................. before = $("#result").text(); $("#result button").click(); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, "~swap shape|~swap line", '{^{on ~swap/}} calls swap helper method on click, with "this" pointer context defaulting to current data object'); // ................................ Reset ................................ $("#result").empty(); thing.type = "shape"; // =============================== Arrange =============================== jsv.templates('{^{on ~util.swap/}} {^{:type}} {^{:check}}') .link("#result", thing, { util: { swap: function(ev, eventArgs) { jsv.observable(this.data).setProperty({ type: this.data.type === "shape" ? "line" : "shape", check: this.data === eventArgs.view.data }); }, data: thing } }); // ................................ Act .................................. before = $("#result").text(); $("#result button").click(); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, "~util.swap shape |~util.swap line true", '{^{on ~util.swap/}} calls util.swap helper method on click, with ~util as this pointer'); // ................................ Reset ................................ $("#result").empty(); thing.type = "shape"; delete thing.check; // =============================== Arrange =============================== jsv.templates('{^{on ~util.swap context=#data/}} {^{:type}}') .link("#result", thing, { util: { swap: swap } }); // ................................ Act .................................. before = $("#result").text(); $("#result button").click(); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, "~util.swap shape|~util.swap line", '{^{on ~util.swap context=#data/}} calls util.swap helper method on click, with current data object as this pointer'); // ................................ Reset ................................ $("#result").empty(); thing.type = "shape"; // =============================== Arrange =============================== jsv.templates('{^{on ~util.swap context=~util.swapCtx/}} {^{:type}} {^{:check}}') .link("#result", thing, { util: { swap: function(ev, eventArgs) { jsv.observable(this.data).setProperty({ type: this.data.type === "shape" ? "line" : "shape", check: this.data === eventArgs.view.data }); }, data: thing, swapCtx: { data: thing } } }); // ................................ Act .................................. before = $("#result").text(); $("#result button").click(); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, "~util.swap shape |~util.swap line true", '{^{on ~util.swap context=~util.swapCtx/}} calls util.swap helper method on click, with util.swapCtx as this pointer'); // ................................ Reset ................................ $("#result").empty(); thing.type = "shape"; delete thing.check; // =============================== Arrange =============================== jsv.templates('{^{on ~util.swap data=#data/}} {^{:type}} {^{:check}}') .link("#result", thing, { util: { swap: function(ev, eventArgs) { jsv.observable(ev.data).setProperty({ type: ev.data.type === "shape" ? "line" : "shape", check: ev.data === eventArgs.view.data }); }, data: thing, swapCtx: { data: thing } } }); // ................................ Act .................................. before = $("#result").text(); $("#result button").click(); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, "~util.swap shape |~util.swap line true", '{^{on ~util.swap data=#data/}} calls util.swap helper method on click, and passes current data #data as ev.data'); // ................................ Reset ................................ $("#result").empty(); thing.type = "shape"; delete thing.check; // =============================== Arrange =============================== jsv.templates('{^{on \'mouseup mousedown blur\' swap/}} {^{:type}}') .link("#result", thing); // ................................ Act .................................. before = $("#result").text(); $("#result button").mouseup(); after = $("#result").text(); $("#result button").mousedown(); after += $("#result").text(); $("#result button").blur(); after += $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, "swap shape|swap lineswap shapeswap line", "{^{on 'mouseup mousedown blur' swap/}} calls util method on mouseup, mousedown and blur"); // ................................ Reset ................................ $("#result").empty(); thing.type = "shape"; delete thing.check; // =============================== Arrange =============================== var res = "1: "; jsv.templates( "
                  {^{on 'keyup keydown' '.inputA' unbind}}{^{on 'keyup' '#inputB' unbind}}{^{on 'mouseup' 'input' test}}{^{on 'mousedown' '#inputB' refresh}}\">" + "" + "" + "{{/on}}{{/on}}{{/on}}{{/on}}
                  ") .link("#result", { unbind: function(ev, eventArgs) { res += "unbind "; eventArgs.linkCtx.tag.onUnbind(); }, refresh: function(ev, eventArgs) { res += "refresh "; eventArgs.linkCtx.tag.refresh(); }, test: function() { res += "test "; } }); // ................................ Act .................................. var events = $._data($("#divForOn")[0]).events, eventBindings = "before: " + events.keydown.length + events.keyup.length + events.mouseup.length + events.mousedown.length; $("#divForOn #inputB").mouseup(); res += "2: "; $("#divForOn .inputA").mouseup(); res += "3: "; $("#divForOn #inputB").keyup(); res += "4: "; $("#divForOn #inputB").keyup(); res += "5: "; $("#divForOn .inputA").keydown(); res += "6: "; $("#divForOn .inputA").keyup(); res += "7: "; $("#divForOn #inputB").mouseup(); res += "8: "; $("#divForOn #inputB").mousedown(); eventBindings += " | after: " + events.keydown + events.keyup + events.mouseup.length + events.mousedown.length; // ............................... Assert ................................. assert.equal(res, "1: test 2: test 3: unbind 4: 5: unbind 6: 7: test 8: refresh ", "multiple {^{on events selector method}} bindings on container attach events on delegated elements. Also, tag.onDispose on tag instances removes specific handlers for corresponding elements/selectors"); // ............................... Assert ................................. assert.equal(eventBindings, "before: 1211 | after: undefinedundefined11", "onDispose removes specific delegated events"); // ................................ Act .................................. res = "1: "; $("#divForOn #inputB").after(""); $("#divForOn #newlyAdded").mouseup(); res += "2: "; $("#divForOn #newlyAdded").keyup(); // ............................... Assert ................................. assert.equal(res, "1: test 2: ", "delegated {^{on events selector method}} binding allows additional elements added to content to bind correctly"); // ................................ Act .................................. $("#result").empty(); eventBindings = "" + events.keydown + events.keyup + events.mouseup + JSON.stringify([_jsv.cbBindings, _jsv.bindings]); // ............................... Assert ................................. assert.equal(eventBindings, "undefinedundefinedundefined[{},{}]", "Removing the element removes all associated attached {on } handlers"); // =============================== Arrange =============================== var tmpl = jsv.templates("{^{on 'click' '#doIt' 754 thisIsTheMethod role 'hey' true process data=option 33 #data context=option}}\ \ \ {{/on}}"), data = { name: "Jo", role: "Advisor", option: { allow: true }, thisIsTheMethod: function(role, text, isFoo, compile, amount, root, ev, eventArgs) { if (compile) { compile.call(root, role, text, isFoo, amount, ev.data.allow, eventArgs.linkCtx.tag.tagCtx.args[2]); } }, process: function(role, text, isFoo, amount, allow, extraParam) { jsv.observable(this).setProperty("res", this.res + role + text + isFoo + amount + " allow:" + allow + " extraParam: " + extraParam + "|"); }, res: "" }; tmpl.link("#result", data); // ................................ Act .................................. $("#doIt").click(); data.option.allow = false; jsv.observable(data).setProperty("role", "Follower"); $("#doIt").click(); // ............................... Assert ................................. assert.equal(data.res, "Advisorheytrue33 allow:true extraParam: 754|Followerheytrue33 allow:false extraParam: 754|", "{^{on 'click' selector otherParams... method params...}} : supports passing params to method, of any type, as well as setting data and context for the function call"); // ................................ Act .................................. $("#result").empty(); // =============================== Arrange =============================== var tmpl = jsv.templates("{^{on doit id='doIt' class='red' width='100' height='100'/}}"), res = "", data = { name: "Jo", doit: function() { var button = $("button"); res = (Math.round(button.width())) + "|" + (Math.round(button.height())) + "|" + button[0].id + "|" + button[0].className; } }; tmpl.link("#result", data); // ................................ Act .................................. $("#doIt").click(); // ............................... Assert ................................. assert.equal(res, "100|100|doIt|red", "{^{on ... id=... class=... width=... height=...}} : supports setting id, class, height and width"); // ................................ Act .................................. $("#result").empty(); jsv.views.settings.advanced({_jsv: false}); }); QUnit.test('data-link="{tag...} and {^{tag}} in same template"', function(assert) { var done; if (assert.async) { done = assert.async() } else { stop() } // ................................ Reset ................................ person1._firstName = "Jo"; // reset Prop person1.lastName = "One"; // reset Prop settings.title = "Mr"; // reset Prop settings.width = 30; // reset Prop jsv.views.settings.advanced({_jsv: true}); // For using viewsAndBindings($) // =============================== Arrange =============================== jsv.templates('{^{tmplTag/}}-{^{:lastName}} -') .link("#result", person1); // ................................ Act .................................. before = $("#result").text() + $("#result input").val(); jsv.observable(person1).setProperty({firstName: "newFirst", lastName: "newLast"}); jsv.observable(settings).setProperty({title: "Sir", width: 40}); after = $("#result").text() + $("#result input").val(); // ............................... Assert ................................. assert.equal(before + "|" + after, 'Name: Mr Jo. Width: 30-One Name: Mr Jo. Width: 30-OneOne|Name: Sir newFirst. Width: 40-newLast Name: Sir newFirst. Width: 40-newLastnewLast', 'Data link using: {^{tmplTag/}} {^{:lastName}} '); // ................................ Reset ................................ $("#result").empty(); person1._firstName = "Jo"; // reset Prop person1.lastName = "One"; // reset Prop settings.title = "Mr"; // reset Prop settings.width = 30; // reset Prop // =============================== Arrange =============================== jsv.templates('prop:{^{:lastName}} computed:{^{:fullName()}} Tag:{^{tmplTag/}}') .link("#result", person1); // ................................ Act .................................. before = $("#result").text() + $("#last").val() + $("#full").val(); jsv.observable(person1).setProperty({firstName: "newFirst", lastName: "newLast"}); jsv.observable(settings).setProperty({title: "Sir", width: 40}); jsv.observable(person1).setProperty({fullName: "compFirst compLast"}); after = $("#result").text() + $("#last").val() + $("#full").val(); // ............................... Assert ................................. assert.equal(before + "|" + after, 'prop:OneOne computed:Mr Jo OneMr Jo One Tag:Name: Mr Jo. Width: 30Name: Mr Jo. Width: 30OneMr Jo One|prop:compLastcompLast computed:Sir compFirst compLastSir compFirst compLast Tag:Name: Sir compFirst. Width: 40Name: Sir compFirst. Width: 40compLastSir compFirst compLast', 'Data link using: {^{:lastName}} {^{:fullName()}} {^{tmplTag/}} '); // ................................ Act .................................. keydown($("#full").val("newFirst newLast")); setTimeout(function() { after = $("#result").text() + $("#last").val() + $("#full").val(); // ............................... Assert ................................. assert.equal(after, "prop:newLastnewLast computed:Sir newFirst newLastSir newFirst newLast Tag:Name: Sir newFirst. Width: 40Name: Sir newFirst. Width: 40newLastnewFirst newLast", 'Two-way binding to a computed observable correctly calls the setter'); // =============================== Arrange =============================== // ................................ Act .................................. $("#result").empty(); // ............................... Assert ................................. assert.ok(!viewsAndBindings($) && !$._data(person1).events && !$._data(settings).events, "$(container).empty removes the views and current listeners from that content"); // ................................ Reset ................................ person1._firstName = "Jo"; // reset Prop person1.lastName = "One"; // reset Prop settings.title = "Mr"; // reset Prop settings.width = 30; // reset Prop // =============================== Arrange =============================== jsv.templates('prop:{^{:lastName}} computed:{^{:fullName()}} Tag:{^{tmplTag/}}') .link("#result", person1); // ................................ Act .................................. before = $("#result").text() + $("#last").val() + $("#full").val(); jsv.observable(person1).setProperty({firstName: "newFirst", lastName: "newLast"}); jsv.observable(settings).setProperty({title: "Sir", width: 40}); jsv.observable(person1).setProperty({fullName: "compFirst compLast"}); after = $("#result").text() + $("#last").val() + $("#full").val(); // ............................... Assert ................................. res = 'prop:OneOne computed:Mr Jo OneMr Jo One Tag:Name: Mr Jo. Width: 30Name: Mr Jo. Width: 30OneMr Jo One|prop:compLastcompLast computed:Sir compFirst compLastSir compFirst compLast Tag:Name: Sir compFirst. Width: 40Name: Sir compFirst. Width: 40compLastSir compFirst compLast'; // ................................ Act .................................. jsv.unlink("#result"); // ............................... Assert ................................. assert.ok(before + "|" + after === res && !viewsAndBindings($) && !$._data(person1).events && !$._data(settings).events, "jsv.unlink(container) removes the views and current listeners from that content"); // ................................ Reset ................................ person1._firstName = "Jo"; // reset Prop person1.lastName = "One"; // reset Prop settings.title = "Mr"; // reset Prop settings.width = 30; // reset Prop // =============================== Arrange =============================== jsv.templates('prop:{^{:lastName}} computed:{^{:fullName()}} Tag:{^{tmplTag/}}') .link("#result", person1); // ................................ Act .................................. $("#result").unlink(); // ............................... Assert ................................. assert.ok(!viewsAndBindings($) && !$._data(person1).events && !$._data(settings).events, "$(container).unlink() removes the views and current listeners from that content"); // =============================== Arrange =============================== jsv.templates('prop:{^{:lastName}} computed:{^{:fullName()}} Tag:{^{tmplTag/}}') .link("#result", person1); // ................................ Act .................................. before = $("#result").text() + $("#last").val() + $("#full").val(); jsv.observable(person1).setProperty({firstName: "newFirst", lastName: "newLast"}); jsv.observable(settings).setProperty({title: "Sir", width: 40}); jsv.observable(person1).setProperty({fullName: "compFirst compLast"}); after = $("#result").text() + $("#last").val() + $("#full").val(); // ............................... Assert ................................. res = 'prop:OneOne computed:Mr Jo OneMr Jo One Tag:Name: Mr Jo. Width: 30Name: Mr Jo. Width: 30OneMr Jo One|prop:compLastcompLast computed:Sir compFirst compLastSir compFirst compLast Tag:Name: Sir compFirst. Width: 40Name: Sir compFirst. Width: 40compLastSir compFirst compLast'; // ................................ Act .................................. viewContent = viewsAndBindings($); jsv.unobserve(person1, "*", settings, "*"); // ............................... Assert ................................. assert.ok(before + "|" + after === res && viewContent === viewsAndBindings($) && !$._data(person1).events && !$._data(settings).events, 'jsv.unobserve(person1, "*", settings, "*") removes the current listeners from that content, but leaves the views'); // ................................ Reset ................................ person1._firstName = "Jo"; // reset Prop person1.lastName = "One"; // reset Prop settings.title = "Mr"; // reset Prop settings.width = 30; // reset Prop // =============================== Arrange =============================== jsv.templates('prop:{^{:lastName}} computed:{^{:fullName()}} Tag:{^{tmplTag/}}') .link("#result", person1); // ................................ Act .................................. before = $("#result").text() + $("#last").val() + $("#full").val(); jsv.observable(person1).setProperty({firstName: "newFirst", lastName: "newLast"}); jsv.observable(settings).setProperty({title: "Sir", width: 40}); jsv.observable(person1).setProperty({fullName: "compFirst compLast"}); after = $("#result").text() + $("#last").val() + $("#full").val(); // ............................... Assert ................................. res = 'prop:OneOne computed:Mr Jo OneMr Jo One Tag:Name: Mr Jo. Width: 30Name: Mr Jo. Width: 30OneMr Jo One|prop:compLastcompLast computed:Sir compFirst compLastSir compFirst compLast Tag:Name: Sir compFirst. Width: 40Name: Sir compFirst. Width: 40compLastSir compFirst compLast'; // ................................ Act .................................. jsv.unlink(); // ............................... Assert ................................. assert.ok(before + "|" + after === res && !viewsAndBindings($) && !$._data(person1).events && !$._data(settings).events, 'jsv.unlink() removes all views and listeners from the page'); // ................................ Reset ................................ person1._firstName = "Jo"; // reset Prop person1.lastName = "One"; // reset Prop settings.title = "Mr"; // reset Prop settings.width = 30; // reset Prop jsv.views.settings.advanced({_jsv: false}); // TODO ADDITIONAL TESTS: // 1: link(null, data) to link whole document if (assert.async) { done() } else { start() } }, 0); }); QUnit.test("Fallbacks for missing or undefined paths: using {^{:some.path onError = 'fallback'}}, etc.", function(assert) { jsv.views.settings.advanced({_jsv: true}); // For using viewsAndBindings($) // =============================== Arrange =============================== jsv.views.tags({ mytag1: function(val) {return val + " from my tag1"; }, mytag2: { template: "{{:}} from my tag2" } }).converters({ upper: function(val) { return val.toUpperCase(); } }); var initial = {a: {b: null}}, updated = {c: {val: 'leaf'}}; jsv.templates( "{^{:a.b^c.val onError=~error + 'A '}} " + "{^{upper:a.b^c.val onError=~error + 'B '}} " + "{^{>a.b^c.val onError=~error + 'C '}} " + "{^{if a.b^c onError=~error + 'D '}}{{:a.b.c.val}}{{/if}} " + "{^{for a.b^c onError=~error + 'E ' ~foo='foo'}}{{:val + ~foo}}{{/for}} " + "{^{mytag1 a.b^c.val onError=~error + 'F '/}} " + "{^{mytag2 a.b^c.val onError=~error + 'G '/}} " + " " + " " + " " + " ") .link("#result", initial, {error: "err:"}); // ................................ Act .................................. before = "" + $._data(initial.a).events.propertyChange.length + " " + !$._data(updated).events + " " + !$._data(updated.c).events + "|" + $("#result").text() + "|"; jsv.observable(initial.a).setProperty('b', updated); after = $("#result").text() + "|"; jsv.observable(initial.a.b.c).setProperty('val', "leaf2"); after += $("#result").text() + "|"; jsv.observable(initial.a.b).setProperty('c', {val: "leaf3"}); after += $("#result").text() + "|"; jsv.observable(initial.a).setProperty('b', {c: {val: "leaf4"}}); after += $("#result").text() + "|"; after += "" + $._data(initial.a).events.propertyChange.length + " " + $._data(initial.a.b).events.propertyChange.length + " " + $._data(initial.a.b.c).events.propertyChange.length + " " + !$._data(updated).events + " " + !$._data(updated.c).events + "|" + $("#result").text() + "|"; var prevB = initial.a.b; jsv.observable(initial.a).setProperty('b', null); after += "" + $._data(initial.a).events.propertyChange.length + " " + !$._data(prevB).events + " " + !$._data(prevB.c).events + "|" + $("#result").text() + "|"; jsv.observable(initial.a).setProperty('b', updated); after += $("#result").text() + "|"; assert.equal(before + after, "11 true true|" + "err:A ERR:B err:C err:D err:E err:F err:G err:H err:I ERR:J err:K |" + "leaf LEAF leaf leaf leaffoo leaf from my tag1 leaf from my tag2 leaf leaf from my tag1 LEAF LEAF |" + "leaf2 LEAF2 leaf2 leaf leaffoo leaf2 from my tag1 leaf2 from my tag2 leaf2 leaf2 from my tag1 LEAF2 LEAF2 |" + "leaf3 LEAF3 leaf3 leaf leaf3foo leaf3 from my tag1 leaf3 from my tag2 leaf3 leaf3 from my tag1 LEAF3 LEAF3 |" + "leaf4 LEAF4 leaf4 leaf leaf4foo leaf4 from my tag1 leaf4 from my tag2 leaf4 leaf4 from my tag1 LEAF4 LEAF4 |" + "11 11 9 true true|" + "leaf4 LEAF4 leaf4 leaf leaf4foo leaf4 from my tag1 leaf4 from my tag2 leaf4 leaf4 from my tag1 LEAF4 LEAF4 |" + "11 true true|" + "err:A ERR:B err:C err:D err:E err:F err:G err:H err:I ERR:J ERR:K |" + "leaf3 LEAF3 leaf3 leaf3 leaf3foo leaf3 from my tag1 leaf3 from my tag2 leaf3 leaf3 from my tag1 LEAF3 LEAF3 |", "deep linking in templates, using onError - correctly re-link to data when missing objects are dynamically replaced"); // ................................ Act .................................. jsv.unlink(); // ............................... Assert ................................. assert.ok(!viewsAndBindings($) && !$._data(initial.a).events && !$._data(initial.a.b).events, 'jsv.unlink() removes all views and listeners from the page'); // =============================== Arrange =============================== function Item(value, title) { this.title = title; this._value = value; this.value = function(val) { if (!arguments.length) { return this._value; } else { this._value = val; } }; this.value.set = true; } initial = new Item("string1", "A"); jsv.templates( "{^{:value() onError='error1'}} {^{:title}} " + "{^{:value()^value() onError='error2'}} {^{:value()^title onError='error2b'}} " + "{^{:value()^value().value() onError='error3'}} {^{:value()^value().title onError='error3b'}} " + "{^{:value()^value().value().value() onError='error4'}} {^{:value()^value().value().title onError='error4b'}} " ).link("#result", initial); // ................................ Act .................................. var B, C, D, a, b, c, d, e; before = $("#result").text() + "|"; jsv.observable(initial).setProperty('value', B = new Item("string2", "B")); after = $("#result").text() + "|"; jsv.observable(initial.value()).setProperty('value', C = new Item("string3", "C")); after += $("#result").text() + "|"; jsv.observable(initial.value().value()).setProperty('value', D = new Item("string4", "D")); after += $("#result").text() + "|"; jsv.observable(initial).removeProperty('value'); after += $("#result").text() + "|"; jsv.observable(initial).setProperty('value', a = new Item(b = new Item(c = new Item(d = new Item(e = new Item("string4", "e"), "d"), "c"), "b"), "a")); after += $("#result").text() + "|"; assert.equal(before + after, "string1 A error2 error3 error3b error4 error4b |" + "[object Object] A string2 B error3 error4 error4b |" + "[object Object] A [object Object] B string3 C error4 |" + "[object Object] A [object Object] B [object Object] C string4 D |" + "[object Object] A error2 error3 error3b error4 error4b |" + "[object Object] A [object Object] a [object Object] b [object Object] c |", "deep linking in templates, using onError - correctly re-link to data when missing objects are dynamically replaced"); // ................................ Act .................................. jsv.unlink(); // ............................... Assert ................................. assert.ok(!viewsAndBindings($) && !$._data(initial.value()).events && !$._data(initial.value().value()).events, 'jsv.unlink() removes all views and listeners from the page'); jsv.views.settings.advanced({_jsv: false}); }); QUnit.test("contextual parameter", function(assert) { // =============================== Arrange =============================== var teams = [ {title: "The A Team", members: [{name: "Jeff"}, {name: "Maria"}]}, {title: "The B Team", members: [{name: "Francis"}]} ]; // ................................ Act .................................. jsv.templates( "{{if members.length ~teamTitle=title ~teamData=#data ~teamIndex=#index}}" + "{{for members itemVar='~member'}}" + "{{:~teamTitle}} " + "{{:~teamData.title}} " + "{{:~teamIndex}} " + "{{:~member.name}} " + "{{/for}}" + "{{/if}}" ).link("#result", teams); // ............................... Assert ................................. assert.equal($("#result").text(), "The A Team The A Team 0 Jeff The A Team The A Team 0 Maria The B Team The B Team 1 Francis ", "contextual parameter passing to inner context"); // =============================== Arrange =============================== var ct = 1; // ................................ Act .................................. jsv.templates("{^{include ~f=~hlp('foo') }}{^{:~f}}{{/include}}").link("#result", {}, { hlp: function() { return ct++; } }); // ............................... Assert ................................. assert.equal($("#result").text(), "1", "contextual parameter function (with quotes) is cached and called only once"); // See https://github.com/BorisMoore/jsviews/issues/440#issuecomment-660853490 // ................................ Act .................................. jsv.templates("{^{if 1 ~a='A'+\"B\"+'\"'+\"'\"+\"\\'\"}}{^{:'Inner'+~a}}{{/if}}").link("#result"); // ............................... Assert ................................. assert.equal($("#result").text(), "InnerAB\"'\\'", "contextual parameter correctly escaping quotes and backslash"); // ................................ Reset ................................ $("#result").empty(); }); QUnit.test('Bound tag properties and contextual parameters', function(assert) { // =============================== Arrange =============================== var things = [ { type: "shape", form: "circle" }, { type: "line", form: "square", thickness: "1" } ]; jsv.templates('Tag: {^{include ^tmpl=~typeTemplates[type]/}} Elem:
                  ') .link("#result", things, { typeTemplates: { shape: "Shape: {^{:form}}\n", line: "Line: {^{:form}} {^{:thickness}}\n" } } ); // ................................ Act .................................. before = $("#result").text(); jsv.observable(things[0]).setProperty({type: "line", thickness: 5}); jsv.observable(things[1]).setProperty({type: "shape"}); jsv.observable(things[1]).removeProperty("thickness"); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, "Tag: Shape: circle\n Elem: Shape: circle\n Tag: Line: square 1\n Elem: Line: square 1\n |Tag: Line: circle 5\n Elem: Line: circle 5\n Tag: Shape: square\n Elem: Shape: square\n ", 'binding to ^tmpl=... :{^{include ^tmpl=~typeTemplates[type]... and data-link="{include ^tmpl=~typeTemplates[type]...'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== var things = [ { type: "shape", form: "circle" }, { type: "line", form: "square", thickness: "1" } ]; jsv.templates('Tag: {^{if true ^tmpl=~typeTemplates[type]/}} Elem:
                  ') .link("#result", things, { typeTemplates: { shape: "Shape: {^{:form}}\n", line: "Line: {^{:form}} {^{:thickness}}\n" } } ); // ................................ Act .................................. before = $("#result").text(); jsv.observable(things[0]).setProperty({type: "line", thickness: 5}); jsv.observable(things[1]).setProperty({type: "shape"}); jsv.observable(things[1]).removeProperty("thickness"); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, "Tag: Shape: circle\n Elem: Shape: circle\n Tag: Line: square 1\n Elem: Line: square 1\n |Tag: Line: circle 5\n Elem: Line: circle 5\n Tag: Shape: square\n Elem: Shape: square\n ", 'binding to ^tmpl=... :{^{if true ^tmpl=~typeTemplates[type]... and data-link="{if true ^tmpl=~typeTemplates[type]...'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== var things = [ { type: "shape", form: "circle" }, { type: "line", form: "square", thickness: "1" } ]; jsv.templates('Tag: {^{if false}}{{else ^tmpl=~typeTemplates[type]}}{{/if}} Elem:
                  ') .link("#result", things, { typeTemplates: { shape: "Shape: {^{:form}}\n", line: "Line: {^{:form}} {^{:thickness}}\n" } } ); // ................................ Act .................................. before = $("#result").text(); jsv.observable(things[0]).setProperty({type: "line", thickness: 5}); jsv.observable(things[1]).setProperty({type: "shape"}); jsv.observable(things[1]).removeProperty("thickness"); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, "Tag: Shape: circle\n Elem: Shape: circle\n Tag: Line: square 1\n Elem: Line: square 1\n |Tag: Line: circle 5\n Elem: Line: circle 5\n Tag: Shape: square\n Elem: Shape: square\n ", 'binding to ^tmpl=... :{^{if false}}{{else ^tmpl=~typeTemplates[type]... and data-link="{if false}{else ^tmpl=~typeTemplates[type]...'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== var things = [ { type: "shape", form: "circle" }, { type: "line", form: "square", thickness: "1" } ]; jsv.templates('Tag: {^{for undefined}}{{else ^tmpl=~typeTemplates[type]}}{{/for}} Elem:
                  ') .link("#result", things, { typeTemplates: { shape: "Shape: {^{:form}}\n", line: "Line: {^{:form}} {^{:thickness}}\n" } } ); // ................................ Act .................................. before = $("#result").text(); jsv.observable(things[0]).setProperty({type: "line", thickness: 5}); jsv.observable(things[1]).setProperty({type: "shape"}); jsv.observable(things[1]).removeProperty("thickness"); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, "Tag: Shape: circle\n Elem: Shape: circle\n Tag: Line: square 1\n Elem: Line: square 1\n |Tag: Line: circle 5\n Elem: Line: circle 5\n Tag: Shape: square\n Elem: Shape: square\n ", 'binding to ^tmpl=... :{^{for undefined}}{{else ^tmpl=~typeTemplates[type]... and data-link="{for undefined}{else ^tmpl=~typeTemplates[type]...'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== things = [ { type: "shape", form: "circle" }, { type: "line", form: "square", thickness: "1" } ]; jsv.templates('Bound condition: {^{include ^~condition=type==="shape"}}{{:type}} {{:~condition}} {{/include}}' + 'Unbound condition: {^{include ~condition=type==="shape"}}{{:type}} {{:~condition}} {{/include}}') .link("#result", things); // ................................ Act .................................. before = $("#result").text(); jsv.observable(things[0]).setProperty({type: "line", thickness: 5}); jsv.observable(things[1]).setProperty({type: "shape"}); jsv.observable(things[1]).removeProperty("thickness"); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, "Bound condition: shape true Unbound condition: shape true Bound condition: line false Unbound condition: line false |Bound condition: line false Unbound condition: shape true Bound condition: shape true Unbound condition: line false ", 'Binding to contextual parameter {^{include ^~condition=... triggers update. Unbound contextual parameter {^{include ~condition=... does not trigger updated content'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== things = [ { type: "shape", form: "circle" }, { type: "line", form: "square", thickness: "1" } ]; jsv.views.tags({ updatingTag: { }, nonUpdatingTag: { onUpdate: function() { return false; } } }); jsv.templates('Updating: {^{updatingTag ^condition=type==="shape"}}{{:type}} {^{:~tagCtx.props.condition}} {{/updatingTag}} ' + 'Non updating: {^{nonUpdatingTag ^condition=type==="shape"}}{{:type}} {^{:~tagCtx.props.condition}} {{/nonUpdatingTag}}') .link("#result", things); // ................................ Act .................................. before = $("#result").text(); jsv.observable(things[0]).setProperty({type: "line", thickness: 5}); jsv.observable(things[1]).setProperty({type: "shape"}); jsv.observable(things[1]).removeProperty("thickness"); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, "Updating: shape true Non updating: shape true Updating: line false Non updating: line false |Updating: line false Non updating: shape false Updating: shape true Non updating: line true ", 'Binding to property triggers update {^{updatingTag ^condition=... unless tag is non-updating: {^{nonUpdatingTag ^condition=...'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== jsv.templates({ myTmpl: "{{>name}} lead:{^{>~team.lead}} - " }); var model = { lead: "Jim", people: [ {name: "Bob"}, {name: "Jim"} ] }; // ............................... Act ................................. jsv.templates("
                  ").link("#result", model); res = $("#result").text(); jsv.observable(model.people).insert({ name: "newName" }); jsv.observable(model).setProperty("lead", "newName"); res += "|" + $("#result").text(); // ............................... Assert ................................. assert.equal(res, ("Bob lead:Jim - Jim lead:Jim - |Bob lead:newName - Jim lead:newName - newName lead:newName - "), "data-link allows passing in new contextual parameters to template: data-link=\"{for people ~team=#data tmpl=..."); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== model = { sortby: "role", cols: ["name", "role"] }; // ............................... Act ................................. jsv.templates('{^{for cols itemVar="~col"}}{^{:~root.sortby === ~col}} {^{:~col}} {{/for}}').link("#result", model); res = $("#result").text(); jsv.observable(model).setProperty("sortby", model.sortby === "role" ? "name" : "role"); res += "|" + $("#result").text(); jsv.observable(model).setProperty("sortby", model.sortby === "role" ? "name" : "role"); res += "|" + $("#result").text(); jsv.observable(model.cols).insert("other"); res += "|" + $("#result").text(); // ............................... Assert ................................. assert.equal(res, "false name true role |true name false role |false name true role |false name true role false other ", "itemVar variables in item list are distinct variables"); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== var data = {items1: [], items: [{name: "Jo"}, {name: "Bob"}]}; // ............................... Act ................................. jsv.templates( '{^{for items1}}{{else items itemVar="~currentItem"}}' + '{^{:name}} ' + '{^{:~currentItem.name}} ' + '{^{include}}' + '{^{:~currentItem.name}} ' + '{{/include}}' + '{^{for itemVar="~currentItem2"}}' + '{^{:name}} ' + '{^{:~currentItem2.name}} ' + '{^{if true}}' + '{^{:~currentItem2.name}} ' + '{{/if}}' + '{{/for}}' + '{^{for ~currentItem itemVar="~currentItem2"}}' + '{^{:name}} ' + '{^{:~currentItem2.name}} ' + '{^{include}}' + '{^{:~currentItem2.name}} ' + '{{/include}}' + '{{/for}}' + '{{/for}}').link("#result", data); jsv.observable(data.items).insert({name: "Jeff"}); var inputs = $("#result input"); res = "|" + $("#result").text() + inputs[0].value; keydown($(inputs[0]).val("Jo0")); res += "|" + $("#result").text() + inputs[1].value; // ............................... Assert ................................. assert.equal(res, "|Jo Jo Jo Jo Jo Jo Jo Jo Jo Bob Bob Bob Bob Bob Bob Bob Bob Bob Jeff Jeff Jeff Jeff Jeff Jeff Jeff Jeff Jeff Jo|Jo0 Jo0 Jo0 Jo0 Jo0 Jo0 Jo0 Jo0 Jo0 Bob Bob Bob Bob Bob Bob Bob Bob Bob Jeff Jeff Jeff Jeff Jeff Jeff Jeff Jeff Jeff Jo0", "itemVar in nested contexts, on else blocks, etc. with two-way binding, works correctly"); // ................................ Reset ................................ $("#result").empty(); }); QUnit.test('Data-linking helpers and contextual parameters', function(assert) { jsv.views.settings.trigger(false); // =============================== Arrange =============================== var data = {name: "Jo", address: {}}; res = ""; jsv.templates( '{{for address ~nm=name}}' + '
                  ' + '' + '{{/for}}' ) .link("#result", data); // ................................ Act .................................. res += $("#result").text() + " | "; jsv.observable(data).setProperty("name", "new"); res += $("#result").text() + " | "; $("#input1").val('changed').change(); res += $("#result").text() + " | "; // ............................... Assert ................................. assert.equal(res, "Jo | new | changed | ", 'contextual parameter two-way binding'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== var things = [ { type: "shape", form: "circle" }, { type: "line", form: "square", thickness: "1" } ]; jsv.templates('{^{include ~condition=type==="shape"}}{{:type}} {^{:type}} {^{:~condition}} {{/include}}') .link("#result", things); // ................................ Act .................................. before = $("#result").text(); jsv.observable(things[0]).setProperty({type: "line", thickness: 5}); jsv.observable(things[1]).setProperty({type: "shape"}); after = $("#result").text(); // ............................... Assert ................................. assert.equal(before + "|" + after, "shape shape true line line false |shape line false line shape true ", 'contextual parameter {^{include ~condition=... does not trigger update but references are bound'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== var getContent = function() { $("#result input").each(function() { res += this.value; }); res += $("#result").text() + "|"; } var tmpl = jsv.templates( '\ \ \ \ {^{include ~f1 = foo}}\ \ {^{include ~f2 = #data.foo}}\ \ \ {^{include ~f3 = #view.data.foo}}\ {^{:~f1}}\ {^{:~f2}}\ {^{:~f3}}\ \ \ \ {{/include}}\ {{/include}}\ {{/include}}\ '); // ............................... Act ................................. tmpl.link("#result", {foo: "F"}); var cnt = 0; res = ""; getContent(); $("#result input").each(function() { $("#result input").val(cnt++).change(); getContent(); }); assert.equal(res, "FFFFFFFFFFFF|000000000000|111111111111|222222222222|333333333333|444444444444|" + "555555555555|666666666666|777777777777|888888888888|", 'Two-way binding to contextual parameters'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== getContent = function getContent() { return $("#outerInput").val() + ":" + $("#innerInput").val() + ":" + $("#innerDiv").text(); } var tmpl = jsv.templates( '{^{if true ~street=address(name)}}\ \ {^{for address ~street=~street^street}}\
                  \ \ {{/for}}\ {{/if}}'); // ............................... Act ................................. var address = {street: "add"}; var altAddress = {street: "alt"}; function getAddress(name) { return (name.length > 2 || data.alt) ? altAddress : address; } getAddress.depends = "alt"; var data = {name: "Jo", address: getAddress, alt: false}; tmpl.link("#result", data); before = getContent(); $("#outerInput").val("addOuter").change(); after = before + "|" + getContent(); $("#innerInput").val("addInner").change(); after += "|" + getContent(); jsv.observable(data).setProperty("name", "John"); after += "|" + getContent(); $("#outerInput").val("altOuter").change(); after += "|" + getContent(); $("#innerInput").val("altInner").change(); after += "|" + getContent(); jsv.observable(data).setProperty("name", "Me"); after += "|" + getContent(); jsv.observable(data).setProperty("alt", true); after += "|" + getContent(); assert.equal(after, "add:add:add|addOuter:addOuter:addOuter|addInner:addInner:addInner" + "|alt:alt:alt|altOuter:altOuter:altOuter|altInner:altInner:altInner" + "|addInner:addInner:addInner|altInner:altInner:altInner", 'Two-way binding to contextual parameters with computed values'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== var tmpl = jsv.templates( '{^{for items}}\ {{for 22 ~ind=#index}}\ {{for 33 ~ind=~ind+1}}\ {{for 33 ~ind=~ind*2}}\ {{if true}}\ {^{:~ind}}\ {{/if}}\ {{/for}}\ {{/for}}\ {{/for}}\ {{/for}}' ); // ............................... Act ................................. var data = {items: [1,2,3]}; tmpl.link("#result", data); before = $("#result").text(); jsv.observable(data.items).remove(); after = before + "|" + $("#result").text(); jsv.observable(data.items).remove(); after += "|" + $("#result").text(); jsv.observable(data.items).insert(4); after += "|" + $("#result").text(); jsv.observable(data.items).insert(5); after += "|" + $("#result").text(); jsv.observable(data.items).insert(6); after += "|" + $("#result").text(); assert.equal(after, "246|24|2|24|246|2468", 'Contextual parameter for index with array change'); // ................................ Reset ................................ $("#result").empty(); jsv.views.settings.trigger(true); }); QUnit.test("JsViews ArrayChange: insert()", function(assert) { // =============================== Arrange =============================== jsv.views.tags({ liTag: function() { return "
                • Tag
                • "; } }); model.things = [{thing: "Orig"}]; // reset Prop jsv.templates('
                    {^{liTag/}}{^{for things}}
                  • {{:thing}}
                  • {^{liTag/}}{{/for}}
                  • |after
                  ') .link("#result", model); // ................................ Act .................................. jsv.observable(model.things).insert(0, {thing: "First"}); jsv.observable(model.things).insert(1, {thing: "Last"}); jsv.observable(model.things).insert(1, {thing: "Middle"}); // ............................... Assert ................................. assert.equal($("#result").text(), "TagFirstTagMiddleTagLastTagOrigTag|after", 'Within element only content, insertion maintains correctly prevNode, nextNode, element order and binding on views and tags'); // ................................ Reset ................................ $("#result").empty(); model.things = []; // reset Prop // =============================== Arrange =============================== jsv.views.tags({ spanTag: function() { return "Tag"; } }); model.things = [{thing: "Orig"}]; // reset Prop jsv.templates('
                  {^{spanTag/}}{^{for things}}{{:thing}}{^{spanTag/}}{{/for}}|after
                  ') .link("#result", model); // ................................ Act .................................. jsv.observable(model.things).insert(0, {thing: "First"}); jsv.observable(model.things).insert(1, {thing: "Last"}); jsv.observable(model.things).insert(1, {thing: "Middle"}); // ............................... Assert ................................. assert.equal($("#result").text(), "TagFirstTagMiddleTagLastTagOrigTag|after", 'Within regular content, insertion finds correctly the previous view, prevNode, nextNode, etc and establishes correct element/textNode order and binding'); // ................................ Reset ................................ $("#result").empty(); model.things = [{thing: "Orig"}]; // reset Prop // =============================== Arrange =============================== jsv.templates('{^{for things}}{{/for}}
                  {{:thing}}
                  ') .link("#result", model); // ................................ Act .................................. jsv.observable(model.things).insert(0, {thing: "First"}); // ............................... Assert ................................. assert.equal($("#result").text(), "FirstOrig", 'Within element only content, insertion maintains correctly prevNode, nextNode, element order and binding on views and tags'); // ................................ Reset ................................ $("#result").empty(); model.things = []; // reset Prop }); QUnit.test("JsViews ArrayChange: remove()", function(assert) { // =============================== Arrange =============================== jsv.views.tags({ liTag: function() { return "
                • Tag
                • "; } }); model.things = [{thing: "Orig"}, {thing: "First"}, {thing: "Middle"}, {thing: "Last"}]; // reset Prop jsv.templates('
                    {^{liTag/}}{^{for things}}
                  • {{:thing}}
                  • {^{liTag/}}{{/for}}
                  • |after
                  ') .link("#result", model); // -> TagOrigTagFirstTagMiddleTagLastTag|after // ................................ Act .................................. jsv.observable(model.things).remove(1); // ............................... Assert ................................. assert.equal($("#result").text(), "TagOrigTagMiddleTagLastTag|after", 'Within element only content, remove(1) maintains correctly prevNode, nextNode, element order and binding on views and tags'); // ................................ Act .................................. jsv.observable(model.things).remove(); // ............................... Assert ................................. assert.equal($("#result").text(), "TagOrigTagMiddleTag|after", 'Within element only content, remove maintains correctly prevNode, nextNode, element order and binding on views and tags'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== model.things = [{thing: "Orig"}, {thing: "First"}, {thing: "Middle"}, {thing: "Last"}]; // reset Prop jsv.templates('
                    {^{for things start=0}}
                  • {{:thing}}
                  • {^{liTag/}}{{/for}}
                  • |after
                  ') .link("#result", model); // -> OrigTagFirstTagMiddleTagLastTag|after // See https://github.com/BorisMoore/jsviews/issues/442 // ................................ Act .................................. jsv.observable(model.things).remove(1); // ............................... Assert ................................. assert.equal($("#result").text(), "OrigTagMiddleTagLastTag|after", 'Within element only content, remove(1) maintains correctly prevNode etc. on views and tags, even when using start=0, and with linked {on} tag'); // ................................ Act .................................. jsv.observable(model.things).remove(); // ............................... Assert ................................. assert.equal($("#result").text(), "OrigTagMiddleTag|after", 'Within element only content, remove maintains correctly prevNode etc. on views and tags, even when using start=0, and with linked {on} tag'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== model.things = [{thing: "Orig"}, {thing: "First"}, {thing: "Middle"}, {thing: "Last"}]; // reset Prop jsv.templates("liTmpl", "
                • {{:thing}}
                • {^{liTag/}}"); jsv.templates('
                    ') .link("#result", model); // -> OrigTagFirstTagMiddleTagLastTag // See https://github.com/BorisMoore/jsviews/issues/442 // ................................ Act .................................. jsv.observable(model.things).remove(1); // ............................... Assert ................................. assert.equal($("#result").text(), "OrigTagMiddleTagLastTag", 'Within element only content, using data-linked element {for}, remove(1) maintains correctly even when using start=0, and with linked {on} tag'); // ................................ Act .................................. jsv.observable(model.things).remove(); // ............................... Assert ................................. assert.equal($("#result").text(), "OrigTagMiddleTag", 'Within element only content, using data-linked element {for}, remove maintains correctly even when using start=0, and with linked {on} tag'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== jsv.views.tags({ spanTag: function() { return "Tag"; } }); model.things = [{thing: "Orig"}, {thing: "First"}, {thing: "Middle"}, {thing: "Last"}]; // reset Prop jsv.templates('
                    {^{spanTag/}}{^{for things}}{{:thing}}{^{spanTag/}}{{/for}}|after
                    ') .link("#result", model); // ................................ Act .................................. jsv.observable(model.things).remove(1); // ............................... Assert ................................. assert.equal($("#result").text(), "TagOrigTagMiddleTagLastTag|after", 'Within regular content, remove(1) finds correctly the previous view, prevNode, nextNode, etc and establishes correct element/textNode order and binding'); // ................................ Act .................................. jsv.observable(model.things).remove(); // ............................... Assert ................................. assert.equal($("#result").text(), "TagOrigTagMiddleTag|after", 'Within regular content, remove() finds correctly the previous view, prevNode, nextNode, etc and establishes correct element/textNode order and binding'); // ................................ Reset ................................ $("#result").empty(); model.things = []; // reset Prop }); QUnit.test("JsViews ArrayChange: move()", function(assert) { // =============================== Arrange =============================== jsv.views.tags({ liTag: function() { return "
                  • Tag
                  • "; } }); model.things = [{thing: "Orig"}, {thing: "First"}, {thing: "Middle"}, {thing: "Last"}]; // reset Prop jsv.templates('
                      {^{liTag/}}{^{for things}}
                    • {{:thing}}
                    • {^{liTag/}}{{/for}}
                    • |after
                    ') .link("#result", model); // -> TagOrigTagFirstTagMiddleTagLastTag|after // ................................ Act .................................. jsv.observable(model.things).move(2, 0, 2); // ............................... Assert ................................. assert.equal($("#result").text(), "TagMiddleTagLastTagOrigTagFirstTag|after", 'Within element only content, move(2, 0, 2) maintains correctly prevNode, nextNode, element order and binding on views and tags'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== function addResult() { result += "|" + $("#result").text(); } var result = ""; model.things = [{thing: "Orig"}, {thing: "First"}, {thing: "Middle"}, {thing: "Last"}]; model.one = false; model.two = false; jsv.templates('
                      {^{if one}}
                    • One
                    • {{/if}}{^{if two}}
                    • Two
                    • {{/if}}{^{for things}}
                    • {{:thing}}
                    • {{/for}}
                    ') .link("#result", model); // ................................ Act .................................. jsv.observable(model.things).move(2, 0, 2); addResult(); jsv.observable(model).setProperty("one", true); addResult(); jsv.observable(model).setProperty("one", false); addResult(); jsv.observable(model).setProperty("one", true); addResult(); jsv.observable(model).setProperty("two", true); addResult(); jsv.observable(model).setProperty("two", false); addResult(); jsv.observable(model).setProperty("two", true); addResult(); jsv.observable(model).setProperty("one", false); addResult(); jsv.observable(model).setProperty("two", false); addResult(); // ............................... Assert ................................. assert.equal(result, "|MiddleLastOrigFirst|OneMiddleLastOrigFirst|MiddleLastOrigFirst|OneMiddleLastOrigFirst|OneTwoMiddleLastOrigFirst|OneMiddleLastOrigFirst|OneTwoMiddleLastOrigFirst|TwoMiddleLastOrigFirst|MiddleLastOrigFirst", 'In element only content with preceding collapsible {{if}} blocks, move(2, 0, 2) maintains correctly prevNode, nextNode, element order and binding on views and tags'); // ................................ Act .................................. result = "" jsv.observable(model.things).refresh([{thing: "Orig"}, {thing: "First"}, {thing: "Middle"}, {thing: "Last"}]); addResult(); jsv.observable(model).setProperty("one", true); addResult(); jsv.observable(model).setProperty("one", false); addResult(); jsv.observable(model).setProperty("one", true); addResult(); jsv.observable(model).setProperty("two", true); addResult(); jsv.observable(model).setProperty("two", false); addResult(); jsv.observable(model).setProperty("two", true); addResult(); jsv.observable(model).setProperty("one", false); addResult(); jsv.observable(model).setProperty("two", false); addResult(); // ............................... Assert ................................. assert.equal(result, "|OrigFirstMiddleLast|OneOrigFirstMiddleLast|OrigFirstMiddleLast|OneOrigFirstMiddleLast|OneTwoOrigFirstMiddleLast|OneOrigFirstMiddleLast|OneTwoOrigFirstMiddleLast|TwoOrigFirstMiddleLast|OrigFirstMiddleLast", 'In element only content with preceding collapsible {{if}} blocks, refresh(...) maintains correctly prevNode, nextNode, element order and binding on views and tags'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== result = ""; model.things = [{thing: "Orig"}, {thing: "First"}, {thing: "Middle"}, {thing: "Last"}]; // reset Prop model.one = []; model.two = []; jsv.templates('
                      {^{for one}}
                    • {{:}}
                    • {{/for}}{^{for two}}
                    • {{:}}
                    • {{/for}}{^{for things}}
                    • {{:thing}}
                    • {{/for}}
                    ') .link("#result", model); // ................................ Act .................................. jsv.observable(model.things).move(2, 0, 2); addResult(); jsv.observable(model.one).insert("one"); addResult(); jsv.observable(model.one).remove(); addResult(); jsv.observable(model.one).insert("one"); addResult(); jsv.observable(model.two).insert("two"); addResult(); jsv.observable(model.two).remove(); addResult(); jsv.observable(model.two).insert("two"); addResult(); jsv.observable(model.one).remove(); addResult(); jsv.observable(model.two).remove(); addResult(); // ............................... Assert ................................. assert.equal(result, "|MiddleLastOrigFirst|oneMiddleLastOrigFirst|MiddleLastOrigFirst|oneMiddleLastOrigFirst|onetwoMiddleLastOrigFirst|oneMiddleLastOrigFirst|onetwoMiddleLastOrigFirst|twoMiddleLastOrigFirst|MiddleLastOrigFirst", 'In element only content with preceding collapsible {{for}} blocks, move(2, 0, 2) maintains correctly prevNode, nextNode, element order and binding on views and tags'); // ................................ Act .................................. result = "" jsv.observable(model.things).refresh([{thing: "Orig"}, {thing: "First"}, {thing: "Middle"}, {thing: "Last"}]); addResult(); jsv.observable(model.one).insert("one"); addResult(); jsv.observable(model.one).remove(); addResult(); jsv.observable(model.one).insert("one"); addResult(); jsv.observable(model.two).insert("two"); addResult(); jsv.observable(model.two).remove(); addResult(); jsv.observable(model.two).insert("two"); addResult(); jsv.observable(model.one).remove(); addResult(); jsv.observable(model.two).remove(); addResult(); // ............................... Assert ................................. assert.equal(result, "|OrigFirstMiddleLast|oneOrigFirstMiddleLast|OrigFirstMiddleLast|oneOrigFirstMiddleLast|onetwoOrigFirstMiddleLast|oneOrigFirstMiddleLast|onetwoOrigFirstMiddleLast|twoOrigFirstMiddleLast|OrigFirstMiddleLast", 'In element only content with preceding collapsible {{for}} blocks, refresh(...) maintains correctly prevNode, nextNode, element order and binding on views and tags'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== jsv.views.tags({ spanTag: function() { return "Tag"; } }); model.things = [{thing: "Orig"}, {thing: "First"}, {thing: "Middle"}, {thing: "Last"}]; // reset Prop jsv.templates('
                    {^{spanTag/}}{^{for things}}{{:thing}}{^{spanTag/}}{{/for}}|after
                    ') .link("#result", model); // ................................ Act .................................. jsv.observable(model.things).move(2, 0, 2); // ............................... Assert ................................. assert.equal($("#result").text(), "TagMiddleTagLastTagOrigTagFirstTag|after", 'Within regular content, move(2, 0, 2) finds correctly the previous view, prevNode, nextNode, etc and establishes correct element/textNode order and binding'); // ................................ Reset ................................ $("#result").empty(); model.things = []; // reset Prop // =============================== Arrange =============================== function move(from, to, number) { jsv.observable(model.items).move(from, to, number); } function remove(index, number) { jsv.observable(model.items).remove(index, number); } function insert(index, item) { jsv.observable(model.items).insert(index, item); } function findRed() { return " at: " + $(".wrap").find("span[style]").view().index + "/" + + $(".wrap2").find("li[style]").view().index + "/" + + $(".wrap3").find("li[style]").view().index; } function current() { var seq = "", redItemAt; for (var i=0; iStart ' + '{^{for items}}' + '{^{:#index}} {{:}}|' + '{{/for}}' + 'End
                    ' + '
                    Start ' + '{^{for items}}' + ' {{:}}|' + '{{/for}}' + 'End
                    ' + 'Start End' + '' ) .link("#result", model); $(".wrap").find("span:first").css("color", "red"); $(".wrap2").find("li:first").css("color", "red"); $(".wrap3").find("li:nth-of-type(2)").css("color", "red"); // ................................ Act .................................. move(0, 1); // ............................... Assert ................................. assert.equal($("#result").text() + findRed(), current(1,0,2,3,4), 'moved one item from 0 to 1'); // ................................ Act .................................. move(0, 1); // ............................... Assert ................................. assert.equal($("#result").text() + findRed(), current(0,1,2,3,4), 'moved one item from 0 to 1 again (actually swaps back to orginal positions'); // ................................ Act .................................. move(1, 0); // ............................... Assert ................................. assert.equal($("#result").text() + findRed(), current(1,0,2,3,4), 'moved one item back from 1 to 0'); // ................................ Act .................................. move(1, 0); // Return to original position move(1, 0, 0); // ............................... Assert ................................. assert.equal($("#result").text() + findRed(), current(0,1,2,3,4), 'move(1, 0, 0) does nothing'); // ................................ Act .................................. move(1, 1); // ............................... Assert ................................. assert.equal($("#result").text() + findRed(), current(0,1,2,3,4), 'move(1, 1) does nothing'); // ................................ Act .................................. move(0, 1, 2); // ............................... Assert ................................. assert.equal($("#result").text() + findRed(), current(2,0,1,3,4), 'move(0, 1, 2) moves 2 items'); // ................................ Act .................................. move(0, 1, 4); // ............................... Assert ................................. assert.equal($("#result").text() + findRed(), current(4,2,0,1,3), 'move(0, 1, 4) moves 4 items'); // ................................ Act .................................. move(1, 0, 4); // ............................... Assert ................................. assert.equal($("#result").text() + findRed(), current(2,0,1,3,4), 'move(1, 0, 4) moves back 4 items'); // ................................ Act .................................. move(0, 1, 5); // ............................... Assert ................................. assert.equal($("#result").text() + findRed(), current(2,0,1,3,4), 'move(0, 1, 5): moving more than total items does nothing'); // ................................ Act .................................. move(1, 2, 4); // ............................... Assert ................................. assert.equal($("#result").text() + findRed(), current(2,0,1,3,4), 'move(1, 2, 4): moving up items beyond last item does nothing'); // ................................ Act .................................. move(2, 1, 8); // ............................... Assert ................................. assert.equal($("#result").text() + findRed(), current(2,1,3,4,0), 'move(2, 1, 8): moving back items from beyond last item will move just the existing ones'); // ................................ Act .................................. remove(1,1); // ............................... Assert ................................. assert.equal($("#result").text() + findRed(), current(2,3,4,0), 'remove(1,1): works correctly'); // ................................ Act .................................. remove(0); // ............................... Assert ................................. assert.equal($("#result").text() + findRed(), current(3,4,0), 'remove(0): works correctly'); // ................................ Act .................................. move(2, 0); // ............................... Assert ................................. assert.equal($("#result").text() + findRed(), current(0,3,4), 'move(2, 0): works correctly'); // ................................ Act .................................. remove(2); move(0,1,2); insert(0, 2); move(2, 0); // ............................... Assert ................................. assert.equal($("#result").text() + findRed(), current(3,2,0), 'multiple operations: works correctly - with the original item with style set to red still there'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== function findRed2() { return " at: " + $(".wrap").find("span[style]").view().getIndex() + "/" + + $(".wrap2").find("li[style]").view().getIndex() + "/" + + $(".wrap3").find("li[style]").view().getIndex(); } model.items = [0,1,2,3,4]; var cnt = 5; jsv.templates({ tags: { tag: function() {return this.tagCtx.render();} }, markup: '
                    Start ' + '{^{for items}}' + '{^{if true}}' + '{^{if true}}' + '{^{if false}}{{else}}' + '{^{tag}}' + '{^{:#getIndex()}} {{:}}|' + '{{/tag}}' + '{{/if}}' + '{{/if}}' + '{{/if}}' + '{{/for}}' + 'End
                    ' + '
                    Start ' + '{^{for items}}' + '{^{if true}}' + '{^{if true}}' + '{^{if false}}{{else}}' + '{^{tag}}' + ' {{:}}|' + '{{/tag}}' + '{{/if}}' + '{{/if}}' + '{{/if}}' + '{{/for}}' + 'End
                    ' + 'Start End' + '' } ).link("#result", model); $(".wrap").find("span:first").css("color", "red"); $(".wrap2").find("li:first").css("color", "red"); $(".wrap3").find("li:nth-of-type(2)").css("color", "red"); // ................................ Act .................................. move(0, 1); // ............................... Assert ................................. assert.equal($("#result").text() + findRed2(), current(1,0,2,3,4), 'Complex template: moved one item from 0 to 1'); // ................................ Act .................................. move(0, 1); // ............................... Assert ................................. assert.equal($("#result").text() + findRed2(), current(0,1,2,3,4), 'Complex template: moved one item from 0 to 1 again (actually swaps back to orginal positions'); // ................................ Act .................................. move(1, 0); // ............................... Assert ................................. assert.equal($("#result").text() + findRed2(), current(1,0,2,3,4), 'Complex template: moved one item back from 1 to 0'); // ................................ Act .................................. move(1, 0); // Return to original position move(1, 0, 0); // ............................... Assert ................................. assert.equal($("#result").text() + findRed2(), current(0,1,2,3,4), 'Complex template: move(1, 0, 0) does nothing'); // ................................ Act .................................. move(1, 1); // ............................... Assert ................................. assert.equal($("#result").text() + findRed2(), current(0,1,2,3,4), 'Complex template: move(1, 1) does nothing'); // ................................ Act .................................. move(0, 1, 2); // ............................... Assert ................................. assert.equal($("#result").text() + findRed2(), current(2,0,1,3,4), 'Complex template: move(0, 1, 2) moves 2 items'); // ................................ Act .................................. move(0, 1, 4); // ............................... Assert ................................. assert.equal($("#result").text() + findRed2(), current(4,2,0,1,3), 'Complex template: move(0, 1, 4) moves 4 items'); // ................................ Act .................................. move(1, 0, 4); // ............................... Assert ................................. assert.equal($("#result").text() + findRed2(), current(2,0,1,3,4), 'Complex template: move(1, 0, 4) moves back 4 items'); // ................................ Act .................................. move(0, 1, 5); // ............................... Assert ................................. assert.equal($("#result").text() + findRed2(), current(2,0,1,3,4), 'Complex template: move(0, 1, 5): moving more than total items does nothing'); // ................................ Act .................................. move(1, 2, 4); // ............................... Assert ................................. assert.equal($("#result").text() + findRed2(), current(2,0,1,3,4), 'Complex template: move(1, 2, 4): moving up items beyond last item does nothing'); // ................................ Act .................................. move(2, 1, 8); // ............................... Assert ................................. assert.equal($("#result").text() + findRed2(), current(2,1,3,4,0), 'Complex template: move(2, 1, 8): moving back items from beyond last item will move just the existing ones'); // ................................ Act .................................. remove(1,1); // ............................... Assert ................................. assert.equal($("#result").text() + findRed2(), current(2,3,4,0), 'Complex template: remove(1,1): works correctly'); // ................................ Act .................................. remove(0); // ............................... Assert ................................. assert.equal($("#result").text() + findRed2(), current(3,4,0), 'Complex template: remove(0): works correctly'); // ................................ Act .................................. move(2, 0); // ............................... Assert ................................. assert.equal($("#result").text() + findRed2(), current(0,3,4), 'Complex template: move(2, 0): works correctly'); // ................................ Act .................................. remove(2); move(0,1,2); insert(0, 2); move(2, 0); // ............................... Assert ................................. assert.equal($("#result").text() + findRed2(), current(3,2,0), 'Complex template: multiple operations: works correctly - with the original item with style set to red still there'); // ................................ Reset ................................ $("#result").empty(); }); QUnit.test("JsViews ArrayChange: refresh()", function(assert) { // =============================== Arrange =============================== jsv.views.tags({ liTag: function() { return "
                  • Tag
                  • "; } }); model.things = [{thing: "Orig"}, {thing: "First"}, {thing: "Middle"}, {thing: "Last"}]; // reset Prop jsv.templates('') .link("#result", model); // -> TagOrigTagFirstTagMiddleTagLastTag|after // ................................ Act .................................. jsv.observable(model.things).refresh([{thing: "A"}, {thing: "B"}, {thing: "C"}]); // ............................... Assert ................................. assert.equal($("#result").text(), "TagATagBTagCTag|after", 'Within element only content, refresh() maintains correctly prevNode, nextNode, element order and binding on views and tags'); // ................................ Reset ................................ $("#result").empty(); // =============================== Arrange =============================== jsv.views.tags({ spanTag: function() { return "Tag"; } }); model.things = [{thing: "Orig"}, {thing: "First"}, {thing: "Middle"}, {thing: "Last"}]; // reset Prop jsv.templates('
                    {^{spanTag/}}{^{for things}}{{:thing}}{^{spanTag/}}{{/for}}|after
                    ') .link("#result", model); // ................................ Act .................................. jsv.observable(model.things).refresh([{thing: "A"}, {thing: "B"}, {thing: "C"}]); // ............................... Assert ................................. assert.equal($("#result").text(), "TagATagBTagCTag|after", 'Within regular content, refresh() finds correctly the previous view, prevNode, nextNode, etc and establishes correct element/textNode order and binding'); // ................................ Reset ................................ $("#result").empty(); model.things = []; // reset Prop }); QUnit.test("JsViews jsv-domchange", function(assert) { // =============================== Arrange =============================== res = ""; function domchangeHandler(ev, tagCtx, linkCtx, eventArgs) { res += ev.type + " " + ev.target.tagName + " " + tagCtx.params.args[0] + " " + (linkCtx.tag === tagCtx.tag) + " " + eventArgs.change + " | " ; } jsv.views.tags({ spanTag: function() { return "Tag"; } }); model.things = [{thing: "Orig"}, {thing: "First"}, {thing: "Middle"}, {thing: "Last"}]; // reset Prop jsv.templates('
                    {^{spanTag/}}{^{for things}}{{:thing}}{^{spanTag/}}{{/for}}|after
                    ') .link("#result", model); $("#result div").on("jsv-domchange", domchangeHandler); // ................................ Act .................................. jsv.observable(model.things).insert({thing: "New"}); jsv.observable(model.things).move(2, 0, 2); jsv.observable(model.things).remove(1, 2); jsv.observable(model.things).refresh([{thing: "A"}, {thing: "B"}, {thing: "C"}]); // ............................... Assert ................................. assert.equal(res, "jsv-domchange DIV things true insert | " + "jsv-domchange DIV things true move | " + "jsv-domchange DIV things true remove | " + "jsv-domchange DIV things true insert | " + "jsv-domchange DIV things true remove | " + "jsv-domchange DIV things true refresh | ", 'Correct behavior of $(...).on("jsv-domchange", domchangeHandler)'); // ................................ Reset ................................ $("#result").empty(); reset(); // =============================== Arrange =============================== model.things = [{thing: "Orig"}, {thing: "First"}, {thing: "Middle"}, {thing: "Last"}]; // reset Prop jsv.templates( '
                    ' + '{^{spanTag/}}{^{for things}}{{:thing}}{^{spanTag/}}{{/for}}|after' + '
                    ') .link("#result", model, { domchange: function(param1, param2, ev, domchangeEventArgs, tagCtx, linkCtx, observableEventArgs) { res += "Params: " + param1 + ", " + param2 + " | "; domchangeHandler(ev, tagCtx, linkCtx, observableEventArgs); } }); // ................................ Act .................................. jsv.observable(model.things).insert({thing: "New"}); jsv.observable(model.things).move(2, 0, 2); jsv.observable(model.things).remove(1, 2); jsv.observable(model.things).refresh([{thing: "A"}, {thing: "B"}, {thing: "C"}]); assert.equal(res, "Params: 333, 444 | jsv-domchange DIV things true insert | " + "Params: 333, 444 | jsv-domchange DIV things true move | " + "Params: 333, 444 | jsv-domchange DIV things true remove | " + "Params: 333, 444 | jsv-domchange DIV things true insert | " + "Params: 333, 444 | jsv-domchange DIV things true remove | " + "Params: 333, 444 | jsv-domchange DIV things true refresh | ", 'Correct behavior of data-link=\'{on "jsv-domchange" ~domchange param1 param2}\''); // ................................ Reset ................................ model.things = []; // reset Prop $("#result").empty(); reset(); }); QUnit.module("API - jsv.observe() (jsv)"); QUnit.test("observe/unobserve alternative signatures", function(assert) { jsv.views.settings.advanced({_jsv: true}); // =============================== Arrange =============================== var person = {last: " L"}; function onch(ev, eventArgs) { } // ................................ Act .................................. jsv.observe(person, "last", onch); jsv.templates("{^{:last}}").link("#result", person); jsv.unobserve(person, "last", onch); $("#result").empty(); // ............................... Assert ................................. assert.equal(JSON.stringify([_jsv.cbBindings, _jsv.bindings, $._data(person).events]), "[{},{},null]", "observe/unobserve API calls combined with template binding: all bindings removed when content removed from DOM and unobserve called"); // ................................ Reset ................................ reset(); // =============================== Arrange =============================== function onch2(ev, eventArgs) {} person = {first: "F", last: " L"}; // ................................ Act .................................. jsv.observe(person, "last", onch); jsv.observe(person, "last", onch2); jsv.templates("{^{:last}} }} {^{:first + last}}").link("#result", person); jsv.observe(person, "first", onch); jsv.observe(person, "first", onch2); jsv.unobserve(person, "last", onch); jsv.unobserve(person, "last", onch2); $("#result").empty(); jsv.unobserve(person, "first", onch); jsv.unobserve(person, "first", onch2); // ............................... Assert ................................. assert.equal(JSON.stringify([_jsv.cbBindings, _jsv.bindings, $._data(person).events]), "[{},{},null]", "Observe API calls combined with template binding (version 2): all bindings removed when content removed from DOM and unobserve called"); // ................................ Reset ................................ reset(); jsv.views.settings.advanced({_jsv: false}); }); QUnit.test("Array", function(assert) { jsv.views.settings.advanced({_jsv: true}); // For using _jsv // =============================== Arrange =============================== var people = [1, 2]; function onch() {} function onch2() {} // ................................ Act .................................. jsv.observe(people, "length", onch); jsv.observe(people, "length", onch2); jsv.observe(people, "length2", onch); jsv.templates("{^{for people}}{{/for}} {^{:people}}").link("#result", {people: people}); jsv.observe(people, "length2", onch2); jsv.unobserve(people, "length2", onch); jsv.unobserve(people, "length2", onch2); $("#result").empty(); jsv.unobserve(people, "length", onch); jsv.unobserve(people, "length", onch2); // ............................... Assert ................................. assert.equal(JSON.stringify([_jsv.cbBindings, _jsv.bindings, $._data(people).events]), "[{},{},null]", "observe/unobserve array - API calls in different orders: all bindings removed when content removed from DOM and unobserve called"); jsv.views.settings.advanced({_jsv: false}); }); QUnit.test("MVVM", function(assert) { jsv.views.settings.trigger(false); reset(); // =============================== Arrange =============================== function Person(name, address, phones) { this._name = name; this._address = address; this._phones = phones; } var personProto = { name: function() { return this._name; }, address: function() { return this._address; }, phones: function() { return this._phones; } }; personProto.name.set = function(val) { this._name = val; }; personProto.address.set = function(val) { this._address = val; }; personProto.phones.set = function(val) { this._phones = val; }; Person.prototype = personProto; function Address(street) { this._street = street; } var addressProto = { street: function() { return this._street; } }; addressProto.street.set = function(val) { this._street = val; }; Address.prototype = addressProto; function Phone(phone) { this._number = phone.number; } var phoneProto = { number: function() { return this._number; } }; phoneProto.number.set = function(val) { this._number = val; }; Phone.prototype = phoneProto; var person = new Person("pete", new Address("1st Ave"), []), message = '', ret = '', input, getResult = function(sep) { ret += (sep || "|") + input.val() + "/" + $("#result").text(); }; // ................................ Act .................................. jsv.templates('{^{:address()^street()}}').link("#result", person); input = $("#result input"); getResult(); $("#result input").val("InputStreet").change(); getResult("--"); jsv.observable(person.address()).setProperty("street", "oldAddressChgStreet"); getResult("--"); jsv.observable(person).setProperty("address", new Address("newAddressStreet")); getResult("--"); jsv.observable(person.address()).setProperty("street", "newAddressChgStreet"); getResult("--"); $("#result").empty(); // ............................... Assert ................................. assert.equal(ret, "|1st Ave/1st Ave--InputStreet/InputStreet--oldAddressChgStreet/oldAddressChgStreet--newAddressStreet/newAddressStreet--newAddressChgStreet/newAddressChgStreet", "Paths with computed/getters: address()^street() - Swapping object higher in path then updating leaf getter, works correctly"); // =============================== Arrange =============================== person = new Person("pete", new Address("1st Ave"), []); // ................................ Act .................................. ret = ""; jsv.templates('{^{:address()^street()}}').link("#result", person); input = $("#result input"); getResult(); input.val("InputStreet").change(); getResult("--"); jsv.observable(person.address()).setProperty("street", "oldAddressChgStreet"); getResult("--"); jsv.observable(person).setProperty("address", new Address("newAddressStreet")); getResult("--"); jsv.observable(person.address()).setProperty("street", "newAddressChgStreet"); getResult("--"); $("#result").empty(); // ............................... Assert ................................. assert.equal(ret, "|1st Ave/1st Ave--InputStreet/InputStreet--oldAddressChgStreet/oldAddressChgStreet--newAddressStreet/newAddressStreet--newAddressChgStreet/newAddressChgStreet", "Paths with computed/getters: address().street() - Paths with computed/getter followed by '.' still update preceding getter" + "- same as if there was a '^' separator"); // =============================== Arrange =============================== person = new Person("pete", new Address("1st Ave"), [new Phone({number: "phone1"}), new Phone({number: "phone2"})]); // ................................ Act .................................. ret = ""; jsv.templates('{^{:address()^street()}}').link("#result", person); var observeAllHandler = function(ev, eventArgs) { message += JSON.stringify(eventArgs) + "\n"; }; var eventsCountBefore = $._data(person).events.propertyChange.length + " " + $._data(person.address()).events.propertyChange.length + "|"; jsv.observable(person).observeAll(observeAllHandler); var eventsCountAfterObserveAll = $._data(person).events.propertyChange.length + " " + $._data(person.address()).events.propertyChange.length + "|"; input = $("#result input"); input.val("InputStreet").change(); jsv.observable(person.address()).setProperty("street", "oldAddressChgStreet"); jsv.observable(person).setProperty("address", new Address("newAddressStreet")); jsv.observable(person.address()).setProperty("street", "newAddressChgStreet"); var eventsCountAfterChanges = $._data(person).events.propertyChange.length + " " + $._data(person.address()).events.propertyChange.length + "|"; // ............................... Assert ................................. assert.equal(message, '{\"change\":\"set\",\"path\":\"street\",\"value\":\"InputStreet\",\"oldValue\":\"1st Ave\"}\n\ {\"change\":\"set\",\"path\":\"street\",\"value\":\"oldAddressChgStreet\",\"oldValue\":\"InputStreet\"}\n\ {\"change\":\"set\",\"path\":\"address\",\"value\":{\"_street\":\"newAddressStreet\"},\"oldValue\":{\"_street\":\"oldAddressChgStreet\"}}\n\ {\"change\":\"set\",\"path\":\"street\",\"value\":\"newAddressChgStreet\",\"oldValue\":\"newAddressStreet\"}\n', "Paths with computed/getters: address().street() - observeAll correctly tracks all changes on all objects, even as object graph changes"); // ................................ Act .................................. ret = ""; message = ""; jsv.observable(person).unobserveAll(observeAllHandler); var eventsCountAfterUnobserveAll = $._data(person).events.propertyChange.length + " " + $._data(person.address()).events.propertyChange.length + "|"; jsv.unobserve(person.address()); var eventsAfterUnobserveAddress = $._data(person).events.propertyChange.length + " " + !$._data(person.address()).events; input.val("InputStreetAfterUnobserve").change(); jsv.observable(person.address()).setProperty("street", "oldAddressChgStreetAfterUnobserve"); jsv.observable(person).setProperty("address", new Address("newAddressStreetAfterUnobserve")); jsv.observable(person.address()).setProperty("street", "newAddressChgStreetAfterUnobserve"); getResult("--"); $("#result").empty(); var eventsAfterEmptyTemplateContainer = !$._data(person).events + " " + !$._data(person.address()).events + "|"; // ............................... Assert ................................. assert.equal(message + ret + eventsCountBefore + eventsCountAfterObserveAll + eventsCountAfterChanges + eventsCountAfterUnobserveAll + eventsAfterUnobserveAddress + eventsAfterEmptyTemplateContainer, "--newAddressChgStreetAfterUnobserve/newAddressChgStreetAfterUnobserve2 2|3 3|3 3|2 2|2 truetrue true|", "Paths with computed/getters: address().street() - unobserveAll is successful"); // =============================== Arrange =============================== getResult = function(sep) { ret += (sep || "|") + $("#result").text(); }; person = new Person("pete", new Address("1st Ave"), [new Phone({number: "phone1"}), new Phone({number: "phone2"})]); // ................................ Act .................................. ret = ""; jsv.templates('{^{for phones()}}{^{:number()}},{{/for}}').link("#result", person); getResult("\nInit>>"); jsv.observable(person.phones()).insert(new Phone({number: "insertedPhone"})); getResult("insert:"); jsv.observable(person.phones()).remove(0); getResult("remove:"); jsv.observable(person.phones()).refresh([new Phone({number: "replacedPhone1"}), new Phone({number: "replacedPhone2"})]); getResult("refresh:"); jsv.observable(person.phones()).insert(1, [new Phone({number: "insertedPhone3a"}), new Phone({number: "insertedPhone3b"})]); getResult("insert:"); jsv.observable(person.phones()).move(1, 3, 2); getResult(" move:"); jsv.observable(person).setProperty("phones", [new Phone({number: "replacedPhone1"})]); getResult("\nSet>>"); jsv.observable(person.phones()).insert(new Phone({number: "insertedPhoneX"})); getResult("insert:"); jsv.observable(person.phones()).remove(0); getResult("remove:"); jsv.observable(person.phones()).refresh([new Phone({number: "replacedPhoneX1"}), new Phone({number: "replacedPhoneX2"})]); getResult("refresh:"); jsv.observable(person.phones()).insert(1, [new Phone({number: "insertedPhoneX3a"}), new Phone({number: "insertedPhoneX3b"})]); getResult("insert:"); jsv.observable(person.phones()).move(1, 3, 2); getResult("move:"); jsv.observable(person).setProperty("phones", []); getResult("\nsetEmpty>>"); jsv.observable(person.phones()).insert(new Phone({number: "insertedPhoneY"})); getResult("insert:"); $("#result").empty(); // ............................... Assert ................................. assert.equal(ret, "\nInit>>phone1,phone2,insert:phone1,phone2,insertedPhone,remove:phone2,insertedPhone,refresh:replacedPhone1,replacedPhone2,insert:replacedPhone1,insertedPhone3a,insertedPhone3b,replacedPhone2, move:replacedPhone1,replacedPhone2,insertedPhone3a,insertedPhone3b,\ \nSet>>replacedPhone1,insert:replacedPhone1,insertedPhoneX,remove:insertedPhoneX,refresh:replacedPhoneX1,replacedPhoneX2,insert:replacedPhoneX1,insertedPhoneX3a,insertedPhoneX3b,replacedPhoneX2,move:replacedPhoneX1,replacedPhoneX2,insertedPhoneX3a,insertedPhoneX3b,\ \nsetEmpty>>insert:insertedPhoneY,", "Array operations with getters allow complete functionality, and track the modified tree at all times"); // =============================== Arrange =============================== person = new Person("pete", new Address("1st Ave"), [new Phone({number: "phone1"}), new Phone({number: "phone2"})]); // ................................ Act .................................. ret = ""; jsv.templates('{^{for phones()}}{^{:number()}},{{/for}}').link("#result", person); eventsCountBefore = $._data(person).events.propertyChange.length + " " + $._data(person.phones()).events.arrayChange.length + " " + $._data(person.phones()[0]).events.propertyChange.length + "|"; jsv.observable(person).observeAll(observeAllHandler); eventsCountAfterObserveAll = $._data(person).events.propertyChange.length + " " + $._data(person.phones()).events.arrayChange.length + " " + $._data(person.phones()[0]).events.propertyChange.length + "|"; jsv.observable(person.phones()).insert(new Phone({number: "insertedPhone"})); jsv.observable(person.phones()).remove(0); jsv.observable(person.phones()).refresh([new Phone({number: "replacedPhone1"}), new Phone({number: "replacedPhone2"})]); jsv.observable(person.phones()).insert(1, [new Phone({number: "insertedPhone3a"}), new Phone({number: "insertedPhone3b"})]); jsv.observable(person.phones()).move(1, 3, 2); jsv.observable(person).setProperty("phones", [new Phone({number: "replacedPhone1"})]); jsv.observable(person.phones()).insert(new Phone({number: "insertedPhoneX"})); jsv.observable(person.phones()).remove(0); jsv.observable(person.phones()).refresh([new Phone({number: "replacedPhoneX1"}), new Phone({number: "replacedPhoneX2"})]); jsv.observable(person.phones()).insert(1, [new Phone({number: "insertedPhoneX3a"}), new Phone({number: "insertedPhoneX3b"})]); jsv.observable(person.phones()).move(1, 3, 2); jsv.observable(person).setProperty("phones", []); jsv.observable(person.phones()).insert(new Phone({number: "insertedPhoneY"})); jsv.observable(person.phones()[0]).setProperty("number", "newNumber"); eventsCountAfterChanges = $._data(person).events.propertyChange.length + " " + $._data(person.phones()).events.arrayChange.length + " " + $._data(person.phones()[0]).events.propertyChange.length + "|"; // ............................... Assert ................................. assert.equal(message, '{\"change\":\"insert\",\"index\":2,\"items\":[{\"_number\":\"insertedPhone\"}]}\n\ {\"change\":\"remove\",\"index\":0,\"items\":[{\"_number\":\"phone1\"}]}\n\ {\"change\":\"insert\",\"index\":0,\"items\":[{\"_number\":\"replacedPhone1\"},{\"_number\":\"replacedPhone2\"}],\"refresh\":true}\n\ {\"change\":\"remove\",\"index\":2,\"items\":[{\"_number\":\"phone2\"},{\"_number\":\"insertedPhone\"}],\"refresh\":true}\n\ {\"change\":\"refresh\",\"oldItems\":[{\"_number\":\"phone2\"},{\"_number\":\"insertedPhone\"}]}\n\ {\"change\":\"insert\",\"index\":1,\"items\":[{\"_number\":\"insertedPhone3a\"},{\"_number\":\"insertedPhone3b\"}]}\n\ {\"change\":\"move\",\"oldIndex\":1,\"index\":2,\"items\":[{\"_number\":\"insertedPhone3a\"},{\"_number\":\"insertedPhone3b\"}]}\n\ {\"change\":\"set\",\"path\":\"phones\",\"value\":[{\"_number\":\"replacedPhone1\"}],\"oldValue\":[{\"_number\":\"replacedPhone1\"},{\"_number\":\"replacedPhone2\"},{\"_number\":\"insertedPhone3a\"},{\"_number\":\"insertedPhone3b\"}]}\n\ \ {\"change\":\"insert\",\"index\":1,\"items\":[{\"_number\":\"insertedPhoneX\"}]}\n\ {\"change\":\"remove\",\"index\":0,\"items\":[{\"_number\":\"replacedPhone1\"}]}\n\ {\"change\":\"insert\",\"index\":0,\"items\":[{\"_number\":\"replacedPhoneX1\"},{\"_number\":\"replacedPhoneX2\"}],\"refresh\":true}\n\ {\"change\":\"remove\",\"index\":2,\"items\":[{\"_number\":\"insertedPhoneX\"}],\"refresh\":true}\n\ {\"change\":\"refresh\",\"oldItems\":[{\"_number\":\"insertedPhoneX\"}]}\n\ {\"change\":\"insert\",\"index\":1,\"items\":[{\"_number\":\"insertedPhoneX3a\"},{\"_number\":\"insertedPhoneX3b\"}]}\n\ {\"change\":\"move\",\"oldIndex\":1,\"index\":2,\"items\":[{\"_number\":\"insertedPhoneX3a\"},{\"_number\":\"insertedPhoneX3b\"}]}\n\ \ {\"change\":\"set\",\"path\":\"phones\",\"value\":[],\"oldValue\":[{\"_number\":\"replacedPhoneX1\"},{\"_number\":\"replacedPhoneX2\"},{\"_number\":\"insertedPhoneX3a\"},{\"_number\":\"insertedPhoneX3b\"}]}\n\ \ {\"change\":\"insert\",\"index\":0,\"items\":[{\"_number\":\"insertedPhoneY\"}]}\n\ {\"change\":\"set\",\"path\":\"number\",\"value\":\"newNumber\",\"oldValue\":\"insertedPhoneY\"}\n', "Paths with computed/getters: phones() - observeAll correctly tracks all array operations, even as object graph changes"); // ................................ Act .................................. ret = ""; message = ""; jsv.observable(person).unobserveAll(observeAllHandler); eventsCountAfterUnobserveAll = $._data(person).events.propertyChange.length + " " + $._data(person.phones()).events.arrayChange.length + " " + $._data(person.phones()[0]).events.propertyChange.length + "|"; jsv.unobserve(person.phones()); var eventsAfterUnobservePhones = $._data(person).events.propertyChange.length + " " + !$._data(person.phones()).events + " " + $._data(person.phones()[0]).events.propertyChange.length + "|"; jsv.observable(person.phones()).insert(new Phone({number: "insertedPhoneZ"})); jsv.observable(person.phones()[0]).setProperty("number", "newNumberZ"); $("#result").empty(); eventsAfterEmptyTemplateContainer = !$._data(person).events + " " + !$._data(person.phones()).events + " " + !$._data(person.phones()[0]).events + "|"; // ............................... Assert ................................. assert.equal(message + ret + eventsCountBefore + eventsCountAfterObserveAll + eventsCountAfterChanges + eventsCountAfterUnobserveAll + eventsAfterUnobservePhones + eventsAfterEmptyTemplateContainer, "1 1 1|2 2 2|2 2 2|1 1 1|1 true 1|true true true|", "Paths with computed/getters: address().street() - unobserveAll is successful"); jsv.views.settings.trigger(true); }); QUnit.test("jsv.views.viewModels", function(assert) { // =============================== Arrange =============================== var Constr = jsv.views.viewModels({getters: ["a", "b"]}); // ................................ Act .................................. var vm = Constr("a1 ", "b1 "); var res = vm.a() + vm.b(); vm.a("a2 "); vm.b("b2 "); res += vm.a() + vm.b(); // ............................... Assert ................................. assert.equal(res, "a1 b1 a2 b2 ", "viewModels, two getters, no methods"); // =============================== Arrange =============================== Constr = jsv.views.viewModels({getters: ["a", "b", "c"], extend: {add: function(val) { this.c(val + this.a() + this.b() + this.c()); }}}); // ................................ Act .................................. vm = Constr("a1 ", "b1 ", "c1 "); vm.add("before "); res = vm.c(); // ............................... Assert ................................. assert.equal(res, "before a1 b1 c1 ", "viewModels, two getters, one method"); // =============================== Arrange =============================== Constr = jsv.views.viewModels({extend: {add: function(val) { this.foo = val; }}}); // ................................ Act .................................. vm = Constr(); vm.add("before"); res = vm.foo; // ............................... Assert ................................. assert.equal(res, "before", "viewModels, no getters, one method"); // =============================== Arrange =============================== Constr = jsv.views.viewModels({getters: []}); // ................................ Act .................................. vm = Constr(); res = JSON.stringify(vm); // ............................... Assert ................................. assert.equal(res, "{}", "viewModels, no getters, no methods"); // =============================== Arrange =============================== jsv.views.viewModels({ T1: { getters: ["a", "b"] } }); // ................................ Act .................................. vm = jsv.views.viewModels.T1.map({a: "a1 ", b: "b1 "}); var changes = ""; function observeAllHandler(ev, evArgs) { changes += evArgs.value; } jsv.observable(vm).observeAll(observeAllHandler); res = vm.a() + vm.b(); vm.a("a2 "); vm.b("b2 "); res += vm.a() + vm.b(); // ............................... Assert ................................. assert.equal(res + "|" + changes, "a1 b1 a2 b2 |a2 b2 ", "viewModels, two getters, no methods"); changes = ""; // ................................ Act .................................. vm.merge({a: "a3 ", b: "b3 "}); res = vm.a() + vm.b(); // ............................... Assert ................................. assert.equal(res + "|" + changes, "a3 b3 |a3 b3 ", "viewModels merge, two getters, no methods"); changes = ""; // ................................ Act .................................. res = vm.unmap(); res = JSON.stringify(res); // ............................... Assert ................................. assert.equal(res, '{"a":"a3 ","b":"b3 "}', "viewModels unmap, two getters, no methods"); // ............................... Reset ................................. jsv.unobserve(observeAllHandler); // =============================== Arrange =============================== var viewModels = jsv.views.viewModels({ T1: { getters: ["a", {getter: "b"}, "c", "d", {getter: "e", type: undefined}, {getter: "f", type: null}, {getter: "g", type: "foo"}, {getter: "h", type: ""}] } }, {}); // ................................ Act .................................. vm = viewModels.T1.map({a: "a1 ", b: "b1 ", c: "c1 ", d: "d1 ", e: "e1 ", f: "f1 ", g: "g1 ", h: "h1 "}); jsv.observable(vm).observeAll(observeAllHandler); res = vm.a() + vm.b() + vm.c() + vm.d() + vm.e() + vm.f() + vm.g() + vm.h(); vm.a("a2 "); vm.b("b2 "); res += vm.a() + vm.b(); // ............................... Assert ................................. assert.equal(res + "|" + changes, "a1 b1 c1 d1 e1 f1 g1 h1 a2 b2 |a2 b2 ", "viewModels, multiple unmapped getters, no methods"); changes = ""; // ................................ Act .................................. vm.merge({a: "a3 ", b: "b3 ", c: "c3 ", d: "d3 ", e: "e3 ", f: "f3 ", g: "g3 ", h: "h3 "}); res = vm.a() + vm.b() + vm.c() + vm.d() + vm.e() + vm.f() + vm.g() + vm.h(); // ............................... Assert ................................. assert.equal(res + "|" + changes, "a3 b3 c3 d3 e3 f3 g3 h3 |a3 b3 c3 d3 e3 f3 g3 h3 ", "viewModels merge, multiple unmapped getters, no methods"); changes = ""; // ................................ Act .................................. res = vm.unmap(); res = JSON.stringify(res); // ............................... Assert ................................. assert.equal(res, '{"a":"a3 ","b":"b3 ","c":"c3 ","d":"d3 ","e":"e3 ","f":"f3 ","g":"g3 ","h":"h3 "}', "viewModels unmap, multiple unmapped getters, no methods"); // ............................... Reset ................................. jsv.unobserve(observeAllHandler); // =============================== Arrange =============================== jsv.views.viewModels({ T1: { getters: ["a", "b", "c"], extend : { add: function(val) { this.c(val + this.a() + this.b() + this.c()); } } } }); // ................................ Act .................................. vm = jsv.views.viewModels.T1.map({a: "a1 ", b: "b1 ", c: "c1 "}); jsv.observable(vm).observeAll(observeAllHandler); vm.add("before "); res = vm.c(); // ............................... Assert ................................. assert.equal(res + "|" + changes, "before a1 b1 c1 |before a1 b1 c1 ", "viewModels, getters and one method"); changes = ""; // ................................ Act .................................. vm.merge({a: "a3 ", b: "b3 ", c: "c3 "}); vm.add("updated "); res = vm.c(); // ............................... Assert ................................. assert.equal(res + "|" + changes, "updated a3 b3 c3 |a3 b3 c3 updated a3 b3 c3 ", "viewModels merge, getters and one method"); changes = ""; // ................................ Act .................................. res = vm.unmap(); res = JSON.stringify(res); // ............................... Assert ................................. assert.equal(res, '{"a":"a3 ","b":"b3 ","c":"updated a3 b3 c3 "}', "viewModels unmap, getters and one method"); changes = ""; // ............................... Reset ................................. jsv.unobserve(observeAllHandler); // =============================== Arrange =============================== jsv.views.viewModels({ T1: { getters: ["a", "b"] }, T2: { getters: [{getter: "t1", type: "T1"}, {getter: "t1Arr", type: "T1"}, {getter: "t1OrNull", type: "T1", defaultVal: null}] } }); viewModels = jsv.views.viewModels; // ................................ Act .................................. var t1 = viewModels.T1.map({a: "a1 ", b: "b1 "}); // Create a T1 var t2 = viewModels.T2.map({t1: {a: "a3 ", b: "b3 "}, t1Arr: [t1.unmap(), {a: "a2 ", b: "b2 "}]}); // Create a T2 (using unmap to scrape values the T1: vm) jsv.observable(t1).observeAll(observeAllHandler); jsv.observable(t2).observeAll(observeAllHandler); res = JSON.stringify(t2.unmap()); // ............................... Assert ................................. assert.equal(res, '{"t1":{"a":"a3 ","b":"b3 "},"t1Arr":[{"a":"a1 ","b":"b1 "},{"a":"a2 ","b":"b2 "}],"t1OrNull":null}', "viewModels, hierarchy"); // ................................ Act .................................. t2.t1Arr()[0].merge({a: "a1x ", b: "b1x "}); // merge not the root, but a VM instance within hierarchy: vm2.t1Arr()[0] - leaving rest unchanged res = JSON.stringify(t2.unmap()); // ............................... Assert ................................. assert.equal(res + "|" + changes, '{"t1":{"a":"a3 ","b":"b3 "},"t1Arr":[{"a":"a1x ","b":"b1x "},{"a":"a2 ","b":"b2 "}],"t1OrNull":null}|a1x b1x ', "viewModels, merge deep node"); changes = ""; // ............................... Reset ................................. jsv.unobserve(observeAllHandler); // ................................ Act .................................. var t1Arr = viewModels.T1.map([{a: "a1 ", b: "b1 "}, {a: "a2 ", b: "b2 "}]); // Create a T1 array var t2FromArr = viewModels.T2.map({t1: {a: "a3 ", b: "b3 "}, t1Arr: t1Arr.unmap()}); // Create a T2 (using unmap to scrape values the T1: vm) res = JSON.stringify(t2FromArr.unmap()); // ............................... Assert ................................. assert.equal(res, '{"t1":{"a":"a3 ","b":"b3 "},"t1Arr":[{"a":"a1 ","b":"b1 "},{"a":"a2 ","b":"b2 "}],"t1OrNull":null}', "viewModels, hierarchy"); // ................................ Act .................................. t1Arr = viewModels.T1.map([{a: "a1 ", b: "b1 "}, {a: "a2 ", b: "b2 "}]); // Create a T1 array t1Arr.push(viewModels.T1("a3 ", "b3 ")); t2FromArr = viewModels.T2.map({t1: {a: "a4 ", b: "b4 "}, t1Arr: t1Arr.unmap()}); // Create a T2 (using unmap to scrape values the T1: vm) res = JSON.stringify(t2FromArr.unmap()); // ............................... Assert ................................. assert.equal(res, '{"t1":{"a":"a4 ","b":"b4 "},"t1Arr":[{"a":"a1 ","b":"b1 "},{"a":"a2 ","b":"b2 "},{"a":"a3 ","b":"b3 "}],"t1OrNull":null}', "viewModels, hierarchy"); // ................................ Act .................................. var t2new = viewModels.T2(viewModels.T1("a3 ", "b3 "), [viewModels.T1("a1 ", "b1 "), viewModels.T1("a2 ", "b2 ")], viewModels.T1("a4 ", "b4 ") ); res = JSON.stringify(t2new.unmap()); // ............................... Assert ................................. assert.equal(res, '{"t1":{"a":"a3 ","b":"b3 "},"t1Arr":[{"a":"a1 ","b":"b1 "},{"a":"a2 ","b":"b2 "}],"t1OrNull":{"a":"a4 ","b":"b4 "}}', "viewModels, hierarchy"); }); QUnit.module("API - depends"); QUnit.test("Computed observables, converters and tags with depends", function(assert) { // =============================== Arrange =============================== function testDepends(template) { // =============================== Arrange =============================== var ret = "", items = ["first"], items2 = ["new0", "new1"], app = { show: true, name: "Jo", itemsProp: items, summary: summary }; jsv.templates(template).link("#result", app, {summary: summary}); ret += "|1:" + $("#result").text(); // ................................ Act .................................. jsv.observable(app).setProperty("name", "Bob"); ret += "|2:" + $("#result").text(); jsv.observable(app.itemsProp).insert(0, "previous"); ret += "|3:" + $("#result").text(); jsv.observable(app).setProperty("name", "Jim"); ret += "|4:" + $("#result").text(); jsv.observable(app).setProperty("itemsProp", items2); ret += "|5:" + $("#result").text(); jsv.observable(app.itemsProp).move(0, 1); ret += "|6:" + $("#result").text(); jsv.observable(app.itemsProp).insert(0, "newPrev"); ret += "|7:" + $("#result").text(); jsv.observable(app).setProperty("name", "Jeff"); ret += "|8:" + $("#result").text() + "/" + !$._data(items).events + "/" + $._data(items2).events.arrayChange.length; jsv.observable(app).setProperty("show", false); ret += "|9:" + $("#result").text() + "/" + !$._data(items).events + "/" + !$._data(items2).events; jsv.observable(app).setProperty("show", true); ret += "|10:" + $("#result").text() + "/" + !$._data(items).events + "/" + $._data(items2).events.arrayChange.length; jsv.observable(app.itemsProp).insert(0, "extraPrev"); ret += "|11:" + $("#result").text() + "/" + !$._data(items).events + "/" + $._data(items2).events.arrayChange.length; // ................................ Reset ................................ $("#result").empty(); jsv.unobserve(app); return ret === "|1:first-1-Jo" + "|2:first-1-Bob" + "|3:previous-2-Bob" + "|4:previous-2-Jim" + "|5:new0-2-Jim" + "|6:new1-2-Jim" + "|7:newPrev-3-Jim" + "|8:newPrev-3-Jeff" + "/true/1" + "|9:/true/true" + "|10:newPrev-3-Jeff" + "/true/1" + "|11:extraPrev-4-Jeff" + "/true/1"; } // =============================== Arrange =============================== var summary = function() { return this.itemsProp[0] + "-" + this.itemsProp.length + "-" + this.name; }; summary.depends = ["itemsProp", "name"]; // ................................ Assert .................................. assert.ok(testDepends("{^{if show}}{^{:summary()}}{{/if}}"), 'Computed observable with depends = ["itemsProp", ...] updates for both array change and property change'); // =============================== Arrange =============================== summary.depends = function(object, callback) { jsv.observe(object, "itemsProp", callback); return "name"; }; // ................................ Assert .................................. assert.ok(testDepends("{^{if show}}{^{:summary()}}{{/if}}"), 'Computed observable with depends function programmatically observing array, with the callback provided as parameter. Works equivalently to the declarative depends for an array'); // =============================== Arrange =============================== var test; summary.depends = function(data1) { var this1 = this; return function (data2) { var this2 = this; return [function (data3) { var this3 = this; test = this1 === data1 && this1 === data2 && this1 === data3 && this1 === this2 && this1 === this3; return "itemsProp"; }, "name"]; }; }; // ................................ Assert .................................. assert.ok(testDepends("{^{if show}}{^{:summary()}}{{/if}}") && test, 'Computed observable with depends including several nested function calls returning (finally) "itemsProp" updates for both array change and property change, and has correct this pointers and data arguments'); // =============================== Arrange =============================== summary.depends = function() { return function (object, callback) { jsv.observe(object, "itemsProp", callback); return [function () { jsv.observe(object, "name", callback); }]; }; }; // ................................ Assert .................................. assert.ok(testDepends("{^{if show}}{^{:summary()}}{{/if}}"), 'Computed observable with depends including several nested function calls (finally) programmatically observing both array and property. Works equivalently to declarative version'); // =============================== Arrange =============================== function listenToArray(object, callback) { jsv.observe(object, "itemsProp", callback); } function listenToName(object, callback) { jsv.observe(object, "name", callback); } summary.depends = [listenToArray, listenToName]; // ................................ Assert .................................. assert.ok(testDepends("{^{if show}}{^{:summary()}}{{/if}}"), 'Computed observable with depends using independently declared functions to programmatically observe any fields. Works equivalently to declarative version'); // =============================== Arrange =============================== function listenTo(field) { return function(object, callback) { jsv.observe(object, field, callback); } } summary.depends = [listenTo("itemsProp"), listenTo("name")]; // ................................ Assert .................................. assert.ok(testDepends("{^{if show}}{^{:summary()}}{{/if}}"), 'Computed observable with depends using generated function to programmatically observe any fields. Works equivalently to declarative version'); // =============================== Arrange =============================== var summary = function() { var data = this.data; // 'this' is the view return data.itemsProp[0] + "-" + data.itemsProp.length + "-" + data.name; } summary.depends = ["itemsProp", "name"]; // ................................ Assert .................................. assert.ok(testDepends("{^{if show}}{^{:~summary()}}{{/if}}"), 'Computed observable helper depends = ["itemsProp", ...] updates for both array change and property change'); // =============================== Arrange =============================== var summary = function() { var data = this.tagCtx.view.data; // 'this' is the tag instance return data.itemsProp[0] + "-" + data.itemsProp.length + "-" + data.name; } summary.depends = ["itemsProp", "name"]; // ................................ Assert .................................. assert.ok(testDepends("{^{if show}}{^{:'foo' convert=~summary}}{{/if}}"), 'Converter (passed as helper) with depends = ["itemsProp", ...] updates for both array change and property change'); // =============================== Arrange =============================== jsv.views.converters("sumry", summary); // ................................ Assert .................................. assert.ok(testDepends("{^{if show}}{^{sumry:'foo'}}{{/if}}"), 'Registered converter with depends = ["itemsProp", ...] updates for both array change and property change'); jsv.views.converters("sumry", null); // =============================== Arrange =============================== summary.depends = null; jsv.views.tags("sumry", { render: summary, depends: ["itemsProp", "name"] }); // ................................ Assert .................................. assert.ok(testDepends("{^{if show}}{^{sumry/}}{{/if}}"), 'Registered tag with depends = ["itemsProp", ...] updates for both array change and property change'); jsv.views.tags("sumry", null); // =============================== Arrange =============================== var ret = "", items = ["first"], items2 = ["new0", "new1"], app = { show: true, name: "Jo", itemsProp: items, summary: summary }; jsv.templates("{^{for").link("#result", app, {summary: summary}); ret += "|1:" + $("#result").text(); // ................................ Act .................................. jsv.observable(app).setProperty("name", "Bob"); ret += "|2:" + $("#result").text(); jsv.observable(app.itemsProp).insert(0, "previous"); ret += "|3:" + $("#result").text(); jsv.observable(app).setProperty("name", "Jim"); // =============================== Arrange =============================== jsv.views.settings.trigger(false); function fullName() { var person = this.ctxPrm ? this.ctxPrm("prson") : this; return person.title + " " + person.name + " " + person.address.street; } fullName.depends = function(data) { return [this, "title", "name", "address^street"]; }; fullName.set = function(val) { var parts = val.split(" "); jsv.observable(this).setProperty({ title: parts.shift(), name: parts.shift(), address: {street: parts.join(" ")} }); }; var prson = { title: "Sir", name: "Jo", address: {street: "1st Ave"}, fullName: fullName, }; jsv.views.templates({ markup: '{^{:~prson.title}} ' + '{^{:~prson.name}} ' + '{^{:~prson.address^street}} ' + 'Full: {^{:~prson.fullName()}} ' + '{^{mytag ~prson.fullName}}' + ' CTP: {^{:~ctp()}}' + '' + '{{/mytag}}' + ' FULLNAMETAG: {^{fullName ~prson/}}', tags: { mytag: { linkedCtxParam: "ctp", onUpdate: false // Could also be true }, fullName: { linkedCtxParam: ["prsn"], render: function(person) { return person.title + " " + person.name + " " + person.address.street + " "; }, depends: function(data) { return [this.tagCtx.args[0], "title", "name", "address^street"]; } } } }).link("#result", 1, { prson: prson }); var input, result = $("#result").text(); // ................................ Act .................................. jsv.observable(prson).setProperty("address", {street: "2nd St"}); result += "\nADDRESS: " + $("#result").text(); input = $("#result input")[0]; $(input).val(input.value + "+").change(); result += "\nTITLE: " + $("#result").text(); input = $("#result input")[1]; $(input).val(input.value + "+").change(); result += "\nNAME: " + $("#result").text(); input = $("#result input")[2]; $(input).val(input.value + "+").change(); result += "\nSTREET: " + $("#result").text(); input = $("#result input")[3]; $(input).val("Mr Bob FullSt").change(); result += "\nFULL: " + $("#result").text(); input = $("#result input")[4]; $(input).val("Lady Jane CtpSt").change(); result += "\nCTP: " + $("#result").text(); input = $("#result input")[5]; $(input).val("Ms Anne TagSt").change(); result += "\nTAG: " + $("#result").text(); // ............................... Assert ................................. var expected = "Sir Jo 1st Ave Full: Sir Jo 1st Ave CTP: Sir Jo 1st Ave FULLNAMETAG: Sir Jo 1st Ave \n" + "ADDRESS: Sir Jo 2nd St Full: Sir Jo 2nd St CTP: Sir Jo 2nd St FULLNAMETAG: Sir Jo 2nd St \n" + "TITLE: Sir+ Jo 2nd St Full: Sir+ Jo 2nd St CTP: Sir+ Jo 2nd St FULLNAMETAG: Sir+ Jo 2nd St \n" + "NAME: Sir+ Jo+ 2nd St Full: Sir+ Jo+ 2nd St CTP: Sir+ Jo+ 2nd St FULLNAMETAG: Sir+ Jo+ 2nd St \n" + "STREET: Sir+ Jo+ 2nd St+ Full: Sir+ Jo+ 2nd St+ CTP: Sir+ Jo+ 2nd St+ FULLNAMETAG: Sir+ Jo+ 2nd St+ \n" + "FULL: Mr Bob FullSt Full: Mr Bob FullSt CTP: Mr Bob FullSt FULLNAMETAG: Mr Bob FullSt \n" + "CTP: Lady Jane CtpSt Full: Lady Jane CtpSt CTP: Lady Jane CtpSt FULLNAMETAG: Lady Jane CtpSt \n" + "TAG: Ms Anne TagSt Full: Ms Anne TagSt CTP: Ms Anne TagSt FULLNAMETAG: Ms Anne TagSt "; assert.equal(result, expected, "Custom tag control {{fullName ...}} using render, with linkedCtxParam, depends, onUpdate not false"); // =============================== Arrange =============================== prson = { title: "Sir", name: "Jo", fullName: fullName, address: {street: "1st Ave"} }; jsv.views.templates({ markup: '{^{:~prson.title}} ' + '{^{:~prson.name}} ' + '{^{:~prson.address^street}} ' + 'Full: {^{:~prson.fullName()}} ' + '{^{mytag ~prson.fullName}}' + ' CTP: {^{:~ctp()}}' + '' + '{{/mytag}}' + ' FULLNAMETAG: {^{fullName ~prson/}}', tags: { mytag: { linkedCtxParam: "ctp", onUpdate: false }, fullName: { linkedCtxParam: ["prsn"], template: "{{:~prsn.title}} {{:~prsn.name}} {{:~prsn.address.street}} ", depends: function(data) { return [this.tagCtx.args[0], "title", "name", "address^street"]; } } } }).link("#result", 1, { prson: prson }); var input, result = $("#result").text(); // ................................ Act .................................. jsv.observable(prson).setProperty("address", {street: "2nd St"}); result += "\nADDRESS: " + $("#result").text(); input = $("#result input")[0]; $(input).val(input.value + "+").change(); result += "\nTITLE: " + $("#result").text(); input = $("#result input")[1]; $(input).val(input.value + "+").change(); result += "\nNAME: " + $("#result").text(); input = $("#result input")[2]; $(input).val(input.value + "+").change(); result += "\nSTREET: " + $("#result").text(); input = $("#result input")[3]; $(input).val("Mr Bob FullSt").change(); result += "\nFULL: " + $("#result").text(); input = $("#result input")[4]; $(input).val("Lady Jane CtpSt").change(); result += "\nCTP: " + $("#result").text(); input = $("#result input")[5]; $(input).val("Ms Anne TagSt").change(); result += "\nTAG: " + $("#result").text(); // ............................... Assert ................................. assert.equal(result, expected, "Custom tag control {{fullName ...}} using template, with linkedCtxParam, depends, onUpdate not false - works correctly"); // =============================== Arrange =============================== prson = { title: "Sir", name: "Jo", fullName: fullName, address: {street: "1st Ave"} }; jsv.views.templates({ markup: 'Title: {^{:person.title}} ' + 'Name: {^{:person.name}} ' + 'Street: {^{:person^address.street}} ' + 'Full: {^{:person.fullName()}} ' + 'TAG: {^{mytag person.fullName ~prson=person}}' // Bind to function itself + '{^{:~ctp()}}' + ' ' + '{{/mytag}}' + 'TAG2: {^{mytag person.fullName() ~prson=person}}' // Bind to evaluated function + '{^{:~ctp}}' + ' ' + '{{/mytag}}', tags: { mytag: { linkedCtxParam: "ctp", onUpdate: false } } }).link("#result", { person: prson }); var input, result = $("#result").text(); // ................................ Act .................................. jsv.observable(prson).setProperty("title","Sir2"); result += "\nTITLE: " + $("#result").text(); jsv.observable(prson).setProperty("name","Jo2"); result += "\nNAME: " + $("#result").text(); jsv.observable(prson).setProperty("address", {street: "2nd St"}); result += "\nADDRESS: " + $("#result").text(); input = $("#result input")[0]; $(input).val("Mr Bob FullSt").change(); result += "\nCTP FUNCTION: " + $("#result").text(); input = $("#result input")[1]; $(input).val("Mr Bob FullSt").change(); result += "\nCTP EVALUATED FUNCTION: " + $("#result").text(); // ............................... Assert ................................. assert.equal(result, "Title: Sir Name: Jo Street: 1st Ave Full: Sir Jo 1st Ave TAG: Sir Jo 1st Ave TAG2: Sir Jo 1st Ave \n" + "TITLE: Title: Sir2 Name: Jo Street: 1st Ave Full: Sir2 Jo 1st Ave TAG: Sir2 Jo 1st Ave TAG2: Sir2 Jo 1st Ave \n" + "NAME: Title: Sir2 Name: Jo2 Street: 1st Ave Full: Sir2 Jo2 1st Ave TAG: Sir2 Jo2 1st Ave TAG2: Sir2 Jo2 1st Ave \n" + "ADDRESS: Title: Sir2 Name: Jo2 Street: 2nd St Full: Sir2 Jo2 2nd St TAG: Sir2 Jo2 2nd St TAG2: Sir2 Jo2 2nd St \n" + "CTP FUNCTION: Title: Mr Name: Bob Street: FullSt Full: Mr Bob FullSt TAG: Mr Bob FullSt TAG2: Mr Bob FullSt \n" + "CTP EVALUATED FUNCTION: Title: Mr Name: Bob Street: FullSt Full: Mr Bob FullSt TAG: Mr Bob FullSt TAG2: Mr Bob FullSt ", "Custom tag control {{fullName ...}} with linkedCtxParam 2-way binding to computed property - either the function itself, or the evaluated function result"); // =============================== Arrange =============================== jsv.views.templates({ markup: '{^{personname person ~psn2=person/}}' // binding to person + '{{for person}}' + '{^{personname #data ~psn2=#data/}}' // binding to #data as default argument + '{^{personname ~psn2=#data/}}' // binding to #data as default argument + '{{/for}}', tags: { personname: { template: "", linkedCtxParam: "psn" } } }).link("#result", {person:{name: "Jo"}}); // ................................ Act .................................. var getValues = function() { result += inputs[0].value + " " + inputs[1].value + " " + inputs[2].value + " " + inputs[3].value + " " + inputs[4].value + " " + inputs[5].value + "\n"; } result = ""; var inputs = $("#result input"); getValues(); $(inputs[0]).val("Jo2").change(); getValues(); $(inputs[1]).val("Jo3").change(); getValues(); $(inputs[2]).val("Jo4").change(); getValues(); $(inputs[3]).val("Jo5").change(); getValues(); $(inputs[4]).val("Jo6").change(); getValues(); $(inputs[5]).val("Jo7").change(); getValues(); // ............................... Assert ................................. assert.equal(result, "Jo Jo Jo Jo Jo Jo\n" + "Jo2 Jo2 Jo2 Jo2 Jo2 Jo2\n" + "Jo3 Jo3 Jo3 Jo3 Jo3 Jo3\n" + "Jo4 Jo4 Jo4 Jo4 Jo4 Jo4\n" + "Jo5 Jo5 Jo5 Jo5 Jo5 Jo5\n" + "Jo6 Jo6 Jo6 Jo6 Jo6 Jo6\n" + "Jo7 Jo7 Jo7 Jo7 Jo7 Jo7\n", "Custom tag control {{personname ...}} using template, with linkedCtxParam and tag contextual parameter, binding to #data, and with 2-way binding to person.name - works correctly"); // =============================== Arrange =============================== jsv.views.templates({ markup: '{^{textbox path=person/}}' + '

                    ' + '{^{textbox2 path=person.name/}}' + '

                    ' + '{^{textbox3 path=person.name/}}' + '
                    ' + '
                    ' + '
                    ', tags: { textbox: { bindTo: "path", linkedCtxParam: "psn", template:"", onUpdate: false }, textbox2: { bindTo: "path", linkedCtxParam: "nm", template:"", onUpdate: false }, textbox3: { bindTo: "path", linkedElement: "input", template:"", onUpdate: false } } }).link("#result", {person:{name: "Jo"}}); // ................................ Act .................................. var getValues = function() { result += inputs[0].value + " " + inputs[1].value + " " + inputs[2].value + " " + inputs[3].value + " " + inputs[4].value + " " + inputs[5].value + " " + inputs[6].value + " " + inputs[7].value + "\n"; } result = ""; var inputs = $("#result input"); getValues(); $(inputs[0]).val("Jo0").change(); getValues(); $(inputs[1]).val("Jo1").change(); getValues(); $(inputs[2]).val("Jo2").change(); getValues(); $(inputs[3]).val("Jo3").change(); getValues(); $(inputs[4]).val("Jo4").change(); getValues(); $(inputs[5]).val("Jo5").change(); getValues(); $(inputs[6]).val("Jo6").change(); getValues(); $(inputs[7]).val("Jo7").change(); getValues(); // ............................... Assert ................................. assert.equal(result, "Jo Jo Jo Jo Jo Jo Jo Jo\n" + "Jo0 Jo0 Jo0 Jo0 Jo0 Jo0 Jo0 Jo0\n" + "Jo1 Jo1 Jo1 Jo1 Jo1 Jo1 Jo1 Jo1\n" + "Jo2 Jo2 Jo2 Jo2 Jo2 Jo2 Jo2 Jo2\n" + "Jo3 Jo3 Jo3 Jo3 Jo3 Jo3 Jo3 Jo3\n" + "Jo4 Jo4 Jo4 Jo4 Jo4 Jo4 Jo4 Jo4\n" + "Jo5 Jo5 Jo5 Jo5 Jo5 Jo5 Jo5 Jo5\n" + "Jo6 Jo6 Jo6 Jo6 Jo6 Jo6 Jo6 Jo6\n" + "Jo7 Jo7 Jo7 Jo7 Jo7 Jo7 Jo7 Jo7\n", "Custom tag control {{texbox ...}} with linkedCtxParam or linkedElement, data-linked as (1) inline tag, (2) data-linked input or (3) data-linked div"); // =============================== Arrange =============================== jsv.views.templates({ markup: '{^{textbox path=name edit=editable/}}' + '
                    ', tags: { textbox: { bindTo: "path", linkedCtxParam: "val", init: function() { this.edit = this.tagCtx.props.edit; // Initialize textbox state }, template:"" + "{^{if ~tag.edit}}" // observable textbox state + "" + "{{else}}" + "" + "{{/if}}", onUpdate: false } } }).link("#result", {name: "Jo", editable: true}); // ................................ Act .................................. var container = $("#result")[0]; result = $("input:text", container)[0].value + " " + $("input:text", container)[1].value; $("input:text").eq(0).val("Fred").change(); // Modify text result += "|" + $("input:text", container)[0].value + " " + $("input:text", container)[1].value; var textBoxes = jsv.view().childTags("textbox"); // Find all the {{textbox}} tags in the view for (var i=0; i', tags: { textbox: { bindTo: "path", linkedCtxParam: "val", init: function() { this.edit = this.tagCtx.props.edit; // Initialize textbox state }, render: function() { this.template = "" // Checkbox to toggle edit + (this.edit // not bound, so driven by 'depends' ? ""// for editing : ""); // for rendering }, depends: function(data) { return [this, "edit"]; // depends on textbox state } } } }).link("#result", {name: "Jo", editable: true}); // ................................ Act .................................. var container = $("#result")[0]; result = $("input:text", container)[0].value + " " + $("input:text", container)[1].value; $("input:text").eq(0).val("Fred").change(); // Modify text result += "|" + $("input:text", container)[0].value + " " + $("input:text", container)[1].value; var textBoxes = jsv.view().childTags("textbox"); // Find all the {{textbox}} tags in the view for (var i=0; i', tags: { textbox: { bindTo: "path", linkedCtxParam: "val", init: function() { this.edit = this.tagCtx.props.edit; ; // Initialize textbox state through edit property }, render: function() { this.template = "" // Checkbox to toggle edit + (this.edit ? "" // for editing : ""); // for rendering }, onUpdate: function(ev, eventArgs, tagCtxs) { this.edit = tagCtxs[0].props.edit; // Respond to changed data-linked edit property. }, onBind: function() { jsv.observe(this, "edit", $.proxy(this.refresh, this)); }, onUnbind: function() { jsv.unobserve(this, "edit"); } } } }).link("#result", data); // ................................ Act .................................. var container = $("#result")[0]; result = $("input:text", container)[0].value + " " + $("input:text", container)[1].value; $("input:text").eq(0).val("Fred").change(); // Modify text result += "|" + $("input:text", container)[0].value + " " + $("input:text", container)[1].value; var textBoxes = jsv.view().childTags("textbox"); // Find all the {{textbox}} tags in the view for (var i=0; i>", "*"); res += "|" + jsv.views.settings.delimiters() + "|" + jsv.views.sub.settings.delimiters; jsv.views.settings.delimiters(current); res += "|" + jsv.templates("A_{{if true}}YES{{/if}}_B").render() + "|" + jsv.views.settings.delimiters() + "|" + jsv.views.sub.settings.delimiters; // ............................... Assert ................................. assert.equal(res, "A_yes_B|@%,%@,^|@%,%@,^|<<,>>,*|<<,>>,*|A_YES_B|{{,}},^|{{,}},^", "Custom delimiters with render()"); // ................................ Act .................................. current = jsv.views.settings.delimiters(); var app = {choose: true, name: "Jo"}; jsv.views.settings.delimiters("_^", "!@", "("); jsv.templates('_(^if choose!@
                    _^else!@no
                    _^/if!@').link("#result", app); res = $("#result").text(); jsv.observable(app).setProperty({choose: false, name: "other"}); res += "|" + $("#result").text() + "|" + jsv.views.settings.delimiters() + "|" + jsv.views.sub.settings.delimiters; $("#result input").val("new").change(); res += "|" + $("#result").text(); jsv.views.settings.delimiters(current); jsv.templates('{^{if choose}}
                    {{else}}NO
                    {{/if}}').link("#result", app); res += "|" + $("#result").text() + "|" + jsv.views.settings.delimiters() + "|" + jsv.views.sub.settings.delimiters; $("#result input").val("NEW").change(); res += "|" + $("#result").text(); // ............................... Assert ................................. assert.equal(res, "Jo|noother2other3|_^,!@,(|_^,!@,(|nonew2new3|NOnew2new3|{{,}},^|{{,}},^|NONEW2NEW3", "Custom delimiters with link()"); // =============================== Arrange =============================== // Debug mode false var oldDebugMode = jsv.views.settings.debugMode(); app = {choose: true, name: "Jo", onerr: "invalid'Jo'"}; jsv.views.settings.debugMode(false); // ................................ Act .................................. res = jsv.views.settings.debugMode(); $("#result").empty(); try { jsv.templates('{{:missing.willThrow}}X').link("#result", app); } catch (e) { res += " " + !!e.message; } res += " " + $("#result").text(); // ............................... Assert ................................. assert.equal(res, 'false true ', 'Debug mode false: {{:missing.willThrow}} throws error - with link()'); // ................................ Act .................................. res = jsv.views.settings.debugMode(); try { jsv.templates('
                    X
                    ').link("#result", app); } catch (e) { res += " " + !!e.message; } res += " " + $("#result").text(); // ............................... Assert ................................. assert.equal(res, 'false true X', 'Debug mode false: - data-link="missing.willThrow" - throws error'); // ................................ Act .................................. // Debug mode true jsv.views.settings.debugMode(true); res = jsv.views.settings.debugMode(); jsv.templates('{{:missing.willThrow}}').link("#result", app); res += " " + $("#result").text(); // ............................... Assert ................................. assert.equal(res.slice(0, 13), 'true {Error: ', 'Debug mode true: {{:missing.willThrow}} renders error - with link()'); // ................................ Act .................................. res = jsv.views.settings.debugMode(); jsv.templates('
                    X
                    ').link("#result", app); res += " " + $("#result").text(); // ............................... Assert ................................. assert.equal(res.slice(0, 13), 'true {Error: ', 'Debug mode true: - data-link="missing.willThrow" - renders error'); app = {choose: true, name: "Jo", onerr: "invalid'Jo'"}; res = ""; // ................................ Act .................................. // Debug mode 'onError' handler function with return value jsv.views.settings.debugMode(function(e, fallback, view) { // Can override using jsv.views.settings({onError: function(...) {...}}); var data = this; return "Override error - " + (fallback||"") + "_" + data.name + " " + (e.message.indexOf("undefined")>-1); // For syntax errors e is a string, and view is undefined }); // ................................ Act .................................. jsv.templates('{{:missing.willThrow}}').link("#result", app); res = $("#result").text(); // ............................... Assert ................................. assert.equal(res, "Override error - _Jo true", "Debug mode 'onError' handler override - with link()"); // ................................ Act .................................. jsv.templates('{{:missing.willThrow onError=onerr}} {^{if missing.willThrow onError=onerr + \' (in if tag)\'}}inside{{/if}}').link("#result", app); res = $("#result").text(); // ............................... Assert ................................. assert.equal(res, "Override error - invalid'Jo'_Jo true Override error - invalid'Jo' (in if tag)_Jo trueOverride error - invalid'Jo' (in data-link)_Jo true", "onError fallback in tags and in data-link expression, with debug mode 'onError' handler override"); // ................................ Act .................................. jsv.templates('{{:missing.willThrow onError=~myErrFn}} {^{if missing.willThrow onError=~myErrFn}}inside{{/if}}').link("#result", app, { myErrFn: function(e, view) { return "myErrFn for <" + this.name + ">"; } }); res = $("#result").text(); // ............................... Assert ................................. assert.equal(res, "Override error - myErrFn for _Jo true Override error - myErrFn for _Jo trueOverride error - myErrFn for _Jo true", "onError handler in tags and in data-link expression, with debug mode 'onError' handler override "); // ................................ Reset .................................. $("#result").empty(); jsv.unlink(); // Need to unlink since when throwing above, view registration was incomplete, so calling $("#result").empty() is not sufficient to clean up child views on topView jsv.views.settings.debugMode(oldDebugMode); jsv.views.settings.trigger(true); }); QUnit.module("API - Declarations"); QUnit.test("Template encapsulation", function(assert) { // =============================== Arrange =============================== jsv.templates({ myTmpl6: { markup: "{{sort reverse=true people}}{{:lastName}}{{/sort}}", tags: { sort: sort } } }); // ................................ Act .................................. jsv.link.myTmpl6("#result", {people: people}); // ............................... Assert ................................. assert.equal($("#result").text(), "TwoOne", "Template with tag resource"); // =============================== Arrange =============================== jsv.templates({ myTmpl7: { markup: "{{if first}}Yes{{:~foo}}{{sort reverse=true people}}{{:lastName}}{{/sort}}{{else}}No{{:~foo}}{{sort reverse=true people}}{{:lastName}}{{/sort}}{{/if}}", tags: { sort: sort }, helpers: { foo: "isFoo" } } }); // ................................ Act .................................. jsv.link.myTmpl7("#result", {people: people, first: false}); // ............................... Assert ................................. assert.equal($("#result").text(), "NoisFooTwoOne", "Can access tag and helper resources from a nested context (i.e. inside {{if}} block)"); // ............................... Reset ................................. $("#result").empty(); }); QUnit.module("API - Views"); QUnit.test("jsv.view() in regular content", function(assert) { // =============================== Arrange =============================== jsv.link.tmplHierarchy("#result", topData); // ................................ Act .................................. var view = jsv.view("#1"); // ............................... Assert ................................. assert.ok(view.ctxPrm("val") === 1 && view.type === "myWrap", 'jsv.view(elem) gets nearest parent view. Custom tag blocks are of type "tmplName"'); // ................................ Act .................................. view = jsv.view("#1", "root"); // ............................... Assert ................................. assert.ok(view.parent.type === "top", 'jsv.view(elem, "root") gets root view (child of top view)'); // ................................ Act .................................. view = jsv.view("#1", "item"); // ............................... Assert ................................. assert.ok(view.type === "item" && view.data.lastName === "One" && view.index === 0, 'jsv.view(elem, "item") gets nearest item view'); // ................................ Act .................................. view = jsv.view("#1", "data"); // ............................... Assert ................................. assert.ok(view.type === "data" && view.data === topData, 'jsv.view(elem, "data") gets nearest data view'); // ................................ Act .................................. view = jsv.view("#1", "if"); // ............................... Assert ................................. assert.ok(view.type === "if" && view.data === people[0], 'jsv.view(elem, "if") gets nearest "if" view'); // ................................ Act .................................. view = jsv.view("#1", "array"); // ............................... Assert ................................. assert.ok(view.type === "array" && view.data === people, 'jsv.view(elem, "array") gets nearest array view'); // ................................ Act .................................. view = jsv.view("#sp1", "myWrap"); // ............................... Assert ................................. assert.ok(view.type === "myWrap" && view.ctx.tag.tagName === "myWrap", 'jsv.view(elem, "mytagName") gets nearest view for content of that tag'); view = jsv.view("#sp1"); // ............................... Assert ................................. assert.ok(view.type === "if" && view.ctx.tag.tagName === "myWrap2", 'Within {{if}} block, jsv.view(elem) gets nearest "if" view, but view.ctx.tag is the nearest non-flow tag, i.e. custom tag that does not have flow set to true'); // ................................ Act .................................. view = jsv.view("#1", true); // ............................... Assert ................................. assert.ok(view.type === "myWrap2", 'jsv.view(elem, true) gets the first nested view. Custom tag blocks are of type "tmplName"'); // ................................ Act .................................. view = jsv.view("#result", true, "myFlow"); // ............................... Assert ................................. assert.ok(view.type === "myFlow", 'jsv.view(elem, true, viewTypeName) gets the first (depth first) nested view of that type'); // =============================== Arrange =============================== var data = []; jsv.templates("").link("#result", data); // ................................ Act .................................. view = jsv.view("#result", true); var view2 = jsv.view("#result").get(true); // ............................... Assert ................................. assert.ok(view.data === data && view.type === "array", 'If elem is a container for a rendered array, and the array is empty, jsv.view(elem, true) returns the array view (even though the element is empty)'); // ................................ Act .................................. var itemView = jsv.view("#result", true, "item"); // ............................... Assert ................................. assert.ok(!itemView, 'If elem is a container for a rendered array, and the array is empty, jsv.view(elem, true, "item") returns nothing'); // =============================== Arrange =============================== data = [1]; jsv.templates("").link("#result", data); // ................................ Act .................................. view = jsv.view("#result", true); // ............................... Assert ................................. assert.ok(view.data === data && view.type === "array", 'If elem is a container for a rendered array rendering nothing, and the array is not empty, jsv.view(elem, true) returns the array view (even though the container element is empty)'); // ................................ Act .................................. itemView = jsv.view("#result", true, "item"); // ............................... Assert ................................. assert.ok(itemView.index === 0, 'If elem is a container for a rendered array rendering nothing, and the array is not empty, jsv.view(elem, true, "item") returns the item view (even though the container element is empty)'); // =============================== Arrange =============================== data = {people: []}; jsv.templates("
                    {{for people}}{{/for}}
                    ").link("#result", data); // ................................ Act .................................. view = jsv.view("#result div", true); // ............................... Assert ................................. assert.ok(view.data === data.people && view.type === "array", 'If elem is a container for a rendered array, and the array is empty, jsv.view(elem, true) returns the array view (even though the element is empty)'); // ................................ Act .................................. itemView = jsv.view("#result div", true, "item"); // ............................... Assert ................................. assert.ok(!itemView, 'If elem is a container for a rendered array, and the array is empty, jsv.view(elem, true, "item") returns nothing'); // =============================== Arrange =============================== data = {people: [1]}; jsv.templates("
                    {{for people}}{{/for}}
                    ").link("#result", data); // ................................ Act .................................. view = jsv.view("#result div", true); // ............................... Assert ................................. assert.ok(view.data === data.people && view.type === "array", 'If elem is a container for a rendered array rendering nothing, and the array is not empty, jsv.view(elem, true) returns the array view (even though the container element is empty)'); // ................................ Act .................................. itemView = jsv.view("#result div", true, "item"); // ............................... Assert ................................. assert.ok(itemView.index === 0, 'If elem is a container for a rendered array rendering nothing, and the array is not empty, jsv.view(elem, true, "item") returns the item view (even though the container element is empty)'); // ............................... Reset ................................. $("#result").empty(); }); QUnit.test("view.get() and view.getIndex() in regular content", function(assert) { // =============================== Arrange =============================== jsv.link.tmplHierarchy("#result", topData); var view1 = jsv.view("#1"); // ................................ Act .................................. var view = view1.get(); // ............................... Assert ................................. assert.ok(view===view1.parent, 'view.get() gets parent view'); // ................................ Act .................................. view = view1.get("item"); // ............................... Assert ................................. assert.ok(view.type === "item" && view.data.lastName === "One" && view.index === 0, 'view.get("item") gets nearest item view'); // ................................ Act .................................. view = view1.get("myWrap"); // ............................... Assert ................................. assert.ok(view === view1, 'view.get("viewTypeName") gets nearest viewTypeName view looking at ancestors starting from the view itself'); // ................................ Act .................................. view = view1.get(true, "myWrap"); // ............................... Assert ................................. assert.ok(view === view1, 'view.get(true, "viewTypeName") gets nearest viewTypeName view looking at descendants starting from the view itself'); // ................................ Act .................................. view = view1.get(true, "myFlow"); // ............................... Assert ................................. assert.ok(view.tmpl.markup === "zz{{if true}}{{myFlow2/}}{{/if}}", 'view.get(true, "viewTypeName") gets nearest viewTypeName view looking at descendants starting from the view itself'); // ................................ Act .................................. view = view1.get(true, "myFlow2"); // ............................... Assert ................................. assert.ok(view.tmpl.markup === "flow2", 'view.get(true, "viewTypeName") gets nearest viewTypeName view looking at descendants starting from the view itself'); // ................................ Act .................................. view = view1.get(true); // ............................... Assert ................................. assert.ok(view.type === "myWrap2" && view === view1.get(true, "myWrap2") && view === jsv.view("#sp0").parent, 'view.get(true) gets nearest child view of any type'); // ............................... Assert ................................. assert.ok(view1.get(true, "nonexistent") === undefined, 'view.get(true, "viewTypeName") returns undefined if no descendant of that type, starting from the view itself'); // ............................... Assert ................................. assert.ok(jsv.view("#1").getIndex() === 0 && jsv.view("#1", "item").index === 0 && jsv.view("#2").getIndex() === 1 && jsv.view("#2", "item").index === 1, 'jsv.view(elem).getIndex() gets index of nearest item view'); // ............................... Reset ................................. $("#result").empty(); }); QUnit.test("view.ctxPrm() tag.ctxPrm()", function(assert) { jsv.views.settings.trigger(false); // =============================== Arrange =============================== jsv.views.tags("mytag", { bindTo: ["height", "width"], linkedElement: [".ht", ".wd"], linkedCtxParam: ["ht", "wd"], mainElement: "div", template: "
                    {{include tmpl=#content/}}

                    ", setValue: function(val, index, tagElse, ev, eventArgs) { if (val === undefined) { val = this.getValue(tagElse)[index]; this.tagCtxs[tagElse].ctxPrm(this.linkedCtxParam[index], val); } else { this.vals[tagElse][index] = val; } // return val; }, getValue: function(tagElse) { return this.vals[tagElse]; }, onUpdate: false, setSize: true, vals: [[38, 48], [33, 44]] }); // =============================== Arrange =============================== var tmpl = jsv.templates(' {^{:~foo}}'); tmpl.link("#result", {}, {}); // ............................... Assert ................................. assert.ok(jsv.view().ctxPrm("toString") === undefined, "ctxPrm() for built-in non-enumerables such as 'toString' returns undefined"); // ................................ Act .................................. var input = $("#1"), view1 = input.view(), content = $("#result"), res = "1: " + (view1.ctxPrm("foo")||"") + "-" + input.val() + ": {" + content.text() + "} "; // ................................ Act .................................. view1.ctxPrm("foo", "set1"); res += "|2: " + view1.ctxPrm("foo") + "-" + input.val() + ": {" + content.text() + "} "; // ................................ Act .................................. input.val("new1"); input.change(); res += "|3: " + view1.ctxPrm("foo") + "-" + input.val() + ": {" + content.text() + "} "; // ............................... Assert ................................. assert.equal(res, "1: -: { } |2: set1-set1: { set1} |3: new1-new1: { new1} ", "Uninitialized context param can be changed observably by two-way binding or by view.ctxPrm(foo, value) call"); // ................................ Act .................................. tmpl.link("#result", {}, {foo: "instance"}); input = $("#1"); view1 = input.view(); content = $("#result"); res = "1: " + (view1.ctxPrm("foo")||"") + "-" + input.val() + ": {" + content.text() + "} "; // ................................ Act .................................. view1.ctxPrm("foo", "set1"); res += "|2: " + view1.ctxPrm("foo") + "-" + input.val() + ": {" + content.text() + "} "; // ................................ Act .................................. input.val("new1"); input.change(); res += "|3: " + view1.ctxPrm("foo") + "-" + input.val() + ": {" + content.text() + "} "; // ............................... Assert ................................. assert.equal(res, "1: instance-instance: { instance} |2: set1-set1: { set1} |3: new1-new1: { new1} ", "Initialized instance context param can be changed observably by two-way binding or by view.ctxPrm(foo, value) call"); // ................................ Act .................................. jsv.views.helpers("foo", "registered", tmpl); tmpl.link("#result", {}); input = $("#1"); view1 = input.view(); content = $("#result"); res = "1: " + (view1.ctxPrm("foo")||"") + "-" + input.val() + ": {" + content.text() + "} "; // ................................ Act .................................. view1.ctxPrm("foo", "set1"); res += "|2: " + view1.ctxPrm("foo") + "-" + input.val() + ": {" + content.text() + "} "; // ................................ Act .................................. input.val("new1"); input.change(); res += "|3: " + view1.ctxPrm("foo") + "-" + input.val() + ": {" + content.text() + "} "; // ............................... Assert ................................. assert.equal(res, "1: registered-registered: { registered} |2: set1-set1: { set1} |3: new1-new1: { new1} ", "Initialized registered helper param can be changed observably by two-way binding or by view.ctxPrm(foo, value) call"); // =============================== Arrange =============================== tmpl = jsv.templates( ' Outer: {^{:~foo}}' + '{{if true}}, Inner: {^{:~foo}}' + '{{if true}}, Nested inner: {^{:~foo}}' + '{{/if}}' + '{{/if}}'); tmpl.link("#result", {}); input = $("#1"); view1 = input.view(); content = $("#result"); res = "1: " + (view1.ctxPrm("foo")||"") + "-" + input.val() + ": {" + content.text() + "} "; // ................................ Act .................................. view1.ctxPrm("foo", "set1"); res += "|2: " + view1.ctxPrm("foo") + "-" + input.val() + ": {" + content.text() + "} "; // ................................ Act .................................. input.val("new1"); input.change(); res += "|3: " + view1.ctxPrm("foo") + "-" + input.val() + ": {" + content.text() + "} "; // ............................... Assert ................................. assert.equal(res, "1: -: { Outer: , Inner: , Nested inner: }" + " |2: set1-set1: { Outer: set1, Inner: set1, Nested inner: set1}" + " |3: new1-new1: { Outer: new1, Inner: new1, Nested inner: new1} ", "Observable contextual parameter is scoped to root view (view below top view)"); // =============================== Arrange =============================== tmpl = jsv.templates( 'Property: {^{:~prp}} |
                    ' + 'Function: {^{:is}} |
                    ' + '{{for items}}' + 'Inner Property: {^{:~prp}} |
                    ' + 'Inner Function: {^{:is}} |
                    ' + '{{/for}}' ); var fn = function() { return this.data.is; }; fn.set = function(val) { jsv.observable(this.data).setProperty("is", val); } tmpl.link("#result", [ {is:"outer1", items: [{is: "inner11"}, {is: "inner12"}]}, {is:"outer2", items: [{is: "inner21"}, {is: "inner22"}]} ], {prp: "PRP", fn: fn} ); var propInput1 = $(".prp").eq(0); var fnInput1 = $(".fn").eq(0); var innerPropInput1 = $(".prpb").eq(0); var innerFnInput1 = $(".fnb").eq(0); var innerFnInput3 = $(".fnb").eq(3); content = $("#result"); res = "1: " + content.text(); // ................................ Act .................................. propInput1.val("prp1"); // first outer propInput propInput1.change(); innerPropInput1.val("prp2"); // first inner propInput innerPropInput1.change(); fnInput1.val("fn1"); // first outer fnInput fnInput1.change(); innerFnInput1.val("fn2"); // first inner fnInput innerFnInput1.change(); innerFnInput3.val("fn3"); // first inner fnInput innerFnInput3.change(); res += "2: " + content.text(); // ............................... Assert ................................. assert.equal(res, "1: Property: PRP | Function: outer1 | " + "Inner Property: PRP | Inner Function: inner11 | Inner Property: PRP | Inner Function: inner12 | " + "Property: PRP | Function: outer2 | " + "Inner Property: PRP | Inner Function: inner21 | Inner Property: PRP | Inner Function: inner22 | " + "2: Property: prp2 | Function: fn1 | " + "Inner Property: prp2 | Inner Function: fn2 | Inner Property: prp2 | Inner Function: inner12 | " + "Property: prp2 | Function: outer2 | " + "Inner Property: prp2 | Inner Function: inner21 | Inner Property: prp2 | Inner Function: fn3 | ", "Observable contextual parameter are scoped: for computed function, to calling view, for properties, not functions, to root view (view below top view)"); // =============================== Arrange =============================== tmpl = jsv.templates(' {^{:~foo}}'); tmpl.link("#result", [0, 1]); var input1 = $(".inp")[0], input2 = $(".inp")[1], view2 = jsv.view(input2); view1 = jsv.view(input1); content = $("#result"); res = "1: View1:" + (view1.ctxPrm("foo")||"") + "-" + input1.value + " View2:" + (view2.ctxPrm("foo")||"") + "-" + input2.value + ": {" + content.text() + "} "; // ................................ Act .................................. view1.ctxPrm("foo", "set1"); view2.ctxPrm("foo", "set2"); res += "|2: View1:" + (view1.ctxPrm("foo")||"") + "-" + input1.value + " View2:" + (view2.ctxPrm("foo")||"") + "-" + input2.value + ": {" + content.text() + "} "; // ................................ Act .................................. input1.value = "new1"; $(input1).change(); input2.value = "new2"; $(input2).change(); res += "|3: View1:" + (view1.ctxPrm("foo")||"") + "-" + input1.value + " View2:" + (view2.ctxPrm("foo")||"") + "-" + input2.value + ": {" + content.text() + "} "; // ............................... Assert ................................. assert.equal(res, "1: View1:- View2:-: { }" + " |2: View1:set2-set2 View2:set2-set2: { set2 set2}" + " |3: View1:new2-new2 View2:new2-new2: { new2 new2} ", "Observable contextual parameter is scoped to root view (view below top view) - which is array view, when rendering/linking an array"); // =============================== Arrange =============================== tmpl = jsv.templates(' Outer: {^{:~foo}}' + '{^{mytag width=cx}}, Inner: {^{:~foo}}' + '{^{mytag}}, Nested inner: {^{:~foo}}' + '{{else width=cx}}, Nested inner: {^{:~foo}}' + '{{/mytag}}' + '{{/mytag}}'); tmpl.link("#result", {cx: 22}); var input1 = $("#1"), view1 = input1.view(), input2 = $("#2"), view2 = input2.view(), input3 = $("#3"), view3 = input3.view(), input4 = $("#4"), view4 = input4.view(); content = $("#result"); res = "1: " + (view1.ctxPrm("foo")||"") + "/" + (view2.ctxPrm("foo")||"") + "/" + (view3.ctxPrm("foo")||"") + "/" + (view4.ctxPrm("foo")||"") + "-" + input1.val() + "/" + input2.val() + "/" + input3.val() + "/" + input4.val() + ": {" + content.text() + "} "; // ................................ Act .................................. view1.ctxPrm("foo", "set1"); view2.ctxPrm("foo", "set2"); view3.ctxPrm("foo", "set3"); view4.ctxPrm("foo", "set4"); res += "|2: " + (view1.ctxPrm("foo")||"") + "/" + (view2.ctxPrm("foo")||"") + "/" + (view3.ctxPrm("foo")||"") + "/" + (view4.ctxPrm("foo")||"") + "-" + input1.val() + "/" + input2.val() + "/" + input3.val() + "/" + input4.val() + ": {" + content.text() + "} "; // ................................ Act .................................. input1.val("new1"); input1.change(); input2.val("new2"); input2.change(); input3.val("new3"); input3.change(); input4.val("new4"); input4.change(); res += "|3: " + (view1.ctxPrm("foo")||"") + "/" + (view2.ctxPrm("foo")||"") + "/" + (view3.ctxPrm("foo")||"") + "/" + (view4.ctxPrm("foo")||"") + "-" + input1.val() + "/" + input2.val() + "/" + input3.val() + "/" + input4.val() + ": {" + content.text() + "} "; // ............................... Assert ................................. assert.equal(res, "1: ///-///: { Outer: , Inner: , Nested inner: , Nested inner: }" + " |2: set1/set2/set4/set4-set1/set2/set4/set4: { Outer: set1, Inner: set2, Nested inner: set4, Nested inner: set4}" + " |3: new1/new2/new4/new4-new1/new2/new4/new4: { Outer: new1, Inner: new2, Nested inner: new4, Nested inner: new4} ", "Observable contextual parameter within linked tag is scoped to tag view, - closest non flow tag ancestor, shared across else blocks"); // ................................ Act .................................. var innerTag = jsv.view().childTags(true, "mytag")[1]; res = "|1: " + (innerTag.ctxPrm("foo")||""); innerTag.ctxPrm("foo", "tagFoo"); innerTag.ctxPrm("newPrm", "tagNewPrm"); res += " |2: " + (innerTag.ctxPrm("foo")||"") + "-" + (innerTag.ctxPrm("newPrm")||"") + "-" + (view1.ctxPrm("newPrm")||"") + "/" + (view2.ctxPrm("newPrm")||"") + "/" + (view3.ctxPrm("newPrm")||"") + "/" + (view4.ctxPrm("newPrm")||"") + "-" + (view1.ctxPrm("foo")||"") + "/" + (view2.ctxPrm("foo")||"") + "/" + (view3.ctxPrm("foo")||"") + "/" + (view4.ctxPrm("foo")||"") + "-" + input1.val() + "/" + input2.val() + "/" + input3.val() + "/" + input4.val() + ": {" + content.text() + "} "; // ............................... Assert ................................. assert.equal(res, "|1: new4" + " |2: tagFoo-tagNewPrm-//tagNewPrm/tagNewPrm-new1/new2/tagFoo/tagFoo-new1/new2/tagFoo/tagFoo: { Outer: new1, Inner: new2, Nested inner: tagFoo, Nested inner: tagFoo} ", "tag.ctxPrm() gets/sets parameter scoped to tag view, shared across else blocks"); // ............................... Assert ................................. assert.equal(innerTag.tagCtxs[0].nodes().length + "|" + innerTag.tagCtxs[1].nodes().length + "|" + innerTag.nodes().length + "-" + innerTag.tagCtxs[0].contents(true, "input").length + "|" + innerTag.tagCtxs[1].contents(true, "input").length + "|" + innerTag.contents(true, "input").length, "2|2|4-1|1|2", "Multiple else blocks: tag.nodes() and tag.content() return content from all else blocks"); // =============================== Arrange =============================== tmpl = jsv.templates(' Outer: {^{:~foo}}' + '{{mytag width=cx}}, Inner: {^{:~foo}}' + '{{mytag}}, Nested inner: {^{:~foo}}' + '{{else width=cx}}, Nested inner: {^{:~foo}}' + '{{/mytag}}' + '{{/mytag}}'); tmpl.link("#result", {cx: 22}); var input1 = $("#1"), view1 = input1.view(), input2 = $("#2"), view2 = input2.view(), input3 = $("#3"), view3 = input3.view(), input4 = $("#4"), view4 = input4.view(); content = $("#result"); res = "1: " + (view1.ctxPrm("foo")||"") + "/" + (view2.ctxPrm("foo")||"") + "/" + (view3.ctxPrm("foo")||"") + "/" + (view4.ctxPrm("foo")||"") + "-" + input1.val() + "/" + input2.val() + "/" + input3.val() + "/" + input4.val() + ": {" + content.text() + "} "; // ................................ Act .................................. view1.ctxPrm("foo", "set1"); view2.ctxPrm("foo", "set2"); view3.ctxPrm("foo", "set3"); view4.ctxPrm("foo", "set4"); res += "|2: " + (view1.ctxPrm("foo")||"") + "/" + (view2.ctxPrm("foo")||"") + "/" + (view3.ctxPrm("foo")||"") + "/" + (view4.ctxPrm("foo")||"") + "-" + input1.val() + "/" + input2.val() + "/" + input3.val() + "/" + input4.val() + ": {" + content.text() + "} "; // ................................ Act .................................. input1.val("new1"); input1.change(); input2.val("new2"); input2.change(); input3.val("new3"); input3.change(); input4.val("new4"); input4.change(); res += "|3: " + (view1.ctxPrm("foo")||"") + "/" + (view2.ctxPrm("foo")||"") + "/" + (view3.ctxPrm("foo")||"") + "/" + (view4.ctxPrm("foo")||"") + "-" + input1.val() + "/" + input2.val() + "/" + input3.val() + "/" + input4.val() + ": {" + content.text() + "} "; // ............................... Assert ................................. assert.equal(res, "1: ///-///: { Outer: , Inner: , Nested inner: , Nested inner: }" + " |2: set1/set2/set4/set4-set1/set2/set4/set4: { Outer: set1, Inner: set2, Nested inner: set4, Nested inner: set4}" + " |3: new1/new2/new4/new4-new1/new2/new4/new4: { Outer: new1, Inner: new2, Nested inner: new4, Nested inner: new4} ", "Observable contextual parameter within unlinked tag is scoped to tag view, - closest non flow tag ancestor, shared across else blocks"); // =============================== Arrange =============================== jsv.views.tags.mytag.vals = [[38, 48], [33, 44]]; tmpl = jsv.templates(' Outer: {^{:~wd}}' + '{^{mytag width=11}}, Inner: {^{:~wd}}' + '{^{mytag}}, Nested inner: {^{:~wd}}' + '{{else width=cx}}, Nested inner: {^{:~wd}}' + '{{/mytag}}' + '{{/mytag}}'); tmpl.link("#result", {cx: 22}); var input1 = $("#1"), view1 = input1.view(), input2 = $("#2"), view2 = input2.view(), input3 = $("#3"), view3 = input3.view(), input4 = $("#4"), view4 = input4.view(); content = $("#result"); res = "1: " + (view1.ctxPrm("wd")||"") + "/" + (view2.ctxPrm("wd")||"") + "/" + (view3.ctxPrm("wd")||"") + "/" + (view4.ctxPrm("wd")||"") + "-" + input1.val() + "/" + input2.val() + "/" + input3.val() + "/" + input4.val() + ": {" + content.text() + "} "; // ................................ Act .................................. view1.ctxPrm("wd", "set1"); view2.ctxPrm("wd", "set2"); view3.ctxPrm("wd", "set3"); view4.ctxPrm("wd", "set4"); res += "|2: " + (view1.ctxPrm("wd")||"") + "/" + (view2.ctxPrm("wd")||"") + "/" + (view3.ctxPrm("wd")||"") + "/" + (view4.ctxPrm("wd")||"") + "-" + input1.val() + "/" + input2.val() + "/" + input3.val() + "/" + input4.val() + ": {" + content.text() + "} "; // ................................ Act .................................. input1.val("new1"); input1.change(); input2.val("new2"); input2.change(); input3.val("new3"); input3.change(); input4.val("new4"); input4.change(); res += "|3: " + (view1.ctxPrm("wd")||"") + "/" + (view2.ctxPrm("wd")||"") + "/" + (view3.ctxPrm("wd")||"") + "/" + (view4.ctxPrm("wd")||"") + "-" + input1.val() + "/" + input2.val() + "/" + input3.val() + "/" + input4.val() + ": {" + content.text() + "} "; // ............................... Assert ................................. assert.equal(res, "1: /11/48/22-/11/48/22: { Outer: , Inner: 11, Nested inner: 48, Nested inner: 22}" + " |2: set1/set2/set3/set4-set1/set2/set3/set4: { Outer: set1, Inner: set2, Nested inner: set3, Nested inner: set4}" + " |3: new1/new2/new3/new4-new1/new2/new3/new4: { Outer: new1, Inner: new2, Nested inner: new3, Nested inner: new4} ", "Observable tag contextual parameter within linked tag is scoped to tag view, - closest non flow tag ancestor, not shared across else blocks"); // =============================== Arrange =============================== tmpl = jsv.templates(' Outer: {^{:~wd}}' + '{{mytag width=11}}, Inner: {^{:~wd}}' + '{{mytag}}, Nested inner: {^{:~wd}}' + '{{else width=cx}}, Nested inner: {^{:~wd}}' + '{{/mytag}}' + '{{/mytag}}'); tmpl.link("#result", {cx: 22}); var input1 = $("#1"), view1 = input1.view(), input2 = $("#2"), view2 = input2.view(), input3 = $("#3"), view3 = input3.view(), input4 = $("#4"), view4 = input4.view(); content = $("#result"); res = "1: " + (view1.ctxPrm("wd")||"") + "/" + (view2.ctxPrm("wd")||"") + "/" + (view3.ctxPrm("wd")||"") + "/" + (view4.ctxPrm("wd")||"") + "-" + input1.val() + "/" + input2.val() + "/" + input3.val() + "/" + input4.val() + ": {" + content.text() + "} "; // ................................ Act .................................. view1.ctxPrm("wd", "set1"); view2.ctxPrm("wd", "set2"); view3.ctxPrm("wd", "set3"); view4.ctxPrm("wd", "set4"); res += "\n|2: " + (view1.ctxPrm("wd")||"") + "/" + (view2.ctxPrm("wd")||"") + "/" + (view3.ctxPrm("wd")||"") + "/" + (view4.ctxPrm("wd")||"") + "-" + input1.val() + "/" + input2.val() + "/" + input3.val() + "/" + input4.val() + ": {" + content.text() + "} "; // ................................ Act .................................. input1.val("new1"); input1.change(); input2.val("new2"); input2.change(); input3.val("new3"); input3.change(); input4.val("new4"); input4.change(); res += "\n|3: " + (view1.ctxPrm("wd")||"") + "/" + (view2.ctxPrm("wd")||"") + "/" + (view3.ctxPrm("wd")||"") + "/" + (view4.ctxPrm("wd")||"") + "-" + input1.val() + "/" + input2.val() + "/" + input3.val() + "/" + input4.val() + ": {" + content.text() + "} "; // ............................... Assert ................................. assert.equal(res, "1: /11//22-/11//22: { Outer: , Inner: 11, Nested inner: , Nested inner: 22}" + " \n|2: set1/set2/set3/set4-set1/set2/set3/set4: { Outer: set1, Inner: set2, Nested inner: set3, Nested inner: set4}" + " \n|3: new1/new2/new3/new4-new1/new2/new3/new4: { Outer: new1, Inner: new2, Nested inner: new3, Nested inner: new4} ", "Observable tag contextual parameter within unlinked tag is scoped to tag view, - closest non flow tag ancestor, not shared across else blocks"); // =============================== Arrange =============================== tmpl = jsv.templates({ markup: 'OUTER{^{:~foo}}{^{namebox/}}', tags: { namebox: { template: 'INNER{^{:~foo}}' } } }); tmpl.link("#result", [1,2]); var inputOuter0 = $("#outer0"), inputOuter1 = $("#outer1"), inputInner0 = $("#inner0"), inputInner1 = $("#inner1"); // ................................ Act .................................. inputInner0.val("inner0!"); inputInner0.change(); inputOuter1.val("outer1!"); inputOuter1.change(); var res = $("#result").text() + "%" + inputOuter0.val() + "|" + inputOuter1.val() + "|" + inputInner0.val() + "|" + inputInner1.val(); // ............................... Assert ................................. assert.equal(res, "OUTERouter1!INNERinner0!OUTERouter1!INNER%outer1!|outer1!|inner0!|", "When contextual parameter not initialized at higher level, it is scoped to nearest tag container, or to root view (view below top view)"); // =============================== Arrange =============================== tmpl = jsv.templates({ markup: 'OUTERMOST{^{:~foo}} {^{include ~foo="wrapper!"}}OUTER{^{:~foo}}{^{namebox/}}{{/include}}', // With {{include ~foo=...}} wrapper tags: { namebox: { template: 'INNER{^{:~foo}}' } } }); tmpl.link("#result", [1,2]); inputOuter0 = $("#outer0"); inputOuter1 = $("#outer1"); inputInner0 = $("#inner0"); inputInner1 = $("#inner1"); res = $("#result").text() + "--" + inputOuter0.val() + "|" + inputOuter1.val() + "|" + inputInner0.val() + "|" + inputInner1.val(); // ............................... Assert ................................. assert.equal(res, "OUTERMOST OUTERwrapper!INNERwrapper!OUTERMOST OUTERwrapper!INNERwrapper!--wrapper!|wrapper!|wrapper!|wrapper!", "When contextual parameter is initialized at higher level, it is initialized to that level"); // ................................ Act .................................. inputInner0.val("inner0!"); inputInner0.change(); inputOuter1.val("outer1!"); inputOuter1.change(); res = $("#result").text() + "%" + inputOuter0.val() + "|" + inputOuter1.val() + "|" + inputInner0.val() + "|" + inputInner1.val(); // ............................... Assert ................................. assert.equal(res, "OUTERMOST OUTERinner0!INNERinner0!OUTERMOST OUTERouter1!INNERouter1!%inner0!|outer1!|inner0!|outer1!", "When contextual parameter is initialized at higher level, it is scoped to that level"); // =============================== Arrange =============================== tmpl = jsv.templates({ markup: 'OUTER{^{:~foo}}{^{namebox/}}', tags: { namebox: { template: 'INNER{^{:~foo}}', } }, helpers: {foo: "helperInit!"} // Global helper }); tmpl.link("#result", [1,2]); inputOuter0 = $("#outer0"); inputOuter1 = $("#outer1"); inputInner0 = $("#inner0"); inputInner1 = $("#inner1"); // ................................ Act .................................. inputInner0.val("inner0!"); inputInner0.change(); inputOuter1.val("outer1!"); inputOuter1.change(); res = $("#result").text() + "%" + inputOuter0.val() + "|" + inputOuter1.val() + "|" + inputInner0.val() + "|" + inputInner1.val(); // ............................... Assert ................................. assert.equal(res, "OUTERouter1!INNERouter1!OUTERouter1!INNERouter1!%outer1!|outer1!|outer1!|outer1!", "When contextual parameter is initialized as global helper, it is scoped to root view (view below top view)"); // =============================== Arrange =============================== tmpl = jsv.templates({ markup: 'OUTER{^{:~foo}}{^{namebox/}}', tags: { namebox: { template: 'INNER{^{:~foo}}' } } }); tmpl.link("#result", [1,2], {foo: "instanceHelperInit!"}); // Instance helper inputOuter0 = $("#outer0"); inputOuter1 = $("#outer1"); inputInner0 = $("#inner0"); inputInner1 = $("#inner1"); // ................................ Act .................................. inputInner0.val("inner0!"); inputInner0.change(); inputOuter1.val("outer1!"); inputOuter1.change(); res = $("#result").text() + "%" + inputOuter0.val() + "|" + inputOuter1.val() + "|" + inputInner0.val() + "|" + inputInner1.val(); // ............................... Assert ................................. assert.equal(res, "OUTERouter1!INNERouter1!OUTERouter1!INNERouter1!%outer1!|outer1!|outer1!|outer1!", "When contextual parameter is initialized as instance helper, it is scoped to root view (view below top view)"); // =============================== Arrange =============================== tmpl = jsv.templates({ markup: 'OUTER{^{:~foo}}{^{namebox/}}', tags: { namebox: { template: 'INNER{{:~foo}}' // Needs refreshing to show ~foo updates, since {{:~foo}} rather than data-linked {^{:~foo}} } }, helpers: {foo: "helperInit!"} // Global helper }); tmpl.link("#result", [1,2]); inputOuter0 = $("#outer0"); inputOuter1 = $("#outer1"); // ................................ Act .................................. inputOuter1.val("inner1!"); inputOuter1.change(); res = $("#result").text() + "%" + inputOuter0.val() + "|" + inputOuter1.val(); // ............................... Assert ................................. assert.equal(res, "OUTERinner1!INNERhelperInit!OUTERinner1!INNERhelperInit!%inner1!|inner1!", "When tag needs to be refreshed, if no depends on tag, will not refresh values of ~foo "); // =============================== Arrange =============================== tmpl = jsv.templates({ markup: 'OUTER{^{:~foo}}{^{namebox/}}', tags: { namebox: { template: 'INNER{{:~foo}}', // Needs refreshing to show ~foo updates, since {{:~foo}} rather than data-linked {^{:~foo}} depends: "~foo" // Depends path to ~foo will force refresh when ~foo changes } }, helpers: {foo: "helperInit!"} // Global helper }); tmpl.link("#result", [1,2]); inputOuter0 = $("#outer0"); inputOuter1 = $("#outer1"); // ................................ Act .................................. inputOuter1.val("inner1!"); inputOuter1.change(); res = $("#result").text() + "%" + inputOuter0.val() + "|" + inputOuter1.val(); // ............................... Assert ................................. assert.equal(res, "OUTERinner1!INNERinner1!OUTERinner1!INNERinner1!%inner1!|inner1!", "When tag needs to be refreshed, if depends='~foo' on tag, will refresh values of ~foo "); // =============================== Arrange =============================== tmpl = jsv.templates(' {^{:~foo}}'); tmpl.link("#result", [1,2], {foo: "val1"}); input1 = $("#a1"); view1 = input1.view(); input2 = $("#a2"); view2 = input2.view(); content = $("#result"); res = "1: " + (view1.ctxPrm("foo")||"") + "/" + (view2.ctxPrm("foo")||"") + "-" + input1.val() + "/" + input2.val() + ": {" + content.text() + "} "; // ................................ Act .................................. input1.val("new1"); input1.change(); res += "\n2: " + (view1.ctxPrm("foo")||"") + "/" + (view2.ctxPrm("foo")||"") + "-" + input1.val() + "/" + input2.val() + ": {" + content.text() + "} "; // ................................ Act .................................. view2.ctxPrm("foo", "set2"); res += "\n3: " + (view1.ctxPrm("foo")||"") + "/" + (view2.ctxPrm("foo")||"") + "-" + input1.val() + "/" + input2.val() + ": {" + content.text() + "} "; // ................................ Act .................................. // Replace and verify context params are scoped within "#result", so re-initialized tmpl.link("#result", [1,2], {foo: "val2"}); input1 = $("#a1"); view1 = input1.view(); input2 = $("#a2"); view2 = input2.view(); res += "\n4: " + (view1.ctxPrm("foo")||"") + "/" + (view2.ctxPrm("foo")||"") + "-" + input1.val() + "/" + input2.val() + ": {" + content.text() + "} "; // ................................ Act .................................. view1.ctxPrm("foo", "set3"); res += "\n5: " + (view1.ctxPrm("foo")||"") + "/" + (view2.ctxPrm("foo")||"") + "-" + input1.val() + "/" + input2.val() + ": {" + content.text() + "} "; // ................................ Act .................................. input2.val("new3"); input2.change(); res += "\n6: " + (view1.ctxPrm("foo")||"") + "/" + (view2.ctxPrm("foo")||"") + "-" + input1.val() + "/" + input2.val() + ": {" + content.text() + "} "; // ............................... Assert ................................. assert.equal(res, "1: val1/val1-val1/val1: { val1 val1}" + " \n2: new1/new1-new1/new1: { new1 new1}" + " \n3: set2/set2-set2/set2: { set2 set2}" + " \n4: val2/val2-val2/val2: { val2 val2}" + " \n5: set3/set3-set3/set3: { set3 set3}" + " \n6: new3/new3-new3/new3: { new3 new3} ", "link() will initialize contextual parameters, which are scoped to the newly linked view - so re-initialize on relinking"); // =============================== Arrange =============================== var outerTmpl = jsv.templates(' {^{:~foo}}
                    '); outerTmpl.link("#result", {}, {foo: "outer"}); content = $("#result"); var inputOuter = $("#outer"); var viewOuter = inputOuter.view(); res = "1: " + (viewOuter.ctxPrm("foo")||"") + "-" + inputOuter.val() + ": {" + content.text() + "} "; // ................................ Act .................................. // Link to "#main" within linked templated content, and ensure context params are scoped within "#main", independent // of context params in outer views under "#result" tmpl = jsv.templates(' {^{:~foo}}'); tmpl.link("#main", [1,2], {foo: "val1"}); input1 = $("#a1"); view1 = input1.view(); input2 = $("#a2"); view2 = input2.view(); res += "\n2: " + (viewOuter.ctxPrm("foo")||"") + "/" + (view1.ctxPrm("foo")||"") + "/" + (view2.ctxPrm("foo")||"") + "-" + inputOuter.val() + "/" + input1.val() + "/" + input2.val() + ": {" + content.text() + "} "; // ................................ Act .................................. viewOuter.ctxPrm("foo", "newouter"); view1.ctxPrm("foo", "new1"); input2.val("new2"); input2.change(); res += "\n3: " + (viewOuter.ctxPrm("foo")||"") + "/" + (view1.ctxPrm("foo")||"") + "/" + (view2.ctxPrm("foo")||"") + "-" + inputOuter.val() + "/" + input1.val() + "/" + input2.val() + ": {" + content.text() + "} "; // ................................ Act .................................. // Link to "#main" within linked templated content, and ensure context params are scoped within "#main", independent // of context params in outer views under "#result" tmpl.link("#main", [1,2], {foo: "val2"}); input1 = $("#a1"); view1 = input1.view(); input2 = $("#a2"); view2 = input2.view(); res += "\n4: " + (viewOuter.ctxPrm("foo")||"") + "/" + (view1.ctxPrm("foo")||"") + "/" + (view2.ctxPrm("foo")||"") + "-" + inputOuter.val() + "/" + input1.val() + "/" + input2.val() + ": {" + content.text() + "} "; // ............................... Assert ................................. assert.equal(res, "1: outer-outer: { outer }" + " \n2: outer/val1/val1-outer/val1/val1: { outer val1 val1}" + " \n3: newouter/new2/new2-newouter/new2/new2: { newouter new2 new2}" + " \n4: newouter/val2/val2-newouter/val2/val2: { newouter val2 val2} ", "link() will initialize contextual parameters, scoped to the newly linked view (within target container), even when the target container is within data-linked content"); // =============================== Arrange =============================== tmpl = jsv.templates('{^{:~nm}} {^{:nm}} {^{:~fnprop()}} {^{:fnprop()}} {^{:~fullName()}} {^{:fullName()}} '); function fn1() {return "FN1"} function fn2() {return "FN2"} function fullNameVM() { return this.nm + jsv.view("#it").ctxPrm("nm") + "Last"; } fullNameVM.depends = ["nm", "~nm"]; fullNameVM.set = function(val) { jsv.observable(this).setProperty("nm", val.slice(0, -4)); }; function fullNameCtx() { return this.data.nm + this.ctxPrm("nm") + "Last"; } fullNameCtx.depends = ["nm", "~nm"]; fullNameCtx.set = function(val) { this.ctxPrm("nm", val.slice(0, -4)); }; var data = { nm: "Jo", fullName: fullNameVM, fnprop: fn1 }, ctx = { nm: "Bob", fullName: fullNameCtx, fnprop: fn1 }; tmpl.link("#result", data, ctx); // ................................ Act .................................. var message = ""; function changeHandler(ev, eventArgs) { message += eventArgs.path + "(" + (eventArgs.ctxPrm||"") + "):" + eventArgs.value; } jsv.observe(data, "nm", changeHandler); jsv.observe(data, "fullName", changeHandler); jsv.observe(jsv.view("#it"), "~nm", changeHandler); jsv.observe(jsv.view("#it"), "~fullName", changeHandler); jsv.view("#it").ctxPrm("nm", "NewCtxProp"); // Get this to work for triggering computed function helper change jsv.observable(data).setProperty("nm", "NewVmProp"); jsv.view("#it").ctxPrm("fnprop", fn2); // Get this to work for triggering computed function helper change jsv.observable(data).setProperty("fnprop", fn2); jsv.view("#it").ctxPrm("fullName", "NewCtxFullname"); // Get this to work for triggering computed function helper change jsv.observable(data).setProperty("fullName", "NewVmFullname"); // ............................... Assert ................................. assert.equal($("#result").text(), "NewCtxFull NewVmFull FN2 FN2 NewVmFullNewCtxFullLast NewVmFullNewCtxFullLast ", "Observe computed with both data and contextual parameters as depends paths"); // ............................... Assert ................................. assert.equal(message, "_ocp(nm):NewCtxPropnm():NewVmProp_ocp(nm):NewCtxFull_ocp(fullName):NewVmPropNewCtxFullLastnm():NewVmFullfullName():NewVmFullNewCtxFullLast", "Observe listener registered for props and computed props, and ctxPrm listener registered for contextual parameters and computed contextual parameters. Trigger correctly."); // =============================== Arrange =============================== tmpl = jsv.templates('{^{:~nm}} {^{:nm}} {^{:~fnprop()}} {^{:fnprop()}} {^{:~fullName()}} {^{:fullName()}} '); function fullNameHlp() { return this.data.nm + this.ctxPrm("nm") + "Last"; } fullNameHlp.depends = ["nm", "~nm"]; fullNameHlp.set = function(val) { this.ctxPrm("nm", val.slice(0, -4)); }; jsv.views.helpers({ nm: "Jim", fnprop: fn1, fullName: fullNameHlp }); data = { nm: "Jo", fullName: fullNameVM, fnprop: fn1 }; tmpl.link("#result", data); // ................................ Act .................................. var message = ""; jsv.observe(data, "nm", changeHandler); jsv.observe(data, "fullName", changeHandler); jsv.observe(jsv.view("#it"), "~nm", changeHandler); jsv.observe(jsv.view("#it"), "~fullName", changeHandler); jsv.view("#it").ctxPrm("nm", "NewCtxProp"); // Get this to work for triggering computed function helper change jsv.observable(data).setProperty("nm", "NewVmProp"); jsv.view("#it").ctxPrm("fnprop", fn2); // Get this to work for triggering computed function helper change jsv.observable(data).setProperty("fnprop", fn2); jsv.view("#it").ctxPrm("fullName", "NewCtxFullname"); // Get this to work for triggering computed function helper change jsv.observable(data).setProperty("fullName", "NewVmFullname"); // ............................... Assert ................................. var fnctions = fullNameHlp === jsv.views.helpers.fullName && fn1 === jsv.views.helpers.fnprop; // Make sure helper function have not been replaced by ctxPrm() call assert.equal(fnctions + $("#result").text(), "trueNewCtxFull NewVmFull FN2 FN2 NewVmFullNewCtxFullLast NewVmFullNewCtxFullLast ", "Observe registered computed helper with both data and contextual parameters as depends paths"); // ............................... Assert ................................. assert.equal(message, "_ocp(nm):NewCtxPropnm():NewVmProp_ocp(nm):NewCtxFull_ocp(fullName):NewVmPropNewCtxFullLastnm():NewVmFullfullName():NewVmFullNewCtxFullLast", "Observe listener registered for props and computed props, and ctxPrm listener registered for contextual parameters and computed contextual parameters. Trigger correctly."); // ................................ Act .................................. // =============================== Arrange =============================== $("#result").empty(); jsv.views.helpers({ nm: null, fnprop: null, fullName: null }); tmpl = jsv.templates('{^{:~nm}} {^{:nm}} {^{:~fnprop()}} {^{:fnprop()}} {^{:~fullName()}} {^{:fullName()}} '); jsv.views.helpers({ nm: "Jim", fnprop: fn1, fullName: fullNameHlp }, tmpl); // local to tmpl data = { nm: "Jo", fullName: fullNameVM, fnprop: fn1 }; tmpl.link("#result", data); // ................................ Act .................................. var message = ""; jsv.observe(data, "nm", changeHandler); jsv.observe(data, "fullName", changeHandler); jsv.observe(jsv.view("#it"), "~nm", changeHandler); jsv.observe(jsv.view("#it"), "~fullName", changeHandler); jsv.view("#it").ctxPrm("nm", "NewCtxProp"); // Get this to work for triggering computed function helper change jsv.observable(data).setProperty("nm", "NewVmProp"); jsv.view("#it").ctxPrm("fnprop", fn2); // Get this to work for triggering computed function helper change jsv.observable(data).setProperty("fnprop", fn2); jsv.view("#it").ctxPrm("fullName", "NewCtxFullname"); // Get this to work for triggering computed function helper change jsv.observable(data).setProperty("fullName", "NewVmFullname"); // ............................... Assert ................................. fnctions = fullNameHlp === tmpl.helpers.fullName && fn1 === tmpl.helpers.fnprop; // Make sure helper function have not been replaced by ctxPrm() call assert.equal(fnctions + $("#result").text(), "trueNewCtxFull NewVmFull FN2 FN2 NewVmFullNewCtxFullLast NewVmFullNewCtxFullLast ", "Observe registered computed helper, local to template, with both data and registered helper contextual parameters as depends paths"); // ............................... Assert ................................. assert.equal(message, "_ocp(nm):NewCtxPropnm():NewVmProp_ocp(nm):NewCtxFull_ocp(fullName):NewVmPropNewCtxFullLastnm():NewVmFullfullName():NewVmFullNewCtxFullLast", "Observe listener registered for props and computed props, and ctxPrm listener registered for contextual parameters and computed contextual parameters. Trigger correctly."); // ................................ Act .................................. message = ""; res = ""; function listeners() { res += $._data(data).events.propertyChange.length + $._data(jsv.view("#it")._ocps.nm[0]).events.propertyChange.length + $._data(jsv.view("#it")._ocps.fullName[0]).events.propertyChange.length + "|"; } listeners(); jsv.unobserve(data, "nm", changeHandler); listeners(); jsv.unobserve(data, "fullName", changeHandler); listeners(); jsv.unobserve(jsv.view("#it"), "~nm", changeHandler); listeners(); jsv.unobserve(jsv.view("#it"), "~fullName", changeHandler); listeners(); jsv.view("#it").ctxPrm("nm", "AddCtxProp"); // Get this to work for triggering computed function helper change jsv.observable(data).setProperty("nm", "AddVmProp"); jsv.view("#it").ctxPrm("fnprop", fn1); // Get this to work for triggering computed function helper change jsv.observable(data).setProperty("fnprop", fn1); jsv.view("#it").ctxPrm("fullName", "AddCtxFullname"); // Get this to work for triggering computed function helper change jsv.observable(data).setProperty("fullName", "AddVmFullname"); // ............................... Assert ................................. assert.equal(message === "" && res, "14|13|11|10|9|", "Unobserve programmatic APIs for data and for contextual parameters works correctly. Event handlers removed, and no longer triggered"); // ............................... Assert ................................. assert.equal($("#result").text(), "AddCtxFull AddVmFull FN1 FN1 AddVmFullAddCtxFullLast AddVmFullAddCtxFullLast ", "After removing programmatically attached handlers for data and for contextual parameters, declarative UI handlers work correctly"); // ............................... Reset ................................. $("#result").empty(); }); QUnit.test("jsv.view() in element-only content", function(assert) { // =============================== Arrange =============================== jsv.link.tmplHierarchyElCnt("#result", topData); // ................................ Act .................................. var view = jsv.view("#tr1"); // ............................... Assert ................................. assert.ok(view.ctxPrm("val") === 1 && view.type === "myWrapElCnt", 'Within element-only content, jsv.view(elem) gets nearest parent view. Custom tag blocks are of type "tmplName"'); // ................................ Act .................................. view = jsv.view("#tr1", "root"); // ............................... Assert ................................. assert.ok(view.parent.type === "top", 'jsv.view(elem, "root") gets root view (child of top view)'); // ................................ Act .................................. view = jsv.view("#tr1", "item"); // ............................... Assert ................................. assert.ok(view.type === "item" && view.data.lastName === "One" && view.index === 0, 'Within element-only content, jsv.view(elem, "item") gets nearest item view'); // ................................ Act .................................. view = jsv.view("#sp1", "item"); // ............................... Assert ................................. assert.ok(view.type === "item" && view.data.lastName === "One" && view.index === 0, 'jsv.view(elem, "item") gets nearest item view, up through both elCnt and regular content views'); // ................................ Act .................................. view = jsv.view("#tr1", "data"); // ............................... Assert ................................. assert.ok(view.type === "data" && view.data === topData, 'Within element-only content, jsv.view(elem, "data") gets nearest data view'); // ................................ Act .................................. view = jsv.view("#tr1", "if"); // ............................... Assert ................................. assert.ok(view.type === "if" && view.data === people[0], 'Within element-only content, jsv.view(elem, "if") gets nearest "if" view'); // ................................ Act .................................. view = jsv.view("#tr1", "array"); // ............................... Assert ................................. assert.ok(view.type === "array" && view.data === people, 'Within element-only content, jsv.view(elem, "array") gets nearest array view'); // ................................ Act .................................. view = jsv.view("#sp1", "myWrapElCnt"); // ............................... Assert ................................. assert.ok(view.type === "myWrapElCnt" && view.ctx.tag.tagName === "myWrapElCnt", 'Within element-only content, jsv.view(elem, "mytagName") gets nearest view for content of that tag'); // ................................ Act .................................. view = jsv.view("#td1"); // ............................... Assert ................................. assert.ok(view.type === "if" && view.ctx.tag.tagName === "myWrapElCnt", 'Within {{if}} block, jsv.view(elem) gets nearest "if" view, but view.ctx.tag is the nearest non-flow tag, i.e. custom tag that does not have flow set to true'); // ................................ Act .................................. view = jsv.view("#spInFlow1"); // ............................... Assert ................................. assert.ok(view.type === "myFlowElCnt" && view.ctx.tag.tagName === "myWrapElCnt", 'Within {{myFlow}} block, for a flow tag, jsv.view(elem) gets nearest "myFlow" view, but view.ctx.tag is the nearest non-flow tag'); // ................................ Act .................................. view = jsv.view("#tr1", true); // ............................... Assert ................................. assert.ok(view.type === "myWrap2ElCnt", 'Within element-only content, jsv.view(elem, true) gets the first nested view. Custom tag blocks are of type "tmplName"'); // ................................ Act .................................. view = jsv.view("#tr1", true, "myFlowElCnt"); // ............................... Assert ................................. assert.ok(view.type === "myFlowElCnt", 'Within element-only content, jsv.view(elem, true, "mytagName") gets the first (depth first) nested view of that type'); // ................................ Act .................................. view = jsv.view("#tr1").get(true); // ............................... Assert ................................. assert.ok(view.type === "myWrap2ElCnt", 'Within element-only content, view.get(true) gets the first nested view. Custom tag blocks are of type "tmplName"'); // ................................ Act .................................. view = jsv.view("#tr1").get(true, "myFlowElCnt"); // ............................... Assert ................................. assert.ok(view.type === "myFlowElCnt", 'Within element-only content, view.get(true, "mytagName") gets the first (depth first) nested view of that type'); // ................................ Act .................................. view = jsv.view("#tr1"); // ............................... Assert ................................. assert.ok(view.type === "myWrapElCnt" && view === view.get("myWrapElCnt"), 'view.get("viewTypeName") gets nearest viewTypeName view looking at ancestors starting from the view itself'); // ............................... Assert ................................. assert.ok(view === view.get(true, "myWrapElCnt"), 'view.get(true, "viewTypeName") gets nearest viewTypeName view looking at descendants starting from the view itself'); // ................................ Act .................................. view = jsv.view("#tr1").get(true, "myFlowElCnt"); // ............................... Assert ................................. assert.ok(view.tmpl.markup === "xx{{if true}}{{myFlow2/}}{{/if}}", 'view.get(true, "viewTypeName") gets nearest viewTypeName view looking at descendants starting from the view itself'); // ................................ Act .................................. view = jsv.view("#tr1").get(true, "myFlow2"); // ............................... Assert ................................. assert.ok(view.tmpl.markup === "flow2", 'view.get(true, "viewTypeName") gets nearest viewTypeName view looking at descendants starting from the view itself'); // ............................... Assert ................................. assert.ok(jsv.view("#tr1").get(true, "nonexistent") === undefined, 'view.get(true, "viewTypeName") returns undefined if no descendant of that type, starting from the view itself'); // =============================== Arrange =============================== var data = []; $("#result").html("
                      "); jsv.templates("").link("#result ul", data); // ................................ Act .................................. view = jsv.view("#result ul", true); // ............................... Assert ................................. assert.ok(view.data === data && view.type === "array", 'If elem is a container for a rendered array, and the array is empty, jsv.view(elem, true) returns the array view (even though the element is empty)'); // ................................ Act .................................. var itemView = jsv.view("#result ul", true, "item"); // ............................... Assert ................................. assert.ok(!itemView, 'If elem is a container for a rendered array, and the array is empty, jsv.view(elem, true, "item") returns nothing'); // =============================== Arrange =============================== data = [1]; $("#result").html("
                        "); jsv.templates("").link("#result ul", data); // ................................ Act .................................. view = jsv.view("#result ul", true); // ............................... Assert ................................. assert.ok(view.data === data && view.type === "array", 'If elem is a container for a rendered array rendering nothing, and the array is not empty, jsv.view(elem, true) returns the array view (even though the container element is empty)'); // ................................ Act .................................. itemView = jsv.view("#result ul", true, "item"); // ............................... Assert ................................. assert.ok(itemView.index === 0, 'If elem is a container for a rendered array rendering nothing, and the array is not empty, jsv.view(elem, true, "item") returns the item view (even though the container element is empty)'); // =============================== Arrange =============================== data = {people: []}; jsv.templates("
                          {{for people}}{{/for}}
                        ").link("#result", data); // ................................ Act .................................. view = jsv.view("#result ul", true); // ............................... Assert ................................. assert.ok(view.data === data.people && view.type === "array", 'If elem is a container for a rendered array, and the array is empty, jsv.view(elem, true) returns the array view (even though the element is empty)'); // ................................ Act .................................. itemView = jsv.view("#result ul", true, "item"); // ............................... Assert ................................. assert.ok(!itemView, 'If elem is a container for a rendered array, and the array is empty, jsv.view(elem, true, "item") returns nothing'); // =============================== Arrange =============================== data = {people: [1]}; jsv.templates("
                          {{for people}}{{/for}}
                        ").link("#result", data); // ................................ Act .................................. view = jsv.view("#result ul", true); // ............................... Assert ................................. assert.ok(view.data === data.people && view.type === "array", 'If elem is a container for a rendered array rendering nothing, and the array is not empty, jsv.view(elem, true) returns the array view (even though the container element is empty)'); // ................................ Act .................................. itemView = jsv.view("#result ul", true, "item"); // ............................... Assert ................................. assert.ok(itemView.index === 0, 'If elem is a container for a rendered array rendering nothing, and the array is not empty, jsv.view(elem, true, "item") returns the item view (even though the container element is empty)'); // ............................... Reset ................................. $("#result").empty(); }); QUnit.test("view.get() and view.getIndex() in element-only content", function(assert) { // =============================== Arrange =============================== jsv.link.tmplHierarchyElCnt("#result", topData); var view1 = jsv.view("#tr1"); // ................................ Act .................................. var view = view1.get(); // ............................... Assert ................................. assert.ok(view === view1.parent, 'In element-only content, view.get() gets parent view'); // ................................ Act .................................. view = view1.get("item"); // ............................... Assert ................................. assert.ok(view.type === "item" && view.data.lastName === "One" && view.index === 0, 'In element-only content, view.get("item") gets nearest item view'); // ................................ Act .................................. view = view1.get("myWrapElCnt"); // ............................... Assert ................................. assert.ok(view.ctxPrm("val") === 1 && view.type === "myWrapElCnt", 'In element-only content, view.get("viewTypeName") gets nearest viewTypeName view - even if is the nearest view'); // ............................... Assert ................................. assert.ok(jsv.view("#tr1").getIndex() === 0 && jsv.view("#tr1", "item").index === 0 && jsv.view("#tr2").getIndex() === 1 && jsv.view("#tr2", "item").index === 1, 'jsv.view(elem).getIndex() gets index of nearest item view, up through elCnt views'); // ............................... Assert ................................. assert.ok(jsv.view("#sp1").getIndex() === 0 && jsv.view("#sp1", "item").index === 0 && jsv.view("#sp2").getIndex() === 1 && jsv.view("#sp2", "item").index === 1, 'jsv.view(elem).getIndex() gets index of nearest item view, up through both elCnt and regular content views'); // ............................... Reset ................................. $("#result").empty(); }); QUnit.module("API - Tag Controls"); QUnit.test("Wrapping", function(assert) { // =============================== Arrange =============================== jsv.templates({ markup: '{{mytag}}{{:name}}{{/mytag}}' + '
                        ', tags: { mytag: { render: function(val) { return "DefaultArg:" + this.tagCtx.args[0].name + "TMPL:" + this.tagCtx.render(val) + "CNT:" + this.tagCtx.content.render(val); } } } }).link("#result", {name: "Jo"}); // ............................... Assert ................................. assert.equal($("#result").text(), "DefaultArg:JoTMPL:JoCNT:Jo" + "DefaultArg:JoTMPL:JoCNT:Jo", "If tag has no template, tagCtx.render() and tagCtx.content.render() both render content. arg[0] defaults to current data context."); // =============================== Arrange =============================== jsv.templates({ markup: '{{mytag}}{{:name}}{{/mytag}}' + '
                        ', tags: { mytag: { render: function(val) { return "TMPL:" + this.tagCtx.render(val) + "CNT:" + this.tagCtx.content.render(val); }, template: "1{{include tmpl=#content/}} 2{{include tmpl=~tagCtx.content/}} 3{{:~tagCtx.args[0].name}} " } } }).link("#result", {name: "Jo"}); // ............................... Assert ................................. assert.equal($("#result").text(), "TMPL:1Jo 2Jo 3Jo CNT:Jo" + "TMPL:1Jo 2Jo 3Jo CNT:Jo", "If tag has a template, tagCtx.render() renders template and tagCtx.content.render() renders content"); // =============================== Arrange =============================== jsv.templates({ markup: '{{mytag}}a{{:name}}{{else #data}}b{{:name}}{{/mytag}}' + '
                        ', tags: { mytag: { render: function(val) { return "TMPL:" + this.tagCtx.render(val) + "CNT:" + this.tagCtx.content.render(val); }, template: "1{{include tmpl=#content/}} 2{{include tmpl=~tagCtx.content/}} 3{{:~tagCtx.args[0].name}}{{:~tagCtx.index}} " } } }).link("#result", {name: "Jo"}); // ............................... Assert ................................. assert.equal($("#result").text(), "TMPL:1aJo 2aJo 3Jo0 CNT:aJo" + "TMPL:1bJo 2bJo 3Jo1 CNT:bJo" + "TMPL:1aJo 2aJo 3Jo0 CNT:aJo" + "TMPL:1bJo 2bJo 3Jo1 CNT:bJo", "If tag has a template, tagCtx.render() renders template and tagCtx.content.render() renders content - also for else blocks"); // =============================== Arrange =============================== jsv.templates({ markup: '{{mytag}}{{:name}}{{/mytag}}' + '
                        ', tags: { mytag: { render: function(val) { var val = this.tagCtx.view.data; return "DefaultArg:" + !!this.tagCtx.args[0] + "TMPL:" + this.tagCtx.render(val) + "CNT:" + this.tagCtx.content.render(val); }, init: function() { this.argDefault = false; } } } }).link("#result", {name: "Jo"}); // ............................... Assert ................................. assert.equal($("#result").text(), "DefaultArg:falseTMPL:JoCNT:Jo" + "DefaultArg:falseTMPL:JoCNT:Jo", "If argDefault is false, arg[0] does not default to current data context. But can programmatically pass in data to tagCtx.render() or tagCtx.content.render()"); // ............................... Reset ................................. $("#result").empty(); }); QUnit.test("view.childTags() and tag.childTags()", function(assert) { // =============================== Arrange =============================== jsv.link.boundTmplHierarchy("#result", topData); var tags, view1 = jsv.view("#result", true, "item"); // ................................ Act .................................. tags = view1.childTags(); // ............................... Assert ................................. assert.ok(tags.length === 4 && tags[0].tagName === "myWrap" && tags[0].tagCtx.props.val === 1 && tags[0].tagCtx.view.getIndex() === 0 && tags[1].tagName === "myWrap" && tags[1].tagCtx.props.val === 2 && tags[1].tagCtx.view.getIndex() === 0 && tags[2].tagName === "mySimpleWrap" && tags[3].tagName === "myWrap2", 'view.childTags() returns top-level bound tags within the view, and skips any unbound tags'); // ................................ Act .................................. tags = view1.childTags(true); // ............................... Assert ................................. assert.ok(tags.length === 8 && tags[0].tagName === "myWrap" && tags[1].tagName === "myWrap2" && tags[2].tagName === "myWrap2" && tags[3].tagName === "myWrap" && tags[4].tagName === "mySimpleWrap" && tags[5].tagName === "mySimpleWrap" && tags[6].tagName === "myWrap2" && tags[7].tagName === "myWrap2" && tags[0].tagCtx.props.val === 1 && tags[0].tagCtx.view.getIndex() === 0, 'view.childTags(true) returns all tags within the view - in document order'); // ................................ Act .................................. tags = view1.childTags("myWrap"); // ............................... Assert ................................. assert.ok(tags.length === 2 && tags[0].tagName === "myWrap" && tags[1].tagName === "myWrap" && tags[0].tagCtx.props.val === 1 && tags[0].tagCtx.view.getIndex() === 0, 'view.childTags("mytagName") returns all top-level tags of the given name within the view - in document order'); // ................................ Act .................................. tags = view1.childTags(true, "myWrap2"); // ............................... Assert ................................. assert.ok(tags.length === 4 && tags[0].tagName === "myWrap2" && tags[1].tagName === "myWrap2" && tags[2].tagName === "myWrap2" && tags[3].tagName === "myWrap2" && tags[0].tagCtx.view.getIndex() === 0, 'view.childTags(true, "mytagName") returns all tags of the given name within the view - in document order'); // ................................ Act .................................. tags = view1.childTags("myWrap2"); // ............................... Assert ................................. assert.ok(tags.length === 1 && tags[0].tagName === "myWrap2", 'view.childTags(true, "mytagName") returns all tags of the given name within the view - in document order'); // ................................ Act .................................. tags = view1.get(true, "myWrap").childTags(); // Get first myWrap view and look for its top-level child tags // ............................... Assert ................................. assert.ok(tags.length === 2 && tags[0].tagName === "myWrap2" && tags[1].tagName === "myWrap2" && tags[1].tagCtx.view.getIndex() === 0, 'tag.childTags() returns top-level bound child tags, and skips any unbound tags'); // ................................ Act .................................. tags = view1.get(true, "myWrap").childTags(true); // Get first myWrap view and look for descendant tags // ............................... Assert ................................. assert.ok(tags.length === 2 && tags[0].tagName === "myWrap2" && tags[1].tagName === "myWrap2", 'tag.childTags(true) returns descendant tags, and skips any unbound tags'); // ................................ Act .................................. tags = view1.childTags("myWrap")[0].childTags(); // Get first myWrap tag and look for its top-level child tags // ............................... Assert ................................. assert.ok(tags.length === 2 && tags[0].tagName === "myWrap2" && tags[1].tagName === "myWrap2" && tags[1].tagCtx.view.getIndex() === 0, 'tag.childTags() returns top-level bound child tags, and skips any unbound tags'); // ................................ Act .................................. tags = view1.childTags("myWrap")[0].childTags(true); // Get first myWrap tag and look for descendant tags // ............................... Assert ................................. assert.ok(tags.length === 2 && tags[0].tagName === "myWrap2" && tags[1].tagName === "myWrap2", 'tag.childTags(true) returns descendant tags, and skips any unbound tags'); // ............................... Reset ................................. $("#result").empty(); }); QUnit.test("view.childTags() in element-only content", function(assert) { // =============================== Arrange =============================== jsv.link.boundTmplHierarchyElCnt("#result", topData); var tags, view1 = jsv.view("#result", true, "item"); // ................................ Act .................................. tags = view1.childTags(); // ............................... Assert ................................. assert.ok(tags.length === 4 && tags[0].tagName === "myWrapElCnt" && tags[0].tagCtx.props.val === 1 && tags[0].tagCtx.view.getIndex() === 0 && tags[1].tagName === "myWrapElCnt" && tags[1].tagCtx.props.val === 2 && tags[1].tagCtx.view.getIndex() === 0 && tags[2].tagName === "mySimpleWrap" && tags[2].tagCtx.props.val === 5 && tags[2].tagCtx.view.getIndex() === 0, 'In element-only content, view.childTags() returns top-level bound non-flow tags within the view, and skips any unbound tags'); // ................................ Act .................................. tags = view1.childTags(true); // ............................... Assert ................................. assert.ok(tags.length === 9 && tags[0].tagName === "myWrapElCnt" && tags[1].tagName === "myWrap2ElCnt" && tags[2].tagName === "mySimpleWrap" && tags[3].tagName === "myWrap2ElCnt" && tags[4].tagName === "myWrapElCnt" && tags[5].tagName === "mySimpleWrap" && tags[6].tagName === "mySimpleWrap" && tags[7].tagName === "myWrap2" && tags[8].tagName === "myWrap2ElCnt" && tags[0].tagCtx.props.val === 1 && tags[0].tagCtx.view.getIndex() === 0, 'In element-only content, view.childTags(true) returns all bound non-flow tags within the view - in document order'); // ................................ Act .................................. tags = view1.childTags("myWrapElCnt"); // ............................... Assert ................................. assert.ok(tags.length === 2 && tags[0].tagName === "myWrapElCnt" && tags[1].tagName === "myWrapElCnt" && tags[0].tagCtx.props.val === 1 && tags[0].tagCtx.view.getIndex() === 0, 'In element-only content, view.childTags("mytagName") returns all top-level bound tags of the given name within the view - in document order'); // ................................ Act .................................. tags = view1.childTags(true, "myWrap2ElCnt"); // ............................... Assert ................................. assert.ok(tags.length === 3 && tags[0].tagName === "myWrap2ElCnt" && tags[1].tagName === "myWrap2ElCnt" && tags[1].tagName === "myWrap2ElCnt" && tags[0].tagCtx.view.getIndex() === 0, 'In element-only content, view.childTags(true, "mytagName") returns all bound tags of the given name within the view - in document order'); // ................................ Act .................................. tags = view1.childTags(true, "myFlow"); // ............................... Assert ................................. assert.ok(tags.length === 2 && tags[0].tagName === "myFlow" && tags[1].tagName === "myFlow" && tags[1].tagCtx.view.getIndex() === 0, 'In element-only content, view.childTags(true, "myFlow") for a flow tag returns all bound tags of the given name within the view - in document order'); // ................................ Act .................................. tags = view1.childTags(true, "if"); // ............................... Assert ................................. assert.ok(tags.length === 1 && tags[0].tagName === "if" && tags[0].tagCtx.view.getIndex() === 0, 'In element-only content, view.childTags(true, "if") for a flow tag ("if" in this case) returns all bound tags of the given name within the view - in document order'); // ................................ Act .................................. tags = view1.childTags("myWrap2ElCnt"); // ............................... Assert ................................. assert.ok(tags.length === 1, 'In element-only content, view.childTags("mytagName") returns all top-level tags of the given name within the view - in document order'); // ................................ Act .................................. tags = view1.get(true, "myWrapElCnt").childTags(); // Get first myWrapElCnt view and look for its top-level child tags // ............................... Assert ................................. assert.ok(tags.length === 2 && tags[0].tagName === "myWrap2ElCnt" && tags[1].tagName === "myWrap2ElCnt" && tags[1].tagCtx.view.getIndex() === 0, 'view.childTags() returns top-level bound child tags, and skips any unbound tags'); // ................................ Act .................................. tags = view1.get(true, "myWrapElCnt").childTags(true); // Get first myWrapElCnt view and look for descendant tags // ............................... Assert ................................. assert.ok(tags.length === 3 && tags[0].tagName === "myWrap2ElCnt" && tags[1].tagName === "mySimpleWrap" && tags[2].tagName === "myWrap2ElCnt", 'view.childTags(true) returns descendant tags, and skips any unbound tags'); // ................................ Act .................................. tags = view1.childTags("myWrapElCnt")[0].childTags(); // Get first myWrapElCnt tag and look for its top-level child tags // ............................... Assert ................................. assert.ok(tags.length === 2 && tags[0].tagName === "myWrap2ElCnt" && tags[1].tagName === "myWrap2ElCnt" && tags[1].tagCtx.view.getIndex() === 0, 'tag.childTags() returns top-level bound child tags, and skips any unbound tags'); // ................................ Act .................................. tags = view1.childTags("myWrapElCnt")[0].childTags(true); // Get first myWrapElCnt tag and look for descendant tags // ............................... Assert ................................. assert.ok(tags.length === 3 && tags[0].tagName === "myWrap2ElCnt" && tags[1].tagName === "mySimpleWrap" && tags[2].tagName === "myWrap2ElCnt", 'tag.childTags(true) returns descendant tags, and skips any unbound tags'); // ................................ Act .................................. tags = view1.childTags("mySimpleWrap")[0].childTags(); // Get first mySimpleWrap tag and look for its top-level child tags // ............................... Assert ................................. assert.ok(tags.length === 1 && tags[0].tagName === "mySimpleWrap", 'tag.childTags() returns top-level bound child tags, and skips any unbound tags'); // ................................ Act .................................. tags = view1.childTags("mySimpleWrap")[0].childTags(true); // Get first mySimpleWrap tag and look for descendant tags // ............................... Assert ................................. assert.ok(tags.length === 2 && tags[0].tagName === "mySimpleWrap" && tags[1].tagName === "myWrap2", 'tag.childTags(true) returns descendant tags, and skips any unbound tags'); // ................................ Act .................................. tags = view1.childTags("mySimpleWrap")[0].childTags(true, "myWrap2"); // Get first mySimpleWrap tag and look for descendant tags of type "myWrap2" // ............................... Assert ................................. assert.ok(tags.length === 1 && tags[0].tagName === "myWrap2", 'tag.childTags(true, "mytagName") returns descendant tags of chosen name, and skips any unbound tags'); // =============================== Arrange =============================== jsv.templates("{^{for row}}{^{mySimpleWrap/}}{{/for}}
                        ").link("#result", {row: {}}); // ................................ Act .................................. var tag = $("#result tr").view().childTags()[0]; // ............................... Assert ................................. assert.ok(tag.tagName === "mySimpleWrap", 'childTags() correctly finds tag which has no output and renders within element contet, inside another tag also in element content'); // ............................... Reset ................................. $("#result").empty(); }); QUnit.test("view.childTags() in element-only content, using data-link", function(assert) { // =============================== Arrange =============================== jsv.link.boundTmplHierarchyElCntWithDataLink("#result", person1); var tags, view1 = jsv.view("#result", true); // ................................ Act .................................. tags = view1.childTags(); // ............................... Assert ................................. assert.ok(tags.length === 1 && tags[0].tagName === "myWrapElCnt" && tags[0].tagCtx.props.val === 1, 'In element-only content, view.childTags() returns top-level bound tags within the view, and skips any unbound tags'); // ................................ Act .................................. tags = view1.childTags(true); // ............................... Assert ................................. assert.ok(tags.length === 2 && tags[0].tagName === "myWrapElCnt" && tags[1].tagName === "myWrap2ElCnt" && tags[0].tagCtx.props.val === 1, 'In element-only content, view.childTags(true) returns all tags within the view - in document order'); // ................................ Act .................................. tags = view1.childTags("myWrapElCnt"); // ............................... Assert ................................. assert.ok(tags.length === 1 && tags[0].tagName === "myWrapElCnt" && view1.childTags("inexistantTag").length === 0, 'In element-only content, view.childTags("mytagName") returns all top-level tags of the given name within the view - in document order'); // ................................ Act .................................. tags = view1.childTags(true, "myWrap2ElCnt"); // ............................... Assert ................................. assert.ok(tags.length === 1 && tags[0].tagName === "myWrap2ElCnt", 'In element-only content, view.childTags(true, "mytagName") returns all tags of the given name within the view - in document order'); // ................................ Act .................................. tags = view1.childTags("myWrap2ElCnt"); // ............................... Assert ................................. assert.ok(tags.length === 0, 'In element-only content, view.childTags(true, "mytagName") returns all tags of the given name within the view - in document order'); // ............................... Reset ................................. $("#result").empty(); }); QUnit.test("lateRender - for deferred API calls", function(assert) { // =============================== Arrange =============================== // ................................ Act .................................. jsv.templates({ markup: // These calls are before the targeted instance, so need lateRender=true (or any other value than false) '
                        ' + '
                        ' + '/
                        ' + '{^{for #get(true, \'mytag\').tag.value tmpl=\'showVal\' lateRender=true/}}' + '{^{for #childTags(\'mytag\')[0].value tmpl=\'showVal\' lateRender=true/}}' + '/{^{:#childTags(\'mytag\')[0].value lateRender=true}}' + // This is the targeted instance '=
                        ' + // These calls are after the targeted instance, so don't need lateRender=true '
                        ' + '
                        ' + '/
                        ' + // But these following tags still need lateRender=true, so the rendering is also deferred to after linking has been completed '{^{for #get(true, \'mytag\').tag.value tmpl=\'showVal\' lateRender=true/}}' + '{^{for #childTags(\'mytag\')[0].value tmpl=\'showVal\' lateRender=true/}}' + '/{^{:#childTags(\'mytag\')[0].value lateRender=true}}' + '/{^{mylaterendertag \'mytag\'/}}' + '/{^{mylaterendertag \'mytag\' lateRender=false/}}', tags: { mytag: { template: 'MyTag', value: 'tagVal' }, mylaterendertag: { render: function() { return this.tagCtx.view.childTags(this.tagCtx.args[0]) .length; }, lateRender: 1 // This will render late, unless lateRender=false on the instance markup } }, templates: { showVal: '-{{:}}' } }).link("#result", {}); // ............................... Assert ................................. assert.equal($("#result").text(), "-tagVal-tagVal/tagVal-tagVal-tagVal/tagVal=MyTag-tagVal-tagVal/tagVal-tagVal-tagVal/tagVal/1/0", "When using APIs such as #childTags() and #get() within binding expressions, to return tag instances, use lateRender=true to defer the API call until linking is complete" ); // ................................ Act .................................. jsv.templates({ markup: // These calls are before the targeted instance, so need lateRender=true '
                        ' + '/
                        ' + '{^{for #get(true, \'mytag\').tag.value tmpl=\'showVal\' lateRender=undefined/}}' + '{^{for #childTags(\'mytag\')[0].value tmpl=\'showVal\' lateRender=null/}}' + // This is the targeted instance '={^{mytag/}}', tags: { mytag: { template: 'MyTag', value: 'tagVal' } }, templates: { showVal: '-{{:}}' } }).link("#result", {}); // ............................... Assert ................................. assert.equal($("#result").text(), "-tagVal/tagVal-tagVal-tagVal=MyTag", "When using APIs such as #childTags() and #get() within binding expressions, to return tag instances, use lateRender=xxx (any value xxx other than false or undefined) to defer the API call until linking is complete" ); // ................................ Act .................................. var myTmpl = jsv.templates({ markup: // These calls are before the targeted instance, so need lateRender=true '
                        ' + // This is the targeted instance '={^{mytag/}}', tags: { mytag: { template: 'MyTag', value: 'tagVal' } }, templates: { showVal: '-{{:}}' } }); var haserror; try { myTmpl.link("#result", {}); } catch(e) { haserror = !!e; } // ............................... Assert ................................. assert.ok(haserror, "When using APIs such as #childTags() within binding expressions, using lateRender=false does not defer the API call" ); // ................................ Reset ................................ $("#result").empty(); }); QUnit.test("this= and @some.path", function(assert) { jsv.views.settings.trigger(false); // =============================== Arrange =============================== // ................................ Act .................................. jsv.templates({ markup: '' + 'BEFORE:{^{:@~o1.tagCtx.props.p}}' + '' + 'TAGS:' + '{^{if this=~o2 ^p=@~o3.tagCtx.props.n n="two "}}B{{/if}}' + '{^{if this=~o3 ^p=@~o1.tagCtx.props.n n="three "}}C{{/if}}' + 'AFTER:{^{:@~o1.tagCtx.props.p}}' + '' + '', tags: { out: { template: "OUT:{^{:~tagCtx.props.p}}", onUpdate: false, } } }).link("#result", {}); // ............................... Assert ................................. assert.equal($("#result").text(), "BEFORE:two two TAGS:DBCAFTER:two two ", "Declarative this=ref binding on built-in flow tag and late path @ref... works correctly"); // ................................ Act .................................. jsv.templates({ markup: '' + 'BEFORE:{^{:@~o1.tagCtx.props.p}}' + '' + 'TAGS:' + '{^{out this=~o2 ^p=@~o3.tagCtx.props.n n="two "/}}' + '{^{out this=~o3 ^p=@~o1.tagCtx.props.n n="three "/}}' + 'AFTER:{^{:@~o1.tagCtx.props.p}}' + '' + '', tags: { out: { template: "OUT:{^{:~tagCtx.props.p}}", onUpdate: false, } } }).link("#result", {}); // ............................... Assert ................................. assert.equal($("#result").text(), "BEFORE:two two TAGS:OUT:two OUT:three OUT:one AFTER:two two ", "Declarative this=ref binding on custom tag and late path @ref... works correctly"); // ................................ Act .................................. var input1 = $("#inp1"), input2 = $("#inp1"), result = ""; input1.val("newp "); input1.change(); result += $("#result").text(); input2.val("newp2 "); input2.change(); result += "|" + $("#result").text(); assert.equal(result, "BEFORE:newp newp TAGS:OUT:newp OUT:three OUT:one AFTER:newp newp |BEFORE:newp2 newp2 TAGS:OUT:newp2 OUT:three OUT:one AFTER:newp2 newp2 ", "Declarative this=ref binding on custom tag and late path @ref... works correctly even for two-way binding"); // ................................ Reset ................................ $("#result").empty(); jsv.views.settings.trigger(true); }); //TODO add tests for tag.refresh() QUnit.test("Modifying content, initializing widgets/tag controls, using data-link", function(assert) { // =============================== Arrange =============================== // ................................ Act .................................. jsv.templates({ markup: '
                        ', tags: { myWidget: { init: function(tagCtx, linkCtx, ctx) { }, render: function() { return " render"; }, onAfterLink: function() { $(this.linkCtx.elem).append(" after"); } } } }).link("#result", person1); // ............................... Assert ................................. assert.equal($("#result div").html().replace(/<\/script>/g, ""), " render after", 'A data-linked tag control allows setting of content on the data-linked element during render and onAfterLink'); // =============================== Arrange =============================== // ................................ Act .................................. jsv.templates({ markup: '
                        ', tags: { myRenderInLinkEventsWidget: { init: function(tagCtx, linkCtx, ctx) { $(linkCtx.elem).append(" init"); }, onAfterLink: function() { $(this.linkCtx.elem).append(" after"); } } } }).link("#result", person1); // ............................... Assert ................................. assert.equal($("#result div").html(), " init after", 'A data-linked tag control which does not render allows setting of content on the data-linked element during init and onAfterLink'); // ............................... Reset ................................. $("#result").empty(); //TODO: Add tests for attaching jQuery UI widgets or similar to tag controls, using data-link and {^{mytag}} inline data binding. }); QUnit.test('two-way bound tag controls', function(assert) { var done; if (assert.async) { done = assert.async() } else { stop() } jsv.views.settings.trigger(false); // =============================== Arrange =============================== var person = {name: "Jo"}; cancelChange = false; noRenderOnUpdate = true; renders = false; eventData = ""; //TODO add tests for convert and convertBack declared on tag def or on tag instance and declared dependencies on tag and/or convert - either arrays or functions. // ELEMENT-BASED DATA-LINKED TAGS ON INPUT // ................................ Act .................................. jsv.templates('') .link("#result", person); var tag = $("#result").view(true).childTags("twoWayTag")[0], linkedEl = $("#linkedElm")[0]; // ............................... Assert ................................. assert.equal(eventData, "init render onBind onAfterLink setValue ", 'Data link using: - event order for init, render, link'); eventData = ""; // ................................ Act .................................. before = tag.value + linkedEl.value; jsv.observable(person).setProperty({name: "newName"}); after = tag.value + linkedEl.value; // ............................... Assert ................................. assert.equal(eventData, "onBeforeChange onUpdate onAfterLink setValue onAfterChange ", 'Data link using: - event order for onUpdate (returning false) - render not called'); eventData = ""; // ............................... Assert ................................. assert.equal(before + "|" + after, "JoJo|newNamenewName", 'Data link using: - binds data to linkedElem'); // ................................ Act .................................. noRenderOnUpdate = false; before = tag.value + linkedEl.value; jsv.observable(person).setProperty({name: "newName2"}); after = tag.value + linkedEl.value; // ............................... Assert ................................. assert.equal(eventData, "onBeforeChange onUpdate onUnbind render onBind onAfterLink setValue onAfterChange ", 'Data link using: - event order for onUpdate (returning true) - render is called, but no render'); eventData = ""; // ............................... Assert ................................. assert.equal(before + "|" + after, "newNamenewName|newName2newName2", 'Data link using: - binds data to linkedElem'); // ................................ Act .................................. noRenderOnUpdate = false; renders = true; before = tag.value + linkedEl.value; jsv.observable(person).setProperty({name: "newName3"}); after = tag.value + linkedEl.value; // ............................... Assert ................................. assert.equal(eventData, "onBeforeChange onUpdate onUnbind render onBind onAfterLink setValue onAfterChange ", 'Data link using: - event order for onUpdate (returning true) - render is called'); eventData = ""; // ............................... Assert ................................. assert.equal(before + "|" + after, "newName2newName2|newName3newName3", 'Data link using: - binds data to linkedElem - (replacing any value set during rendering)'); // ................................ Reset .................................. noRenderOnUpdate = true; renders = false; // ................................ Act .................................. before = tag.value + person.name; linkedEl.value = "newVal"; $(linkedEl).change(); after = tag.value + person.name; // ............................... Assert ................................. assert.equal(eventData, "onBeforeUpdateVal setValue onBeforeChange onUpdate onAfterLink onAfterChange ", 'Data link using: - event order for onChange'); eventData = ""; // ............................... Assert ................................. assert.equal(before + "|" + after, "newName3newName3|newValnewVal", 'Data link using: - binds linkedElem back to data'); // ................................ Act .................................. before = tag.value + person.name; cancelChange = true; linkedEl.value = "2ndNewVal"; $(linkedEl).change(); after = tag.value + person.name; // ............................... Assert ................................. assert.equal(eventData, "onBeforeUpdateVal setValue onBeforeChange ", 'Data link using: - event order for cancelled onBeforeChange'); eventData = ""; // ............................... Assert ................................. assert.equal(before + "|" + after, "newValnewVal|newVal2ndNewVal", 'Data link using: - if onBeforeChange returns false -> data changes but no relinking of tag'); // ................................ Reset .................................. cancelChange = false; cancelUpdate = true; // ................................ Act .................................. before = tag.value + person.name; linkedEl.value = "3rdNewVal"; $(linkedEl).change(); after = tag.value + person.name; // ............................... Assert ................................. assert.equal(eventData, "onBeforeUpdateVal ", 'Data link using: - event order for cancelled onBeforeUpdateVal'); eventData = ""; // ............................... Assert ................................. assert.equal(before + "|" + after, "newVal2ndNewVal|newVal2ndNewVal", 'Data link using: - if onBeforeUpdateVal returns false -> data does not change, and no relinking of tag'); // ................................ Reset .................................. cancelUpdate = false; noRenderOnUpdate = true; renders = false; // ................................ Act .................................. person.name = "updatedName"; linkedEl.value = "updatedVal"; before = tag.value + linkedEl.value; tag.refresh(); linkedEl = $("#linkedElm")[0]; after = tag.value + linkedEl.value + tag.linkedElem[0].value; // ............................... Assert ................................. assert.equal(eventData, "onUnbind render onBind onAfterLink setValue ", 'Data link using: - event order for tag.refresh'); eventData = ""; // ............................... Assert ................................. assert.equal(before + "|" + after, "newValupdatedVal|updatedNameupdatedNameupdatedName", 'Data link using: - tag.refresh() calls render and onAfterLink - reset to current data, and updates target (input value)'); // ................................ Act .................................. $("#result").empty(); // ............................... Assert ................................. assert.equal(eventData, "onUnbind onDispose ", 'Data link using: - event order for onDispose'); eventData = ""; // ................................ Reset .................................. person.name = "Jo"; // =============================== Arrange =============================== // ................................ Act .................................. jsv.templates({ markup: '', converters: { myupper: function(val) { return val.toUpperCase(); } } }).link("#result", person, { lower: function(val) { return val.toLowerCase(); } }); tag = $("#result").view(true).childTags("twoWayTag")[0]; linkedEl = $("#linkedElm")[0]; // ............................... Assert ................................. assert.equal(linkedEl.value + "|" + tag.value, "JO|Jo", 'Data link using: - (tag.convert setting) - initial linking: converts the value on the target input'); // ................................ Act .................................. jsv.observable(person).setProperty({name: "ANewName"}); // ............................... Assert ................................. assert.equal(linkedEl.value + "|" + tag.value, "ANEWNAME|ANewName", 'Data link using: - (tag.convert setting) - on data change: converts the value on the target input'); // ................................ Act .................................. linkedEl.value = "ChangeTheName"; $(linkedEl).change(); // ............................... Assert ................................. assert.equal(person.name + "|" + tag.value, "changethename|changethename", 'Data link using: - (tag.convertBack setting) on element change: converts the data, and sets on data'); // ................................ Reset .................................. $("#result").empty(); person.name = "Jo"; cancelChange = false; cancelUpdate = false; noRenderOnUpdate = true; renders = false; eventData = ""; // =============================== Arrange =============================== var lower = function(val) { return val.toLowerCase(); }, upper = function(val) { return val.toUpperCase(); }, options = {cvt: upper}; // ................................ Act .................................. jsv.templates({ markup: '', converters: { myupper: function(val) { return val.toUpperCase(); } } }).link("#result", person, {options: options}); tag = $("#result").view(true).childTags("twoWayTag")[0]; linkedEl = $("#linkedElm")[0]; // ............................... Assert ................................. assert.equal(linkedEl.value + "|" + tag.value, "JO|Jo", 'Data link using: - converter specified by data-linked convert property'); // ................................ Act .................................. jsv.observable(options).setProperty({cvt: lower}); jsv.observable(person).setProperty({name: "ANewName"}); // ............................... Assert ................................. assert.equal(linkedEl.value + "|" + tag.value, "anewname|ANewName", 'Data link using: - data-linked swapping of converter from one function to another'); // ................................ Act .................................. jsv.observable(options).setProperty({cvt: "myupper"}); // ............................... Assert ................................. assert.equal(linkedEl.value + "|" + tag.value, "ANEWNAME|ANewName", 'Data link using: - data-linked swapping of converter from function to named converter'); // ................................ Reset .................................. $("#result").empty(); person.name = "Jo"; cancelChange = false; noRenderOnUpdate = true; renders = false; eventData = ""; // =============================== Arrange =============================== //INLINE DATA-LINKED TAGS ON INPUT // ................................ Act .................................. jsv.templates('{^{twoWayTag name}}{{/twoWayTag}}') .link("#result", person); tag = $("#result").view(true).childTags("twoWayTag")[0]; linkedEl = $("#linkedElm")[0]; // ............................... Assert ................................. assert.equal(eventData, "init render onBind onAfterLink setValue ", 'Data link using: {^{twoWayTag name}} - event order for init, render, link'); eventData = ""; // ................................ Act .................................. before = tag.value + linkedEl.value; jsv.observable(person).setProperty({name: "newName"}); after = tag.value + linkedEl.value; // ............................... Assert ................................. assert.equal(eventData + !!linkedEl.parentNode, "onBeforeChange onUpdate onAfterLink setValue onAfterChange true", 'Data link using: {^{twoWayTag name}} - event order for onUpdate (returning false) - render not called; linkedElem not replaced'); eventData = ""; // ............................... Assert ................................. assert.equal(before + "|" + after, "JoJo|newNamenewName", 'Data link using: {^{twoWayTag name}} - binds data to linkedElem'); // ................................ Act .................................. noRenderOnUpdate = false; before = tag.value + linkedEl.value; jsv.observable(person).setProperty({name: "newName2"}); // ............................... Assert ................................. assert.equal(eventData + !!linkedEl.parentNode, "onBeforeChange onUpdate onUnbind render onBind onAfterLink setValue onAfterChange false", 'Data link using: {^{twoWayTag name}} - event order for onUpdate (returning true) - render is called; linkedElem is replaced'); eventData = ""; linkedEl = $("#linkedElm")[0]; after = tag.value + linkedEl.value; // ............................... Assert ................................. assert.equal(before + "|" + after, "newNamenewName|newName2newName2", 'Data link using: {^{twoWayTag name}} - binds data to linkedElem'); // ................................ Act .................................. noRenderOnUpdate = false; renders = true; before = tag.value + linkedEl.value; jsv.observable(person).setProperty({name: "newName3"}); after = tag.value + linkedEl.value; // ............................... Assert ................................. assert.equal(eventData + !!linkedEl.parentNode, "onBeforeChange onUpdate onUnbind render onBind onAfterLink setValue onAfterChange false", 'Data link using: {^{twoWayTag name}} - event order for onUpdate (returning true) - render is called; linkedElem is replaced'); eventData = ""; linkedEl = $("#linkedElm")[0]; after = tag.value + linkedEl.value; // ............................... Assert ................................. assert.equal(before + "|" + after, "newName2newName2|newName3newName3", 'Data link using: {^{twoWayTag name}} - binds data to newly rendered linkedElem'); // ................................ Reset .................................. noRenderOnUpdate = true; renders = false; // ................................ Act .................................. before = tag.value + person.name; linkedEl.value = "newVal"; $(linkedEl).change(); after = tag.value + person.name; // ............................... Assert ................................. assert.equal(eventData, "onBeforeUpdateVal setValue onBeforeChange onUpdate onAfterLink onAfterChange ", 'Data link using: {^{twoWayTag name}} - event order for onChange'); eventData = ""; // ............................... Assert ................................. assert.equal(before + "|" + after, "newName3newName3|newValnewVal", 'Data link using: {^{twoWayTag name}} - binds linkedElem back to data'); // ................................ Act .................................. before = tag.value + person.name; cancelChange = true; linkedEl.value = "2ndNewVal"; $(linkedEl).change(); after = tag.value + person.name; // ............................... Assert ................................. assert.equal(eventData, "onBeforeUpdateVal setValue onBeforeChange ", 'Data link using: {^{twoWayTag name}} - event order for cancelled onBeforeChange'); eventData = ""; // ............................... Assert ................................. assert.equal(before + "|" + after, "newValnewVal|newVal2ndNewVal", 'Data link using: {^{twoWayTag name}} - if onBeforeChange returns false -> data changes but no relinking of tag'); // ................................ Reset .................................. cancelChange = false; noRenderOnUpdate = true; renders = false; // ................................ Act .................................. person.name = "updatedName"; linkedEl.value = "updatedVal"; before = tag.value + linkedEl.value; tag.refresh(); linkedEl = $("#linkedElm")[0]; after = tag.value + linkedEl.value + tag.linkedElem[0].value; // ............................... Assert ................................. assert.equal(eventData, "onUnbind render onBind onAfterLink setValue ", 'Data link using: {^{twoWayTag name}} - event order for tag.refresh'); eventData = ""; // ............................... Assert ................................. assert.equal(before + "|" + after, "newValupdatedVal|updatedNameupdatedNameupdatedName", 'Data link using: {^{twoWayTag name}} - tag.refresh() calls onUnbind, render, onBind and onAfterLink - reset to current data, and updates target (input value)'); // ................................ Act .................................. $("#result").empty(); // ............................... Assert ................................. assert.equal(eventData, "onUnbind onDispose ", 'Data link using: {^{twoWayTag name}} - event order for onDispose'); eventData = ""; // ................................ Reset .................................. person.name = "Jo"; // =============================== Arrange =============================== jsv.templates({ markup: '{^{twoWayTag name convert="myupper" convertBack=~lower}}{{/twoWayTag}}', converters: { myupper: function(val) { return val.toUpperCase(); } } }).link("#result", person, { lower: function(val) { return val.toLowerCase(); } }); tag = $("#result").view(true).childTags("twoWayTag")[0]; linkedEl = $("#linkedElm")[0]; // ............................... Assert ................................. assert.equal(linkedEl.value + "|" + tag.value, "JO|Jo", 'Data link using: {^{twoWayTag name convert=\'myupper\'}} - (tag.convert setting) - initial linking: converts the value on the target input'); // ................................ Act .................................. jsv.observable(person).setProperty({name: "ANewName"}); // ............................... Assert ................................. assert.equal(linkedEl.value + "|" + tag.value, "ANEWNAME|ANewName", 'Data link using: {^{twoWayTag name convert=\'myupper\'}} - (tag.convert setting) - on data change: converts the value on the target input'); // ................................ Act .................................. linkedEl = $("#linkedElm")[0]; linkedEl.value = "ChangeTheName"; $(linkedEl).change(); // ............................... Assert ................................. assert.equal(person.name + "|" + tag.value, "changethename|changethename", 'Data link using: {^{twoWayTag name convertBack=~lower}} - (tag.convertBack setting) on element change: converts the data, and sets on data'); // ................................ Reset .................................. $("#result").empty(); person.name = "Jo"; cancelChange = false; noRenderOnUpdate = true; renders = false; eventData = ""; // =============================== Arrange =============================== //INLINE DATA-LINKED SELF-CLOSED TAG rendering INPUT // ................................ Act .................................. jsv.templates('{^{twoWayTag name/}}') .link("#result", person); tag = $("#result").view(true).childTags("twoWayTag")[0]; // ............................... Assert ................................. assert.equal(eventData, "init render onBind onAfterLink setValue ", 'Data link using: {^{twoWayTag name/}} - event order for init, render, link'); eventData = ""; // ................................ Act .................................. linkedEl = tag.linkedElem[0]; before = tag.value + linkedEl.value; jsv.observable(person).setProperty({name: "newName"}); after = tag.value + tag.linkedElem[0].value; // ............................... Assert ................................. assert.equal(eventData + !!linkedEl.parentNode, "onBeforeChange onUpdate onAfterLink setValue onAfterChange true", 'Data link using: {^{twoWayTag name/}} - event order for onUpdate (returning false) - render not called; linkedElem not replaced'); eventData = ""; // ............................... Assert ................................. assert.equal(before + "|" + after, "JoJo|newNamenewName", 'Data link using: {^{twoWayTag name/}} - binds data to linkedElem'); // ................................ Act .................................. noRenderOnUpdate = false; linkedEl = tag.linkedElem[0]; before = tag.value + linkedEl.value; jsv.observable(person).setProperty({name: "newName2"}); after = tag.value + tag.linkedElem[0].value; // ............................... Assert ................................. assert.equal(eventData + !!linkedEl.parentNode, "onBeforeChange onUpdate onUnbind render onBind onAfterLink setValue onAfterChange false", 'Data link using: {^{twoWayTag name/}} - event order for onUpdate (returning true) - render is called; linkedElem is replaced'); eventData = ""; // ............................... Assert ................................. assert.equal(before + "|" + after, "newNamenewName|newName2newName2", 'Data link using: {^{twoWayTag name/}} - binds data to linkedElem'); // ................................ Act .................................. noRenderOnUpdate = false; renders = true; linkedEl = tag.linkedElem[0]; before = tag.value + linkedEl.value; jsv.observable(person).setProperty({name: "newName3"}); after = tag.value + tag.linkedElem[0].value; // ............................... Assert ................................. assert.equal(eventData + !!linkedEl.parentNode, "onBeforeChange onUpdate onUnbind render onBind onAfterLink setValue onAfterChange false", 'Data link using: {^{twoWayTag name/}} - event order for onUpdate (returning true) - render is called; linkedElem is replaced'); eventData = ""; // ............................... Assert ................................. assert.equal(before + "|" + after, "newName2newName2|newName3newName3", 'Data link using: {^{twoWayTag name/}} - binds data to newly rendered linkedElem'); // ................................ Reset .................................. noRenderOnUpdate = true; renders = false; // ................................ Act .................................. before = tag.value + person.name; tag.linkedElem[0].value = "newVal"; tag.linkedElem.change(); after = tag.value + person.name; // ............................... Assert ................................. assert.equal(eventData, "onBeforeUpdateVal setValue onBeforeChange onUpdate onAfterLink onAfterChange ", 'Data link using: {^{twoWayTag name/}} - event order for onChange'); eventData = ""; // ............................... Assert ................................. assert.equal(before + "|" + after, "newName3newName3|newValnewVal", 'Data link using: {^{twoWayTag name/}} - binds linkedElem back to dataonChange'); // ................................ Act .................................. before = tag.value + person.name; cancelChange = true; tag.linkedElem[0].value = "2ndNewVal"; tag.linkedElem.change(); after = tag.value + person.name; // ............................... Assert ................................. assert.equal(eventData, "onBeforeUpdateVal setValue onBeforeChange ", 'Data link using: {^{twoWayTag name/}} - event order for cancelled onBeforeChange'); eventData = ""; // ............................... Assert ................................. assert.equal(before + "|" + after, "newValnewVal|newVal2ndNewVal", 'Data link using: {^{twoWayTag name/}} - if onBeforeChange returns false -> data changes but no relinking of tag'); // ................................ Reset .................................. cancelChange = false; noRenderOnUpdate = true; renders = false; // ................................ Act .................................. person.name = "updatedName"; tag.linkedElem[0].value = "updatedVal"; before = tag.value + tag.linkedElem[0].value; tag.refresh(); after = tag.value + tag.linkedElem[0].value; // ............................... Assert ................................. assert.equal(eventData, "onUnbind render onBind onAfterLink setValue ", 'Data link using: {^{twoWayTag name/}} - event order for tag.refresh'); eventData = ""; // ............................... Assert ................................. assert.equal(before + "|" + after, "newValupdatedVal|updatedNameupdatedName", 'Data link using: {^{twoWayTag name/}} - tag.refresh() calls render and onAfterLink - reset to current data, and updates target (input value)'); // ................................ Act .................................. $("#result").empty(); // ............................... Assert ................................. assert.equal(eventData, "onUnbind onDispose ", 'Data link using: {^{twoWayTag name/}} - event order for onDispose'); eventData = ""; // ................................ Reset .................................. person.name = "Jo"; // =============================== Arrange =============================== jsv.templates({ markup: '{^{twoWayTag name convert="myupper" convertBack=~lower/}}', converters: { myupper: function(val) { return val.toUpperCase(); } } }).link("#result", person, { lower: function(val) { return val.toLowerCase(); } }); tag = $("#result").view(true).childTags("twoWayTag")[0]; // ............................... Assert ................................. assert.equal(tag.linkedElem[0].value + "|" + tag.value, "JO|Jo", 'Data link using: {^{twoWayTag name convert="myupper"/}} - (tag.convert setting) - initial linking: converts the value on the target input'); // ................................ Act .................................. jsv.observable(person).setProperty({name: "ANewName"}); // ............................... Assert ................................. assert.equal(tag.linkedElem[0].value + "|" + tag.value, "ANEWNAME|ANewName", 'Data link using: {^{twoWayTag name convert="myupper"/}} - (tag.convert setting) - on data change: converts the value on the target input'); // ................................ Act .................................. tag.linkedElem[0].value = "ChangeTheName"; tag.linkedElem.change(); // ............................... Assert ................................. assert.equal(person.name + "|" + tag.value, "changethename|changethename", 'Data link using: {^{twoWayTag name convertBack=~lower/}} - (tag.convertBack setting) on element change: converts the data, and sets on data'); // ................................ Reset .................................. person.name = "Jo"; person.name2 = "Jo"; // =============================== Arrange =============================== jsv.templates({ markup: '{^{textbox name /}} {^{textbox name2 convert="cvt" convertBack=~cvtBk/}}', tags: { textbox: { linkedElement: "input", template: "", onUpdate: false, // No need to re-render whole tag, when content updates. dataBoundOnly: true, onAfterLink: function(tagCtx) { this.value = tagCtx.args[0]; }, convert: function(val) { return val.toUpperCase(); }, convertBack: function(val) { return val.toLowerCase(); } } }, converters: { cvt: function(val) { return val + "cvt"; } } }).link("#result", person, { cvtBk: function(val) { return val + "cvtBk"; } }); var tagWithDefaultConverters = $("#result").view(true).childTags("textbox")[0]; var tagWithOverrideConverters = $("#result").view(true).childTags("textbox")[1]; // ............................... Assert ................................. assert.equal(tagWithDefaultConverters.linkedElem[0].value + "|" + tagWithDefaultConverters.value + " % " + tagWithOverrideConverters.linkedElem[0].value + "|" + tagWithOverrideConverters.value, "JO|Jo % Jocvt|Jo", 'Data linked tag with default convert and convertBack, on initial linking: {^{textbox/}} uses default convert and {^{textbox convert=.../}} uses overridden convert'); // ................................ Act .................................. jsv.observable(person).setProperty({name: "ANewName", name2: "ANewName2"}); // ............................... Assert ................................. assert.equal(tagWithDefaultConverters.linkedElem[0].value + "|" + tagWithDefaultConverters.value + " % " + tagWithOverrideConverters.linkedElem[0].value + "|" + tagWithOverrideConverters.value, "ANEWNAME|ANewName % ANewName2cvt|ANewName2", 'Data linked tag with default convert and convertBack, on data change: {^{textbox/}} uses default convert and {^{textbox convert=.../}} uses overridden convert'); // ................................ Act .................................. tagWithDefaultConverters.linkedElem[0].value = "ChangeTheName"; tagWithOverrideConverters.linkedElem[0].value = "ChangeTheName2"; tagWithDefaultConverters.linkedElem.change(); tagWithOverrideConverters.linkedElem.change(); // ............................... Assert ................................. assert.equal(person.name + "|" + tagWithDefaultConverters.value + " % " + person.name2 + "|" + tagWithOverrideConverters.value, "changethename|changethename % ChangeTheName2cvtBk|ChangeTheName2cvtBk", 'Data linked tag with default convert and convertBack, on element change: {^{textbox/}}uses default convertBAck and {^{textbox convertBack=.../}} uses overridden convertBack'); // =============================== Arrange =============================== function conv1(val) { return "ONE(" + val + " " + this.tagCtx.props.type + ")"; } conv1.depends = function(data) { return [data, "foo"]; } function conv2(val) { return "TWO(" + val + " " + this.tagCtx.props.type + ")"; } conv2.depends = function(data) { return [data, "foo"]; } function conv3(val) { return "THREE(" + val + " " + this.tagCtx.props.type + ")"; } conv3.depends = function(data) { return [data, "foo"]; } var person = {name: "Jo", foo: "foo1", bar: "bar1"}; jsv.templates({ markup: '{^{:name type=foo convert=~cvt}} {^{:name type=foo convert="cvt"}} {^{cvt:name type=foo }}', converters: { cvt: conv1 } }).link("#result", person, { cvt: conv2 }); // ................................ Act .................................. var result = $("#result").text(); jsv.observable(person).setProperty({name: "Jo2"}); result += "\nChangeData-name: " + $("#result").text(); jsv.observable(person).setProperty({foo: "foo2"}); result += "\nChangeConverterDepends-foo: " + $("#result").text(); // ............................... Assert ................................. assert.equal(result, "TWO(Jo foo1) ONE(Jo foo1) ONE(Jo foo1)\n" + "ChangeData-name: TWO(Jo2 foo1) ONE(Jo2 foo1) ONE(Jo2 foo1)\n" + "ChangeConverterDepends-foo: TWO(Jo2 foo2) ONE(Jo2 foo2) ONE(Jo2 foo2)", '{^{:...}} tag with converters with depends - updates correctly in response to dependent observable change'); // =============================== Arrange =============================== var person = {name: "Jo", foo: "foo1", bar: "bar1"}; jsv.templates({ markup: '{^{textbox name type=foo convert=~cvt/}} {^{textbox name type=foo convert="cvt"/}} {^{textbox name type=foo /}}', tags: { textbox: { state: "state1", template: "{{:}} {{:#parent.data.bar}} {{:~tag.state}} {{:~tagCtx.props.type}}", convert: conv3, depends: function(data) { return [data, "bar", this, "state" ]; } } }, converters: { cvt: conv1 } }).link("#result", person, { cvt: conv2 }); var textbox2 = $("#result").view().childTags("textbox")[1]; // ................................ Act .................................. result = $("#result").text(); result = $("#result").text(); jsv.observable(person).setProperty({name: "Jo2"}); result += "\nChangeData-name: " + $("#result").text(); jsv.observable(person).setProperty({foo: "foo2"}); result += "\nChangeConverterDepends-foo: " + $("#result").text(); jsv.observable(person).setProperty({bar: "bar2"}); result += "\nChangeTagDependsData-bar: " + $("#result").text(); jsv.observable(textbox2).setProperty({state: "state2"}); result += "\nChangeTagDependsState-state: " + $("#result").text(); // ............................... Assert ................................. assert.equal(result, "TWO(Jo foo1) bar1 state1 foo1 ONE(Jo foo1) bar1 state1 foo1 THREE(Jo foo1) bar1 state1 foo1\n" + "ChangeData-name: TWO(Jo2 foo1) bar1 state1 foo1 ONE(Jo2 foo1) bar1 state1 foo1 THREE(Jo2 foo1) bar1 state1 foo1\n" + "ChangeConverterDepends-foo: TWO(Jo2 foo2) bar1 state1 foo2 ONE(Jo2 foo2) bar1 state1 foo2 THREE(Jo2 foo2) bar1 state1 foo2\n" + "ChangeTagDependsData-bar: TWO(Jo2 foo2) bar2 state1 foo2 ONE(Jo2 foo2) bar2 state1 foo2 THREE(Jo2 foo2) bar2 state1 foo2\n" + "ChangeTagDependsState-state: TWO(Jo2 foo2) bar2 state1 foo2 ONE(Jo2 foo2) bar2 state2 foo2 THREE(Jo2 foo2) bar2 state1 foo2", '{^{textbox}} tag with depends, and with converters with depends - updates correctly in response to dependent observable changes'); // =============================== Arrange =============================== var res = ""; jsv.templates('{^{twoWayTag name trigger="keydown mouseup"/}}').link("#result", person); var linkedElem = $("#result input")[0], events = $._data(linkedElem).events, handlers = "|" + events.mouseup.length + events.keydown.length; tag = $("#result").view(true).childTags("twoWayTag")[0]; jsv.observable(person).setProperty({name: "FirstName"}); handlers += "|" + events.mouseup.length + events.keydown.length; // ................................ Act .................................. res += " 1: " + person.name + "|" + tag.value; tag.linkedElem[0].value = "SecondName"; tag.linkedElem.keydown(); setTimeout(function() { res += " 2: " + person.name + "|" + tag.value; tag.linkedElem[0].value = "ThirdName"; tag.linkedElem.mouseup(); setTimeout(function() { res += " 3: " + person.name + "|" + tag.value; tag.linkedElem[0].value = "FourthName"; tag.linkedElem.keydown(); setTimeout(function() { res += " 4: " + person.name + "|" + tag.value; handlers += "|" + events.mouseup.length + events.keydown.length; // ............................... Assert ................................. assert.equal(res, " 1: FirstName|FirstName 2: SecondName|SecondName 3: ThirdName|ThirdName 4: FourthName|FourthName", 'Data link, global trigger false, using: {^{twoWayTag name trigger="event1 event2"/}} triggers on specified events'); // ............................... Assert ................................. assert.equal(handlers, "|11|11|11", 'Data link, global trigger false, using: {^{twoWayTag name trigger="event1 event2"/}} has no duplicate handlers after relinking'); // ................................ Act .................................. jsv.unlink("#result"); // ............................... Assert ................................. assert.ok($._data(linkedElem).events === undefined, 'Data link, global trigger false, using: {^{twoWayTag name trigger="event1 event2"/}}: handlers are removed by jsv.unlink(container)'); // =============================== Arrange =============================== res = ""; jsv.templates('').link("#result", person); linkedElem = $("#result input")[0]; events = $._data(linkedElem).events; handlers = "|" + events.mouseup.length + events.keydown.length; jsv.observable(person).setProperty({name: "FirstName"}); handlers += "|" + events.mouseup.length + events.keydown.length; // ................................ Act .................................. res += " 1: " + person.name; linkedElem.value = "SecondName"; $(linkedElem).keydown(); setTimeout(function() { res += " 2: " + person.name; linkedElem.value = "ThirdName"; $(linkedElem).mouseup(); setTimeout(function() { res += " 3: " + person.name; linkedElem.value = "FourthName"; $(linkedElem).keydown(); setTimeout(function() { res += " 4: " + person.name; handlers += "|" + events.mouseup.length + events.keydown.length; // ............................... Assert ................................. assert.equal(res, " 1: FirstName 2: SecondName 3: ThirdName 4: FourthName", 'Data link, global trigger false, using: triggers on specified events'); // ............................... Assert ................................. assert.equal(handlers, "|11|11|11", 'Data link, global trigger false, using: has no duplicate handlers after relinking'); // ................................ Act .................................. jsv.unlink("#result"); // ............................... Assert ................................. assert.ok($._data(linkedElem).events === undefined, 'Data link, global trigger false, using: : handlers are removed by jsv.unlink(container)'); // =============================== Arrange =============================== res = ""; jsv.templates('
                        some content
                        ').link("#result", person); linkedElem = $("#result div")[0]; events = $._data(linkedElem).events; handlers = "|" + events.mouseup.length + events.keydown.length; jsv.observable(person).setProperty({name: "First Name"}); handlers += "|" + events.mouseup.length + events.keydown.length; // ................................ Act .................................. res += " 1: " + person.name; linkedElem.innerHTML = "Second Name2"; $(linkedElem).keydown(); setTimeout(function() { res += " 2: " + person.name; linkedElem.innerHTML = "Third Name3"; $(linkedElem).mouseup(); setTimeout(function() { res += " 3: " + person.name; linkedElem.innerHTML = "Fourth Name4"; $(linkedElem).keydown(); setTimeout(function() { res += " 4: " + person.name; handlers += "|" + events.mouseup.length + events.keydown.length; // ............................... Assert ................................. assert.equal(res, " 1: First Name 2: Second Name2 3: Third Name3 4: Fourth Name4", 'Data link, global trigger false, using:
                        triggers on specified events'); // ............................... Assert ................................. assert.equal(handlers, "|11|11|11", 'Data link, global trigger false, using:
                        has no duplicate handlers after relinking'); // ................................ Act .................................. jsv.unlink("#result"); // ............................... Assert ................................. assert.ok($._data(linkedElem).events === undefined, 'Data link, global trigger false, using:
                        : handlers are removed by jsv.unlink(container)'); // =============================== Arrange =============================== res = ""; $("#result").html(''); linkedElem = $("#result input")[0]; jsv.link(true, "#result", person); events = $._data(linkedElem).events; handlers = "|" + events.mouseup.length + events.keydown.length; jsv.observable(person).setProperty({name: "FirstName"}); handlers += "|" + events.mouseup.length + events.keydown.length; // ................................ Act .................................. res += " 1: " + person.name; linkedElem.value = "SecondName"; $(linkedElem).keydown(); setTimeout(function() { res += " 2: " + person.name; linkedElem.value = "ThirdName"; $(linkedElem).mouseup(); setTimeout(function() { res += " 3: " + person.name; linkedElem.value = "FourthName"; $(linkedElem).keydown(); setTimeout(function() { res += " 4: " + person.name; handlers += "|" + events.mouseup.length + events.keydown.length; // ............................... Assert ................................. assert.equal(res, " 1: FirstName 2: SecondName 3: ThirdName 4: FourthName", 'Top-level data link, global trigger false, using: triggers on specified events'); // ............................... Assert ................................. assert.equal(handlers, "|11|11|11", 'Top-level data link, global trigger false, using: has no duplicate handlers after relinking'); // ................................ Act .................................. jsv.unlink("#result"); // ............................... Assert ................................. assert.ok($._data(linkedElem).events === undefined, 'Top-level data link using: : handlers are removed by jsv.unlink(container)'); $("#result").empty(); jsv.views.settings.trigger(true); if (assert.async) { done() } else { start() } }, 0); }, 0); }, 0); }, 0); }, 0); }, 0); }, 0); }, 0); }, 0); }, 0); }, 0); }, 0); }); QUnit.test("Tag options versus setting in init()", function(assert) { jsv.views.settings.trigger(false); // =============================== Arrange =============================== var getValues = function() { result += "{{mytag}}: " + inputs[0].value + " " + inputs[1].value + " " + inputs[2].value + " " + inputs[3].value + " {{else}}: " + inputs[4].value + " " + inputs[5].value + " " + inputs[6].value + " " + inputs[7].value + " |data: " + inputs[8].value + " " + inputs[9].value + " " + inputs[10].value + " |text: " + $("#result").text() + "\n"; } var person = {first: "Jo", last: "Blow", title: "Sir"}; jsv.views.templates({ markup: '{^{mytag 0 prop=first last title=title}}{{else 0 prop=last first title=title}}{{/mytag}}' + '', tags: { mytag: { init: function(tagCtx) { this.bindTo = ["prop", 1]; this.linkedElement = [".a", ".b"]; this.linkedCtxParam = ["a", "b"]; this.template = '{^{:~tagCtx.props.title}}'; this.boundProps = ["title"]; //this.depends = "title"; }, onUpdate: false } } }).link("#result", person); // ................................ Act .................................. var container = $("#result")[0], inputs = $("input:text", container), result = ""; getValues(); $(inputs[0]).val("Jo0").change(); getValues(); $(inputs[1]).val("Blow1").change(); getValues(); $(inputs[2]).val("Jo2").change(); getValues(); $(inputs[3]).val("Blow3").change(); getValues(); $(inputs[4]).val("Blow4").change(); getValues(); $(inputs[5]).val("Jo5").change(); getValues(); $(inputs[8]).val("Jo8").change(); getValues(); $(inputs[9]).val("Blow9").change(); getValues(); $(inputs[10]).val("Sir10").change(); getValues(); // ............................... Assert ................................. assert.equal(result, "{{mytag}}: Jo Blow Jo Blow {{else}}: Blow Jo Blow Jo |data: Jo Blow Sir |text: SirSir\n" + "{{mytag}}: Jo0 Blow Jo0 Blow {{else}}: Blow Jo Blow Jo0 |data: Jo0 Blow Sir |text: SirSir\n" + "{{mytag}}: Jo0 Blow1 Jo0 Blow1 {{else}}: Blow Jo Blow1 Jo0 |data: Jo0 Blow1 Sir |text: SirSir\n" + "{{mytag}}: Jo2 Blow1 Jo2 Blow1 {{else}}: Blow Jo Blow1 Jo2 |data: Jo2 Blow1 Sir |text: SirSir\n" + "{{mytag}}: Jo2 Blow3 Jo2 Blow3 {{else}}: Blow Jo Blow3 Jo2 |data: Jo2 Blow3 Sir |text: SirSir\n" + "{{mytag}}: Jo2 Blow3 Jo2 Blow4 {{else}}: Blow4 Jo Blow4 Jo2 |data: Jo2 Blow4 Sir |text: SirSir\n" + "{{mytag}}: Jo2 Blow3 Jo5 Blow4 {{else}}: Blow4 Jo5 Blow4 Jo5 |data: Jo5 Blow4 Sir |text: SirSir\n" + "{{mytag}}: Jo8 Blow4 Jo8 Blow4 {{else}}: Blow4 Jo8 Blow4 Jo8 |data: Jo8 Blow4 Sir |text: SirSir\n" + "{{mytag}}: Jo8 Blow9 Jo8 Blow9 {{else}}: Blow9 Jo8 Blow9 Jo8 |data: Jo8 Blow9 Sir |text: SirSir\n" + "{{mytag}}: Jo8 Blow9 Jo8 Blow9 {{else}}: Blow9 Jo8 Blow9 Jo8 |data: Jo8 Blow9 Sir10 |text: Sir10Sir10\n", "Setting bindTo, linkedElement, linkedCtxParam, template and boundProps in init(), and using two-way binding" + " including on {{else}}, works correctly (onUpdate set to false)"); // =============================== Arrange =============================== person = {first: "Jo", last: "Blow", title: "Sir"}; jsv.views.templates({ markup: '{^{mytag 0 prop=first last title=title}}{{else 0 prop=last first title=title}}{{/mytag}}' + '', tags: { mytag: { init: function(tagCtx) { this.bindTo = ["prop", 1]; this.linkedElement = [".a", ".b"]; this.linkedCtxParam = ["a", "b"]; this.template = '{{:~tagCtx.props.title}}'; //this.boundProps = ["title"]; this.depends = "title"; } //onUpdate: false } } }).link("#result", person); // ................................ Act .................................. container = $("#result")[0]; inputs = $("input:text", container); result = ""; getValues(); $(inputs[0]).val("Jo0").change(); inputs = $("input:text", container); // Refresh inputs array, since onUpdate is true, so input will have been rerendered getValues(); $(inputs[1]).val("Blow1").change(); inputs = $("input:text", container); getValues(); $(inputs[2]).val("Jo2").change(); inputs = $("input:text", container); getValues(); $(inputs[3]).val("Blow3").change(); inputs = $("input:text", container); getValues(); inputs = $("input:text", container); $(inputs[4]).val("Blow4").change(); inputs = $("input:text", container); getValues(); $(inputs[5]).val("Jo5").change(); inputs = $("input:text", container); getValues(); $(inputs[8]).val("Jo8").change(); inputs = $("input:text", container); getValues(); $(inputs[9]).val("Blow9").change(); inputs = $("input:text", container); getValues(); $(inputs[10]).val("Sir10").change(); inputs = $("input:text", container); getValues(); // ............................... Assert ................................. assert.equal(result, "{{mytag}}: Jo Blow Jo Blow {{else}}: Blow Jo Blow Jo |data: Jo Blow Sir |text: SirSir\n" + "{{mytag}}: Jo0 Blow Jo0 Blow {{else}}: Blow Jo Blow Jo0 |data: Jo0 Blow Sir |text: SirSir\n" + "{{mytag}}: Jo0 Blow1 Jo0 Blow1 {{else}}: Blow Jo Blow1 Jo0 |data: Jo0 Blow1 Sir |text: SirSir\n" + "{{mytag}}: Jo2 Blow1 Jo2 Blow1 {{else}}: Blow Jo Blow1 Jo2 |data: Jo2 Blow1 Sir |text: SirSir\n" + "{{mytag}}: Jo2 Blow3 Jo2 Blow3 {{else}}: Blow Jo Blow3 Jo2 |data: Jo2 Blow3 Sir |text: SirSir\n" + "{{mytag}}: Jo2 Blow3 Jo2 Blow4 {{else}}: Blow4 Jo Blow4 Jo2 |data: Jo2 Blow4 Sir |text: SirSir\n" + "{{mytag}}: Jo2 Blow3 Jo5 Blow4 {{else}}: Blow4 Jo5 Blow4 Jo5 |data: Jo5 Blow4 Sir |text: SirSir\n" + "{{mytag}}: Jo8 Blow4 Jo8 Blow4 {{else}}: Blow4 Jo8 Blow4 Jo8 |data: Jo8 Blow4 Sir |text: SirSir\n" + "{{mytag}}: Jo8 Blow9 Jo8 Blow9 {{else}}: Blow9 Jo8 Blow9 Jo8 |data: Jo8 Blow9 Sir |text: SirSir\n" + "{{mytag}}: Jo8 Blow9 Jo8 Blow9 {{else}}: Blow9 Jo8 Blow9 Jo8 |data: Jo8 Blow9 Sir10 |text: Sir10Sir10\n", "Setting bindTo, linkedElement, linkedCtxParam, template and depends in init(), and using two-way binding including on {{else}}," + " works correctly (onUpdate set to true)"); // =============================== Arrange =============================== person = {heading: "HEAD"}; jsv.views.templates({ markup: '
                        One
                        ' + '
                        Two
                        ' + '
                        Two
                        ' + '
                        Three
                        ', tags: { mytag: { init: function(tagCtx, linkCtx) { this.template = (linkCtx.attr||tagCtx.props.attr||this.attr) + ': {{:heading}}'; if (tagCtx.props.attr) { this.attr = tagCtx.props.attr; } }, onUpdate: false, attr: "data-foo" // Default } }, }).link("#result", person); // ............................... Assert ................................. assert.equal( $("#result div").eq(0).prop("foo") + " " + $("#result div").eq(1).attr("title") + " " + $("#result div").eq(2).prop("bar") + " " + $("#result div").eq(3).attr("data-foo"), "prop-foo: HEAD title: HEAD prop-bar: HEAD data-foo: HEAD", "Setting attr in init() works correctly - including specifying prop - to set a property as target"); // =============================== Arrange =============================== person = {first: "Jo", width: 32}; jsv.views.templates({ markup: '{^{mytag first width=width height=66 class="box" id="a"}}' + '{{else first id="b"}}' + '{{else first width=width-10 height=46 id="c"}}{{/mytag}}', tags: { mytag: { init: function(tagCtx) { this.boundProps = ["width"]; this.linkedElement = "input"; this.displayElement = ".foot"; this.mainElement = ".head"; // this.template = '
                        head
                        foot
                        '; // this.width = 55; // We set this in onBind instead - either will work // this.className = "myclass"; // We set this in onBind instead - either will work this.setSize = true; }, onBind: function(tagCtx) { this.tagCtxs[2].mainElem = this.tagCtxs[2].contents(true, ".foot"); this.tagCtxs[0].props.width = 102; this.width = 300; this.className = "myclass"; }, width: "155px", height: "10em", onUpdate: true, } } }).link("#result", person); // ............................... Assert ................................. var result = ""; $("#result div").each(function(i, elem) { result += " |" + i + ": " + (elem.id ? " id: " + elem.id : "") + (elem.className ? " class: " + elem.className : "") + (elem.style.width ? " width: " + elem.style.width : "") + (elem.style.height ? " height: " + elem.style.height : ""); }) assert.equal(result, " |0: class: top |1: id: a class: head width: 102px height: 66px |2: class: foot box" + " |3: class: top |4: id: b class: head width: 300px height: 10em |5: class: foot myclass" + " |6: class: top |7: class: head |8: id: c class: foot myclass width: 22px height: 46px", "Setting width, height, class, setSize, as tag options or in init()/onBind()," + " or setting width height or class as props, or setting displayElement or mainElemet as tag option or in init() all work correctly"); // =============================== Arrange =============================== person = {first: "Jo", width: 32}; jsv.views.templates({ markup: '
                        ', tags: { mytag: { init: function(tagCtx) { this.boundProps = ["width"]; this.linkedElement = "input"; this.displayElement = ".foot"; this.mainElement = ".head"; // this.template = '
                        head
                        foot
                        '; // this.width = 55; // We set this in onBind instead - either will work // this.className = "myclass"; // We set this in onBind instead - either will work this.setSize = true; }, onBind: function(tagCtx) { this.tagCtxs[2].mainElem = this.tagCtxs[2].contents(true, ".foot"); this.tagCtxs[0].props.width = 102; this.width = 300; this.className = "myclass"; }, width: "155px", height: "10em", onUpdate: true, } } }).link("#result", person); // ............................... Assert ................................. result = ""; $("#result div div").each(function(i, elem) { result += " |" + i + ": " + (elem.id ? " id: " + elem.id : "") + (elem.className ? " class: " + elem.className : "") + (elem.style.width ? " width: " + elem.style.width : "") + (elem.style.height ? " height: " + elem.style.height : ""); }) assert.equal(result, " |0: class: top |1: id: a class: head width: 102px height: 66px |2: class: foot box" + " |3: class: top |4: id: b class: head width: 300px height: 10em |5: class: foot myclass" + " |6: class: top |7: class: head |8: id: c class: foot myclass width: 22px height: 46px", "Setting width, height, class, setSize, as tag options or in init()/onBind()," + " or setting width height or class as props, or setting displayElement as tag option or in init() all work correctly. (With data-linked div)"); // =============================== Arrange =============================== person = {first: "Jo"}; jsv.views.templates({ markup: '1
                        {^{mytag useData=false/}}' + '2
                        {^{mytag useData=true/}}' + '3
                        {^{mytag ~person2 useOuterCtx=false}}{{else ~person2 useOuterCtx=false}}{{/mytag}}' + '4
                        {^{mytag ~person2 useOuterCtx=true}}{{else ~person2 useOuterCtx=true}}{{/mytag}}', tags: { mytag: { init: function(tagCtx) { this.argDefault = tagCtx.props.useData; this.contentCtx = tagCtx.props.useOuterCtx; }, onUpdate: false, template: '{{:!!~tagCtx.args[0] && ~tagCtx.args[0].first}} {{:first}} ' } } }).link("#result", {first: "Jo"}, {person2: {first: "Fred"}}); // ............................... Assert ................................. result = ""; assert.equal($("#result").text(), "1 false Jo false Jo " + "2 Jo Jo Jo Jo " + "3 Fred Fred Fred Fred Fred Fred Fred Fred " + "4 Fred Jo Fred Jo Fred Jo Fred Jo ", "Setting argDefault or contentCtx in init() work correctly."); // =============================== Arrange =============================== person = {first: "Jo"}; jsv.views.templates({ markup: "1: {{mytag person=~person2/}} " + "2: {{mytag person=~person2}}inner block: {{:first}}{{/mytag}} " + "3: {{myrendertag person=~person2}}inner block: {{:first}}{{/myrendertag}}", tags: { mytag: { contentCtx: function() { return this.tagCtx.props.person; }, onUpdate: false, template: 'In template: {{:first}} {{include tmpl=#content/}}' }, myrendertag: { contentCtx: function() { return this.tagCtx.props.person; }, onUpdate: false, render: function() { return this.tagCtx.render(); } } } }).link("#result", {first: "Jo"}, {person2: {first: "Fred"}}); // ............................... Assert ................................. result = ""; assert.equal($("#result").text(), "1: In template: Fred " + (" ") + "2: In template: Fred inner block: Fred " + "3: inner block: Fred", "contentCtx as a function returns the data context both for the template and for block content."); jsv.views.settings.trigger(true); }); QUnit.test("Global trigger=false local trigger=true - triggers after keydown: ", function(assert) { // =============================== Arrange =============================== jsv.views.settings.trigger(false); var done; if (assert.async) { done = assert.async() } else { stop() } var res = "", person = {name: "Jo"}; jsv.templates('').link("#result", person); var linkedElem = $("#result input")[0]; var events = $._data(linkedElem).events, handlers = "|" + events[inputOrKeydown].length; jsv.observable(person).setProperty({name: "FirstName"}); events = $._data(linkedElem).events; handlers += "|" + events[inputOrKeydown].length; // ................................ Act .................................. res += " 1: " + person.name; linkedElem.value = "SecondName"; keydown($(linkedElem)); jsv.views.settings.trigger(true); setTimeout(function() { res += " 2: " + person.name; handlers += "|" + events[inputOrKeydown].length; // ............................... Assert ................................. assert.equal(res, " 1: FirstName 2: SecondName", 'Data link using: triggers after keydown'); // ............................... Assert ................................. assert.equal(handlers, "|1|1|1", 'Data link using: has no duplicate handlers after relinking'); // ................................ Act .................................. jsv.unlink("#result"); // ............................... Assert ................................. assert.ok($._data(linkedElem).events === undefined, 'Data link using: : handlers are removed by jsv.unlink(container)'); if (assert.async) { done() } else { start() } }, 0); }); QUnit.test("Global trigger=true local trigger=false - does not trigger after keydown: ", function(assert) { // =============================== Arrange =============================== var done; if (assert.async) { done = assert.async() } else { stop() } var res = "", person = {name: "Jo"}; jsv.templates('').link("#result", person); var linkedElem = $("#result input")[0]; var events = $._data(linkedElem).events; jsv.observable(person).setProperty({name: "FirstName"}); // ................................ Act .................................. res += " 1: " + person.name; linkedElem.value = "SecondName"; keydown($(linkedElem)); setTimeout(function() { res += " 2: " + person.name; // ............................... Assert ................................. assert.equal(res, " 1: FirstName 2: FirstName", 'Data link using: does not trigger after keydown'); // ............................... Assert ................................. assert.equal(!events && !$._data(linkedElem).events, true, 'Data link using: has no handlers after relinking'); // ................................ Act .................................. jsv.unlink("#result"); // ............................... Assert ................................. assert.ok($._data(linkedElem).events === undefined, 'Data link using: : No handlers after jsv.unlink(container)'); if (assert.async) { done() } else { start() } }, 0); }); QUnit.test("Global trigger=true - triggers after keydown: ", function(assert) { // =============================== Arrange =============================== var done; if (assert.async) { done = assert.async() } else { stop() } var res = "", person = {name: "Jo"}; jsv.templates('').link("#result", person); var linkedElem = $("#result input")[0]; var events = $._data(linkedElem).events, handlers = "|" + events[inputOrKeydown].length; jsv.observable(person).setProperty({name: "FirstName"}); events = $._data(linkedElem).events; handlers += "|" + events[inputOrKeydown].length; // ................................ Act .................................. res += " 1: " + person.name; linkedElem.value = "SecondName"; keydown($(linkedElem)); setTimeout(function() { res += " 2: " + person.name; handlers += "|" + events[inputOrKeydown].length; // ............................... Assert ................................. assert.equal(res, " 1: FirstName 2: SecondName", 'Data link using: triggers after keydown'); // ............................... Assert ................................. assert.equal(handlers, "|1|1|1", 'Data link using: has no duplicate handlers after relinking'); // ................................ Act .................................. jsv.unlink("#result"); // ............................... Assert ................................. assert.ok($._data(linkedElem).events === undefined, 'Data link using: : handlers are removed by jsv.unlink(container)'); if (assert.async) { done() } else { start() } }, 0); }); QUnit.test("Global trigger=true - ", function(assert) { // =============================== Arrange =============================== var done; if (assert.async) { done = assert.async() } else { stop() } var res = "", person = {member: true}; jsv.templates('').link("#result", person); var linkedElem = $("#result input")[0]; var events = $._data(linkedElem).events; jsv.observable(person).setProperty({member: false}); events = $._data(linkedElem).events; // ................................ Act .................................. res += " 1: " + person.member; linkedElem.checked = true; $(linkedElem).change(); setTimeout(function() { res += " 2: " + person.member; // ............................... Assert ................................. assert.equal(res, " 1: false 2: true", 'Data link using: triggers after change'); // ............................... Assert ................................. assert.equal(events, undefined, 'Data link using: has no events'); // ................................ Act .................................. jsv.unlink("#result"); if (assert.async) { done() } else { start() } }, 0); }); QUnit.test("trigger=\'keydown\' - triggers after keydown: ", function(assert) { // =============================== Arrange =============================== var done; if (assert.async) { done = assert.async() } else { stop() } var res = "", person = {name: "Jo"}; jsv.templates('').link("#result", person); var linkedElem = $("#result input")[0]; var events = $._data(linkedElem).events, handlers = "|" + events.keydown.length; jsv.observable(person).setProperty({name: "FirstName"}); events = $._data(linkedElem).events; handlers += "|" + events.keydown.length; // ................................ Act .................................. res += " 1: " + person.name; linkedElem.value = "SecondName"; $(linkedElem).keydown(); setTimeout(function() { res += " 2: " + person.name; handlers += "|" + events.keydown.length; // ............................... Assert ................................. assert.equal(res, " 1: FirstName 2: SecondName", 'Data link using: triggers after keydown'); // ............................... Assert ................................. assert.equal(handlers, "|1|1|1", 'Data link using: has no duplicate handlers after relinking'); // ................................ Act .................................. jsv.unlink("#result"); // ............................... Assert ................................. assert.ok($._data(linkedElem).events === undefined, 'Data link using: : handlers are removed by jsv.unlink(container)'); if (assert.async) { done() } else { start() } }, 0); }); QUnit.test("Global trigger=true - triggers after keydown: {^{twoWayTag}}", function(assert) { // =============================== Arrange =============================== var done; if (assert.async) { done = assert.async() } else { stop() } var before = "", person = {name: "Jo"}; jsv.templates({ markup: '{^{twoWayTag name convert="myupper" convertBack=~lower/}}', converters: { myupper: function(val) { return val.toUpperCase(); } } }).link("#result", person, { lower: function(val) { return val.toLowerCase(); } }); var tag = $("#result").view(true).childTags("twoWayTag")[0], linkedElem = tag.linkedElem[0], events = $._data(linkedElem).events, handlers = "|" + events[inputOrKeydown].length; // ................................ Act .................................. before = linkedElem.value; linkedElem.value = "ChangeTheName"; keydown(tag.linkedElem); setTimeout(function() { // ............................... Assert ................................. assert.equal(before + "|" + person.name, "JO|changethename", 'Data link using: {^{twoWayTag name convertBack=~lower/}} - triggers after keydown, converts the data, and sets on data'); handlers += "|" + events[inputOrKeydown].length; // ............................... Assert ................................. assert.equal(handlers, "|1|1", 'Top-level data link using: {^{twoWayTag name convertBack=~lower/}} has no duplicate handlers after relinking'); // ................................ Act .................................. jsv.unlink("#result"); // ............................... Assert ................................. assert.ok($._data(linkedElem).events === undefined, 'Top-level data link using: {^{twoWayTag name convertBack=~lower/}}: handlers are removed by jsv.unlink(container)'); if (assert.async) { done() } else { start() } }, 0); }); QUnit.test("Global trigger=true - triggers - after keydown: {^{textbox}}", function(assert) { // =============================== Arrange =============================== var done; if (assert.async) { done = assert.async() } else { stop() } var before = "", person = {name: "Jo"}; jsv.views.tags({ textbox: { onAfterLink: function() { // Find input in contents, if not already found this.linkedElem = this.linkedElem || this.contents("input"); }, onUpdate: function() { // No need to re-render whole tag, when content updates. return false; // }, template: "" } }); jsv.templates({ markup: '{^{textbox name convert="myupper" convertBack=~lower/}}', converters: { myupper: function(val) { return val.toUpperCase(); } } }).link("#result", person, { lower: function(val) { return val.toLowerCase(); } }); var tag = $("#result").view(true).childTags("textbox")[0]; var linkedElem = tag.linkedElem[0], events = $._data(linkedElem).events, handlers = "|" + events[inputOrKeydown].length; // ................................ Act .................................. before = linkedElem.value; linkedElem.value = "ChangeTheName"; keydown(tag.linkedElem); setTimeout(function() { // ............................... Assert ................................. assert.equal(before + "|" + person.name, "JO|changethename", 'Data link using: {^{textbox name convertBack=~lower/}} - triggers after keydown, converts the data, and sets on data'); handlers += "|" + events[inputOrKeydown].length; // ............................... Assert ................................. assert.equal(handlers, "|1|1", 'Top-level data link using: {^{textbox name convertBack=~lower/}} has no duplicate handlers after relinking'); // ................................ Act .................................. jsv.unlink("#result"); // ............................... Assert ................................. assert.ok($._data(linkedElem).events === undefined, 'Top-level data link using: {^{textbox name convertBack=~lower/}}: handlers are removed by jsv.unlink(container)'); if (assert.async) { done() } else { start() } }, 0); }); QUnit.test("Custom tag trigger:... option", function(assert) { // =============================== Arrange =============================== var done; if (assert.async) { done = assert.async() } else { stop() } var before = "", person = {name: "Jo", trig:true}; jsv.views.tags({ textbox: { linkedElement: "input", onUpdate: false, template: "", trigger: false, } }); jsv.templates({ markup: '{^{textbox name/}} {^{textbox name trigger=true/}}', }).link("#result", person); var tags = $("#result").view(true).childTags("textbox"); var linkedElem1 = $("#result input")[0]; var linkedElem2 = $("#result input")[1]; // ................................ Act .................................. before = person.name; linkedElem1.value = "NewName1"; keydown($(linkedElem1)); setTimeout(function() { before += "|" + person.name; linkedElem2.value = "NewName2"; keydown($(linkedElem2)); setTimeout(function() { before += "|" + person.name; linkedElem1.value = "AnotherNewName1"; $(linkedElem1).change(); before += "|" + person.name; // ............................... Assert ................................. assert.equal(before, "Jo|Jo|NewName2|AnotherNewName1", '{^{textbox/}} with option trigger:false - triggers after change, not after keydown. But {^{textbox trigger=true/}} overrides option and triggers on keydown'); if (assert.async) { done() } else { start() } }, 0); }, 0); }); QUnit.test("Global trigger=true - triggers after keydown: {^{contentEditable}}", function(assert) { // =============================== Arrange =============================== jsv.views.settings.trigger(true); var done; if (assert.async) { done = assert.async() } else { stop() } var before = "", person = {name: "Jo Smith"}; jsv.views.tags({ contentEditable: { onAfterLink: function() { // Find contentEditable div in contents, if not already found this.linkedElem = this.linkedElem || this.contents("[contentEditable]"); }, onUpdate: function() { // No need to re-render whole tag, when content updates. return false; }, template: "
                        " } }); jsv.templates({ markup: '{^{contentEditable name convert="myupper" convertBack=~lower/}}', converters: { myupper: function(val) { return val.toUpperCase(); } } }).link("#result", person, { lower: function(val) { return val.toLowerCase(); } }); var tag = $("#result").view(true).childTags("contentEditable")[0], linkedElem = tag.linkedElem[0], events = $._data(linkedElem).events, handlers = "|" + events[inputOrKeydownContentEditable].length; // ................................ Act .................................. before = linkedElem.innerHTML; linkedElem.innerHTML = "New Name"; tag.linkedElem.trigger(inputOrKeydownContentEditable); setTimeout(function() { before += "|" + person.name; linkedElem.innerHTML = "New2

                        Name2

                        "; tag.linkedElem.trigger(inputOrKeydownContentEditable); setTimeout(function() { before += "|" + person.name; linkedElem.innerHTML = "New3
                        Name3
                        "; tag.linkedElem.trigger(inputOrKeydownContentEditable); setTimeout(function() { // ............................... Assert ................................. assert.equal(before + "|" + person.name, "JO SMITH|new name|new2

                        name2

                        |new3
                        name3
                        ", 'Data link using: {^{contentEditable name convertBack=~lower/}} - triggers after keydown, converts the data, and sets on data'); handlers += "|" + events[inputOrKeydownContentEditable].length; // ............................... Assert ................................. assert.equal(handlers, "|1|1", 'Top-level data link using: {^{contentEditable name convertBack=~lower/}} has no duplicate handlers after relinking'); // ................................ Act .................................. jsv.unlink("#result"); // ............................... Assert ................................. assert.ok($._data(linkedElem).events === undefined, 'Top-level data link using: {^{contentEditable name convertBack=~lower/}}: handlers are removed by jsv.unlink(container)'); jsv.views.settings.trigger(true); if (assert.async) { done() } else { start() } }, 0); }, 0); }, 0); }); QUnit.test("Global trigger=true - triggers after keydown: {^{plaintextonly}}", function(assert) { // =============================== Arrange =============================== jsv.views.settings.trigger(true); var done; if (assert.async) { done = assert.async() } else { stop() } var before = "", person = {name: "Jo Smith"}; jsv.views.tags({ plaintextonly: { onAfterLink: function() { // Find contentEditable div in contents, if not already found this.linkedElem = this.linkedElem || this.contents("[contentEditable]"); }, onUpdate: function() { // No need to re-render whole tag, when content updates. return false; }, template: "
                        " } }); jsv.templates({ markup: '{^{plaintextonly name convert="myupper" convertBack=~lower/}}', converters: { myupper: function(val) { return val.toUpperCase(); } } }).link("#result", person, { lower: function(val) { return val.toLowerCase(); } }); var tag = $("#result").view(true).childTags("plaintextonly")[0], linkedElem = tag.linkedElem[0], events = $._data(linkedElem).events, handlers = "|" + events[inputOrKeydownContentEditable].length; // ................................ Act .................................. before = linkedElem.innerHTML; linkedElem.innerHTML = "New Name"; tag.linkedElem.trigger(inputOrKeydownContentEditable); setTimeout(function() { before += "|" + person.name; linkedElem.innerHTML = "New2

                        Name2

                        "; tag.linkedElem.trigger(inputOrKeydownContentEditable); setTimeout(function() { before += "|" + person.name; linkedElem.innerHTML = "New3
                        Name3
                        "; tag.linkedElem.trigger(inputOrKeydownContentEditable); setTimeout(function() { // ............................... Assert ................................. assert.equal(before + "|" + person.name, "JO SMITH|new name|new2

                        name2

                        |new3
                        name3
                        ", 'Data link using: {^{plaintextonly name convertBack=~lower/}} - triggers after keydown, converts the data, and sets on data'); handlers += "|" + events[inputOrKeydownContentEditable].length; // ............................... Assert ................................. assert.equal(handlers, "|1|1", 'Top-level data link using: {^{plaintextonly name convertBack=~lower/}} has no duplicate handlers after relinking'); // ................................ Act .................................. jsv.unlink("#result"); // ............................... Assert ................................. assert.ok($._data(linkedElem).events === undefined, 'Top-level data link using: {^{plaintextonly name convertBack=~lower/}}: handlers are removed by jsv.unlink(container)'); jsv.views.settings.trigger(true); if (assert.async) { done() } else { start() } }, 0); }, 0); }, 0); }); QUnit.test('linkTo for {:source linkTo=target:} or {twoWayTag source linkTo=target}', function(assert) { jsv.views.settings.trigger(false); // =============================== Arrange =============================== var before, after, person = {name: "Jo", name2: "Jo2"}, cancelChange = false, eventData = ""; jsv.views.tags({ twoWayTag: { init: function(tagCtx, linkCtx, ctx) { eventData += "init "; if (this.inline && !tagCtx.content) { this.template = ""; } }, render: function(val) { eventData += "render "; }, onAfterLink: function(tagCtx, linkCtx, ctx, ev, eventArgs) { eventData += "onAfterLink "; this.value = tagCtx.args[0]; this.linkedElem = this.linkedElem || (this.inline ? this.contents("input,div") : $(linkCtx.elem)); }, onBeforeUpdateVal: function(ev, eventArgs) { eventData += "onBeforeUpdateVal "; }, onUpdate: function(ev, eventArgs, newTagCtxs) { eventData += "onUpdate "; return false; }, onBind: function(tagCtx, linkCtx, ctx, ev, eventArgs) { eventData += "onBind "; }, onUnbind: function(tagCtx, linkCtx, ctx, ev, eventArgs) { eventData += "onUnbind "; }, onBeforeChange: function(ev, eventArgs) { eventData += "onBeforeChange "; return !cancelChange; }, setValue: function() { eventData += "setValue "; }, onAfterChange: function(ev, eventArgs) { eventData += "onAfterChange "; return !cancelChange; }, onDispose: function() { eventData += "onDispose "; } } }); // ELEMENT-BASED DATA-LINKED TAGS ON INPUT - WITH linkTo EXPRESSION jsv.templates('') .link("#result", person); var linkedEl = $("#linkedElm")[0]; // ................................ Act .................................. before = linkedEl.value; jsv.observable(person).setProperty({name: "newName"}); after = linkedEl.value; // ............................... Assert ................................. assert.equal(before + "|" + after, "Jo|newName", 'Data link using: - binds data to linkedElem'); // ................................ Act .................................. before = "name:" + person.name + " name2:" + person.name2; linkedEl.value = "newVal"; $(linkedEl).change(); after = "name:" + person.name + " name2:" + person.name2; // ............................... Assert ................................. assert.equal(before + "|" + after, "name:newName name2:Jo2|name:newName name2:newVal", 'Data link using: - binds linkedElem back to "linkTo" target data - using return value of onChange'); // ................................ Act .................................. before = "name:" + person.name + " name2:" + person.name2; cancelChange = true; linkedEl.value = "2ndNewVal"; $(linkedEl).change(); after = "name:" + person.name + " name2:" + person.name2; // =============================== Arrange =============================== person.name = "Jo"; person.name2 = "Jo2"; jsv.templates('') .link("#result", person); linkedEl = $("#linkedElm")[0]; // ................................ Act .................................. before = "value: " + linkedEl.value + " name:" + person.name + " name2:" + person.name2; linkedEl.value = "3rdNewVal"; $(linkedEl).change(); after = "name:" + person.name + " name2:" + person.name2; // ............................... Assert ................................. assert.equal(before + "|" + after, "value: initialValue name:Jo name2:Jo2|name:Jo name2:3rdNewVal", 'Data link using: - Initializes to provided string, and binds updated value of linkedElem back to "linkTo" target'); // =============================== Arrange =============================== person.name = "Jo"; person.name2 = "Jo2"; jsv.templates('') .link("#result", person); linkedEl = $("#linkedElm")[0]; // ................................ Act .................................. before = "value: " + linkedEl.value + " name:" + person.name + " name2:" + person.name2; linkedEl.value = "4thNewVal"; $(linkedEl).change(); after = "name:" + person.name + " name2:" + person.name2; // ............................... Assert ................................. assert.equal(before + "|" + after, "value: [object Object] name:Jo name2:Jo2|name:Jo name2:4thNewVal", 'Data link using: - Initializes to current data item, and binds updated value of linkedElem back to "linkTo" target'); // =============================== Arrange =============================== // ................................ Act .................................. jsv.templates({ markup: '', converters: { myupper: function(val) { return val.toUpperCase(); }, mylower: function(val) { return val.toLowerCase(); } } }).link("#result", person); linkedEl = $("#linkedElm")[0]; // ............................... Assert ................................. assert.equal(linkedEl.value, "JO", 'Data link using: - (tag.convert setting) - on data change: converts the value on the target input'); // ................................ Act .................................. linkedEl.value = "ChangeTheName"; $(linkedEl).change(); // ............................... Assert ................................. assert.equal("name:" + person.name + " name2:" + person.name2, "name:ANewName name2:changethename", 'Data link using: ') .link("#result", person); var tag = $("#result").view(true).childTags("twoWayTag")[0]; linkedEl = $("#linkedElm")[0]; // ............................... Assert ................................. assert.equal(eventData, "init render onBind onAfterLink setValue ", 'Data link using: - event order for init, render, link'); eventData = ""; // ................................ Act .................................. before = tag.value + linkedEl.value; jsv.observable(person).setProperty({name: "newName"}); after = tag.value + linkedEl.value; // ............................... Assert ................................. assert.equal(eventData, "onBeforeChange onUpdate onAfterLink setValue onAfterChange ", 'Data link using: - event order for onUpdate'); eventData = ""; // ............................... Assert ................................. assert.equal(before + "|" + after, "JoJo|newNamenewName", 'Data link using: - binds data to linkedElem'); // ................................ Act .................................. before = "value:" + tag.value + " name:" + person.name + " name2:" + person.name2; linkedEl.value = "newVal"; $(linkedEl).change(); after = "value:" + tag.value + " name:" + person.name + " name2:" + person.name2; // ............................... Assert ................................. assert.equal(eventData, "onBeforeUpdateVal setValue ", 'Data link using: - event order for onChange'); eventData = ""; // ............................... Assert ................................. assert.equal(before + "|" + after, "value:newName name:newName name2:Jo2|value:newName name:newName name2:newVal", 'Data link using: - binds linkedElem back to "linkTo" target dataonChange'); eventData = ""; // ................................ Reset .................................. cancelChange = false; person.name = "Jo"; // =============================== Arrange =============================== // ................................ Act .................................. jsv.templates({ markup: '', converters: { myupper: function(val) { return val.toUpperCase(); } } }).link("#result", person, { lower: function(val) { return val.toLowerCase(); } }); tag = $("#result").view(true).childTags("twoWayTag")[0]; linkedEl = $("#linkedElm")[0]; // ............................... Assert ................................. assert.equal(linkedEl.value + "|" + tag.value, "JO|Jo", 'Data link using: - (tag.convert setting) - initial linking: converts the value on the target input'); // ................................ Act .................................. jsv.observable(person).setProperty({name: "ANewName"}); // ............................... Assert ................................. assert.equal(linkedEl.value + "|" + tag.value, "ANEWNAME|ANewName", 'Data link using: - (tag.convert setting) - on data change: converts the value on the target input'); // ................................ Act .................................. linkedEl.value = "ChangeTheName"; $(linkedEl).change(); // ............................... Assert ................................. assert.equal("name:" + person.name + " name2:" + person.name2 + " value:" + tag.value, "name:ANewName name2:changethename value:ANewName", 'Data link using: - (tag.convertBack setting) on element change: converts the data, and sets on "linkTo" target data'); // ................................ Reset .................................. $("#result").empty(); person.name = "Jo"; person.name2 = "Jo2"; cancelChange = false; eventData = ""; // =============================== Arrange =============================== //INLINE DATA-LINKED TAGS ON INPUT - WITH linkTo EXPRESSION // ................................ Act .................................. jsv.templates('{^{twoWayTag name linkTo=name2}}{{/twoWayTag}}') .link("#result", person); tag = $("#result").view(true).childTags("twoWayTag")[0]; linkedEl = $("#linkedElm")[0]; // ............................... Assert ................................. assert.equal(eventData, "init render onBind onAfterLink setValue ", 'Data link using: {^{twoWayTag name linkTo=name2}} - event order for init, render, link'); eventData = ""; // ................................ Act .................................. before = tag.value + linkedEl.value; jsv.observable(person).setProperty({name: "newName"}); after = tag.value + linkedEl.value; // ............................... Assert ................................. assert.equal(eventData, "onBeforeChange onUpdate onAfterLink setValue onAfterChange ", 'Data link using: {^{twoWayTag name linkTo=name2}} - event order for onUpdate'); eventData = ""; // ............................... Assert ................................. assert.equal(before + "|" + after, "JoJo|newNamenewName", 'Data link using: {^{twoWayTag name linkTo=name2}} - binds data to linkedElem'); // ................................ Act .................................. before = "value:" + tag.value + " name:" + person.name + " name2:" + person.name2; linkedEl.value = "newVal"; $(linkedEl).change(); after = "value:" + tag.value + " name:" + person.name + " name2:" + person.name2; // ............................... Assert ................................. assert.equal(eventData, "onBeforeUpdateVal setValue ", 'Data link using: {^{twoWayTag name linkTo=name2}} - event order for onChange'); eventData = ""; // ............................... Assert ................................. assert.equal(before + "|" + after, "value:newName name:newName name2:Jo2|value:newName name:newName name2:newVal", 'Data link using: {^{twoWayTag name linkTo=name2}} - binds linkedElem back to "linkTo" target data'); // ................................ Reset .................................. person.name = "Jo"; person.name2 = "Jo2"; // =============================== Arrange =============================== jsv.templates({ markup: '{^{twoWayTag name linkTo=name2 convert="myupper" convertBack=~lower}}{{/twoWayTag}}', converters: { myupper: function(val) { return val.toUpperCase(); } } }).link("#result", person, { lower: function(val) { return val.toLowerCase(); } }); tag = $("#result").view(true).childTags("twoWayTag")[0]; linkedEl = $("#linkedElm")[0]; // ............................... Assert ................................. assert.equal(linkedEl.value + "|" + tag.value, "JO|Jo", 'Data link using: {^{twoWayTag name linkTo=name2 convert="myupper"}} - (tag.convert setting) - initial linking: converts the value on the target input'); // ................................ Act .................................. jsv.observable(person).setProperty({name: "ANewName"}); // ............................... Assert ................................. assert.equal(linkedEl.value + "|" + tag.value, "ANEWNAME|ANewName", 'Data link using: {^{twoWayTag name linkTo=name2 convert="myupper"} - (tag.convert setting) - on data change: converts the value on the target input'); // ................................ Act .................................. linkedEl = $("#linkedElm")[0]; linkedEl.value = "ChangeTheName"; $(linkedEl).change(); // ............................... Assert ................................. assert.equal("name:" + person.name + " name2:" + person.name2 + " value:" + tag.value, "name:ANewName name2:changethename value:ANewName", 'Data link using: {^{twoWayTag name linkTo=name2 convertBack=~lower}} - (tag.convertBack setting) on element change: converts the data, and sets on "linkTo" target data'); // ............................... Reset ................................. $("#result").empty(); jsv.views.settings.trigger(true); }); QUnit.test('Custom Tag Controls - two-way binding (multiple targets)', function(assert) { jsv.views.settings.trigger(false); // =============================== Arrange =============================== var person = {first: "Jo", last: "Blow"}; jsv.templates({ markup: '{^{mytag first/}}', tags: { mytag: { template: ' {^{on ~tag.doupdate id="updateBtn"}}Update{{/on}}', onBind: function() { this.linkedElem = this.contents("input").first(); // Programmatically set linkedElem }, onAfterLink: function() { this.contents(".nm").text(this.bndArgs()[0]); // Programmatically update span content }, doupdate: function() { var val = this.bndArgs()[0] + this.cnt++ this.updateValue(val).setValue(val); // Programmatically call update() from within tag }, onUpdate: false, cnt: 0 // Counter for programmatically updated content } } }).link("#result", person); var mytag = jsv.view().childTags("mytag")[0]; var linkedEl = $("#linkedElm")[0]; var result = mytag.bndArgs() + "|" + linkedEl.value; // ................................ Act .................................. jsv.observable(person).setProperty({first: "Bob"}); result += "--" + mytag.bndArgs() + "|" + linkedEl.value; linkedEl.value = "newName" $(linkedEl).change(); result += "--" + mytag.bndArgs() + "|" + person.first; // ............................... Assert ................................. assert.equal(result, "Jo|Jo--Bob|Bob--newName|newName", "linkedElem set in onBind"); // ................................ Act .................................. result = "" + mytag.bndArgs(); mytag.updateValue("updatedFirst").setValue("updatedFirst"); result += "--" + mytag.bndArgs() + "|" + linkedEl.value + "|" + person.first + "|" + $("#result").text(); // ................................ Act .................................. $("#updateBtn").click(); result += "--" + mytag.bndArgs() + "|" + linkedEl.value + "|" + person.first + "|" + $("#result").text(); assert.equal(result, "newName--updatedFirst|updatedFirst|updatedFirst|updatedFirst Update" + "--updatedFirst0|updatedFirst0|updatedFirst0|updatedFirst0 Update", "With linkedElem set in onBind, mytag.bndArgs() and mytag.updateValue().setValue() correctly access/update two-way bound values"); // =============================== Arrange =============================== person = {first: "Jo", last: "Blow"}; jsv.templates({ markup: '{^{mytag first last/}}', tags: { mytag: { bindTo: [0, 1], template: ' ', onBind: function(tagCtx) { var inputs = tagCtx.contentView.contents("input"); tagCtx.linkedElems = [$(inputs[0]), $(inputs[1])]; // Programmatically set linkedElems }, onAfterLink: function() { this.contents(".nm").text(this.bndArgs()[0] + " " + this.bndArgs()[1]); // Programmatically update span content }, onUpdate: false } } }).link("#result", person); mytag = jsv.view().childTags("mytag")[0]; var linkedEl1 = $("#linkedElm1")[0]; var linkedEl2 = $("#linkedElm2")[0]; result = mytag.bndArgs() + "|" + linkedEl1.value + "|" + linkedEl2.value; // ................................ Act .................................. jsv.observable(person).setProperty({first: "Bob", last: "Puff"}); result += "--" + mytag.bndArgs() + "|" + linkedEl1.value + "|" + linkedEl2.value + "|" + $("#result").text(); linkedEl1.value = "newFirst" linkedEl2.value = "newLast" $(linkedEl1).change(); $(linkedEl2).change(); result += "--" + mytag.bndArgs() + "|" + person.first + "|" + person.last + "|" + $("#result").text(); // ............................... Assert ................................. assert.equal(result, "Jo,Blow|Jo|Blow--Bob,Puff|Bob|Puff| Bob Puff--newFirst,newLast|newFirst|newLast| newFirst newLast", 'With bindTo: [0, 1], and 2 linkedElems set in onBind, mytag.bndArgs() and mytag.updateValue() ' + 'correctly access/update two-way bound values'); // =============================== Arrange =============================== person = {first: "Jo", last: "Blow"}; jsv.templates({ markup: '{^{mytag 1 first/}}', tags: { mytag: { bindTo: 1, template: ' ', onBind: function() { var inputs = this.contents("input"); this.linkedElem = $(inputs[0]); // Programmatically set linkedElems }, onAfterLink: function() { this.contents(".nm").text(this.bndArgs()[0]); // Programmatically update span content }, onUpdate: false } } }).link("#result", person); mytag = jsv.view().childTags("mytag")[0]; linkedEl1 = $("#linkedElm1")[0]; result = mytag.bndArgs() + "|" + linkedEl1.value ; // ................................ Act .................................. jsv.observable(person).setProperty({first: "Bob"}); result += "--" + mytag.bndArgs() + "|" + linkedEl1.value + "|" + $("#result").text(); linkedEl1.value = "newFirst" $(linkedEl1).change(); result += "--" + mytag.bndArgs() + "|" + person.first + "|" + $("#result").text(); // ............................... Assert ................................. assert.equal(result, "Jo|Jo--Bob|Bob| Bob--newFirst|newFirst| newFirst", 'With bindTo: 1, and linkedElem set in onBind, mytag.bndArgs() and mytag.updateValue()' + 'correctly access/update two-way bound values'); // =============================== Arrange =============================== person = {first: "Jo", last: "Blow"}; jsv.templates({ markup: '{^{mytag name1=first name2=last/}}', tags: { mytag: { bindTo: ["name1", "name2"], template: ' ', onBind: function(tagCtx) { var inputs = tagCtx.contentView.contents("input"); tagCtx.linkedElems = [$(inputs[0]), $(inputs[1])]; // Programmatically set linkedElems }, onAfterLink: function() { this.contents(".nm").text(this.bndArgs()[0] + " " + this.bndArgs()[1]); // Programmatically update span content }, onUpdate: false } } }).link("#result", person); mytag = jsv.view().childTags("mytag")[0]; linkedEl1 = $("#linkedElm1")[0]; linkedEl2 = $("#linkedElm2")[0]; result = mytag.bndArgs() + "|" + linkedEl1.value + "|" + linkedEl2.value; // ................................ Act .................................. jsv.observable(person).setProperty({first: "Bob", last: "Puff"}); result += "--" + mytag.bndArgs() + "|" + linkedEl1.value + "|" + linkedEl2.value + "|" + $("#result").text(); linkedEl1.value = "newFirst" linkedEl2.value = "newLast" $(linkedEl1).change(); $(linkedEl2).change(); result += "--" + mytag.bndArgs() + "|" + person.first + "|" + person.last + "|" + $("#result").text(); // ............................... Assert ................................. assert.equal(result, "Jo,Blow|Jo|Blow--Bob,Puff|Bob|Puff| Bob Puff--newFirst,newLast|newFirst|newLast| newFirst newLast", 'With bindTo: ["name1", "name2"], and 2 linkedElems set in onBind, mytag.bndArgs() and mytag.updateValue())' + 'correctly access/update two-way bound values'); // =============================== Arrange =============================== person = {first: "Jo", last: "Blow"}; jsv.templates({ markup: '{^{mytag first/}}', tags: { mytag: { linkedElement: "input", template: '', onUpdate: false } } }).link("#result", person); mytag = jsv.view().childTags("mytag")[0]; linkedEl = $("#linkedElm")[0]; result = linkedEl.value; // ................................ Act .................................. jsv.observable(person).setProperty({first: "Bob"}); result += "--" + linkedEl.value; linkedEl.value = "newName" $(linkedEl).change(); result += "--" + person.first; mytag.updateValue("updatedFirst").setValue("updatedFirst"); result += "--" + person.first + "|" + linkedEl.value; // ............................... Assert ................................. assert.equal(result, "Jo--Bob--newName--updatedFirst|updatedFirst", "linkedElem set using linkedElement declaration"); // =============================== Arrange =============================== person = {first: "Jo", last: "Blow"}; jsv.templates({ markup: '{^{mytag first/}}', tags: { mytag: { linkedCtxParam: "frst", template: '{^{:~frst}}', onUpdate: false } } }).link("#result", person); mytag = jsv.view().childTags("mytag")[0]; linkedEl = $("#linkedElm")[0]; result = linkedEl.value + "|" + $("#result").text(); // ................................ Act .................................. jsv.observable(person).setProperty({first: "Bob"}); result += "--" + linkedEl.value + "|" + $("#result").text(); linkedEl.value = "newName" $(linkedEl).change(); result += "--" + person.first + "|" + $("#result").text(); mytag.updateValue("updatedFirst").setValue("updatedFirst"); result += "--" + person.first + "|" + linkedEl.value + "|" + $("#result").text(); // ............................... Assert ................................. assert.equal(result, "Jo|Jo--Bob|Bob--newName|newName--updatedFirst|updatedFirst|updatedFirst", "linkedCtxParam declaring a tag contextual parameter"); // =============================== Arrange =============================== person = {first: "Jo", last: "Blow"}; jsv.templates({ markup: '{^{mytag first/}}', tags: { mytag: { bindTo: [0], linkedCtxParam: ["frst"], template: '{^{:~frst}}', onUpdate: false } } }).link("#result", person); mytag = jsv.view().childTags("mytag")[0]; linkedEl = $("#linkedElm")[0]; result = linkedEl.value + "|" + $("#result").text(); // ................................ Act .................................. jsv.observable(person).setProperty({first: "Bob"}); result += "--" + linkedEl.value + "|" + $("#result").text(); linkedEl.value = "newName" $(linkedEl).change(); result += "--" + person.first + "|" + $("#result").text(); mytag.updateValue("updatedFirst"); result += "--" + person.first + "|" + linkedEl.value + "|" + $("#result").text(); // ............................... Assert ................................. assert.equal(result, "Jo|Jo--Bob|Bob--newName|newName--updatedFirst|updatedFirst|updatedFirst", "bindTo and linkedCtxParam as arrays, declaring a tag contextual parameter"); // =============================== Arrange =============================== person = {first: "Jo", last: "Blow"}; jsv.templates({ markup: '{^{mytag first last/}}', tags: { mytag: { bindTo: [1, 0], linkedCtxParam: ["lst", "frst"], template: '{^{:~frst}} {^{:~lst}}', onUpdate: false } } }).link("#result", person); mytag = jsv.view().childTags("mytag")[0]; linkedEl1 = $("#linkedElm1")[0]; linkedEl2 = $("#linkedElm2")[0]; result = linkedEl1.value + "|" + linkedEl2.value + "|" + $("#result").text(); // ................................ Act .................................. jsv.observable(person).setProperty({first: "Bob", last: "Puff"}); result += "--" + linkedEl1.value + "|" + linkedEl2.value + "|" + $("#result").text(); linkedEl1.value = "newFirst" linkedEl2.value = "newLast" $(linkedEl1).change(); $(linkedEl2).change(); result += "--" + person.first + "|" + person.last + "|" + $("#result").text(); // ............................... Assert ................................. assert.equal(result, "Jo|Blow|Jo Blow--Bob|Puff|Bob Puff--newFirst|newLast|newFirst newLast", "bindTo and linkedCtxParam as arrays, declaring tag contextual parameters"); // ................................ Act .................................. result = "" + mytag.bndArgs(); mytag.updateValues("updatedFirst", "updatedLast").setValues("updatedFirst", "updatedLast"); result += "--" + mytag.bndArgs() + "|" + linkedEl1.value + "|" + linkedEl2.value + "|" + person.first + "|" + person.last + "|" + $("#result").text(); assert.equal(result, "newLast,newFirst--updatedFirst,updatedLast|updatedLast|updatedFirst|updatedLast|updatedFirst|updatedLast updatedFirst", "mytag.bndArgs() and mytag.updateValues().setValues() work correctly for accessing/updating two-way bound tag contextual parameters"); // =============================== Arrange =============================== person = {first: "Jo", last: "Blow"}; jsv.templates({ markup: '{^{mytag name1=first 1 last/}}', tags: { mytag: { bindTo: ["name1", "1"], linkedElement: ["input", "#linkedElm2"], template: ' ', onAfterLink: function() { this.linkedElems[1].text(this.bndArgs()[1]); } } } }).link("#result", person); mytag = jsv.view().childTags("mytag")[0]; linkedEl1 = $("#linkedElm1")[0]; linkedEl2 = $("#linkedElm2")[0]; result = linkedEl1.value + "|" + linkedEl2.innerText + "|" + $("#result").text(); // ................................ Act .................................. mytag.updateValues("updatedFirst", "updatedLast").setValues("updatedFirst", "updatedLast"); result += "--" + mytag.bndArgs() + "|" + linkedEl1.value + "|" + person.first + "|" + person.last + "|" + $("#result").text(); // ............................... Assert ................................. assert.equal(result, "Jo|Blow| Blow--updatedFirst,updatedLast|updatedFirst|updatedFirst|updatedLast| updatedLast", "mytag.bndArgs() and mytag.updateValues().setValues() work correctly for accessing/updating two-way bound linkedElems"); // =============================== Arrange =============================== person = {first: "Jo", last: "Blow"}; jsv.templates({ markup: '{^{mytag name1=first 1 last/}}', tags: { mytag: { bindTo: ["name1", "1"], linkedElement: [undefined, "#linkedElm2"], template: ' ', onBind: function() { var inputs = this.contents("input"); this.linkedElems[0] = $(inputs[0]); // Programmatically set linkedElems //}, //onAfterLink: function() { // this.linkedElems[1].text(this.bndArgs()[1]); } } } }).link("#result", person); mytag = jsv.view().childTags("mytag")[0]; linkedEl1 = $("#linkedElm1")[0]; linkedEl2 = $("#linkedElm2")[0]; result = "" + (linkedEl1 === mytag.linkedElems[0][0]) + (linkedEl2 === mytag.linkedElems[1][0]) + (linkedEl1 === mytag.linkedElem[0]) + linkedEl1.value + "|" + linkedEl2.innerText + "|" + $("#result").text(); // ................................ Act .................................. mytag.updateValues("updatedFirst", "updatedLast").setValues("updatedFirst", "updatedLast"); result += "--" + mytag.bndArgs() + "|" + linkedEl1.value + "|" + person.first + "|" + person.last + "|" + $("#result").text(); // ............................... Assert ................................. assert.equal(result, "truetruetrueJo|Blow| Blow--updatedFirst,updatedLast|updatedFirst|updatedFirst|updatedLast| updatedLast", "Mixed declarative linkedElement and programmatic onBind approaches for defining linkedElems"); // =============================== Arrange =============================== person = {first: "Jo", last: "Blow"}; jsv.templates({ markup: '{^{mytag name1=first 1 last/}}', tags: { mytag: { bindTo: ["name1", "1"], linkedElement: [undefined, "#linkedElm2"], template: ' ', onBind: function() { var inputs = this.contents("input"); this.linkedElems[0] = $(inputs[0]); // Programmatically set linkedElems //}, //onAfterLink: function() { // this.linkedElems[1].text(this.bndArgs()[0]); } } } }).link("#result", person); mytag = jsv.view().childTags("mytag")[0]; linkedEl1 = $("#linkedElm1")[0]; linkedEl2 = $("#linkedElm2")[0]; result = linkedEl1.value + "|" + linkedEl2.innerText + "|" + $("#result").text(); // ................................ Act .................................. mytag.updateValues("updatedFirst", "updatedLast").setValues("updatedFirst", "updatedLast"); result += "--" + mytag.bndArgs() + "|" + linkedEl1.value + "|" + person.first + "|" + person.last + "|" + $("#result").text(); // ............................... Assert ................................. assert.equal(result, "Jo|Blow| Blow--updatedFirst,updatedLast|updatedFirst|updatedFirst|updatedLast| updatedLast", "Mixed declarative linkedElement and programmatic onBind approaches for defining linkedElems"); // =============================== Arrange =============================== person = {first: "Jo", last: "Blow"}; jsv.templates({ markup: '{^{mytag name1=first 1 last/}}', tags: { mytag: { bindTo: ["name1", "1"], linkedElement: [undefined, "input"], linkedCtxParam: ["foo", undefined], template: ' ' } } }).link("#result", person); mytag = jsv.view().childTags("mytag")[0]; linkedEl1 = $("#linkedElm1")[0]; linkedEl2 = $("#linkedElm2")[0]; result = linkedEl1.value + "|" + linkedEl2.innerText + "|" + $("#result").text(); // ................................ Act .................................. mytag.updateValues("updatedFirst", "updatedLast").setValues("updatedFirst", "updatedLast"); result += "--" + mytag.bndArgs() + "|" + linkedEl1.value + "|" + person.first + "|" + person.last + "|" + $("#result").text(); // ............................... Assert ................................. assert.equal(result, "Blow|Jo| Jo--updatedFirst,updatedLast|updatedLast|updatedFirst|updatedLast| updatedFirst", "linkedCtxParam and linkedElem"); // =============================== Arrange =============================== person = {first: "Jo", last: "Blow"}; jsv.templates({ markup: '{^{mytag first last/}}', tags: { mytag: { bindTo: [0, 1], linkedCtxParam: ["foo", undefined], template: ' ', onBind: function(tagCtx) { var inputs = tagCtx.contentView.contents("input"); tagCtx.linkedElems = [undefined, $(inputs[0])]; // Programmatically set linkedElems }, } } }).link("#result", person); mytag = jsv.view().childTags("mytag")[0]; linkedEl1 = $("#linkedElm1")[0]; linkedEl2 = $("#linkedElm2")[0]; result = linkedEl1.value + "|" + linkedEl2.innerText + "|" + $("#result").text(); // ................................ Act .................................. mytag.updateValues("updatedFirst", "updatedLast").setValues("updatedFirst", "updatedLast"); result += "--" + mytag.bndArgs() + "|" + linkedEl1.value + "|" + person.first + "|" + person.last + "|" + $("#result").text(); // ............................... Assert ................................. assert.equal(result, "Blow|Jo| Jo--updatedFirst,updatedLast|updatedLast|updatedFirst|updatedLast| updatedFirst", "linkedCtxParam plus programmatic setting of linkedElem"); // =============================== Arrange =============================== person = {first: "Jo", last: "Blow", cvt: function(first) { return first.toUpperCase(); // bindTo: 1 (or [1]) so only one parameter }, cvtbk: function(first) { return first.toLowerCase(); } }; jsv.templates({ markup: '{^{mytag 1 first convert=cvt convertBack=cvtbk/}}', tags: { mytag: { bindTo: 1, linkedElement: ["input"], template: '', } } }).link("#result", person); mytag = jsv.view().childTags("mytag")[0]; linkedEl = mytag.linkedElem[0]; result = linkedEl.value; linkedEl.value += "+"; mytag.linkedElem.change(); result = person.first + "|" + person.last; // ................................ Act .................................. mytag.updateValue("updatedFirst").setValue(mytag.bndArgs()[0]); result += "--" + mytag.bndArgs() + "|" + linkedEl.value + "|" + person.first + "|" + person.last; // ............................... Assert ................................. assert.equal(result, "jo+|Blow--UPDATEDFIRST|UPDATEDFIRST|updatedfirst|Blow", "bindTo:1 with converters, using linkedElement with selector"); // =============================== Arrange =============================== person = {first: "Jo", last: "Blow", cvt: function(arg1, arg2) { return arg1.toUpperCase(); // No bindTo specified, so parameters are all the args }, cvtbk: function(first) { return first.toLowerCase(); // No bindTo specified, so parameter is the linkedElem value } }; jsv.templates({ markup: '{^{mytag first last convert=cvt convertBack=cvtbk/}}', tags: { mytag: { linkedElement: ["input"], template: '', } } }).link("#result", person); mytag = jsv.view().childTags("mytag")[0]; linkedEl = mytag.linkedElem[0]; linkedEl.value += "+"; mytag.linkedElem.change(); result = person.first + "|" + person.last; // ................................ Act .................................. mytag.updateValues("updatedFirst", "updatedLast").setValues(mytag.bndArgs()[0], mytag.bndArgs()[1]); result += "--" + mytag.bndArgs() + "|" + linkedEl.value + "|" + person.first + "|" + person.last; // ............................... Assert ................................. assert.equal(result, "jo+|Blow--UPDATEDFIRST|UPDATEDFIRST|updatedfirst|Blow", "linkedEl with converters (no bindTo), using linkedElement with selector"); // =============================== Arrange =============================== person = {first: "Jo", last: "Blow", cvt: function(first, last) { return [first + "*", last.toUpperCase()]; // bindTo: ["name1", 1] so two parameters - the bound args (as returned by tag.bndArgs()) }, cvtbk: function(first, last) { return [first, last.toLowerCase()]; } }; jsv.templates({ markup: '{^{mytag name1=first 1 last convert=cvt convertBack=cvtbk/}}', tags: { mytag: { bindTo: ["name1", "1"], linkedElement: [undefined, "input"], linkedCtxParam: ["foo", undefined], template: ' ' } } }).link("#result", person); mytag = jsv.view().childTags("mytag")[0]; linkedEl1 = mytag.contents("#linkedElm1")[0]; // first - param linkedEl2 = mytag.linkedElems[1][0]; // last - el result = linkedEl1.innerText + "," + linkedEl2.value; // Jo*, BLOW linkedEl2.value += "+"; // BLOW+ mytag.linkedElems[1].change(); result += "|" + person.first + "," + person.last; // ................................ Act .................................. mytag.updateValues("updatedFirst", "updatedLast"); mytag.setValues(mytag.bndArgs()[0], mytag.bndArgs()[1]); result += "|" + mytag.bndArgs() + "|" + linkedEl1.innerText + "," + linkedEl2.value + "|" + person.first + "," + person.last; // ............................... Assert ................................. assert.equal(result, "Jo*,BLOW|Jo,blow+|updatedFirst*,UPDATEDLAST|updatedFirst*,UPDATEDLAST|updatedFirst,updatedlast", "bindTo with converters, using linkedElement and linkedCtxParam"); // =============================== Arrange =============================== jsv.views.tags("mytag", { bindTo: ["height", "width"], linkedElement: [".ht", ".wd"], linkedCtxParam: ["ht", "wd"], mainElement: "div", template: "
                        {{include tmpl=#content/}}
                        ", onBind: function() { var tag = this; tag.mainElem.mousedown(function(ev) { var mainElem = tag.mainElem[0], addedWidth = tag.mainElem.width() - ev.clientX, addedHeight = tag.mainElem.height() - ev.clientY; if (document.elementFromPoint(ev.clientX, ev.clientY) === mainElem) { $(document.body).mousemove(function(ev2) { var moveToX = ev2.clientX + addedWidth; var moveToY = ev2.clientY + addedHeight; tag.updateValues(moveToY, moveToX); }); } }); $(document.body).mouseup(function() { $(document.body).off("mousemove"); }); }, setValue: function(val, index, tagElse, ev, eventArgs) { if (val === undefined) { val = this.getValue(tagElse)[index]; } else { this.mainElem[index ? "width" : "height"](val || 0); } this.tagCtxs[tagElse].ctxPrm(this.linkedCtxParam[index], val); return val; }, getValue: function() { var mainElem = this.mainElem; return [mainElem.height(), mainElem.width()]; }, onUpdate: false, setSize: true }); var tmpl = jsv.templates('{^{mytag}}' + ' ' + ' ' + '{{/mytag}}'); // ................................ Act .................................. tmpl.link("#result"); mytag = jsv.view().childTags("mytag")[0]; var linkedElHt = mytag.contents(true, ".ht")[0]; var linkedElHt2 = mytag.linkedElems[0][0]; var linkedElWd = mytag.contents(true, ".wd")[0]; var linkedElWd2 = mytag.linkedElems[1][0]; var linkedCtxPrmHts = mytag.contents(true, ".htc"); var linkedCtxPrmWds = mytag.contents(true, ".wdc"); result = linkedElHt === linkedElHt2 && linkedElWd === linkedElWd2 && linkedElHt.value === linkedCtxPrmHts[0].value && linkedElHt.value === linkedCtxPrmHts[1].value && linkedElWd.value === linkedCtxPrmWds[0].value && linkedElWd.value === linkedCtxPrmWds[1].value; // ............................... Assert ................................. assert.ok(result, "linkedElement and linkedCtxParam for unset params are initialized by getValue"); // ................................ Act .................................. linkedElHt.value = 40; $(linkedElHt).change(); linkedCtxPrmWds[1].value = 60; $(linkedCtxPrmWds[1]).change(); result = linkedElHt.value === "40" && linkedCtxPrmHts[0].value === "40" && linkedCtxPrmHts[1].value === "40" && linkedElWd.value === "60" && linkedCtxPrmWds[0].value === "60" && linkedCtxPrmWds[1].value === "60"; result += "|" + mytag.getValue(); assert.equal(result, "true|40,60", "linkedElement and linkedCtxParam for unset params continue to 2way-bind to shared value which is the tag.setValue/getValue"); // ................................ Act .................................. tmpl = jsv.templates('{^{mytag height=30 width=40}}' + ' ' + ' ' + '{{/mytag}}'); tmpl.link("#result"); mytag = jsv.view().childTags("mytag")[0]; linkedElHt = mytag.contents(true, ".ht")[0]; linkedElWd = mytag.contents(true, ".wd")[0]; linkedCtxPrmHts = mytag.contents(true, ".htc"); linkedCtxPrmWds = mytag.contents(true, ".wdc"); result = linkedElHt.value === "30" && linkedCtxPrmHts[0].value === "30" && linkedCtxPrmHts[1].value === "30" && linkedElWd.value === "40" && linkedCtxPrmWds[0].value === "40" && linkedCtxPrmWds[1].value === "40"; result += "|" + mytag.getValue(); // ............................... Assert ................................. assert.equal(result, "true|30,40", "linkedElement and linkedCtxParam and tag.getValue() for static initial values initialize correctly"); // ................................ Act .................................. linkedElHt.value = 40; $(linkedElHt).change(); linkedCtxPrmWds[1].value = 60; $(linkedCtxPrmWds[1]).change(); result = linkedElHt.value === "40" && linkedCtxPrmHts[0].value === "40" && linkedCtxPrmHts[1].value === "40" && linkedElWd.value === "60" && linkedCtxPrmWds[0].value === "60" && linkedCtxPrmWds[1].value === "60"; result += "|" + mytag.getValue(); // ............................... Assert ................................. assert.equal(result, "true|40,60", "linkedElement and linkedCtxParam for static initial values continue to 2way-bind to shared value which is the tag.setValue/getValue value"); // ................................ Act .................................. mytag.setValues(140, 160); mytag.updateValues(140, 160); result = linkedElHt.value === "140" && linkedCtxPrmHts[0].value === "140" && linkedCtxPrmHts[1].value === "140" && linkedElWd.value === "160" && linkedCtxPrmWds[0].value === "160" && linkedCtxPrmWds[1].value === "160"; result += "|" + mytag.getValue(); // ............................... Assert ................................. assert.equal(result, "true|140,160", "linkedElement and linkedCtxParam for static initial values continue to 2way-bind to shared value set by tag.setValues(), mytag.updateValues()"); // ................................ Act .................................. tmpl = jsv.templates(' ' + '{^{mytag height=cy width=cx}}' + ' ' + ' ' + '{{/mytag}}'); tmpl.link("#result", {cx: 40, cy: 30}); mytag = jsv.view().childTags("mytag")[0]; linkedElHt = mytag.contents(true, ".ht")[0]; linkedElWd = mytag.contents(true, ".wd")[0]; linkedCtxPrmHts = mytag.contents(true, ".htc"); linkedCtxPrmWds = mytag.contents(true, ".wdc"); var linkedElCx = $(".cx")[0], linkedElCy = $(".cy")[0]; result = linkedElCy.value === "30" && linkedElHt.value === "30" && linkedCtxPrmHts[0].value === "30" && linkedCtxPrmHts[1].value === "30" && linkedElCx.value === "40" && linkedElWd.value === "40" && linkedCtxPrmWds[0].value === "40" && linkedCtxPrmWds[1].value === "40"; result += "|" + mytag.getValue(); // ............................... Assert ................................. assert.equal(result, "true|30,40", "linkedElement and linkedCtxParam and tag.getValue() initialize correctly"); // ................................ Act .................................. linkedElHt.value = 40; $(linkedElHt).change(); linkedCtxPrmWds[1].value = 60; $(linkedCtxPrmWds[1]).change(); result = linkedElCy.value === "40" && linkedElHt.value === "40" && linkedCtxPrmHts[0].value === "40" && linkedCtxPrmHts[1].value === "40" && linkedElCx.value === "60" && linkedElWd.value === "60" && linkedCtxPrmWds[0].value === "60" && linkedCtxPrmWds[1].value === "60"; result += "|" + mytag.getValue(); // ............................... Assert ................................. assert.equal(result, "true|40,60", "linkedElement and linkedCtxParam continue to 2way-bind to shared value which is the tag.setValue/getValue value"); // ................................ Act .................................. linkedElCy.value = 50; $(linkedElCy).change(); linkedElCx.value = 70; $(linkedElCx).change(); result = linkedElCy.value === "50" && linkedElHt.value === "50" && linkedCtxPrmHts[0].value === "50" && linkedCtxPrmHts[1].value === "50" && linkedElCx.value === "70" && linkedElWd.value === "70" && linkedCtxPrmWds[0].value === "70" && linkedCtxPrmWds[1].value === "70"; result += "|" + mytag.getValue(); // ............................... Assert ................................. assert.equal(result, "true|50,70", "linkedElement and linkedCtxParam continue to 2way-bind to shared value when external bindTo target value changes"); // ................................ Act .................................. mytag.setValues(140, 160); mytag.updateValues(140, 160); result = linkedElCy.value === "140" && linkedElHt.value === "140" && linkedCtxPrmHts[0].value === "140" && linkedCtxPrmHts[1].value === "140" && linkedElCx.value === "160" && linkedElWd.value === "160" && linkedCtxPrmWds[0].value === "160" && linkedCtxPrmWds[1].value === "160"; result += "|" + mytag.getValue(); // ............................... Assert ................................. assert.equal(result, "true|140,160", "linkedElement and linkedCtxParam continue to 2way-bind to shared value set by tag.setValues() - tag.updateValues()"); // ................................ Act .................................. tmpl = jsv.templates('{^{mytag convert=~plus convertBack=~minus}}' + ' ' + ' ' + '{{/mytag}}'); var data = {cx: 100, cy: 200}; tmpl.link("#result", data, { plus: function(height, width) { return [height !== undefined ? parseInt(height)+5 : height, width !== undefined ? parseInt(width)+10 : width]; }, minus: function(height, width) { return [height !== undefined ? parseInt(height)-5 : height, width !== undefined ? parseInt(width)-10 : width]; } }); mytag = jsv.view().childTags("mytag")[0]; var linkedElHt = mytag.contents(true, ".ht")[0]; var linkedElHt2 = mytag.linkedElems[0][0]; var linkedElWd = mytag.contents(true, ".wd")[0]; var linkedElWd2 = mytag.linkedElems[1][0]; var linkedCtxPrmHts = mytag.contents(true, ".htc"); var linkedCtxPrmWds = mytag.contents(true, ".wdc"); result = linkedElHt === linkedElHt2 && linkedElWd === linkedElWd2 && linkedElHt.value === linkedCtxPrmHts[0].value && linkedElHt.value === linkedCtxPrmHts[1].value && linkedElWd.value === linkedCtxPrmWds[0].value && linkedElWd.value === linkedCtxPrmWds[1].value && linkedElWd.value === "" + mytag.getValue()[1] && linkedElHt.value === "" + mytag.getValue()[0]; // ............................... Assert ................................. assert.ok(result, "linkedElement and linkedCtxParam for unset params are initialized by getValue (with converters)"); // ................................ Act .................................. linkedElHt.value = 40; $(linkedElHt).change(); linkedCtxPrmWds[1].value = 60; $(linkedCtxPrmWds[1]).change(); result = linkedElHt.value === "40" && linkedCtxPrmHts[0].value === "40" && linkedCtxPrmHts[1].value === "40" && linkedElWd.value === "60" && linkedCtxPrmWds[0].value === "60" && linkedCtxPrmWds[1].value === "60"; result += "|" + mytag.getValue(); // ............................... Assert ................................. assert.equal(result, "true|40,60", "linkedElement and linkedCtxParam for unset params continue to 2way-bind to shared value which is the tag.setValue/getValue (with converters)"); // ................................ Act .................................. tmpl = jsv.templates('{^{mytag height=30 width=40 convert=~plus convertBack=~minus}}' + ' ' + ' ' + '{{/mytag}}'); tmpl.link("#result", {}, { plus: function(height, width) { return [height !== undefined ? parseInt(height)+5 : height, width !== undefined ? parseInt(width)+10 : width]; }, minus: function(height, width) { return [height !== undefined ? parseInt(height)-5 : height, width !== undefined ? parseInt(width)-10 : width]; } }); mytag = jsv.view().childTags("mytag")[0]; linkedElHt = mytag.contents(true, ".ht")[0]; linkedElWd = mytag.contents(true, ".wd")[0]; linkedCtxPrmHts = mytag.contents(true, ".htc"); linkedCtxPrmWds = mytag.contents(true, ".wdc"); result = linkedElHt.value === "35" && linkedCtxPrmHts[0].value === "35" && linkedCtxPrmHts[1].value === "35" && linkedElWd.value === "50" && linkedCtxPrmWds[0].value === "50" && linkedCtxPrmWds[1].value === "50"; result += "|" + mytag.getValue(); // ............................... Assert ................................. assert.equal(result, "true|35,50", "linkedElement and linkedCtxParam and tag.getValue() for static initial values initialize correctly (with converters)"); // ................................ Act .................................. linkedElHt.value = 40; $(linkedElHt).change(); linkedCtxPrmWds[1].value = 60; $(linkedCtxPrmWds[1]).change(); result = linkedElHt.value === "40" && linkedCtxPrmHts[0].value === "40" && linkedCtxPrmHts[1].value === "40" && linkedElWd.value === "60" && linkedCtxPrmWds[0].value === "60" && linkedCtxPrmWds[1].value === "60"; result += "|" + mytag.getValue(); // ............................... Assert ................................. assert.equal(result, "true|40,60", "linkedElement and linkedCtxParam for static initial values continue to 2way-bind to shared value which is the tag.setValue/getValue value (with converters)"); // ................................ Act .................................. mytag.setValue(140, 0); mytag.setValue(160, 1); mytag.updateValues(140, 160); result = linkedElHt.value === "140" && linkedCtxPrmHts[0].value === "140" && linkedCtxPrmHts[1].value === "140" && linkedElWd.value === "160" && linkedCtxPrmWds[0].value === "160" && linkedCtxPrmWds[1].value === "160"; result += "|" + mytag.getValue(); // ............................... Assert ................................. assert.equal(result, "true|140,160", "linkedElement and linkedCtxParam for static initial values continue to 2way-bind to shared value set by tag.setValue(), mytag.updateValues() (with converters)"); // ................................ Act .................................. tmpl = jsv.templates(' ' + '{^{mytag height=cy width=cx convert=~plus convertBack=~minus}}' + ' ' + ' ' + '{{/mytag}}'); tmpl.link("#result", {cx: 40, cy: 30}, { plus: function(height, width) { return [height !== undefined ? parseInt(height)+5 : height, width !== undefined ? parseInt(width)+10 : width]; }, minus: function(height, width) { return [height !== undefined ? parseInt(height)-5 : height, width !== undefined ? parseInt(width)-10 : width]; } }); mytag = jsv.view().childTags("mytag")[0]; linkedElHt = mytag.contents(true, ".ht")[0]; linkedElWd = mytag.contents(true, ".wd")[0]; linkedCtxPrmHts = mytag.contents(true, ".htc"); linkedCtxPrmWds = mytag.contents(true, ".wdc"); linkedElCx = $(".cx")[0]; linkedElCy = $(".cy")[0]; result = linkedElCy.value === "30" && linkedElHt.value === "35" && linkedCtxPrmHts[0].value === "35" && linkedCtxPrmHts[1].value === "35" && linkedElCx.value === "40" && linkedElWd.value === "50" && linkedCtxPrmWds[0].value === "50" && linkedCtxPrmWds[1].value === "50"; result += "|" + mytag.getValue(); // ............................... Assert ................................. assert.equal(result, "true|35,50", "linkedElement and linkedCtxParam and tag.getValue() initialize correctly (with converters)"); // ................................ Act .................................. linkedElHt.value = 40; $(linkedElHt).change(); linkedCtxPrmWds[1].value = 60; $(linkedCtxPrmWds[1]).change(); result = linkedElCy.value === "35" && linkedElHt.value === "40" && linkedCtxPrmHts[0].value === "40" && linkedCtxPrmHts[1].value === "40" && linkedElCx.value === "50" && linkedElWd.value === "60" && linkedCtxPrmWds[0].value === "60" && linkedCtxPrmWds[1].value === "60"; result += "|" + mytag.getValue(); // ............................... Assert ................................. assert.equal(result, "true|40,60", "linkedElement and linkedCtxParam continue to 2way-bind to shared value which is the tag.setValue/getValue value (with converters)"); // ................................ Act .................................. linkedElCy.value = 50; $(linkedElCy).change(); linkedElCx.value = 70; $(linkedElCx).change(); result = linkedElCy.value === "50" && linkedElHt.value === "55" && linkedCtxPrmHts[0].value === "55" && linkedCtxPrmHts[1].value === "55" && linkedElCx.value === "70" && linkedElWd.value === "80" && linkedCtxPrmWds[0].value === "80" && linkedCtxPrmWds[1].value === "80"; result += "|" + mytag.getValue(); // ............................... Assert ................................. assert.equal(result, "true|55,80", "linkedElement and linkedCtxParam continue to 2way-bind to shared value when external bindTo target value changes (with converters)"); // ................................ Act .................................. mytag.setValue(140, 0); mytag.setValue(160, 1); mytag.updateValues(140, 160); result = linkedElCy.value === "135" && linkedElHt.value === "140" && linkedCtxPrmHts[0].value === "140" && linkedCtxPrmHts[1].value === "140" && linkedElCx.value === "150" && linkedElWd.value === "160" && linkedCtxPrmWds[0].value === "160" && linkedCtxPrmWds[1].value === "160"; result += "|" + mytag.getValue(); // ............................... Assert ................................. assert.equal(result, "true|140,160", "linkedElement and linkedCtxParam continue to 2way-bind to shared value set by tag.setValue() - tag.updateValues() (with converters)"); // ................................ Act .................................. tmpl = jsv.templates(' ' + '{^{mytag width=cx convert=~plus convertBack=~minus}}' + ' ' + ' ' + '{{/mytag}}'); tmpl.link("#result", {cx: 40, cy: 30}, { plus: function(height, width) { return [height !== undefined ? parseInt(height)+5 : height, width !== undefined ? parseInt(width)+10 : width]; }, minus: function(height, width) { return [height !== undefined ? parseInt(height)-5 : height, width !== undefined ? parseInt(width)-10 : width]; } }); mytag = jsv.view().childTags("mytag")[0]; linkedElHt = mytag.contents(true, ".ht")[0]; linkedElWd = mytag.contents(true, ".wd")[0]; linkedCtxPrmHts = mytag.contents(true, ".htc"); linkedCtxPrmWds = mytag.contents(true, ".wdc"); linkedElCx = $(".cx")[0]; linkedElCy = $(".cy")[0]; result = linkedElHt.value === linkedCtxPrmHts[0].value && linkedElHt.value === linkedCtxPrmHts[1].value && Math.round(10*linkedElHt.value) === Math.round(10*mytag.getValue()[0]) && linkedElCx.value === "40" && linkedElWd.value === "50" && linkedCtxPrmWds[0].value === "50" && linkedCtxPrmWds[1].value === "50"; result += "|" + mytag.getValue()[1]; // ............................... Assert ................................. assert.equal(result, "true|50", "linkedElement and linkedCtxParam and tag.getValue() initialize correctly (One bindTo param bound, other uninitialized. With converters)"); // ................................ Act .................................. linkedElHt.value = 40; $(linkedElHt).change(); linkedCtxPrmWds[1].value = 60; $(linkedCtxPrmWds[1]).change(); result = linkedElCy.value === "30" && linkedElHt.value === "40" && linkedCtxPrmHts[0].value === "40" && linkedCtxPrmHts[1].value === "40" && linkedElCx.value === "50" && linkedElWd.value === "60" && linkedCtxPrmWds[0].value === "60" && linkedCtxPrmWds[1].value === "60"; result += "|" + mytag.getValue(); // ............................... Assert ................................. assert.equal(result, "true|40,60", "linkedElement and linkedCtxParam continue to 2way-bind to shared value which is the tag.setValue/getValue value (One bindTo param bound, other uninitialized. With converters)"); // ................................ Act .................................. linkedElCy.value = 50; $(linkedElCy).change(); linkedElCx.value = 70; $(linkedElCx).change(); result = linkedElCy.value === "50" && linkedElHt.value === "40" && linkedCtxPrmHts[0].value === "40" && linkedCtxPrmHts[1].value === "40" && linkedElCx.value === "70" && linkedElWd.value === "80" && linkedCtxPrmWds[0].value === "80" && linkedCtxPrmWds[1].value === "80"; result += "|" + mytag.getValue(); // ............................... Assert ................................. assert.equal(result, "true|40,80", "linkedElement and linkedCtxParam continue to 2way-bind to shared value when external bindTo target value changes (One bindTo param bound, other uninitialized. With converters)"); // ................................ Act .................................. mytag.setValues(140, 160); mytag.updateValues(140, 160); result = linkedElCy.value === "50" && linkedElHt.value === "140" && linkedCtxPrmHts[0].value === "140" && linkedCtxPrmHts[1].value === "140" && linkedElCx.value === "150" && linkedElWd.value === "160" && linkedCtxPrmWds[0].value === "160" && linkedCtxPrmWds[1].value === "160"; result += "|" + mytag.getValue(); // ............................... Assert ................................. assert.equal(result, "true|140,160", "linkedElement and linkedCtxParam continue to 2way-bind to shared value set by tag.setValues() - tag.updateValues() (One bindTo param bound, other uninitialized. With converters)"); // =============================== Arrange =============================== person = {first: "Jo", middle: "Herbert", last: "Blow"}; jsv.templates({ markup: '{^{textbox first label="First"}} {^{child ~nm/}}' + '{{else middle label="Middle"}} {^{child ~nm/}}' + '{{else last label="Last"}} {^{child ~nm/}}' + '{{/textbox}}' + '' + '' + ': ' + '{^{:first}} {^{:middle}} {^{:last}}', tags: { textbox: { linkedCtxParam: "nm", onBind: function() { // Find input in contents var l = this.tagCtxs.length; while (l--) { var tagCtx = this.tagCtxs[l]; tagCtx.linkedElems = [tagCtx.contents("input")]; } }, template: " {{:~tagCtx.props.label}}{^{include tmpl=#content/}}", onUpdate: false, // No need to re-render whole tag, when content updates. }, child: function(val) { return val; } } }).link("#result", person); var linkedElFirst = $("#First")[0], linkedElMiddle = $("#Middle")[0], linkedElLast = $("#Last")[0], mytag = jsv.view(linkedElFirst).tag; function getResult() { return "Data: " + person.first + "-" + person.middle + "-" + person.last + " Inputs: " + linkedElFirst.value + "-" + linkedElMiddle.value + "-" + linkedElLast.value + " Text:" + $("#result").text() + "|"; } // ............................... Assert ................................. assert.equal(getResult(), "Data: Jo-Herbert-Blow Inputs: Jo-Herbert-Blow Text: First Jo Middle Herbert Last Blow: Jo Herbert Blow|", "Two-way bound tag with multiple else blocks - initial render"); // ................................ Act .................................. jsv.observable(person).setProperty({first: "Bob", middle: "Xavier", last: "Smith"}); // ............................... Assert ................................. assert.equal(getResult(), "Data: Bob-Xavier-Smith Inputs: Bob-Xavier-Smith Text: First Bob Middle Xavier Last Smith: Bob Xavier Smith|", "Two-way bound tag with multiple else blocks - observable update"); // ................................ Act .................................. linkedElFirst.value = "newJo" linkedElMiddle.value = "newHerbert" linkedElLast.value = "newBlow" $(linkedElFirst).change(); $(linkedElMiddle).change(); $(linkedElLast).change(); // ............................... Assert ................................. assert.equal(getResult(), "Data: newJo-newHerbert-newBlow Inputs: newJo-newHerbert-newBlow" + " Text: First newJo Middle newHerbert Last newBlow: newJo newHerbert newBlow|", "Two-way bound tag with multiple else blocks - updated inputs"); // ................................ Act .................................. mytag.updateValue("updatedFirst", 0, 0); mytag.updateValue("updatedMiddle", 0, 1); mytag.updateValue("updatedLast", 0, 2); // ............................... Assert ................................. assert.equal(getResult(), "Data: updatedFirst-updatedMiddle-updatedLast Inputs: newJo-newHerbert-newBlow" + " Text: First updatedFirst Middle updatedMiddle Last updatedLast: updatedFirst updatedMiddle updatedLast|", "Two-way bound tag with multiple else blocks - tag.updateValue() updates outer bindings and linkedCtxPrm, but not linkedElems"); // ................................ Act .................................. mytag.updateValues("updatedFirst2"); // ............................... Assert ................................. assert.equal(getResult(), "Data: updatedFirst2-updatedMiddle-updatedLast Inputs: newJo-newHerbert-newBlow" + " Text: First updatedFirst2 Middle updatedMiddle Last updatedLast: updatedFirst2 updatedMiddle updatedLast|", "Two-way bound tag with multiple else blocks - tag.updateValues() updates outer bindings and linkedCtxPrm, but not linkedElems"); // ................................ Act .................................. mytag.setValue("changedFirst", 0, 0); mytag.setValue("changedMiddle", 0, 1); mytag.setValue("changedLast", 0, 2); // ............................... Assert ................................. assert.equal(getResult(), "Data: changedFirst-changedMiddle-changedLast Inputs: changedFirst-changedMiddle-changedLast" + " Text: First changedFirst Middle changedMiddle Last changedLast: changedFirst changedMiddle changedLast|", "Two-way bound tag with multiple else blocks - tag.setValue() updates linkedElems and linkedCtxPrms"); // ................................ Act .................................. mytag.setValues("changedFirst2"); // ............................... Assert ................................. assert.equal(getResult(), "Data: changedFirst2-changedMiddle-changedLast Inputs: changedFirst2-changedMiddle-changedLast" + " Text: First changedFirst2 Middle changedMiddle Last changedLast: changedFirst2 changedMiddle changedLast|", "Two-way bound tag with multiple else blocks - tag.setValues() updates linkedElems and linkedCtxPrms"); // ............................... Assert ................................. assert.ok(mytag.contents("input")[1] === mytag.tagCtxs[1].contents("input")[0] && mytag.nodes()[5] === mytag.tagCtxs[1].nodes()[1] && mytag.nodes()[5] === mytag.tagCtxs[1].contentView.nodes()[1] && mytag.childTags("child")[1] === mytag.tagCtxs[1].childTags("child")[0] && mytag.childTags("child").length === mytag.tagCtxs[0].childTags("child").length + mytag.tagCtxs[1].childTags("child").length + mytag.tagCtxs[2].childTags("child").length, "Two-way bound tag, multiple else blocks: calls tagCtx.contents() tagCtx.nodes() tagCtx.childTags() return from one else block." + " (Whereas tag.contents() etc returns from all else blocks)"); // =============================== Arrange =============================== person = {first: "Jo", middle: "Herbert", last: "Blow"}; jsv.templates({ markup: '{^{textbox first label="First"}} {^{child ~nm/}}' + '{{else middle label="Middle"}} {^{child ~nm/}}' + '{{else last label="Last"}} {^{child ~nm/}}' + '{{/textbox}}' + '' + '' + ': ' + '{^{:first}} {^{:middle}} {^{:last}}', tags: { textbox: { render: function() { return " " + this.tagCtx.props.label + "" + this.tagCtx.render(); }, linkedCtxParam: "nm", onBind: function() { // Find input in contents var l = this.tagCtxs.length; while (l--) { var tagCtx = this.tagCtxs[l]; tagCtx.linkedElems = [tagCtx.contents("input")]; } }, onUpdate: false, // No need to re-render whole tag, when content updates. }, child: function(val) { return val; } } }).link("#result", person); linkedElFirst = $("#First")[0]; linkedElMiddle = $("#Middle")[0]; linkedElLast = $("#Last")[0]; mytag = jsv.view(linkedElFirst).tag; // ............................... Assert ................................. assert.equal(getResult(), "Data: Jo-Herbert-Blow Inputs: Jo-Herbert-Blow Text: First Jo Middle Herbert Last Blow: Jo Herbert Blow|", "Two-way bound tag (using render method) with multiple else blocks - initial render"); // ................................ Act .................................. jsv.observable(person).setProperty({first: "Bob", middle: "Xavier", last: "Smith"}); // ............................... Assert ................................. assert.equal(getResult(), "Data: Bob-Xavier-Smith Inputs: Bob-Xavier-Smith Text: First Bob Middle Xavier Last Smith: Bob Xavier Smith|", "Two-way bound tag (using render method) with multiple else blocks - observable update"); // ................................ Act .................................. linkedElFirst.value = "newJo" linkedElMiddle.value = "newHerbert" linkedElLast.value = "newBlow" $(linkedElFirst).change(); $(linkedElMiddle).change(); $(linkedElLast).change(); // ............................... Assert ................................. assert.equal(getResult(), "Data: newJo-newHerbert-newBlow Inputs: newJo-newHerbert-newBlow" + " Text: First newJo Middle newHerbert Last newBlow: newJo newHerbert newBlow|", "Two-way bound tag (using render method) with multiple else blocks - updated inputs"); // ................................ Act .................................. mytag.updateValue("updatedFirst", 0, 0); mytag.updateValue("updatedMiddle", 0, 1); mytag.updateValue("updatedLast", 0, 2); // ............................... Assert ................................. assert.equal(getResult(), "Data: updatedFirst-updatedMiddle-updatedLast Inputs: newJo-newHerbert-newBlow" + " Text: First updatedFirst Middle updatedMiddle Last updatedLast: updatedFirst updatedMiddle updatedLast|", "Two-way bound tag (using render method) with multiple else blocks - tag.updateValue() updates outer bindings and linkedCtxPrm, but not linkedElems"); // ................................ Act .................................. mytag.updateValues("updatedFirst2"); // ............................... Assert ................................. assert.equal(getResult(), "Data: updatedFirst2-updatedMiddle-updatedLast Inputs: newJo-newHerbert-newBlow" + " Text: First updatedFirst2 Middle updatedMiddle Last updatedLast: updatedFirst2 updatedMiddle updatedLast|", "Two-way bound tag (using render method) with multiple else blocks - tag.updateValues() updates outer bindings and linkedCtxPrm, but not linkedElems"); // ................................ Act .................................. mytag.setValue("changedFirst", 0, 0); mytag.setValue("changedMiddle", 0, 1); mytag.setValue("changedLast", 0, 2); // ............................... Assert ................................. assert.equal(getResult(), "Data: changedFirst-changedMiddle-changedLast Inputs: changedFirst-changedMiddle-changedLast" + " Text: First changedFirst Middle changedMiddle Last changedLast: changedFirst changedMiddle changedLast|", "Two-way bound tag (using render method) with multiple else blocks - tag.setValue() updates linkedElems and linkedCtxPrms"); // ................................ Act .................................. mytag.setValues("changedFirst2"); // ............................... Assert ................................. assert.equal(getResult(), "Data: changedFirst2-changedMiddle-changedLast Inputs: changedFirst2-changedMiddle-changedLast" + " Text: First changedFirst2 Middle changedMiddle Last changedLast: changedFirst2 changedMiddle changedLast|", "Two-way bound tag (using render method) with multiple else blocks - tag.setValues() updates linkedElems and linkedCtxPrms"); // ............................... Assert ................................. assert.ok(mytag.contents("input")[1] === mytag.tagCtxs[1].contents("input")[0] && mytag.nodes()[5] === mytag.tagCtxs[1].nodes()[1] && mytag.childTags("child")[1] === mytag.tagCtxs[1].childTags("child")[0], "Two-way bound tag (using render method) with multiple else blocks - calls to tagCtx.contents() tagCtx.nodes() tagCtx.childTags() work correctly"); // =============================== Arrange =============================== var person = {first: "Jo", last: "Blow"}; jsv.templates({ markup: '{^{mytag 0 prop=first last}}{{else 0 prop=last first}}{{/mytag}} {^{:first}} {^{:last}}', tags: { mytag: { bindTo: ["prop", 1], template: '
                        ', onBind: function() { var tagCtx0 = this.tagCtxs[0]; var tagCtx1 = this.tagCtxs[1]; var inputs0 = tagCtx0.contents("input"); var inputs1 = tagCtx1.contents("input"); tagCtx0.linkedElems = [$(inputs0[0]), $(inputs0[1])]; // Programmatically set linkedElems tagCtx1.linkedElems = [$(inputs1[0]), $(inputs1[1])]; // Programmatically set linkedElems }, onAfterLink: function() { var tagCtx0 = this.tagCtxs[0]; var tagCtx1 = this.tagCtxs[1]; var span0 = tagCtx0.contents(".nm"); var span1 = tagCtx1.contents(".nm"); span0.text(tagCtx0.bndArgs()[0] + " " + tagCtx0.bndArgs()[1]); // Programmatically update span content span1.text(tagCtx1.bndArgs()[0] + " " + tagCtx1.bndArgs()[1]); // Programmatically update span content }, onUpdate: false } } }).link("#result", person); mytag = jsv.view().childTags()[0]; var linkedElems0 = mytag.tagCtx.contents("input"); var linkedElems1 = mytag.tagCtxs[1].contents("input"); function getResult2() { return "Data: " + person.first + "-" + person.last + " Inputs: " + linkedElems0[0].value + "-" + linkedElems0[1].value + "-" + linkedElems1[0].value + "-" + linkedElems1[1].value + " Text:" + $("#result").text() + "|"; } // ............................... Assert ................................. assert.equal(getResult2(), "Data: Jo-Blow Inputs: Jo-Blow-Blow-Jo Text: Jo Blow Blow Jo Jo Blow|", "Two-way tag with multiple bindings and multiple else blocks - initial render"); // ................................ Act .................................. jsv.observable(person).setProperty({first: "Bob", last: "Smith"}); // ............................... Assert ................................. assert.equal(getResult2(), "Data: Bob-Smith Inputs: Bob-Smith-Smith-Bob Text: Bob Smith Smith Bob Bob Smith|", "Two-way tag with multiple bindings and multiple else blocks - observable update"); // ................................ Act .................................. // Change inputs of {{else}} block linkedElems1[0].value = "newBlow" linkedElems1[1].value = "newJo" $(linkedElems1[0]).change(); $(linkedElems1[1]).change(); // ............................... Assert ................................. assert.equal(getResult2(), "Data: newJo-newBlow Inputs: Bob-Smith-newBlow-newJo Text: newJo newBlow newBlow newJo newJo newBlow|", "Two-way tag with multiple bindings and multiple else blocks - updated inputs"); // ................................ Act .................................. // Update each value for {{else}} block mytag.updateValue("updatedLast", 0, 1); mytag.updateValue("updatedFirst", 1, 1); // ............................... Assert ................................. assert.equal(getResult2(), "Data: updatedFirst-updatedLast Inputs: Bob-Smith-newBlow-newJo" + " Text: updatedFirst updatedLast updatedLast updatedFirst updatedFirst updatedLast|", "Two-way tag with multiple bindings and multiple else blocks - tag.updateValue() updates outer bindings but not linkedElems"); // ................................ Act .................................. // Update values for tag (main block) mytag.updateValues("updatedFirst2", "updatedLast2"); // ............................... Assert ................................. assert.equal(getResult2(), "Data: updatedFirst2-updatedLast2 Inputs: Bob-Smith-newBlow-newJo" + " Text: updatedFirst2 updatedLast2 updatedLast2 updatedFirst2 updatedFirst2 updatedLast2|", "Two-way tag with multiple bindings and multiple else blocks - tag.updateValues() updates outer bindings but not linkedElems"); // ................................ Act .................................. // Set each value for {{else}} block mytag.setValue("changedLast", 0, 1); mytag.setValue("changedFirst", 1, 1); // ............................... Assert ................................. assert.equal(getResult2(), "Data: updatedFirst2-updatedLast2 Inputs: Bob-Smith-changedLast-changedFirst" + " Text: updatedFirst2 updatedLast2 updatedLast2 updatedFirst2 updatedFirst2 updatedLast2|", "Two-way tag with multiple bindings and multiple else blocks - tag.setValue() updates linkedElems only"); // ................................ Act .................................. // Set values for {{else}} block mytag.tagCtxs[1].setValues("changedLast2", "changedFirst2"); // ............................... Assert ................................. assert.equal(getResult2(), "Data: updatedFirst2-updatedLast2 Inputs: Bob-Smith-changedLast2-changedFirst2" + " Text: updatedFirst2 updatedLast2 updatedLast2 updatedFirst2 updatedFirst2 updatedLast2|", "Two-way tag with multiple bindings and multiple else blocks - tagCtx.setValues() updates linkedElems only"); // ................................ Act .................................. // Set values for tag (main block) mytag.setValues("changedFirst3", "changedLast3"); // ............................... Assert ................................. assert.equal(getResult2(), "Data: updatedFirst2-updatedLast2 Inputs: changedFirst3-changedLast3-changedLast2-changedFirst2" + " Text: updatedFirst2 updatedLast2 updatedLast2 updatedFirst2 updatedFirst2 updatedLast2|", "Two-way tag with multiple bindings and multiple else blocks - tag.setValues() updates linkedElems only"); // ............................... Assert ................................. assert.ok(mytag.contents("input")[3] === mytag.tagCtxs[1].contents("input")[1] && mytag.nodes()[6] === mytag.tagCtxs[1].nodes()[1], "Two-way tag with multiple bindings and multiple else blocks - calls to tagCtx.contents() tagCtx.nodes() work correctly"); // ............................... Assert ................................. assert.equal("" + mytag.cvtArgs() + "|" + mytag.tagCtxs[0].cvtArgs() + "|" + mytag.tagCtxs[1].cvtArgs() + "--" + mytag.bndArgs() + "|" + mytag.tagCtxs[0].bndArgs() + "|" + mytag.tagCtxs[1].bndArgs(), "0,updatedLast2|0,updatedLast2|0,updatedFirst2--updatedFirst2,updatedLast2|updatedFirst2,updatedLast2|updatedLast2,updatedFirst2", "Two-way tag with multiple bindings and multiple else blocks - calls to tag.cvtArgs(), tagCtx.cvtArgs() tag.bndArgs() tagCtx.bndArgs() work correctly"); // ............................... Reset ................................. $("#result").empty(); // =============================== Arrange =============================== $("#result").html( '
                        ' + '
                        ' ); jsv.views.tags("namebox", { setSize: true, template: '
                        X
                        ', linkedElement: "input", displayElement: "span", mainElement: "div", onBind: function(tagCtx) { var tagCtx1 = this.tagCtxs[1]; logElems && logElems(this.linkedElems, this.linkedElem, this.mainElem, this.displayElem, tagCtx.linkedElems, tagCtx.mainElem, tagCtx.displayElem, tagCtx1.linkedElems, tagCtx1.mainElem, tagCtx1.displayElem); } }); ret = ""; data = { first: "Jo", last: "Blow" }; // ................................ Act .................................. var logElems = function(linkedElems, linkedElem, mainElem, displayElem, linkedElems0, mainElem0, displayElem0, linkedElems1, mainElem1, displayElem1 ) { ret += "|" + (linkedElems === linkedElems0) + " " + (linkedElem === linkedElems[0]) + " " + (mainElem === mainElem0) + " " + (displayElem === displayElem0) + " " + linkedElem[0].tagName + mainElem[0].tagName + displayElem[0].tagName }; jsv.templates('' + '{^{namebox first class="nm1" id="id1" width=66}}{{else last width=24 class="nm2" id="id2" }}{{/namebox}}' + '

                        ') .link("#page", data); jsv.link(true, ".top", data); logElems = undefined; // ............................... Assert ................................. assert.equal(ret, "|true true true true INPUTDIVSPAN|true true true true INPUTDIVSPAN|true true true true INPUTDIVSPAN", "Two-way tag: linkedElements|displayElement|mainElement settings lead to appropriate linkedElems|linkedElem|displayElem|mainElem values on tag and on tagCtx, in onBind()"); assert.ok($(".nm1").length === 3 && $(".nm2").length === 3 && $("#id1")[0].tagName === "DIV" && $("#id2")[0].tagName === "DIV" && $("#id3")[0].tagName === "DIV" && $("#id4")[0].tagName === "DIV" && $("#id6")[0].tagName === "DIV" && !$("#id5")[0], "Two-way tag: displayElement and mainElement settings lead to class and id assignment as expected on displayElem and mainElem"); // ................................ Act .................................. var inputs = $("#result input"); ret = inputs[0].value + inputs[1].value + inputs[2].value + inputs[3].value + inputs[4].value + inputs[5].value + inputs[6].value + inputs[7].value; jsv.observable(data).setProperty({first: "Jeff", last: "Blye"}); inputs = $("#result input"); ret += "|" + inputs[0].value + inputs[1].value + inputs[2].value + inputs[3].value + inputs[4].value + inputs[5].value + inputs[6].value + inputs[7].value; $(inputs[0]).val("Pete").change(); inputs = $("#result input"); ret += "|" + inputs[0].value + inputs[1].value + inputs[2].value + inputs[3].value + inputs[4].value + inputs[5].value + inputs[6].value + inputs[7].value; $(inputs[3]).val("Bains").change(); inputs = $("#result input"); ret += "|" + inputs[0].value + inputs[1].value + inputs[2].value + inputs[3].value + inputs[4].value + inputs[5].value + inputs[6].value + inputs[7].value; $(inputs[4]).val("Bill").change(); inputs = $("#result input"); ret += "|" + inputs[0].value + inputs[1].value + inputs[2].value + inputs[3].value + inputs[4].value + inputs[5].value + inputs[6].value + inputs[7].value; $(inputs[7]).val("Banks").change(); inputs = $("#result input"); ret += "|" + inputs[0].value + inputs[1].value + inputs[2].value + inputs[3].value + inputs[4].value + inputs[5].value + inputs[6].value + inputs[7].value; // ............................... Assert ................................. assert.equal(ret, "JoBlowJoBlowJoBlowJoBlow|JeffBlyeJeffBlyeJeffBlyeJeffBlye|PeteBlyePeteBlyePeteBlyePeteBlye|PeteBainsPeteBainsPeteBainsPeteBains|BillBainsBillBainsBillBainsBillBains|BillBanksBillBanksBillBanksBillBanks", "Two-way tag: linkedElems 2-way binding is correct whether inline, data-linked, or top-level data-linked tag"); // =============================== Arrange =============================== $("#result").html( '
                        ' + '
                        ' ); jsv.views.tags("namebox", { onUpdate: false, setSize: true, template: '
                        X
                        ', linkedElement: "input", displayElement: "span", mainElement: "div", onBind: function(tagCtx) { var tagCtx1 = this.tagCtxs[1]; logElems && logElems(this.linkedElems, this.linkedElem, this.mainElem, this.displayElem, tagCtx.linkedElems, tagCtx.mainElem, tagCtx.displayElem, tagCtx1.linkedElems, tagCtx1.mainElem, tagCtx1.displayElem); } }); ret = ""; data = { first: "Jo", last: "Blow" }; logElems = function(linkedElems, linkedElem, mainElem, displayElem, linkedElems0, mainElem0, displayElem0, linkedElems1, mainElem1, displayElem1 ) { ret += "|" + (linkedElems === linkedElems0) + " " + (linkedElem === linkedElems[0]) + " " + (mainElem === mainElem0) + " " + (displayElem === displayElem0) + " " + linkedElem[0].tagName + mainElem[0].tagName + displayElem[0].tagName }; jsv.templates('' + '{^{namebox first class="nm1" id="id1" width=66}}{{else last width=24 class="nm2" id="id2" }}{{/namebox}}' + '

                        ') .link("#page", data); jsv.link(true, ".top", data); logElems = undefined; // ............................... Assert ................................. assert.equal(ret, "|true true true true INPUTDIVSPAN|true true true true INPUTDIVSPAN|true true true true INPUTDIVSPAN", "Two-way tag (onUpdate false): linkedElements|displayElement|mainElement settings lead to appropriate linkedElems|linkedElem|displayElem|mainElem values on tag and on tagCtx, in onBind()"); assert.ok($(".nm1").length === 3 && $(".nm2").length === 3 && $("#id1")[0].tagName === "DIV" && $("#id2")[0].tagName === "DIV" && $("#id3")[0].tagName === "DIV" && $("#id4")[0].tagName === "DIV" && $("#id6")[0].tagName === "DIV" && !$("#id5")[0], "Two-way tag (onUpdate false): displayElement and mainElement settings lead to class and id assignment as expected on displayElem and mainElem"); // ................................ Act .................................. inputs = $("#result input"); ret = inputs[0].value + inputs[1].value + inputs[2].value + inputs[3].value + inputs[4].value + inputs[5].value + inputs[6].value + inputs[7].value; jsv.observable(data).setProperty({first: "Jeff", last: "Blye"}); ret += "|" + inputs[0].value + inputs[1].value + inputs[2].value + inputs[3].value + inputs[4].value + inputs[5].value + inputs[6].value + inputs[7].value; $(inputs[0]).val("Pete").change(); ret += "|" + inputs[0].value + inputs[1].value + inputs[2].value + inputs[3].value + inputs[4].value + inputs[5].value + inputs[6].value + inputs[7].value; $(inputs[3]).val("Bains").change(); ret += "|" + inputs[0].value + inputs[1].value + inputs[2].value + inputs[3].value + inputs[4].value + inputs[5].value + inputs[6].value + inputs[7].value; $(inputs[4]).val("Bill").change(); ret += "|" + inputs[0].value + inputs[1].value + inputs[2].value + inputs[3].value + inputs[4].value + inputs[5].value + inputs[6].value + inputs[7].value; $(inputs[7]).val("Banks").change(); ret += "|" + inputs[0].value + inputs[1].value + inputs[2].value + inputs[3].value + inputs[4].value + inputs[5].value + inputs[6].value + inputs[7].value; // ............................... Assert ................................. assert.equal(ret, "JoBlowJoBlowJoBlowJoBlow|JeffBlyeJeffBlyeJeffBlyeJeffBlye|PeteBlyePeteBlyePeteBlyePeteBlye|PeteBainsPeteBainsPeteBainsPeteBains|BillBainsBillBainsBillBainsBillBains|BillBanksBillBanksBillBanksBillBanks", "Two-way tag (onUpdate false): linkedElems 2-way binding is correct whether inline, data-linked, or top-level data-linked tag"); // =============================== Arrange =============================== $("#result").html( '
                        ' + '
                        ' ); jsv.views.tags("namebox", { setSize: true, template: '
                        X
                        ', onBind: function(tagCtx) { this.tagCtxs[0].linkedElems=[this.tagCtxs[0].contents(true, "input")]; this.tagCtxs[0].displayElem=this.tagCtxs[0].contents(true, "span"); this.tagCtxs[0].mainElem=this.tagCtxs[0].contents(true, "div"); this.tagCtxs[1].linkedElems=[this.tagCtxs[1].contents(true, "input")]; this.tagCtxs[1].displayElem=this.tagCtxs[1].contents(true, "div"); this.tagCtxs[1].mainElem=this.tagCtxs[1].contents(true, "span"); } }); ret = ""; data = { first: "Jo", last: "Blow" }; jsv.templates('' + '{^{namebox first class="nm1" id="id1" width=66}}{{else last width=24 class="nm2" id="id2" }}{{/namebox}}' + '

                        ') .link("#page", data); jsv.link(true, ".top", data); // ............................... Assert ................................. assert.ok( $(".nm1").length === 3 && $(".nm2").length === 3 && $("#id1")[0].tagName === "DIV" && $("#id2")[0].tagName === "SPAN" && $("#id3")[0].tagName === "DIV" && $("#id4")[0].tagName === "SPAN" && $("#id5")[0].tagName === "DIV" && $("#id6")[0].tagName === "SPAN", "Two-way tag: setting tagCtx.linkedElems|displayElem|mainElem in onBind lead to class and id assignment as expected on displayElem and mainElem"); // ................................ Act .................................. inputs = $("#result input"); ret = inputs[0].value + inputs[1].value + inputs[2].value + inputs[3].value + inputs[4].value + inputs[5].value + inputs[6].value + inputs[7].value; jsv.observable(data).setProperty({first: "Jeff", last: "Blye"}); inputs = $("#result input"); ret += "|" + inputs[0].value + inputs[1].value + inputs[2].value + inputs[3].value + inputs[4].value + inputs[5].value + inputs[6].value + inputs[7].value; $(inputs[0]).val("Pete").change(); inputs = $("#result input"); ret += "|" + inputs[0].value + inputs[1].value + inputs[2].value + inputs[3].value + inputs[4].value + inputs[5].value + inputs[6].value + inputs[7].value; $(inputs[3]).val("Bains").change(); inputs = $("#result input"); ret += "|" + inputs[0].value + inputs[1].value + inputs[2].value + inputs[3].value + inputs[4].value + inputs[5].value + inputs[6].value + inputs[7].value; $(inputs[4]).val("Bill").change(); inputs = $("#result input"); ret += "|" + inputs[0].value + inputs[1].value + inputs[2].value + inputs[3].value + inputs[4].value + inputs[5].value + inputs[6].value + inputs[7].value; $(inputs[7]).val("Banks").change(); inputs = $("#result input"); ret += "|" + inputs[0].value + inputs[1].value + inputs[2].value + inputs[3].value + inputs[4].value + inputs[5].value + inputs[6].value + inputs[7].value; // ............................... Assert ................................. assert.equal(ret, "JoBlowJoBlowJoBlowJoBlow|JeffBlyeJeffBlyeJeffBlyeJeffBlye|PeteBlyePeteBlyePeteBlyePeteBlye|PeteBainsPeteBainsPeteBainsPeteBains|BillBainsBillBainsBillBainsBillBains|BillBanksBillBanksBillBanksBillBanks", "Two-way tag: setting tagCtx.linkedElems|displayElem|mainElem in onBind lead to correct 2-way binding, whether inline, data-linked, or top-level data-linked tag"); // =============================== Arrange =============================== $("#result").html( '
                        ' + '
                        ' ); jsv.views.tags("namebox", { onUpdate: false, setSize: true, template: '
                        X
                        ', onBind: function(tagCtx) { this.linkedElems=[this.tagCtxs[0].contents(true, "input")]; // this.linkedElem=this.tagCtxs[0].contents(true, "input"); // also works this.displayElem=this.tagCtxs[0].contents(true, "span"); this.mainElem=this.tagCtxs[0].contents(true, "div"); this.tagCtxs[1].linkedElems=[this.tagCtxs[1].contents(true, "input")]; this.tagCtxs[1].displayElem=this.tagCtxs[1].contents(true, "div"); this.tagCtxs[1].mainElem=this.tagCtxs[1].contents(true, "span"); } }); ret = ""; data = { first: "Jo", last: "Blow" }; jsv.templates('' + '{^{namebox first class="nm1" id="id1" width=66}}{{else last width=24 class="nm2" id="id2" }}{{/namebox}}' + '

                        ') .link("#page", data); jsv.link(true, ".top", data); // ............................... Assert ................................. assert.ok( $(".nm1").length === 3 && $(".nm2").length === 3 && $("#id1")[0].tagName === "DIV" && $("#id2")[0].tagName === "SPAN" && $("#id3")[0].tagName === "DIV" && $("#id4")[0].tagName === "SPAN" && $("#id5")[0].tagName === "DIV" && $("#id6")[0].tagName === "SPAN", "Two-way tag: setting tag.linkedElem(s) and tagCtx|displayElem|mainElem in onBind lead to class and id assignment as expected on displayElem and mainElem"); // ................................ Act .................................. inputs = $("#result input"); ret = inputs[0].value + inputs[1].value + inputs[2].value + inputs[3].value + inputs[4].value + inputs[5].value + inputs[6].value + inputs[7].value; jsv.observable(data).setProperty({first: "Jeff", last: "Blye"}); ret += "|" + inputs[0].value + inputs[1].value + inputs[2].value + inputs[3].value + inputs[4].value + inputs[5].value + inputs[6].value + inputs[7].value; $(inputs[0]).val("Pete").change(); ret += "|" + inputs[0].value + inputs[1].value + inputs[2].value + inputs[3].value + inputs[4].value + inputs[5].value + inputs[6].value + inputs[7].value; $(inputs[3]).val("Bains").change(); ret += "|" + inputs[0].value + inputs[1].value + inputs[2].value + inputs[3].value + inputs[4].value + inputs[5].value + inputs[6].value + inputs[7].value; $(inputs[4]).val("Bill").change(); ret += "|" + inputs[0].value + inputs[1].value + inputs[2].value + inputs[3].value + inputs[4].value + inputs[5].value + inputs[6].value + inputs[7].value; $(inputs[7]).val("Banks").change(); ret += "|" + inputs[0].value + inputs[1].value + inputs[2].value + inputs[3].value + inputs[4].value + inputs[5].value + inputs[6].value + inputs[7].value; // ............................... Assert ................................. assert.equal(ret, "JoBlowJoBlowJoBlowJoBlow|JeffBlyeJeffBlyeJeffBlyeJeffBlye|PeteBlyePeteBlyePeteBlyePeteBlye|PeteBainsPeteBainsPeteBainsPeteBains|BillBainsBillBainsBillBainsBillBains|BillBanksBillBanksBillBanksBillBanks", "Two-way tag: setting tag.linkedElem(s) and tagCtx|displayElem|mainElem in onBind lead to correct 2-way binding, whether inline, data-linked, or top-level data-linked tag"); // =============================== Arrange =============================== jsv.views.tags("namebox", { onUpdate: false, setSize: true, onBind: function(tagCtx) { this.linkedElems=[this.tagCtxs[0].contents(true, "input")]; // this.linkedElem=this.tagCtxs[0].contents(true, "input"); // also works this.displayElem=this.tagCtxs[0].contents(true, "span"); this.mainElem=this.tagCtxs[0].contents(true, "div"); this.tagCtxs[1].linkedElems=[this.tagCtxs[1].contents(true, "input")]; this.tagCtxs[1].displayElem=this.tagCtxs[1].contents(true, "div"); this.tagCtxs[1].mainElem=this.tagCtxs[1].contents(true, "span"); } }); ret = ""; data = { first: "Jo", last: "Blow" }; jsv.templates('' + '{^{namebox first class="nm1" id="id1" width=66}}
                        X
                        {{else last width=24 class="nm2" id="id2" }}
                        X
                        {{/namebox}}') .link("#result", data); // ............................... Assert ................................. assert.ok( $(".nm1").length === 1 && $(".nm2").length === 1 && $("#id1")[0].tagName === "DIV" && $("#id2")[0].tagName === "SPAN", "Two-way tag, no template: setting tag.linkedElem(s) and tagCtx|displayElem|mainElem in onBind lead to class and id assignment as expected on displayElem and mainElem, within wrapped content"); // ................................ Act .................................. inputs = $("#result input"); ret = inputs[0].value + inputs[1].value + inputs[2].value + inputs[3].value; jsv.observable(data).setProperty({first: "Jeff", last: "Blye"}); ret += "|" + inputs[0].value + inputs[1].value + inputs[2].value + inputs[3].value; $(inputs[0]).val("Pete").change(); ret += "|" + inputs[0].value + inputs[1].value + inputs[2].value + inputs[3].value; $(inputs[1]).val("Bains").change(); ret += "|" + inputs[0].value + inputs[1].value + inputs[2].value + inputs[3].value; $(inputs[2]).val("Jim").change(); ret += "|" + inputs[0].value + inputs[1].value + inputs[2].value + inputs[3].value; $(inputs[3]).val("Banks").change(); ret += "|" + inputs[0].value + inputs[1].value + inputs[2].value + inputs[3].value; // ............................... Assert ................................. assert.equal(ret, "JoBlowJoBlow|JeffBlyeJeffBlye|PeteBlyePeteBlye|PeteBainsPeteBains|JimBainsJimBains|JimBanksJimBanks", "Two-way tag, no template: setting tag.linkedElem(s) and tagCtx|displayElem|mainElem in onBind lead to correct 2-way binding, on inputs in wrapped content"); // ............................... Reset ................................. $("#result").empty(); // =============================== Arrange =============================== var ret, data = {current: "cur", modified: "mod"}; jsv.templates({ markup: '{^{mytag current modified/}}', tags: { mytag: { onUpdate: false, bindTo: 1, bindFrom: 0, linkedElement: "input", linkedCtxParam: "fm", template: "", update: function(val) { this.updateValue(val); // Update external data, through two-way binding } } } }).link("#result", data); mytag = jsv.view().childTags()[0]; var linkedElem = mytag.tagCtx.contents("input")[0]; ret = data.current + "-" + data.modified + ":" + linkedElem.value; // ............................... Assert ................................. assert.equal(ret, "cur-mod:cur", "Two-way tag with bindTo and bindFrom to different paths (1, 0): initial render"); // ................................ Act .................................. jsv.observable(data).setProperty({current: "cur2"}); ret = data.current + "-" + data.modified + ":" + linkedElem.value; // ............................... Assert ................................. assert.equal(ret, "cur2-mod:cur2", "Two-way tag with bindTo and bindFrom to different paths (1, 0): bindFrom binding updates from data"); // ................................ Act .................................. jsv.observable(data).setProperty({modified: "mod2"}); ret = data.current + "-" + data.modified + ":" + linkedElem.value; // ............................... Assert ................................. assert.equal(ret, "cur2-mod2:cur2", "Two-way tag with bindTo and bindFrom to different paths (1, 0): bindTo binding does not update from data"); // ................................ Act .................................. linkedElem.value = "set1"; $(linkedElem).change(); ret = data.current + "-" + data.modified + ":" + linkedElem.value; // ............................... Assert ................................. assert.equal(ret, "cur2-set1:set1", "Two-way tag with bindTo and bindFrom to different paths (1, 0): bindTo binding updates to data - bindFrom binding does not"); // ................................ Act .................................. mytag.setValue("setval", 0, 0); ret = data.current + "-" + data.modified + ":" + linkedElem.value; // ............................... Assert ................................. assert.equal(ret, "cur2-set1:setval", "Two-way tag with bindTo and bindFrom to different paths (1, 0): setValue works"); // ................................ Act .................................. mytag.setValues("setval2"); ret = data.current + "-" + data.modified + ":" + linkedElem.value; // ............................... Assert ................................. assert.equal(ret, "cur2-set1:setval2", "Two-way tag with bindTo and bindFrom to different paths (1, 0): setValues works"); // ................................ Act .................................. mytag.update("updated1"); ret = data.current + "-" + data.modified + ":" + linkedElem.value; // ............................... Assert ................................. assert.equal(ret, "cur2-updated1:setval2", "Two-way tag with bindTo and bindFrom to different paths (1, 0): update() updates bindTo target"); // ................................ Act .................................. mytag.updateValue("updated2"); ret = data.current + "-" + data.modified + ":" + linkedElem.value; // ............................... Assert ................................. assert.equal(ret, "cur2-updated2:setval2", "Two-way tag with bindTo and bindFrom to different paths (1, 0): update() updates bindTo target (variant)"); // ............................... Reset ................................. $("#result").empty(); // =============================== Arrange =============================== data = {current: "cur", modified: "mod"}; jsv.templates({ markup: '{^{mytag modified current/}}', tags: { mytag: { onUpdate: false, bindTo: 0, bindFrom: 1, linkedElement: "input", linkedCtxParam: "fm", template: "", update: function(val) { this.updateValue(val); // Update external data, through two-way binding } } } }).link("#result", data); mytag = jsv.view().childTags()[0]; var linkedElem = mytag.tagCtx.contents("input")[0]; ret = data.current + "-" + data.modified + ":" + linkedElem.value; // ............................... Assert ................................. assert.equal(ret, "cur-mod:cur", "Two-way tag with bindTo and bindFrom to different paths (0, 1): initial render"); // ................................ Act .................................. jsv.observable(data).setProperty({current: "cur2"}); ret = data.current + "-" + data.modified + ":" + linkedElem.value; // ............................... Assert ................................. assert.equal(ret, "cur2-mod:cur2", "Two-way tag with bindTo and bindFrom to different paths (0, 1): bindFrom binding updates from data"); // ................................ Act .................................. jsv.observable(data).setProperty({modified: "mod2"}); ret = data.current + "-" + data.modified + ":" + linkedElem.value; // ............................... Assert ................................. assert.equal(ret, "cur2-mod2:cur2", "Two-way tag with bindTo and bindFrom to different paths (0, 1): bindTo binding does not update from data"); // ................................ Act .................................. linkedElem.value = "set1"; $(linkedElem).change(); ret = data.current + "-" + data.modified + ":" + linkedElem.value; // ............................... Assert ................................. assert.equal(ret, "cur2-set1:set1", "Two-way tag with bindTo and bindFrom to different paths (0, 1): bindTo binding updates to data - bindFrom binding does not"); // ................................ Act .................................. mytag.setValue("setval", 0, 0); ret = data.current + "-" + data.modified + ":" + linkedElem.value; // ............................... Assert ................................. assert.equal(ret, "cur2-set1:setval", "Two-way tag with bindTo and bindFrom to different paths (0, 1): setValue works"); // ................................ Act .................................. mytag.setValues("setval2"); ret = data.current + "-" + data.modified + ":" + linkedElem.value; // ............................... Assert ................................. assert.equal(ret, "cur2-set1:setval2", "Two-way tag with bindTo and bindFrom to different paths (0, 1): setValues works"); // ................................ Act .................................. mytag.update("updated1"); ret = data.current + "-" + data.modified + ":" + linkedElem.value; // ............................... Assert ................................. assert.equal(ret, "cur2-updated1:setval2", "Two-way tag with bindTo and bindFrom to different paths (0, 1): update() updates bindTo target"); // ................................ Act .................................. mytag.updateValue("updated2"); ret = data.current + "-" + data.modified + ":" + linkedElem.value; // ............................... Assert ................................. assert.equal(ret, "cur2-updated2:setval2", "Two-way tag with bindTo and bindFrom to different paths (0, 1): update() updates bindTo target (variant)"); // ............................... Reset ................................. $("#result").empty(); // =============================== Arrange =============================== data = {current: "cur", modified: "mod"}; jsv.templates({ markup: '{^{mytag modified current convert=~cvt convertBack=~cvb/}}', tags: { mytag: { onUpdate: false, bindTo: 0, bindFrom: 1, linkedElement: "input", linkedCtxParam: "fm", template: "", update: function(val) { this.updateValue(val); // Update external data, through two-way binding } } } }).link("#result", data, { cvt: function(val) { return val; }, cvb: function(val) { return val; } }); mytag = jsv.view().childTags()[0]; var linkedElem = mytag.tagCtx.contents("input")[0]; ret = data.current + "-" + data.modified + ":" + linkedElem.value; // ............................... Assert ................................. assert.equal(ret, "cur-mod:cur", "Two-way tag plus cvt/cvtback with bindTo and bindFrom to different paths (0, 1): initial render"); // ................................ Act .................................. jsv.observable(data).setProperty({current: "cur2"}); ret = data.current + "-" + data.modified + ":" + linkedElem.value; // ............................... Assert ................................. assert.equal(ret, "cur2-mod:cur2", "Two-way tag plus cvt/cvtback with bindTo and bindFrom to different paths (0, 1): bindFrom binding updates from data"); // ................................ Act .................................. jsv.observable(data).setProperty({modified: "mod2"}); ret = data.current + "-" + data.modified + ":" + linkedElem.value; // ............................... Assert ................................. assert.equal(ret, "cur2-mod2:cur2", "Two-way tag plus cvt/cvtback with bindTo and bindFrom to different paths (0, 1): bindTo binding does not update from data"); // ................................ Act .................................. linkedElem.value = "set1"; $(linkedElem).change(); ret = data.current + "-" + data.modified + ":" + linkedElem.value; // ............................... Assert ................................. assert.equal(ret, "cur2-set1:set1", "Two-way tag plus cvt/cvtback with bindTo and bindFrom to different paths (0, 1): bindTo binding updates to data - bindFrom binding does not"); // ................................ Act .................................. mytag.setValue("setval", 0, 0); ret = data.current + "-" + data.modified + ":" + linkedElem.value; // ............................... Assert ................................. assert.equal(ret, "cur2-set1:setval", "Two-way tag plus cvt/cvtback with bindTo and bindFrom to different paths (0, 1): setValue works"); // ................................ Act .................................. mytag.setValues("setval2"); ret = data.current + "-" + data.modified + ":" + linkedElem.value; // ............................... Assert ................................. assert.equal(ret, "cur2-set1:setval2", "Two-way tag plus cvt/cvtback with bindTo and bindFrom to different paths (0, 1): setValues works"); // ................................ Act .................................. mytag.update("updated1"); ret = data.current + "-" + data.modified + ":" + linkedElem.value; // ............................... Assert ................................. assert.equal(ret, "cur2-updated1:setval2", "Two-way tag plus cvt/cvtback with bindTo and bindFrom to different paths (0, 1): update() updates bindTo target"); // ................................ Act .................................. mytag.updateValue("updated2"); ret = data.current + "-" + data.modified + ":" + linkedElem.value; // ............................... Assert ................................. assert.equal(ret, "cur2-updated2:setval2", "Two-way tag plus cvt/cvtback with bindTo and bindFrom to different paths (0, 1): update() updates bindTo target (variant)"); // ............................... Reset ................................. $("#result").empty(); // =============================== Arrange =============================== data = {current: "cur", modified: "mod"}; jsv.templates({ markup: '{^{mytag modified fm=current/}}', tags: { mytag: { onUpdate: false, bindTo: 0, bindFrom: "fm", linkedElement: "input", linkedCtxParam: "fm", template: "", update: function(val) { this.updateValue(val); // Update external data, through two-way binding } } } }).link("#result", data); mytag = jsv.view().childTags()[0]; var linkedElem = mytag.tagCtx.contents("input")[0]; ret = data.current + "-" + data.modified + ":" + linkedElem.value + "-" + $("#result").text(); // ............................... Assert ................................. assert.equal(ret, "cur-mod:-cur", "Two-way tag with bindTo and bindFrom to different paths ('fm', 0): initial render"); // ................................ Act .................................. jsv.observable(data).setProperty({current: "cur2"}); ret = data.current + "-" + data.modified + ":" + linkedElem.value + "-" + $("#result").text(); // ............................... Assert ................................. assert.equal(ret, "cur2-mod:-cur2", "Two-way tag with bindTo and bindFrom to different paths ('fm', 0): bindFrom binding updates from data"); // ................................ Act .................................. jsv.observable(data).setProperty({modified: "mod2"}); ret = data.current + "-" + data.modified + ":" + linkedElem.value + "-" + $("#result").text(); // ............................... Assert ................................. assert.equal(ret, "cur2-mod2:-cur2", "Two-way tag with bindTo and bindFrom to different paths ('fm', 0): bindTo binding does not update from data"); // ................................ Act .................................. linkedElem.value = "set1"; $(linkedElem).change(); ret = data.current + "-" + data.modified + ":" + linkedElem.value + "-" + $("#result").text(); // ............................... Assert ................................. assert.equal(ret, "cur2-set1:set1-cur2", "Two-way tag with bindTo and bindFrom to different paths ('fm', 0): bindTo binding updates to data - bindFrom binding does not"); // ................................ Act .................................. mytag.setValue("setval", 0, 0); ret = data.current + "-" + data.modified + ":" + linkedElem.value + "-" + $("#result").text(); // ............................... Assert ................................. assert.equal(ret, "cur2-set1:set1-setval", "Two-way tag with bindTo and bindFrom to different paths ('fm', 0): setValue sets the bindFrom linkedPrm but not the bindTo linkedElem"); // ................................ Act .................................. mytag.setValues("setval2"); ret = data.current + "-" + data.modified + ":" + linkedElem.value + "-" + $("#result").text(); // ............................... Assert ................................. assert.equal(ret, "cur2-set1:set1-setval2", "Two-way tag with bindTo and bindFrom to different paths ('fm', 0): setValues sets the bindFrom linkedPrm but not the bindTo linkedElem"); // ................................ Act .................................. mytag.update("updated1"); ret = data.current + "-" + data.modified + ":" + linkedElem.value + "-" + $("#result").text(); // ............................... Assert ................................. assert.equal(ret, "cur2-updated1:set1-setval2", "Two-way tag with bindTo and bindFrom to different paths ('fm', 0): update() updates bindTo target"); // ................................ Act .................................. mytag.updateValue("updated2"); ret = data.current + "-" + data.modified + ":" + linkedElem.value + "-" + $("#result").text(); // ............................... Assert ................................. assert.equal(ret, "cur2-updated2:set1-setval2", "Two-way tag with bindTo and bindFrom to different paths ('fm', 0): update() updates bindTo target (variant)"); // ............................... Reset ................................. $("#result").empty(); // =============================== Arrange =============================== data = {current: "cur", modified: "mod", two: 2, title: "Title"}; jsv.templates({ markup: '{^{mytag modified 1 two fm=current title=title/}}', tags: { mytag: { onUpdate: false, bindTo: [0, "title"], bindFrom: ["fm", 1, 2], linkedElement: ["input", ".title"], linkedCtxParam: ["fm", undefined, "arg2"], template: "{^{:~arg2}}", convert: function(from, one, two) { return [from + "F", one + "O", two + "T"]; }, setValue: function(val, ind, tagElse, ev, eventArgs) { return val + "V" + ind; }, convertBack: function(val, one) { return [ val ? val + "V" : undefined, one ? one + "O" : undefined ]; } } } }).link("#result", data); mytag = jsv.view().childTags()[0]; var linkedElem = mytag.tagCtx.contents("input")[0]; ret = data.current + "-" + data.modified + ":" + linkedElem.value + "-" + $("#result").text(); // ............................... Assert ................................. assert.equal(ret, "cur-mod:curFV0-2TV2", "Two-way tag with bindTo and bindFrom to different paths ('fm', 0) and convert/convertBack: initial render"); // ................................ Act .................................. jsv.observable(data).setProperty({current: "cur2", two: "two2"}); ret = data.current + "-" + data.modified + ":" + linkedElem.value + "-" + $("#result").text(); // ............................... Assert ................................. assert.equal(ret, "cur2-mod:cur2FV0-two2TV2", "Two-way tag with bindTo and bindFrom to different paths ('fm', 0) and convert/convertBack: bindFrom binding updates from data"); // ................................ Act .................................. jsv.observable(data).setProperty({modified: "mod2"}); ret = data.current + "-" + data.modified + ":" + linkedElem.value + "-" + $("#result").text(); // ............................... Assert ................................. assert.equal(ret, "cur2-mod2:cur2FV0-two2TV2", "Two-way tag with bindTo and bindFrom to different paths ('fm', 0) and convert/convertBack: bindTo binding does not update from data"); // ................................ Act .................................. linkedElem.value = "set1"; $(linkedElem).change(); ret = data.current + "-" + data.modified + ":" + linkedElem.value + "-" + $("#result").text(); // ............................... Assert ................................. assert.equal(ret, "cur2-set1V:set1-two2TV2", "Two-way tag with bindTo and bindFrom to different paths ('fm', 0) and convert/convertBack: bindTo binding updates to data - bindFrom binding does not"); // ................................ Act .................................. mytag.setValue("setval", 0, 0); mytag.setValue("setval2", 2, 0); ret = data.current + "-" + data.modified + ":" + linkedElem.value + "-" + $("#result").text(); // ............................... Assert ................................. assert.equal(ret, "cur2-set1V:setvalV0-setval2V2", "Two-way tag with bindTo and bindFrom to different paths ('fm', 0) and convert/convertBack: setValue sets the bindFrom linkedPrm but not the bindTo linkedElem"); // ................................ Act .................................. mytag.setValues("setval3", "setval4", "setval5"); ret = data.current + "-" + data.modified + ":" + linkedElem.value + "-" + $("#result").text(); // ............................... Assert ................................. assert.equal(ret, "cur2-set1V:setval3V0-setval2V2", "Two-way tag with bindTo and bindFrom to different paths ('fm', 0) and convert/convertBack: setValues sets the bindFrom linkedPrm but not the bindTo linkedElem"); // ................................ Act .................................. mytag.updateValue("updatedMod2", 0); mytag.updateValue("updatedTitle2", 1); ret = data.current + "-" + data.modified + "-" + data.title + ":" + linkedElem.value + "-" + $("#result").text(); // ............................... Assert ................................. assert.equal(ret, "cur2-updatedMod2V-updatedTitle2O:setval3V0-setval2V2", "Two-way tag with bindTo and bindFrom to different paths ('fm', 0) and convert/convertBack: update() updates bindTo target (variant)"); // ................................ Act .................................. mytag.updateValues("updateMod3", "updateTitle3"); ret = data.current + "-" + data.modified + "-" + data.title + ":" + linkedElem.value + "-" + $("#result").text(); // ............................... Assert ................................. assert.equal(ret, "cur2-updateMod3V-updateTitle3O:setval3V0-setval2V2", "Two-way tag with bindTo and bindFrom to different paths ('fm', 0) and convert/convertBack: update() updates bindTo target (variant)"); // ............................... Reset ................................. $("#result").empty(); // =============================== Arrange =============================== data = {current1: "cur1", modified1: "mod1", current2: "cur2", modified2: "mod2"}; jsv.templates({ markup: "
                        " + "
                        " + "{^{mytag current1 modified1}}" + "{{else current2 modified2 }}" + "{{/mytag}}", tags: { mytag: { onUpdate: false, bindTo: 1, bindFrom: 0, linkedElement: "input", linkedCtxParam: "fm", template: "
                        ", update: function(index, val) { this.updateValue(val, 0, index); }, set: function(index, val) { this.tagCtxs[index].setValues(val); }, convert: function(val) { return val + "C"; }, convertBack: function(val) { return val + "B"; } } } }).link("#result", data); mytag = jsv.view().childTags()[0]; var fromCur1 = $("#result .fromCur1")[0], toMod1 = $("#result .toMod1")[0], fromCur2 = $("#result .fromCur2")[0], toMod2 = $("#result .toMod2")[0], linkedEl1 = mytag.tagCtx.contents(".linkedEl")[0], linkedPrm1 = mytag.tagCtx.contents(".linkedPrm")[0], linkedEl2 = mytag.tagCtxs[1].contents(".linkedEl")[0], linkedPrm2 = mytag.tagCtxs[1].contents(".linkedPrm")[0], getRet = function() { return fromCur1.value + "|" + toMod1.value + "|" + fromCur2.value + "|" + toMod2.value + "|" + linkedEl1.value + "|" + linkedPrm1.value + "|" + linkedEl2.value + "|" + linkedPrm2.value; }; // ............................... Assert ................................. assert.equal(getRet(), "cur1|mod1|cur2|mod2||cur1C||cur2C", "Two-way tag with bindTo and bindFrom to different paths (1, 0): initial render"); // ................................ Act .................................. fromCur1.value = "from1"; $(fromCur1).change(); // ............................... Assert ................................. assert.equal(getRet(), "from1|mod1|cur2|mod2||from1C||cur2C", "Two-way tag with bindTo and bindFrom to different paths (1, 0): change from"); // ................................ Act .................................. toMod1.value = "to1"; $(toMod1).change(); // ............................... Assert ................................. assert.equal(getRet(), "from1|to1|cur2|mod2||from1C||cur2C", "Two-way tag with bindTo and bindFrom to different paths (1, 0): change to"); // ................................ Act .................................. fromCur2.value = "from2"; $(fromCur2).change(); // ............................... Assert ................................. assert.equal(getRet(), "from1|to1|from2|mod2||from1C||from2C", "Two-way tag with bindTo and bindFrom to different paths (1, 0): change from {{else}}"); // ................................ Act .................................. toMod2.value = "to2"; $(toMod2).change(); // ............................... Assert ................................. assert.equal(getRet(), "from1|to1|from2|to2||from1C||from2C", "Two-way tag with bindTo and bindFrom to different paths (1, 0): change to {{else}}"); // ................................ Act .................................. linkedEl1.value = "el1"; $(linkedEl1).change(); // ............................... Assert ................................. assert.equal(getRet(), "from1|el1B|from2|to2|el1|from1C||from2C", "Two-way tag with bindTo and bindFrom to different paths (1, 0): change linked"); // ................................ Act .................................. linkedPrm1.value = "prm1"; $(linkedPrm1).change(); // ............................... Assert ................................. assert.equal(getRet(), "from1|el1B|from2|to2|el1|prm1||from2C", "Two-way tag with bindTo and bindFrom to different paths (1, 0): change ctx prm"); // ................................ Act .................................. linkedEl2.value = "el2"; $(linkedEl2).change(); // ............................... Assert ................................. assert.equal(getRet(), "from1|el1B|from2|el2B|el1|prm1|el2|from2C", "Two-way tag with bindTo and bindFrom to different paths (1, 0): change linked {{else}}"); // ................................ Act .................................. linkedPrm2.value = "prm2"; $(linkedPrm2).change(); // ............................... Assert ................................. assert.equal(getRet(), "from1|el1B|from2|el2B|el1|prm1|el2|prm2", "Two-way tag with bindTo and bindFrom to different paths (1, 0): change ctx prm {{else}}"); // ................................ Act .................................. mytag.update(0, "upd1"); // ............................... Assert ................................. assert.equal(getRet(), "from1|upd1B|from2|el2B|el1|prm1|el2|prm2", "Two-way tag with bindTo and bindFrom to different paths (1, 0): updateValue"); // ................................ Act .................................. mytag.update(1, "upd2"); // ............................... Assert ................................. assert.equal(getRet(), "from1|upd1B|from2|upd2B|el1|prm1|el2|prm2", "Two-way tag with bindTo and bindFrom to different paths (1, 0): updateValue {{else}}"); // ................................ Act .................................. mytag.set(0, "set1"); // ............................... Assert ................................. assert.equal(getRet(), "from1|upd1B|from2|upd2B|el1|set1|el2|prm2", "Two-way tag with bindTo and bindFrom to different paths (1, 0): setValues"); // ................................ Act .................................. mytag.set(1, "set2"); // ............................... Assert ................................. assert.equal(getRet(), "from1|upd1B|from2|upd2B|el1|set1|el2|set2", "Two-way tag with bindTo and bindFrom to different paths (1, 0): setValues {{else}}"); // ............................... Reset ................................. $("#result").empty(); // =============================== Arrange =============================== data = { arrA:[{val:'value 1'}], objA2:{0: {val:'value 2'}}, objA3:{x: {val:'value 3'}}, objA4:{x: {val:'value 4'}}, arrB:[{val:'value 1'}], objB2:{0: {val:'value 2'}}, objB3:{x: {val:'value 3'}}, objB4:{x: {val:'value 4'}}, arrC:[{val:'value 1'}], objC2:{0: {val:'value 2'}}, objC3:{x: {val:'value 3'}}, objC4:{x: {val:'value 4'}}, arrD:[{val:'value 1'}], objD2:{0: {val:'value 2'}}, objD3:{x: {val:'value 3'}}, objD4:{x: {val:'value 4'}} }; jsv.templates({ markup: "{^{mytag arrA[0].val objA2['0'].val objA3['x'].val objA4.x.val p1=arrB[0].val p2=objB2['0'].val p3=objB3['x'].val p4=objB4.x.val}}" + "{{else arrC[0].val objC2['0'].val objC3['x'].val objC4.x.val p1=arrD[0].val p2=objD2['0'].val p3=objD3['x'].val p4=objD4.x.val}}" + "{{/mytag}}", tags: { mytag: { onUpdate: false, bindTo: [0 ,1, 2, 3, 'p1', 'p2', 'p3', 'p4'], template: "" } } }).link("#result", data); mytag = jsv.view().childTags()[0]; // ................................ Act .................................. mytag.updateValue("new1").updateValue("new2", 1).updateValue("new3", 2).updateValue("new4", 3) .updateValue("new5", 4).updateValue("new6", 5).updateValue("new7", 6).updateValue("new8", 7) .updateValue("new9", 0, 1).updateValue("new10", 1, 1).updateValue("new11", 2, 1).updateValue("new12", 3, 1) .updateValue("new13", 4, 1).updateValue("new14", 5, 1).updateValue("new15", 6, 1).updateValue("new16", 7, 1); // ............................... Assert ................................. assert.equal(data.arrA[0].val + data.objA2['0'].val + data.objA3.x.val + data.objA4.x.val + data.arrB[0].val + data.objB2['0'].val + data.objB3.x.val + data.objB4.x.val + data.arrC[0].val + data.objC2['0'].val + data.objC3.x.val + data.objC4.x.val + data.arrD[0].val + data.objD2['0'].val + data.objD3.x.val + data.objD4.x.val, "new1new2new3new4new5new6new7new8new9new10new11new12new13new14new15new16", "updateValue() binds to paths with [...].val accessors"); // ................................ Act .................................. mytag.updateValues("more1", "more2", "more3", "more4", "more5", "more6", "more7", "more8"); // ............................... Assert ................................. assert.equal(data.arrA[0].val + data.objA2['0'].val + data.objA3.x.val + data.objA4.x.val + data.arrB[0].val + data.objB2['0'].val + data.objB3.x.val + data.objB4.x.val + data.arrC[0].val + data.objC2['0'].val + data.objC3.x.val + data.objC4.x.val + data.arrD[0].val + data.objD2['0'].val + data.objD3.x.val + data.objD4.x.val, "more1more2more3more4more5more6more7more8new9new10new11new12new13new14new15new16", "updateValues() binds to paths with [...].val accessors"); // ............................... Reset ................................. $("#result").empty(); // =============================== Arrange =============================== var store = { arrA: {val:'value 1'}, objA2: {val:'value 2'}, objA3: {val:'value 3'}, objA4: {val:'value 4'}, arrB: {val:'value 1'}, objB2: {val:'value 2'}, objB3: {val:'value 3'}, objB4: {val:'value 4'}, arrC: {val:'value 1'}, objC2: {val:'value 2'}, objC3: {val:'value 3'}, objC4: {val:'value 4'}, arrD: {val:'value 1'}, objD2: {val:'value 2'}, objD3: {val:'value 3'}, objD4: {val:'value 4'} }; data = { arrA: function() { return store.arrA }, objA2: function() { return store.objA2 }, objA3: function() { return store.objA3 }, objA4: function() { return store.objA4 }, arrB: function() { return store.arrB }, objB2: function() { return store.objB2 }, objB3: function() { return store.objB3 }, objB4: function() { return store.objB4 }, arrC: function() { return store.arrC }, objC2: function() { return store.objC2 }, objC3: function() { return store.objC3 }, objC4: function() { return store.objC4 }, arrD: function() { return store.arrD }, objD2: function() { return store.objD2 }, objD3: function() { return store.objD3 }, objD4: function() { return store.objD4 }, }; jsv.templates({ markup: "{^{mytag arrA().val objA2().val objA3().val objA4().val p1=arrB().val p2=objB2().val p3=objB3().val p4=objB4().val}}" + "{{else arrC().val objC2().val objC3().val objC4().val p1=arrD().val p2=objD2().val p3=objD3().val p4=objD4().val}}" + "{{/mytag}}", tags: { mytag: { onUpdate: false, bindTo: [0 ,1, 2, 3, 'p1', 'p2', 'p3', 'p4'], template: "" } } }).link("#result", data); mytag = jsv.view().childTags()[0]; // ................................ Act .................................. mytag.updateValue("new1").updateValue("new2", 1).updateValue("new3", 2).updateValue("new4", 3) .updateValue("new5", 4).updateValue("new6", 5).updateValue("new7", 6).updateValue("new8", 7) .updateValue("new9", 0, 1).updateValue("new10", 1, 1).updateValue("new11", 2, 1).updateValue("new12", 3, 1) .updateValue("new13", 4, 1).updateValue("new14", 5, 1).updateValue("new15", 6, 1).updateValue("new16", 7, 1); // ............................... Assert ................................. assert.equal(store.arrA.val + store.objA2.val + store.objA3.val + store.objA4.val + store.arrB.val + store.objB2.val + store.objB3.val + store.objB4.val + store.arrC.val + store.objC2.val + store.objC3.val + store.objC4.val + store.arrD.val + store.objD2.val + store.objD3.val + store.objD4.val, "new1new2new3new4new5new6new7new8new9new10new11new12new13new14new15new16", "updateValue() binds to paths with computed().val paths"); // ................................ Act .................................. mytag.updateValues("more1", "more2", "more3", "more4", "more5", "more6", "more7", "more8"); // ............................... Assert ................................. assert.equal(store.arrA.val + store.objA2.val + store.objA3.val + store.objA4.val + store.arrB.val + store.objB2.val + store.objB3.val + store.objB4.val + store.arrC.val + store.objC2.val + store.objC3.val + store.objC4.val + store.arrD.val + store.objD2.val + store.objD3.val + store.objD4.val, "more1more2more3more4more5more6more7more8new9new10new11new12new13new14new15new16", "updateValues() binds to paths computed().val paths"); // =============================== Arrange =============================== jsv.templates({ markup: "{^{mytag arg prop=prop/}}", tags: { mytag: { bindTo: [0, 'prop'], template: '', init: function() { this.onPropChange = this.onPropChange.bind(this); jsv.observe(this.tagCtx.props,'prop',this.onPropChange); }, onPropChange: function() { this.updateValue("newArg", 0); } } } }).link("#result", {arg: "theArg", prop: "theProp"}); mytag = jsv.view().childTags()[0]; mytag.updateValue("newProp", 1); // ............................... Assert ................................. assert.equal(mytag.tagCtx.props.prop + "|" + mytag.tagCtx.args[0], "newProp|newArg", "Changing prop triggers changing arg - works as expected"); // =============================== Arrange =============================== jsv.templates({ markup: "{^{mytag prop=prop prop2=prop2 /}}", tags: { mytag: { bindTo: ['prop', 'prop2'], template: '', init: function() { this.onPropChange = this.onPropChange.bind(this); jsv.observe(this.tagCtx.props, 'prop2', this.onPropChange); }, onPropChange: function() { this.updateValue("newProp", 0); } } } }).link("#result", {prop2: "theProp2", prop: "theProp"}); mytag = jsv.view().childTags()[0]; mytag.updateValue("newProp2", 1); // ............................... Assert ................................. assert.equal(mytag.tagCtx.props.prop + "|" + mytag.tagCtx.props.prop2, "newProp|newProp2", "Changing prop triggers changing other prop - works as expected"); // ............................... Reset ................................. $("#result").empty(); jsv.views.settings.trigger(true); }); QUnit.test('Custom Tag Controls - two-way binding, with array-valued properties', function(assert) { // =============================== Arrange =============================== var model = {title: "t", val: "v", mode: "m", arr: ["a", "b", "c"], items: ["a", "b", "c", "d"]}; var cvt, cvtBk; res = ""; cvt = function(arr, val, title, mode) { var ret = [arr, val, title, mode]; return ret; } cvtBk = function(arr, val, title) { var ret = [arr, val, title]; return ret; } jsv.templates({ markup: '
                        ' + '{^{checkboxgroup arr convert="same" convertBack="same" }}{^{for items}}
                        {{/for}}{{/checkboxgroup}}
                        ' + '{^{for items}}
                        {{/for}}
                        ' + '
                        ' + '{^{mytag arr val title=title mode=mode/}}', tags: { mytag: { onUpdate: false, bindTo: [0, 1, "title"], bindFrom: [0, 1, "title", "mode"], linkedElement: [".multi", undefined, ".title"], linkedCtxParam: [undefined, "sel", undefined, "mode"], template: "" + "{^{:~mode}}", convert: "from", convertBack: "to" } }, converters: { from: function(arr, val, title, mode) { return cvt(arr, val, title, mode); }, to: function(arr, val, title) { return cvtBk(arr, val, title); }, same: function(val) { return val; } } }).link("#result", model); function getSelection() { res += $("#result select option:selected").text() + "-" + $("#result input:checked").parent().text() + ":" + model.arr + ":" + model.val + ":" + model.title + ":" + model.mode + "|"; } getSelection(); $("#result select").eq(0).val(["a"]).change(); getSelection(); $("#result select").eq(0).val(["a", "b"]).change(); getSelection(); $("#result select").eq(0).val(["a", "b", "c"]).change(); getSelection(); $("#result select").eq(0).val(["a", "b", "c", "d"]).change(); getSelection(); $("#result select").eq(0).val([]).change(); getSelection(); $("#result input:checkbox").eq(0).prop("checked", true).change(); // Check first checkbox button of {{checkboxgroup}} getSelection(); $("#result input:checkbox").eq(6).prop("checked", true).change(); // Check third checkbox button of direct data-linked group getSelection(); $("#result select").eq(1).val(["a"]).change(); getSelection(); $("#result select").eq(1).val(["a", "b"]).change(); getSelection(); $("#result select").eq(1).val(["a", "b", "c"]).change(); getSelection(); $("#result select").eq(1).val(["a", "b", "c", "d"]).change(); getSelection(); jsv.observable(model.arr).refresh(["d", "b"]); getSelection(); jsv.observable(model).setProperty("arr", ["c", "a", "b"]); getSelection(); $("#result input").eq(0).val("newVal").trigger("input"); getSelection(); $("#result input").eq(1).val("newTitle").trigger("input"); getSelection(); $("#result input").eq(2).val("newMode").trigger("input"); getSelection(); $("#result .sel").val("newVal2").trigger("input"); getSelection(); $("#result .title").val("newTitle2").trigger("input"); getSelection(); // ............................... Assert ................................. assert.equal(res, "abcabc- a b c a b c:a,b,c:v:t:m|aa- a a:a:v:t:m|abab- a b a b:a,b:v:t:m|abcabc- a b c a b c:a,b,c:v:t:m|" + "abcdabcd- a b c d a b c d:a,b,c,d:v:t:m|-::v:t:m|aa- a a:a:v:t:m|acac- a c a c:a,c:v:t:m|aa- a a:a:v:t:m|abab- a b a b:a,b:v:t:m|" + "abcabc- a b c a b c:a,b,c:v:t:m|abcdabcd- a b c d a b c d:a,b,c,d:v:t:m|bdbd- b d b d:d,b:v:t:m|abcabc- a b c a b c:c,a,b:v:t:m|" + "abcabc- a b c a b c:c,a,b:newVal:t:m|abcabc- a b c a b c:c,a,b:newVal:newTitle:m|abcabc- a b c a b c:c,a,b:newVal:newTitle:newMode|" + "abcabc- a b c a b c:c,a,b:newVal2:newTitle:newMode|abcabc- a b c a b c:c,a,b:newVal2:newTitle2:newMode|", "Return aray of params"); // ............................... Reset ................................. model.title = "t"; model.val = "v"; model.mode = "m"; model.arr = ["a", "b", "c"]; model.items = ["a", "b", "c", "d"]; res = ""; // =============================== Arrange =============================== cvt = function(arr, val, title, mode) { var ret = [arr, val, title, mode, 1]; ret.arg0 = false; return ret; } cvtBk = function(arr, val, title) { var ret = [arr, val, title, 2]; ret.arg0 = false; return ret; } getSelection(); $("#result select").eq(0).val(["a"]).change(); getSelection(); $("#result select").eq(0).val(["a", "b"]).change(); getSelection(); $("#result select").eq(0).val(["a", "b", "c"]).change(); getSelection(); $("#result select").eq(0).val(["a", "b", "c", "d"]).change(); getSelection(); $("#result select").eq(0).val([]).change(); getSelection(); $("#result input:checkbox").eq(0).prop("checked", true).change(); // Check first checkbox button of {{checkboxgroup}} getSelection(); $("#result input:checkbox").eq(6).prop("checked", true).change(); // Check third checkbox button of direct data-linked group getSelection(); $("#result select").eq(1).val(["a"]).change(); getSelection(); $("#result select").eq(1).val(["a", "b"]).change(); getSelection(); $("#result select").eq(1).val(["a", "b", "c"]).change(); getSelection(); $("#result select").eq(1).val(["a", "b", "c", "d"]).change(); getSelection(); jsv.observable(model.arr).refresh(["d", "b"]); getSelection(); jsv.observable(model).setProperty("arr", ["c", "a", "b"]); getSelection(); $("#result input").eq(0).val("newVal").trigger("input"); getSelection(); $("#result input").eq(1).val("newTitle").trigger("input"); getSelection(); $("#result input").eq(2).val("newMode").trigger("input"); getSelection(); $("#result .sel").val("newVal2").trigger("input"); getSelection(); $("#result .title").val("newTitle2").trigger("input"); getSelection(); // ............................... Assert ................................. assert.equal(res, "abcabc- a b c a b c:a,b,c:v:t:m|aa- a a:a:v:t:m|abab- a b a b:a,b:v:t:m|abcabc- a b c a b c:a,b,c:v:t:m|" + "abcdabcd- a b c d a b c d:a,b,c,d:v:t:m|-::v:t:m|aa- a a:a:v:t:m|acac- a c a c:a,c:v:t:m|aa- a a:a:v:t:m|abab- a b a b:a,b:v:t:m|" + "abcabc- a b c a b c:a,b,c:v:t:m|abcdabcd- a b c d a b c d:a,b,c,d:v:t:m|bdbd- b d b d:d,b:v:t:m|abcabc- a b c a b c:c,a,b:v:t:m|" + "abcabc- a b c a b c:c,a,b:newVal:t:m|abcabc- a b c a b c:c,a,b:newVal:newTitle:m|abcabc- a b c a b c:c,a,b:newVal:newTitle:newMode|" + "abcabc- a b c a b c:c,a,b:newVal2:newTitle:newMode|abcabc- a b c a b c:c,a,b:newVal2:newTitle2:newMode|", "Return array of wrong size, with arg0 flag false"); // ............................... Reset ................................. model.title = "t"; model.val = "v"; model.mode = "m"; model.arr = ["a", "b", "c"]; model.items = ["a", "b", "c", "d"]; res = ""; // =============================== Arrange =============================== cvt = function(arr, val, title, mode) { arr.arg0 = true; return arr; } cvtBk = function(arr, val, title) { arr.arg0 = true; return arr; } getSelection(); $("#result select").eq(0).val(["a"]).change(); getSelection(); $("#result select").eq(0).val(["a", "b"]).change(); getSelection(); $("#result select").eq(0).val(["a", "b", "c"]).change(); getSelection(); $("#result select").eq(0).val(["a", "b", "c", "d"]).change(); getSelection(); $("#result select").eq(0).val([]).change(); getSelection(); $("#result input:checkbox").eq(0).prop("checked", true).change(); // Check first checkbox button of {{checkboxgroup}} getSelection(); $("#result input:checkbox").eq(6).prop("checked", true).change(); // Check third checkbox button of direct data-linked group getSelection(); $("#result select").eq(1).val(["a"]).change(); getSelection(); $("#result select").eq(1).val(["a", "b"]).change(); getSelection(); $("#result select").eq(1).val(["a", "b", "c"]).change(); getSelection(); $("#result select").eq(1).val(["a", "b", "c", "d"]).change(); getSelection(); jsv.observable(model.arr).refresh(["d", "b"]); getSelection(); jsv.observable(model).setProperty("arr", ["c", "a", "b"]); getSelection(); // ............................... Assert ................................. assert.equal(res, "abcabc- a b c a b c:a,b,c:v:t:m|aa- a a:a:v:t:m|abab- a b a b:a,b:v:t:m|abcabc- a b c a b c:a,b,c:v:t:m|" + "abcdabcd- a b c d a b c d:a,b,c,d:v:t:m|-::v:t:m|aa- a a:a:v:t:m|acac- a c a c:a,c:v:t:m|aa- a a:a:v:t:m|abab- a b a b:a,b:v:t:m|" + "abcabc- a b c a b c:a,b,c:v:t:m|abcdabcd- a b c d a b c d:a,b,c,d:v:t:m|bdbd- b d b d:d,b:v:t:m|abcabc- a b c a b c:c,a,b:v:t:m|", "Return just first param, an array, with arg0 flag true"); // ............................... Reset ................................. model.title = "t"; model.val = "v"; model.mode = "m"; model.arr = ["a", "b", "c"]; model.items = ["a", "b", "c", "d"]; res = ""; // =============================== Arrange =============================== cvt = function(arr) { return arr; } cvtBk = function(arr) { return arr; } jsv.templates({ markup: '{^{checkboxgroup arr convert="same" convertBack="same" }}{^{for items}}
                        {{/for}}{{/checkboxgroup}}
                        ' + '{^{for items}}
                        {{/for}}
                        ' + '
                        ' + '{^{mytag arr convert="from" convertBack="to"/}}', tags: { mytag: { onUpdate: false, linkedElement: [".multi"], template: "", } }, converters: { from: function(arr, val, title, mode) { return cvt(arr, val, title, mode); }, to: function(arr, val, title) { return cvtBk(arr, val, title); }, same: function(val) { return val; } } }).link("#result", model); getSelection(); $("#result select").eq(0).val(["a"]).change(); getSelection(); $("#result select").eq(0).val(["a", "b"]).change(); getSelection(); $("#result select").eq(0).val(["a", "b", "c"]).change(); getSelection(); $("#result select").eq(0).val(["a", "b", "c", "d"]).change(); getSelection(); $("#result select").eq(0).val([]).change(); getSelection(); $("#result input:checkbox").eq(0).prop("checked", true).change(); // Check first checkbox button of {{checkboxgroup}} getSelection(); $("#result input:checkbox").eq(6).prop("checked", true).change(); // Check third checkbox button of direct data-linked group getSelection(); $("#result select").eq(1).val(["a"]).change(); getSelection(); $("#result select").eq(1).val(["a", "b"]).change(); getSelection(); $("#result select").eq(1).val(["a", "b", "c"]).change(); getSelection(); $("#result select").eq(1).val(["a", "b", "c", "d"]).change(); getSelection(); jsv.observable(model.arr).refresh(["d", "b"]); getSelection(); jsv.observable(model).setProperty("arr", ["c", "a", "b"]); getSelection(); // ............................... Assert ................................. assert.equal(res, "abcabc- a b c a b c:a,b,c:v:t:m|aa- a a:a:v:t:m|abab- a b a b:a,b:v:t:m|abcabc- a b c a b c:a,b,c:v:t:m|" + "abcdabcd- a b c d a b c d:a,b,c,d:v:t:m|-::v:t:m|aa- a a:a:v:t:m|acac- a c a c:a,c:v:t:m|aa- a a:a:v:t:m|abab- a b a b:a,b:v:t:m|" + "abcabc- a b c a b c:a,b,c:v:t:m|abcdabcd- a b c d a b c d:a,b,c,d:v:t:m|bdbd- b d b d:d,b:v:t:m|abcabc- a b c a b c:c,a,b:v:t:m|", "Only one array param"); // ............................... Reset ................................. model.title = "t"; model.val = "v"; model.mode = "m"; model.arr = ["a", "b", "c"]; model.items = ["a", "b", "c", "d"]; res = ""; // =============================== Arrange =============================== cvt = function(arr) { return arr; } cvtBk = function(arr) { return arr; } jsv.templates({ markup: '{^{checkboxgroup arr convert="same" convertBack="same" }}{^{for items}}
                        {{/for}}{{/checkboxgroup}}
                        ' + '{^{for items}}
                        {{/for}}
                        ' + '
                        ' + '{^{mytag arr convert="from" convertBack="to"/}}', tags: { mytag: { mainElement: ".multi", onUpdate: false, onBind: function(tagCtx) { var tag = this; tagCtx.mainElem.on("change", function(ev) { // debugger; // var selected = Array.apply(null, ev.target.selectedOptions).map(function(el) { // return el.value; // }); var selected = Array.apply(null, ev.target.options).filter(function(el) { return el.selected; }) .map(function(el) { return el.value; }); tag.updateValues(selected); ev.preventDefault(); }); }, setValue: function(value) { this.tagCtx.mainElem.val(value); }, template: "", } }, converters: { from: function(arr, val, title, mode) { return cvt(arr, val, title, mode); }, to: function(arr, val, title) { return cvtBk(arr, val, title); }, same: function(val) { return val; } } }).link("#result", model); getSelection(); $("#result select").eq(0).val(["a"]).change(); getSelection(); $("#result select").eq(0).val(["a", "b"]).change(); getSelection(); $("#result select").eq(0).val(["a", "b", "c"]).change(); getSelection(); $("#result select").eq(0).val(["a", "b", "c", "d"]).change(); getSelection(); $("#result select").eq(0).val([]).change(); getSelection(); $("#result input:checkbox").eq(0).prop("checked", true).change(); // Check first checkbox button of {{checkboxgroup}} getSelection(); $("#result input:checkbox").eq(6).prop("checked", true).change(); // Check third checkbox button of direct data-linked group getSelection(); $("#result select").eq(1).val(["a"]).change(); getSelection(); $("#result select").eq(1).val(["a", "b"]).change(); // Gives "ab-::v:t:m|", should give abab- a b a b:a,b:v:t:m" getSelection(); $("#result select").eq(1).val(["a", "b", "c"]).change(); getSelection(); $("#result select").eq(1).val(["a", "b", "c", "d"]).change(); getSelection(); jsv.observable(model.arr).refresh(["d", "b"]); getSelection(); jsv.observable(model).setProperty("arr", ["c", "a", "b"]); getSelection(); // ............................... Assert ................................. assert.equal(res, "abcabc- a b c a b c:a,b,c:v:t:m|aa- a a:a:v:t:m|abab- a b a b:a,b:v:t:m|abcabc- a b c a b c:a,b,c:v:t:m|" + "abcdabcd- a b c d a b c d:a,b,c,d:v:t:m|-::v:t:m|aa- a a:a:v:t:m|acac- a c a c:a,c:v:t:m|aa- a a:a:v:t:m|abab- a b a b:a,b:v:t:m|" + "abcabc- a b c a b c:a,b,c:v:t:m|abcdabcd- a b c d a b c d:a,b,c,d:v:t:m|bdbd- b d b d:d,b:v:t:m|abcabc- a b c a b c:c,a,b:v:t:m|", "Programmatic, with only one array param"); // ............................... Reset ................................. $("#result").empty(); }); QUnit.test("Tag control events", function(assert) { // =============================== Arrange =============================== var eventData = ""; model.things = [{thing: "box"}]; // reset Prop // ................................ Act .................................. jsv.templates({ markup: '
                        {^{myWidget person1.lastName things/}}
                        ', tags: { myWidget: { init: function(tagCtx, linkCtx, ctx) { eventData += "init "; }, render: function(name, things) { eventData += "render "; return "" + name + " " + things.length + " " + this.getType() + ""; }, onBeforeUpdateVal: function(ev, eventArgs) { eventData += "onBeforeUpdateVal "; }, onUpdate: function(ev, eventArgs, newTagCtxs) { eventData += "update "; }, onAfterLink: function(tagCtx, linkCtx, ctx, ev, eventArgs) { eventData += "after "; }, onBind: function(tagCtx, linkCtx, ctx, ev, eventArgs) { eventData += "onBind "; }, onUnbind: function(tagCtx, linkCtx, ctx, ev, eventArgs) { eventData += "onUnbind "; }, onBeforeChange: function(ev, eventArgs) { eventData += "onBeforeChange "; }, setValue: function(val, index, tagElse, ev, eventArgs) { eventData += "setValue (arg index: " + index + "-" + (this.tagCtx.args[index]===val) + ") "; }, onAfterChange: function(ev, eventArgs) { eventData += "onAfterChange "; }, onDispose: function() { eventData += "dispose "; }, getType: function() { eventData += "getType "; return this.type; }, type: "special" } } }).link("#result", model); // ............................... Assert ................................. assert.equal($("#result").text() + "|" + eventData, "One 1 special|init render getType onBind after setValue (arg index: 1-true) setValue (arg index: 0-true) ", '{^{myWidget/}} - Events fire in order during rendering'); eventData = ""; // ................................ Act .................................. jsv.observable(person1).setProperty("lastName", "Two"); // ............................... Assert ................................. assert.equal($("#result").text() + "|" + eventData, "Two 1 special|onBeforeChange update onUnbind render getType onBind after setValue (arg index: 0-true) onAfterChange ", '{^{myWidget/}} - Events fire in order during update (setProperty)'); eventData = ""; // ................................ Act .................................. jsv.observable(model.things).insert(0, {thing: "tree"}); // ............................... Assert ................................. assert.equal($("#result").text() + "|" + eventData, "Two 2 special|onBeforeChange update onUnbind render getType onBind after onAfterChange ", '{^{myWidget/}} - Events fire in order during update (insert)'); eventData = ""; // ................................ Act .................................. jsv.observable(model.things).refresh([{thing: "bush"}, {thing: "flower"}]); // ............................... Assert ................................. assert.equal($("#result").text() + "|" + eventData, "Two 2 special|onBeforeChange update onUnbind render getType onBind after onAfterChange ", '{^{myWidget/}} - Events fire in order during update (refresh)'); eventData = ""; // ................................ Act .................................. $("#result").empty(); // ............................... Assert ................................. assert.equal($("#result").text() + "|" + eventData, "|onUnbind dispose ", '{^{myWidget/}} - onDispose fires when container element is emptied or removed'); // ................................ Reset ................................ person1.lastName = "One"; model.things = []; eventData = ""; // =============================== Arrange =============================== eventData = ""; model.things = [{thing: "box"}]; // reset Prop // ................................ Act .................................. jsv.templates({ markup: '
                        {^{myCustomArrayChangeWidget person1.lastName things/}}
                        ', tags: { myCustomArrayChangeWidget: { init: function(tagCtx, linkCtx, ctx) { eventData += "init "; }, render: function(name, things) { eventData += "render "; return "" + name + " " + things.length + " " + this.getType() + ""; }, onBeforeUpdateVal: function(ev, eventArgs) { eventData += "onBeforeUpdateVal "; }, onUpdate: function(ev, eventArgs, newTagCtxs) { eventData += "update "; }, onArrayChange: function(ev, eventArgs) { eventData += "onArrayChange" + (eventArgs.refresh ? "(refresh) " : " "); }, onAfterLink: function(tagCtx, linkCtx, ctx, ev, eventArgs) { var tag = this, data = tagCtx.args[1]; if (tag._boundArray && data !== tag._boundArray) { jsv.unobserve(tag._boundArray, tag._arCh); // Different array, so remove handler from previous array tag._boundArray = undefined; } if (!tag._boundArray && $.isArray(data)) { jsv.observe(tag._boundArray = data, tag._arCh = function(ev, eventArgs) { // Store array data as tag._boundArray, and arrayChangeHandler as tag._arCh tag.onArrayChange(ev, eventArgs); }); } eventData += "after "; }, onBind: function(tagCtx, linkCtx, ctx, ev, eventArgs) { eventData += "onBind "; }, onUnbind: function(tagCtx, linkCtx, ctx, ev, eventArgs) { eventData += "onUnbind "; }, onBeforeChange: function(ev, eventArgs) { eventData += "onBeforeChange "; }, setValue: function(val, index, tagElse, ev, eventArgs) { eventData += "setValue (arg index: " + index + "-" + (this.tagCtx.args[index]===val) + ") "; }, onAfterChange: function(ev, eventArgs) { eventData += "onAfterChange "; }, onDispose: function() { var tag = this; if (tag._boundArray) { jsv.unobserve(tag._boundArray, tag._arCh); // Remove arrayChange handler from bound array } eventData += "dispose "; }, getType: function() { eventData += "getType "; return this.type; }, type: "special" } } }).link("#result", model); // ............................... Assert ................................. assert.equal($("#result").text() + "|" + eventData, "One 1 special|init render getType onBind after setValue (arg index: 1-true) setValue (arg index: 0-true) ", '{^{myCustomArrayChangeWidget/}} - Events fire in order during rendering'); eventData = ""; // ................................ Act .................................. jsv.observable(person1).setProperty("lastName", "Two"); // ............................... Assert ................................. assert.equal($("#result").text() + "|" + eventData, "Two 1 special|onBeforeChange update onUnbind render getType onBind after setValue (arg index: 0-true) onAfterChange ", '{^{myCustomArrayChangeWidget/}} - Events fire in order during update (setProperty)'); eventData = ""; // ................................ Act .................................. jsv.observable(model.things).insert(0, {thing: "tree"}); // ............................... Assert ................................. assert.equal($("#result").text() + "|" + eventData, "Two 1 special|onArrayChange ", '{^{myCustomArrayChangeWidget/}} - No update during insert - instead, custom onArrayChange handler called'); eventData = ""; // ................................ Act .................................. jsv.observable(model.things).refresh([{thing: "bush"}, {thing: "flower"}]); // ............................... Assert ................................. assert.equal($("#result").text() + "|" + eventData, "Two 1 special|onArrayChange(refresh) onArrayChange(refresh) onArrayChange ", '{^{myCustomArrayChangeWidget/}} - No update during refresh - instead, custom onArrayChange handler called for each event'); eventData = ""; // ................................ Act .................................. $("#result").empty(); // ............................... Assert ................................. assert.equal($("#result").text() + "|" + eventData, "|onUnbind dispose ", '{^{myCustomArrayChangeWidget/}} - onDispose fires when container element is emptied or removed'); // ................................ Reset ................................ person1.lastName = "One"; model.things = []; eventData = ""; // =============================== Arrange =============================== var i=0, vm = { a: "A", p1: "P1", p2: "P2", elsea: "elseA", elsep1: "elseP1", elsep2: "elseP2", other: "O" }; // ................................ Act .................................. jsv.views.templates({ markup: "{^{mytag prop1=p1 prop2=p2 a ^other=other}}{{else prop1=elsep1 prop2=elsep2 elsea}}{{/mytag}}", tags: { mytag: { bindTo:["prop1", "prop2", 0], linkedElement: ["input", ".two", ".zero"], template: " ", setValue: function(value, index, elseBlock, ev, eventArgs) { this.contents("span").text(i); return value + "(index:" + index + " else:" + elseBlock + " iteration:" + i++ + (ev ? " propChange: " + eventArgs.value : "") + ") "; }, onUpdate: false } } }).link("#result", vm); var inputs = $("#result input"), spans = $("#result span"); // ............................... Assert ................................. assert.equal("myTag: " + spans[0].innerText + " inputs: " + inputs[0].value + inputs[1].value + inputs[2].value + "myTagElse: " + spans[1].innerText + " inputs: " + inputs[3].value + inputs[4].value + inputs[5].value, "myTag: 5 inputs: P1(index:0 else:0 iteration:5) P2(index:1 else:0 iteration:4) A(index:2 else:0 iteration:3) " + "myTagElse: 5 inputs: elseP1(index:0 else:1 iteration:2) elseP2(index:1 else:1 iteration:1) elseA(index:2 else:1 iteration:0) ", 'SetValue used correctly during rendering'); // ................................ Act .................................. jsv.observable(vm).setProperty("a", "AX") .setProperty("p1", "p1X") .setProperty("p2", "p2X") .setProperty("elsea", "elseAX") .setProperty("elsep1", "elseP1X") .setProperty("elsep2", "elseP2X") .setProperty("other", "OX"); // ............................... Assert ................................. assert.equal("myTag: " + spans[0].innerText + " inputs: " + inputs[0].value + inputs[1].value + inputs[2].value + "myTagElse: " + spans[1].innerText + " inputs: " + inputs[3].value + inputs[4].value + inputs[5].value, "myTag: 11 inputs: p1X(index:0 else:0 iteration:7 propChange: p1X) p2X(index:1 else:0 iteration:8 propChange: p2X) AX(index:2 else:0 iteration:6 propChange: AX) " + "myTagElse: 11 inputs: elseP1X(index:0 else:1 iteration:10 propChange: elseP1X) elseP2X(index:1 else:1 iteration:11 propChange: elseP2X) elseAX(index:2 else:1 iteration:9 propChange: elseAX) ", 'SetValue used correctly during updating'); // ................................ Act .................................. $("#result").empty(); // =============================== Arrange =============================== var i=0, vm = { a: undefined, p1: undefined, p2: undefined, elsea: undefined, elsep1: undefined, elsep2: undefined, other: undefined }; // ................................ Act .................................. jsv.views.templates({ markup: "{^{mytag prop1=p1 prop2=p2 a ^other=other}}{{else prop1=elsep1 prop2=elsep2 elsea}}{{/mytag}}", tags: { mytag: { bindTo:["prop1", "prop2", 0], linkedElement: ["input", ".two", ".zero"], template: " ", setValue: function(value, index, elseBlock, ev, eventArgs) { this.contents("span").text(i); return value + "(index:" + index + " else:" + elseBlock + " iteration:" + i++ + (ev ? " propChange: " + eventArgs.value : "") + ") "; }, onUpdate: false } } }).link("#result", vm); var inputs = $("#result input"), spans = $("#result span"); // ............................... Assert ................................. assert.equal("myTag: " + spans[0].innerText + " inputs: " + inputs[0].value + inputs[1].value + inputs[2].value + "myTagElse: " + spans[1].innerText + " inputs: " + inputs[3].value + inputs[4].value + inputs[5].value, "myTag: 5 inputs: undefined(index:0 else:0 iteration:5) undefined(index:1 else:0 iteration:4) undefined(index:2 else:0 iteration:3) " + "myTagElse: 5 inputs: undefined(index:0 else:1 iteration:2) undefined(index:1 else:1 iteration:1) undefined(index:2 else:1 iteration:0) ", 'With undefined initial values for bound args/props SetValue is used correctly during rendering'); // ................................ Act .................................. jsv.observable(vm).setProperty("a", "AX") .setProperty("p1", "p1X") .setProperty("p2", "p2X") .setProperty("elsea", "elseAX") .setProperty("elsep1", "elseP1X") .setProperty("elsep2", "elseP2X") .setProperty("other", "OX"); // ............................... Assert ................................. assert.equal("myTag: " + spans[0].innerText + " inputs: " + inputs[0].value + inputs[1].value + inputs[2].value + "myTagElse: " + spans[1].innerText + " inputs: " + inputs[3].value + inputs[4].value + inputs[5].value, "myTag: 11 inputs: p1X(index:0 else:0 iteration:7 propChange: p1X) p2X(index:1 else:0 iteration:8 propChange: p2X) AX(index:2 else:0 iteration:6 propChange: AX) " + "myTagElse: 11 inputs: elseP1X(index:0 else:1 iteration:10 propChange: elseP1X) elseP2X(index:1 else:1 iteration:11 propChange: elseP2X) elseAX(index:2 else:1 iteration:9 propChange: elseAX) ", 'With undefined initial values for bound args/props SetValue is used correctly during updating'); // ................................ Act .................................. $("#result").empty(); // =============================== Arrange =============================== // ................................ Act .................................. jsv.templates({ markup: '
                        {^{myNoRenderWidget/}}
                        ', tags: { myNoRenderWidget: { init: function(tagCtx, linkCtx, ctx) { eventData += "init "; }, onAfterLink: function() { eventData += "after "; }, onBind: function(tagCtx, linkCtx, ctx, ev, eventArgs) { eventData += "onBind "; }, onUnbind: function(tagCtx, linkCtx, ctx, ev, eventArgs) { eventData += "onUnbind "; }, onBeforeChange: function(ev, eventArgs) { eventData += "onBeforeChange "; }, setValue: function(val, index, tagElse, ev, eventArgs) { eventData += "setValue (arg index: " + index + "-" + (this.tagCtx.args[index]===val) + ") "; }, onAfterChange: function(ev, eventArgs) { eventData += "onAfterChange "; } } } }).link("#result", person1); // ............................... Assert ................................. assert.equal($("#result").text() + "|" + eventData, "|init onBind after setValue (arg index: 0-true) ", "Event sequence for a non-rendering custom tag control is correct"); // ................................ Reset ................................ person1.lastName = "One"; model.things = []; $("#result").empty(); // =============================== Arrange =============================== // ................................ Act .................................. eventData = "Init| "; jsv.templates({ markup: '
                        {^{mytag lastName/}}
                        ', tags: { mytag: { setValue: function(val, index, tagElse, ev, eventArgs) { eventData += "setValue(arg:" + val + " index: " + index + "-" + (this.tagCtx.args[index]===val) + ") "; }, test1: function(ev, eventArgs) { this.updateValue("One"); }, test2: function(ev, eventArgs) { this.updateValue() } } } }).link("#result", person1); var mytag = jsv.view().childTags("mytag")[0]; eventData += "\nUpdate to One| "; mytag.updateValue("One"); // Same value so no-op jsv.observable(person1).setProperty("lastName", "One"); // Same value so no-op eventData += "\nUpdate to Two| "; mytag.updateValue("Two"); // Change value jsv.observable(person1).setProperty("lastName", "One"); // Change back to value previously passed in setValue() call eventData += "\nUpdate to Two and setValue to Three, twice| "; mytag.updateValue("Two"); mytag.setValue("Three"); jsv.observable(person1).setProperty("lastName", "Three"); // ............................... Assert ................................. assert.equal(eventData, "Init| setValue(arg:One index: 0-true) \nUpdate to One| \nUpdate to Two| setValue(arg:One index: 0-true) \nUpdate to Two and setValue to Three, twice| setValue(arg:Three index: 0-false) setValue(arg:Three index: 0-true) ", "SetValue correctly called twice in a row for the same value - in response to bindFrom data value being returned to previous value, following call to tag.updateValue()"); // ................................ Act .................................. $("#result").empty(); }); QUnit.test("tag onArrayChange options", function(assert) { // =============================== Arrange =============================== var data = { items: ["aa", "AA"], items2: ["bb", "BB"] }; // ................................ Act .................................. var template = "First: {{:#data[0]}} Last: {{:#data[length-1]}} First2: {{:~tagCtx.args[1][0]}} Last2: {{:~tagCtx.args[1][~tagCtx.args[1].length-1]}}"; jsv.templates({ markup: '1 {^{mytag items items2/}} ' + '2 {^{arrChFn items items2/}} ' + '2b {^{arrChFalse items items2/}} ' + '3 ' + '4 ' + '4b ' + '5 {^{> items[0]+\'-\'+items2[0]}} ' + '6 ' + '7 ' + '8 ' + '9 {^{if items[0] ==="aa"}}not a{{/if}} ' + '10 {^{for items2[0]==="bb" && items}} {{:}}{{/for}} ' + '1 {^{mytag items items2 onArrayChange=false/}} ' + '2 {^{arrChFn items items2 onArrayChange=true/}} ' + '2b {^{arrChFalse items items2 onArrayChange=true/}} ' + '3 ' + '4 ' + '4b ' + '5 {^{> items[0]+\'-\'+items2[0] onArrayChange=false}} ' + '6 ' + '7 ' + '8 ' + '9 {^{if items[0] ==="aa" onArrayChange=false}}not a{{/if}} ' + '10 {^{for items2[0]==="bb" && items onArrayChange=true}} {{:}}{{/for}}', tags: { mytag: template, arrChFn: { template: template, onArrayChange: function() {} }, arrChFalse: { template: template, onArrayChange: function() {} } } }).link("#result", data); // ............................... Assert ................................. assert.equal($("#result").text(), "1 First: aa Last: AA First2: bb Last2: BB 2 First: aa Last: AA First2: bb Last2: BB 2b First: aa Last: AA First2: bb Last2: BB 3 First: aa Last: AA First2: bb Last2: BB 4 First: aa Last: AA First2: bb Last2: BB 4b First: aa Last: AA First2: bb Last2: BB 5 aa-bb 6 aa-bb 7 aa-bb 8 aa-bb 9 not a 10 aa AA 1 First: aa Last: AA First2: bb Last2: BB 2 First: aa Last: AA First2: bb Last2: BB 2b First: aa Last: AA First2: bb Last2: BB 3 First: aa Last: AA First2: bb Last2: BB 4 First: aa Last: AA First2: bb Last2: BB 4b First: aa Last: AA First2: bb Last2: BB 5 aa-bb 6 aa-bb 7 aa-bb 8 aa-bb 9 not a 10 aa AA", 'Initial rendering'); // ................................ Act .................................. jsv.observable(data.items).move(0, data.items.length-1); jsv.observable(data.items2).move(0, data.items2.length-1); // ............................... Assert ................................. assert.equal($("#result").text(), "1 First: AA Last: aa First2: BB Last2: bb 2 First: aa Last: AA First2: bb Last2: BB 2b First: aa Last: AA First2: bb Last2: BB 3 First: AA Last: aa First2: BB Last2: bb 4 First: aa Last: AA First2: bb Last2: BB 4b First: aa Last: AA First2: bb Last2: BB 5 AA-BB 6 AA-BB 7 AA-BB 8 AA-BB 9 10 AA aa 1 First: aa Last: AA First2: bb Last2: BB 2 First: AA Last: aa First2: BB Last2: bb 2b First: AA Last: aa First2: BB Last2: bb 3 First: aa Last: AA First2: bb Last2: BB 4 First: AA Last: aa First2: BB Last2: bb 4b First: AA Last: aa First2: BB Last2: bb 5 aa-bb 6 aa-bb 7 aa-bb 8 aa-bb 9 not a 10 false", 'After array change "move"'); // ................................ Act .................................. jsv.observable(data).setProperty({items: ["xx", "XX"], items2: ["yy", "YY"]}); // ............................... Assert ................................. assert.equal($("#result").text(), "1 First: xx Last: XX First2: yy Last2: YY 2 First: xx Last: XX First2: yy Last2: YY 2b First: xx Last: XX First2: yy Last2: YY 3 First: xx Last: XX First2: yy Last2: YY 4 First: xx Last: XX First2: yy Last2: YY 4b First: xx Last: XX First2: yy Last2: YY 5 xx-yy 6 xx-yy 7 xx-yy 8 xx-yy 9 10 false 1 First: xx Last: XX First2: yy Last2: YY 2 First: xx Last: XX First2: yy Last2: YY 2b First: xx Last: XX First2: yy Last2: YY 3 First: xx Last: XX First2: yy Last2: YY 4 First: xx Last: XX First2: yy Last2: YY 4b First: xx Last: XX First2: yy Last2: YY 5 xx-yy 6 xx-yy 7 xx-yy 8 xx-yy 9 10 false", 'After property change "setProperty"'); // ................................ Act .................................. jsv.observable(data.items).refresh(["aa", "AA"]); // ............................... Assert ................................. assert.equal($("#result").text(), "1 First: aa Last: AA First2: yy Last2: YY 2 First: xx Last: XX First2: yy Last2: YY 2b First: xx Last: XX First2: yy Last2: YY 3 First: aa Last: AA First2: yy Last2: YY 4 First: xx Last: XX First2: yy Last2: YY 4b First: xx Last: XX First2: yy Last2: YY 5 aa-yy 6 aa-yy 7 aa-yy 8 aa-yy 9 not a 10 false 1 First: xx Last: XX First2: yy Last2: YY 2 First: aa Last: AA First2: yy Last2: YY 2b First: aa Last: AA First2: yy Last2: YY 3 First: xx Last: XX First2: yy Last2: YY 4 First: aa Last: AA First2: yy Last2: YY 4b First: aa Last: AA First2: yy Last2: YY 5 xx-yy 6 xx-yy 7 xx-yy 8 xx-yy 9 10 false", 'After array change "refresh"'); // ................................ Reset ................................ $("#result").empty(); }); })(this.jsviews || this.jQuery, this.jsviews && this.jsviews.$ || this.jQuery);