Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/samples/thirdpartycpts/chart.hsp
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ var ChartCpt=klass({
},
$refresh:function() {
if (!this.chart) {
var canvas=this.$getElement(0);
var canvas=this.$getElement("canvas");
this.chart=new Chart(canvas.getContext("2d"));
}
if (this.type==="bar") {
Expand Down
2 changes: 1 addition & 1 deletion docs/samples/thirdpartycpts/description.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ This example uses [Chart.js] that allows several types of charts to be drawn on
Wrapping a 3rd party widget into a component involves at least 2 methods:

- **$refresh()** which is an optional method that the component controller may implement. This method is automatically called when one of the component attribute changes - right after the component's template has been refreshed. As a consequence it is the right place to tell the 3rd party library used by the component to update its DOM.
- **$getElement(index)** that is automatically added to component controllers that implement *$refresh()*and that allows to retrieve any root ELEMENT_NODEs generated by the component's template. In other words, this method allows to directly access the DOM generated by the component (as you can guess the index argument refers to the element's index - but note that non-element nodes are ignored and cannot be retrieved). As Hashspace may manipulate the DOM, this method should be used with care, preferably on Element nodes that have few interactions with Hashspace (such as the *canvas* element of this example)
- **$getElement(idxOrSelector)** that is automatically added to component controllers and that allows to retrieve any ELEMENT_NODEs generated by the component's template. Its argument can be either an index (*number*) or a CSS selector (*string*). When the index is used, this method will return the nth root element generated by the component's template. As such child elements cannot be retrieved with the index argument - CSS selectors have to be used instead. As Hashspace may manipulate the DOM, this method should be used with care, preferably on Element nodes that have few interactions with Hashspace (such as the *canvas* element of this example)

As a reminder there are 2 other methods that are likely to be used on component's controllers when wrapping 3rd party libraries:

Expand Down
6 changes: 3 additions & 3 deletions hsp/rt/cptwrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -352,16 +352,16 @@ var CptWrapper = klass({
* in the DOM generated by its template
* Note: this method only returns element nodes - i.e. node of type 1 (ELEMENT_NODE)
* As a consequence $getElement(0) will return the first element, even if a text node is inserted before
* @param {Integer} index the position of the element (e.g. 0 for the first element)
* @param {String|Integer} idxOrSelector either CSS selector or index of the position of the root element (e.g. 0 for the first element)
* @retrun {DOMElementNode}
*/
$getElement:function(index) {
$getElement:function(idxOrSelector) {
var nd=this.nodeInstance;
if (!nd) {
nd=this.root;
}
if (nd) {
return nd.getElementNode(index);
return nd.getElementNode(idxOrSelector);
}
return null;
},
Expand Down
108 changes: 84 additions & 24 deletions hsp/rt/tnode.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,57 @@
var hsp = require("../rt"),
klass = require("../klass"),
log = require("./log"),
ExpHandler = require("./exphandler");
ExpHandler = require("./exphandler"),
doc = require("./document");

var ELEMENT_NODE=1; // type of a DOM Element node

/**
* Recursively clone an element for a querySelector query
* Only node elements and selector-compatible attributes are cloned
* @param {DOMElement} elt the element to clone
* @param {DOMEleemnt} parent the parent element to which the cloned element will be appended
* @param {Array} nodeCol an array of all the node created (each node will register in the array
* and store its index through the data-idx attribute)
*/
function cloneEltForSelector(elt,parent,nodeCol) {
if (elt.nodeType!==ELEMENT_NODE) {
return;
}
var tn=elt.tagName, nd, id, idx=nodeCol.length, cls=elt.className;
if (tn==="INPUT") {
// special creation mode as IE doesn't support dynamic type and name change
var div = doc.createElement('div'), nodeType=elt.type, nodeName=elt.name, id=elt.id;
div.innerHTML = '<' + tn
+ (id? ' id="' + id + '"' : '')
+ (nodeType?' type="' + nodeType + '"' : '')
+ (nodeName?' name="' + nodeName + '"' : '')
+ ' >';

nd = div.childNodes[0];
} else {
// node is not an input
nd = doc.createElement(tn);
id=elt.id;
if (id) {
nd.id=elt.id;
}
}
if (cls) {
nd.className=cls;
}
// register in the node collection
nd.dataIdx=idx;
nodeCol[idx]=elt;
var cn=elt.childNodes;
if (cn) {
// recursively clone child nodes
for (var i=0;cn.length>i;i++) {
cloneEltForSelector(cn[i],nd,nodeCol);
}
}
parent.appendChild(nd);
}

/**
* Template node - base class of all nodes
Expand Down Expand Up @@ -331,34 +381,44 @@ var TNode = klass({

/**
* Helper function to get the nth DOM child node of type ELEMENT_NODE
* @param {Integer} index the position of the element (e.g. 0 for the first element)
* @param {String|Integer} idxOrSelector either CSS selector or index of the position of the root element (e.g. 0 for the first element)
* @retrun {DOMElementNode}
*/
getElementNode:function(index) {
if (this.node) {
var cn=this.node.childNodes, nd, idx=-1;
var n1=this.node1, n2=this.node2; // for TNode using comments to delimit their content
if (!n2) {
n2=null;
}
var process=(n1)? false : true;
for (var i=0;cn.length>i;i++) {
nd=cn[i];
if (process) {
if (nd===n2) {
break;
}
if (nd.nodeType===1) {
// 1 = ELEMENT_NODE
idx++;
if (idx===index) {
return nd;
getElementNode:function(idxOrSelector) {
if (!this.node) {
return null;
}
var isSelector=typeof(idxOrSelector)==="string";
var cn=this.node.childNodes, nd, idx=-1;
var n1=this.node1, n2=this.node2; // for TNode using comments to delimit their content
if (!n2) {
n2=null;
}
var process=(n1)? false : true;
for (var i=0;cn.length>i;i++) {
nd=cn[i];
if (process) {
if (nd===n2) {
break;
}
if (nd.nodeType===ELEMENT_NODE) {
idx++;

if (isSelector) {
// clone element to peform a querySelector query
var df=doc.createDocumentFragment(), ncol=[];
cloneEltForSelector(nd,df,ncol);
var n=df.querySelector(idxOrSelector);
if (n) {
return ncol[n.dataIdx];
}
} else if (idx===idxOrSelector) {
// argument is an integer and we look for the nth element
return nd;
}
} else if (nd===n1) {
process=true;
}

} else if (nd===n1) {
process=true;
}
}
return null;
Expand Down
98 changes: 80 additions & 18 deletions test/rt/cptintegration.spec.hsp
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,6 @@ var msg='', refreshCount=0;
var TestCtrl1=klass({
attributes:{
"value":{type:"string",binding:"2-way"}
},
$init:function() {

},
onValueChange:function(newValue,oldValue) {
var div=this.$getElement(1);
Expand Down Expand Up @@ -61,47 +58,114 @@ var TestCtrl1=klass({
{/if}
{/template}

var QSelectorCtrl=klass({
attributes:{
"qselector":{type:"string"}
},
$refresh:function() {
var elt=this.$getElement(this.qselector);
if (elt) {
elt.title="$refresh";
}
}
});

{template qsel using c:QSelectorCtrl}
<div class="divA">
<div class="divA1 bar" title="A1">A1</div>
<div class="divA2">
<div class="divA21" id="blah">A21</div>
</div>
</div>
<div class="divB">
<div class="divB1">B1</div>
<div class="divB2">
<input id="foo" type="text"/>
</div>
</div>
{/template}

{template test4(selector)}
<div>
<#qsel qselector="{selector}"/>
</div>
{/template}

describe("External component integration", function () {
beforeEach(function() {
var h;

beforeEach(function () {
h=ht.newTestContext();
msg='';
refreshCount=0;
});

afterEach(function () {
h.$dispose();
});


it("validates $getElement() method through direct template call", function() {
var h=ht.newTestContext(), d={value:"hello"};
var d={value:"hello"};

test1(d).render(h.container);
expect(msg).to.equal("hello");
expect(refreshCount).to.equal(1);

h.$dispose();
});

it("validates $getElement() method through component call", function() {
var h=ht.newTestContext();

test2().render(h.container);
expect(msg).to.equal("hello");
expect(refreshCount).to.equal(1);
});

h.$dispose();
it("validates $getElement() with selector 1", function() {
test4(".divA > .divA1").render(h.container);
expect(h(".divA > .divA1").attribute("title")).to.equal("$refresh");
});

it("validates $getElement() with selector 2", function() {
test4(".divB1").render(h.container);
expect(h(".divB1").attribute("title")).to.equal("$refresh");
});

it("validates $getElement() with selector 3", function() {
test4("#foo").render(h.container);
expect(h("#foo").attribute("title")).to.equal("$refresh");
});

it("validates $getElement() with selector 4", function() {
test4(".divB").render(h.container);
expect(h(".divB").attribute("title")).to.equal("$refresh");
});

it("validates $getElement() with selector 5", function() {
test4("input[type=text]").render(h.container);
expect(h("input[type=text]").attribute("title")).to.equal("$refresh");
});

it("validates $getElement() with selector 6", function() {
test4(".bar").render(h.container);
expect(h(".bar").attribute("title")).to.equal("$refresh");
});

it("validates $getElement() with selector 7", function() {
test4("#blah").render(h.container);
expect(h("#blah").attribute("title")).to.equal("$refresh");
});

it("validates $refresh() call when attribute changes", function() {
var h=ht.newTestContext(), dm={ok:true,input:"foo"};
var dm={ok:true,input:"foo"};

test3(dm).render(h.container);

h.$set(dm,"input","bar");
expect(refreshCount).to.equal(2);
expect(h('.bar').attribute('title')).to.equal("bar");

h.$dispose();
});

it("validates $refresh() call in {if} statements", function() {
var h=ht.newTestContext(), dm={ok:false,input:"foo"};
var dm={ok:false,input:"foo"};

test3(dm).render(h.container);

Expand All @@ -114,9 +178,7 @@ describe("External component integration", function () {
expect(refreshCount).to.equal(1);

h.$set(dm,"ok",true);
expect(refreshCount).to.equal(2);

h.$dispose();
expect(refreshCount).to.equal(2);
});

});