diff --git a/src/htmx.js b/src/htmx.js index 41ae9b382..3dd93e6af 100644 --- a/src/htmx.js +++ b/src/htmx.js @@ -313,6 +313,7 @@ var htmx = (function() { getAttributeValue, getClosestAttributeValue, getClosestMatch, + getElementValue, getExpressionVars, getHeaders, getInputValues, @@ -693,9 +694,10 @@ var htmx = (function() { * @property {ListenerInfo[]} [listenerInfos] * @property {boolean} [cancelled] * @property {boolean} [triggeredOnce] - * @property {number} [delayed] + * @property {WeakMap} [delayed] * @property {number|null} [throttle] - * @property {string} [lastValue] + * @property {WeakMap>} [lastValue] + * @property {WeakMap>} [pendingValue] * @property {boolean} [loaded] * @property {string} [path] * @property {string} [verb] @@ -2389,6 +2391,17 @@ var htmx = (function() { return false } + /* + * @param {Element} elt + */ + function getElementValue(elt) { + if (elt.tagName.toLowerCase() == "input" && elt.getAttribute("type") == "checkbox") { + return elt.checked ? elt.value : '' + } + // @ts-ignore value will be undefined for non-input elements, which is fine + return elt.value + } + /** * @param {Node} elt * @param {TriggerHandler} handler @@ -2405,12 +2418,24 @@ var htmx = (function() { } else { eltsToListenOn = [elt] } + if (triggerSpec.delay > 0) { + if (!('delayed' in elementData)) { + elementData.delayed = new WeakMap() + } + if (!('pendingValue' in elementData)) { + elementData.pendingValue = new WeakMap() + } + } // store the initial values of the elements, so we can tell if they change if (triggerSpec.changed) { + if (!('lastValue' in elementData)) { + elementData.lastValue = new WeakMap() + } eltsToListenOn.forEach(function(eltToListenOn) { - const eltToListenOnData = getInternalData(eltToListenOn) - // @ts-ignore value will be undefined for non-input elements, which is fine - eltToListenOnData.lastValue = eltToListenOn.value + if (!elementData.lastValue.has(triggerSpec)) { + elementData.lastValue.set(triggerSpec, new WeakMap()) + } + elementData.lastValue.get(triggerSpec).set(eltToListenOn, getElementValue(eltToListenOn)) }) } forEach(eltsToListenOn, function(eltToListenOn) { @@ -2452,16 +2477,25 @@ var htmx = (function() { } } if (triggerSpec.changed) { - const eltToListenOnData = getInternalData(eltToListenOn) - // @ts-ignore value will be undefined for non-input elements, which is fine - const value = eltToListenOn.value - if (eltToListenOnData.lastValue === value) { - return + const node = event.target + const value = getElementValue(node) + const lastValue = elementData.lastValue.get(triggerSpec) + if (triggerSpec.delay > 0) { + if (!elementData.pendingValue.has(triggerSpec)) { + elementData.pendingValue.set(triggerSpec, new Map()) + } + const pendingValue = elementData.pendingValue.get(triggerSpec) + pendingValue.set(node, value) + } else { + if (lastValue.has(node) && lastValue.get(node) === value) { + return + } + lastValue.set(node, value) } - eltToListenOnData.lastValue = value } - if (elementData.delayed) { - clearTimeout(elementData.delayed) + if (elementData.delayed && elementData.delayed.has(triggerSpec)) { + clearTimeout(elementData.delayed.get(triggerSpec)) + elementData.delayed.delete(triggerSpec) } if (elementData.throttle) { return @@ -2476,10 +2510,27 @@ var htmx = (function() { }, triggerSpec.throttle) } } else if (triggerSpec.delay > 0) { - elementData.delayed = getWindow().setTimeout(function() { + elementData.delayed.set(triggerSpec, getWindow().setTimeout(function() { + elementData.delayed.delete(triggerSpec) + if (triggerSpec.changed) { + const lastValue = elementData.lastValue.get(triggerSpec) || new WeakMap() + const pendingValue = elementData.pendingValue.get(triggerSpec) || new Map() + elementData.pendingValue.set(triggerSpec, new Map()) + + let changed = false + pendingValue.forEach((new_value, new_node) => { + if (lastValue.get(new_node) !== new_value) { + changed = true + lastValue.set(new_node, new_value) + } + }) + if (!changed) { + return + } + } triggerEvent(elt, 'htmx:trigger') handler(elt, evt) - }, triggerSpec.delay) + }, triggerSpec.delay)) } else { triggerEvent(elt, 'htmx:trigger') handler(elt, evt) @@ -2836,12 +2887,6 @@ var htmx = (function() { triggerEvent(elt, 'htmx:beforeProcessNode') - // @ts-ignore value will be undefined for non-input elements, which is fine - if (elt.value) { - // @ts-ignore - nodeData.lastValue = elt.value - } - const triggerSpecs = getTriggerSpecs(elt) const hasExplicitHttpAction = processVerbs(elt, nodeData, triggerSpecs) diff --git a/test/attributes/hx-trigger.js b/test/attributes/hx-trigger.js index a83a29e16..ee9524b8f 100644 --- a/test/attributes/hx-trigger.js +++ b/test/attributes/hx-trigger.js @@ -40,6 +40,22 @@ describe('hx-trigger attribute', function() { div.innerHTML.should.equal('Requests: 1') }) + it('changed modifier works for checkboxes', function() { + var requests = 0 + this.server.respondWith('GET', '/test', function(xhr) { + requests++ + xhr.respond(200, {}, 'Requests: ' + requests) + }) + var input = make('') + var div = make('
') + input.click() + this.server.respond() + div.innerHTML.should.equal('Requests: 1') + input.click() + this.server.respond() + div.innerHTML.should.equal('Requests: 2') + }) + it('changed modifier works along from clause with single input', function() { var requests = 0 this.server.respondWith('GET', '/test', function(xhr) { @@ -64,6 +80,7 @@ describe('hx-trigger attribute', function() { div.innerHTML.should.equal('Requests: 1') }) + // This test and the next one should be kept in sync. it('changed modifier works along from clause with two inputs', function() { var requests = 0 this.server.respondWith('GET', '/test', function(xhr) { @@ -106,6 +123,92 @@ describe('hx-trigger attribute', function() { div.innerHTML.should.equal('Requests: 2') }) + // This test and the previous one should be kept in sync. + it('changed modifier counts each triggerspec separately', function() { + var requests = 0 + this.server.respondWith('GET', '/test', function(xhr) { + requests++ + xhr.respond(200, {}, 'Requests: ' + requests) + }) + var input1 = make('') + var input2 = make('') + make('
') + make('
') + var div = make('
') + + input1.click() + this.server.respond() + div.innerHTML.should.equal('') + input2.click() + this.server.respond() + div.innerHTML.should.equal('') + + input1.value = 'bar' + input2.click() + this.server.respond() + div.innerHTML.should.equal('') + input1.click() + this.server.respond() + div.innerHTML.should.equal('Requests: 2') + + input1.click() + this.server.respond() + div.innerHTML.should.equal('Requests: 2') + input2.click() + this.server.respond() + div.innerHTML.should.equal('Requests: 2') + + input2.value = 'foo' + input1.click() + this.server.respond() + div.innerHTML.should.equal('Requests: 2') + input2.click() + this.server.respond() + div.innerHTML.should.equal('Requests: 4') + }) + + it('separate changed modifier works along from clause with two inputs', function() { + var requests = 0 + this.server.respondWith('GET', '/test', function(xhr) { + requests++ + xhr.respond(200, {}, 'Requests: ' + requests) + }) + var input1 = make('') + var input2 = make('') + make('
') + var div = make('
') + + input1.click() + this.server.respond() + div.innerHTML.should.equal('') + input2.click() + this.server.respond() + div.innerHTML.should.equal('') + + input1.value = 'bar' + input2.click() + this.server.respond() + div.innerHTML.should.equal('') + input1.click() + this.server.respond() + div.innerHTML.should.equal('Requests: 1') + + input1.click() + this.server.respond() + div.innerHTML.should.equal('Requests: 1') + input2.click() + this.server.respond() + div.innerHTML.should.equal('Requests: 1') + + input2.value = 'foo' + input1.click() + this.server.respond() + div.innerHTML.should.equal('Requests: 1') + input2.click() + this.server.respond() + div.innerHTML.should.equal('Requests: 2') + }) + it('once modifier works', function() { var requests = 0 this.server.respondWith('GET', '/test', function(xhr) { @@ -723,6 +826,60 @@ describe('hx-trigger attribute', function() { }, 50) }) + it('two delays on the same node are independent', function(done) { + var requests = 0 + var server = this.server + this.server.respondWith('GET', '/test', function(xhr) { + requests++ + xhr.respond(200, {}, 'Requests: ' + requests) + }) + this.server.respondWith('GET', '/bar', 'bar') + var button = make('') + var div = make("
") + + div.click() + button.click() + this.server.respond() + div.innerText.should.equal('') + + setTimeout(function() { + server.respond() + div.innerText.should.equal('Requests: 1') + + setTimeout(function() { + server.respond() + div.innerText.should.equal('Requests: 2') + + done() + }, 50) + }, 20) + }) + + it('delay for multiple nodes are shared', function(done) { + var requests = 0 + var server = this.server + this.server.respondWith('GET', '/test', function(xhr) { + requests++ + xhr.respond(200, {}, 'Requests: ' + requests) + }) + this.server.respondWith('GET', '/bar', 'bar') + var button1 = make('') + var button2 = make('') + var div = make("
") + + button1.click() + button2.click() + this.server.respond() + div.innerText.should.equal('') + + setTimeout(function() { + server.respond() + div.innerText.should.equal('Requests: 1') + + done() + }, 20) + }) + it('A 0 delay does not delay the request', function(done) { var requests = 0 this.server.respondWith('GET', '/test', function(xhr) {