diff --git a/src/js/nodeset.js b/src/js/nodeset.js index 40a9b4452..c4d8beb7e 100644 --- a/src/js/nodeset.js +++ b/src/js/nodeset.js @@ -246,6 +246,7 @@ Nodeset.prototype.remove = function () { nodes: null, repeatPath, repeatIndex, + removed: true, // Introduced to handle relevance on model nodes with no form controls (calculates) }) ); diff --git a/src/js/relevant.js b/src/js/relevant.js index d05f99e10..067b2d1b2 100644 --- a/src/js/relevant.js +++ b/src/js/relevant.js @@ -203,8 +203,16 @@ export default { */ let context = p.path; if ( - getChild(node, `.or-repeat-info[data-name="${p.path}"]`) && - !getChild(node, `.or-repeat[name="${p.path}"]`) + (getChild(node, `.or-repeat-info[data-name="${p.path}"]`) && + !getChild(node, `.or-repeat[name="${p.path}"]`)) || + // Special cases below for model nodes with no visible form control: if repeat instance removed or if + // no instances at all (e.g. during load with `jr:count="0"`) + (insideRepeat && + repeatParent == null && + (options.removed || + this.form.view.html.querySelector( + `.or-repeat[name="${CSS.escape(repeatPath)}"]` + ) == null)) ) { context = null; } diff --git a/test/forms/repeat-count-relevant-on-calculate.xml b/test/forms/repeat-count-relevant-on-calculate.xml new file mode 100644 index 000000000..b5b94ba69 --- /dev/null +++ b/test/forms/repeat-count-relevant-on-calculate.xml @@ -0,0 +1,37 @@ + + + + Repeat count 0 with relevant on calculate + + + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/forms/repeat-relevant-on-calculate.xml b/test/forms/repeat-relevant-on-calculate.xml new file mode 100644 index 000000000..2c4a8e830 --- /dev/null +++ b/test/forms/repeat-relevant-on-calculate.xml @@ -0,0 +1,32 @@ + + + + Relevant on calculate in repeat + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/spec/repeat.spec.js b/test/spec/repeat.spec.js index d2eff5d3f..8a50d5d29 100644 --- a/test/spec/repeat.spec.js +++ b/test/spec/repeat.spec.js @@ -132,6 +132,43 @@ describe('repeat functionality', () => { }); }); + describe('instance removal', () => { + it('removes an instance that contains a field without a form control', async () => { + const form = loadForm('repeat-relevant-on-calculate.xml'); + const errors = form.init(); + + expect( + form.view.html.querySelectorAll( + '.or-repeat[name="/data/repeat"]' + ).length + ).to.equal(1); + + const q1 = form.view.html.querySelector( + 'input[name="/data/repeat/q1"]' + ); + q1.value = 2; + q1.dispatchEvent(event.Change()); + + expect(form.model.xml.querySelector('q1_x2').textContent).to.equal( + '4' + ); + + form.view.html + .querySelector('.or-repeat[name="/data/repeat"]') + .querySelector('button.remove') + .click(); + + await timers.runAllAsync(); + + expect( + form.view.html.querySelectorAll( + '.or-repeat[name="/data/repeat"]' + ).length + ).to.equal(0); + expect(errors.length).to.equal(0); + }); + }); + describe('fixes unique ids in cloned repeats', () => { // Avoiding problems in the autocomplete widget, https://github.com/enketo/enketo-core/issues/521 it('ensures uniqueness of datalist ids, so cascading selects inside repeats work', () => { @@ -479,6 +516,28 @@ describe('repeat functionality', () => { ).to.equal('2'); }); + it('and works with relevance on a field without a form control when repeat count is 0', () => { + const form = loadForm('repeat-count-relevant-on-calculate.xml'); + const errors = form.init(); + expect(errors.length).to.equal(0); + + const count = form.view.html.querySelector( + 'input[name="/data/count' + ); + count.value = 1; + count.dispatchEvent(event.Change()); + + const q1 = form.view.html.querySelector( + 'input[name="/data/repeat/q1"]' + ); + q1.value = 2; + q1.dispatchEvent(event.Change()); + + expect(form.model.xml.querySelector('q1_x2').textContent).to.equal( + '4' + ); + }); + it('and correctly deals with nested repeats that have a repeat count', () => { const form = loadForm('repeat-count-nested-2.xml'); const a = '.or-repeat[name="/data/repeat_A"]';