Skip to content

Commit 7c03a91

Browse files
committed
fixes #321: Binding sometimes doesn't work - added tests
1 parent 3ab7938 commit 7c03a91

4 files changed

Lines changed: 115 additions & 24 deletions

File tree

src/createNodes.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,59 @@ export default function createNodes(xpath, baseElement, foreElement) {
184184

185185
if (!steps.length) return null;
186186

187+
/**
188+
* If the first step of this path already exists as a real child of baseElement, reuse it
189+
* instead of creating a duplicate sibling. Without this, a multi-step ref whose last step is
190+
* missing (e.g. `cac:InvoicePeriod/cbc:DescriptionCode` when `cac:InvoicePeriod` already holds
191+
* real data) would create a brand new, empty `cac:InvoicePeriod` alongside the existing one,
192+
* orphaning the real data behind a duplicate element with the same name.
193+
*/
194+
const firstStep = steps[0];
195+
const isSimpleElementStep =
196+
firstStep.nameTest &&
197+
firstStep.nameTest !== '.' &&
198+
!firstStep.nameTest.trim().startsWith('@') &&
199+
firstStep.predicates.length === 0;
200+
201+
if (isSimpleElementStep && baseElement.children) {
202+
const parsedFirst = parseName(firstStep.nameTest);
203+
if (!parsedFirst.isValue) {
204+
const wantedNs = parsedFirst.namespaceURI || null;
205+
const existingChild = Array.from(baseElement.children).find(
206+
child =>
207+
child.localName === parsedFirst.localName && (child.namespaceURI || null) === wantedNs,
208+
);
209+
210+
if (existingChild) {
211+
const remainingSteps = steps.slice(1);
212+
if (!remainingSteps.length) {
213+
// The requested step already exists in full - nothing to create.
214+
return null;
215+
}
216+
217+
const remainingXPath = remainingSteps
218+
.map(step => `${step.nameTest}${step.predicates.map(p => `[${p}]`).join('')}`)
219+
.join('/');
220+
221+
const tailResult = createNodes(remainingXPath, existingChild, foreElement);
222+
if (tailResult) {
223+
const alreadyAttached =
224+
tailResult.nodeType === Node.ATTRIBUTE_NODE
225+
? !!tailResult.ownerElement
226+
: !!tailResult.parentNode;
227+
if (!alreadyAttached) {
228+
if (tailResult.nodeType === Node.ATTRIBUTE_NODE) {
229+
existingChild.setAttributeNode(tailResult);
230+
} else {
231+
existingChild.appendChild(tailResult);
232+
}
233+
}
234+
}
235+
return tailResult;
236+
}
237+
}
238+
}
239+
187240
/**
188241
* Process a single step
189242
*

src/fx-fore.js

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2079,14 +2079,16 @@ export class FxFore extends HTMLElement {
20792079
if (!newNode || !parentNodeset) {
20802080
continue;
20812081
}
2082-
if (newNode.nodeType === Node.ATTRIBUTE_NODE) {
2083-
parentNodeset.setAttributeNode(newNode);
2084-
} else {
2085-
const referenceNode = this._findReferenceNodeForNewElement(newNode, parentNodeset, null);
2086-
if (referenceNode) {
2087-
referenceNode.after(newNode);
2082+
if (!this._isNodeAlreadyAttached(newNode)) {
2083+
if (newNode.nodeType === Node.ATTRIBUTE_NODE) {
2084+
parentNodeset.setAttributeNode(newNode);
20882085
} else {
2089-
parentNodeset.prepend(newNode);
2086+
const referenceNode = this._findReferenceNodeForNewElement(newNode, parentNodeset, null);
2087+
if (referenceNode) {
2088+
referenceNode.after(newNode);
2089+
} else {
2090+
parentNodeset.prepend(newNode);
2091+
}
20902092
}
20912093
}
20922094
bound.evalInContext();
@@ -2130,23 +2132,25 @@ export class FxFore extends HTMLElement {
21302132
continue;
21312133
}
21322134

2133-
if (newNode.nodeType === Node.ATTRIBUTE_NODE) {
2134-
parentNodeset.setAttributeNode(newNode);
2135-
} else {
2136-
let referenceNode = this._findReferenceNodeForNewElement(
2137-
newNode,
2138-
parentNodeset,
2139-
siblingControl,
2140-
);
2135+
if (!this._isNodeAlreadyAttached(newNode)) {
2136+
if (newNode.nodeType === Node.ATTRIBUTE_NODE) {
2137+
parentNodeset.setAttributeNode(newNode);
2138+
} else {
2139+
let referenceNode = this._findReferenceNodeForNewElement(
2140+
newNode,
2141+
parentNodeset,
2142+
siblingControl,
2143+
);
21412144

2142-
if (referenceNode) {
2143-
if (referenceNode.nodeType === Node.DOCUMENT_NODE) {
2144-
referenceNode.firstElementChild.append(newNode);
2145+
if (referenceNode) {
2146+
if (referenceNode.nodeType === Node.DOCUMENT_NODE) {
2147+
referenceNode.firstElementChild.append(newNode);
2148+
} else {
2149+
referenceNode.after(newNode);
2150+
}
21452151
} else {
2146-
referenceNode.after(newNode);
2152+
parentNodeset.prepend(newNode);
21472153
}
2148-
} else {
2149-
parentNodeset.prepend(newNode);
21502154
}
21512155
}
21522156

@@ -2159,6 +2163,17 @@ export class FxFore extends HTMLElement {
21592163
}
21602164
}
21612165
}
2166+
/**
2167+
* `createNodes()` sometimes splices a created node directly into an existing real parent
2168+
* (when it reused an already-existing intermediate step) instead of returning a detached
2169+
* node for the caller to position. Callers use this to skip their own insertion logic in
2170+
* that case.
2171+
* @param {Node} node
2172+
*/
2173+
_isNodeAlreadyAttached(node) {
2174+
return node.nodeType === Node.ATTRIBUTE_NODE ? !!node.ownerElement : !!node.parentNode;
2175+
}
2176+
21622177
/**
21632178
* Create Nodes from an XPath
21642179
* @param {string} ref

test/createNodes.test.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,29 @@ describe('createNodes', () => {
103103
expect(result).to.equal(null, 'The result should be null');
104104
});
105105

106+
it('reuses an existing intermediate element instead of creating a duplicate sibling (#321)', () => {
107+
baseElement = new window.DOMParser().parseFromString(
108+
'<xml><cac:InvoicePeriod xmlns:cac="urn:cac"><cbc:StartDate xmlns:cbc="urn:cbc">2019-02-01</cbc:StartDate><cbc:EndDate xmlns:cbc="urn:cbc">2019-05-07</cbc:EndDate></cac:InvoicePeriod></xml>',
109+
'application/xml',
110+
).documentElement;
111+
foreElement.setAttribute('xmlns:cac', 'urn:cac');
112+
foreElement.setAttribute('xmlns:cbc', 'urn:cbc');
113+
114+
const result = createNodes('cac:InvoicePeriod/cbc:DescriptionCode', baseElement, foreElement);
115+
116+
// The new DescriptionCode leaf is returned...
117+
expect(result).to.not.equal(null, 'The result should not be null');
118+
expect(result.localName).to.equal('DescriptionCode');
119+
// ...already spliced into the *existing* InvoicePeriod, not a fresh duplicate.
120+
expect(result.parentNode).to.equal(baseElement.querySelector('InvoicePeriod'));
121+
expect(baseElement.querySelectorAll('InvoicePeriod')).to.have.lengthOf(
122+
1,
123+
'no duplicate InvoicePeriod should have been created',
124+
);
125+
expect(baseElement.querySelector('StartDate').textContent).to.equal('2019-02-01');
126+
expect(baseElement.querySelector('EndDate').textContent).to.equal('2019-05-07');
127+
});
128+
106129
describe('recursive processing', () => {
107130
it('can make a path with new expressions in the predicate', () => {
108131
const xpath = 'a[b/c]';

test/multistep.test.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ describe('multi-step refs', () => {
6464

6565
expect(el.getModel().getModelItem('$default/AllowanceCharge[1]')).to.exist;
6666
expect(el.getModel().getModelItem('$default/AllowanceCharge[1]/TaxCategory[1]/ID[1]')).to.exist;
67-
expect(el.getModel().getModelItem('$default/AllowanceCharge[1]/TaxCategory[2]/Percent[1]')).to
67+
expect(el.getModel().getModelItem('$default/AllowanceCharge[1]/TaxCategory[1]/Percent[1]')).to
6868
.exist;
6969
});
7070

@@ -130,7 +130,7 @@ describe('multi-step refs', () => {
130130
expect(el.getModel().getModelItem('$default/AllowanceCharge[2]_1')).to.exist;
131131
expect(el.getModel().getModelItem('$default/AllowanceCharge[2]_1/TaxCategory[1]/ID[1]')).to
132132
.exist;
133-
expect(el.getModel().getModelItem('$default/AllowanceCharge[2]_1/TaxCategory[2]/Percent[1]')).to
133+
expect(el.getModel().getModelItem('$default/AllowanceCharge[2]_1/TaxCategory[1]/Percent[1]')).to
134134
.exist;
135135
});
136136

@@ -279,6 +279,6 @@ describe('multi-step refs', () => {
279279
control = repeatitems[1].querySelector('#BT-96');
280280
expect(control).to.exist;
281281
expect(mi6.observers.has(control)).to.be.true;
282-
expect(mi6.path).to.equal('$default/AllowanceCharge[2]_1/TaxCategory[2]/Percent[1]');
282+
expect(mi6.path).to.equal('$default/AllowanceCharge[2]_1/TaxCategory[1]/Percent[1]');
283283
});
284284
});

0 commit comments

Comments
 (0)