Skip to content

Commit df92b29

Browse files
authored
Change hx-trigger's changed modifier to work for independent trigger specifications (#2891)
* Adjust hx-trigger's changed modifier for multiple sources The `changed` trigger modifier can see different event targets, either due to the `from` modifier or event bubbling. The existing behavior trigger only for one node (`from` modifier) or inconsistently (bubbling). Use a nested weak map to keep track of the last value per distinguished (trigger specification, event target node) pair. The weak map ensures that Garbage Collection can still recycle the nodes. If a event target was not seen via `from`, it is assumed changed for the first time the trigger is hit. * Add test case for separate triggers with changed modifier
1 parent 4916ce4 commit df92b29

File tree

2 files changed

+100
-13
lines changed

2 files changed

+100
-13
lines changed

src/htmx.js

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -695,7 +695,7 @@ var htmx = (function() {
695695
* @property {boolean} [triggeredOnce]
696696
* @property {number} [delayed]
697697
* @property {number|null} [throttle]
698-
* @property {string} [lastValue]
698+
* @property {WeakMap<HtmxTriggerSpecification,WeakMap<EventTarget,string>>} [lastValue]
699699
* @property {boolean} [loaded]
700700
* @property {string} [path]
701701
* @property {string} [verb]
@@ -2417,10 +2417,15 @@ var htmx = (function() {
24172417
}
24182418
// store the initial values of the elements, so we can tell if they change
24192419
if (triggerSpec.changed) {
2420+
if (!('lastValue' in elementData)) {
2421+
elementData.lastValue = new WeakMap()
2422+
}
24202423
eltsToListenOn.forEach(function(eltToListenOn) {
2421-
const eltToListenOnData = getInternalData(eltToListenOn)
2424+
if (!elementData.lastValue.has(triggerSpec)) {
2425+
elementData.lastValue.set(triggerSpec, new WeakMap())
2426+
}
24222427
// @ts-ignore value will be undefined for non-input elements, which is fine
2423-
eltToListenOnData.lastValue = eltToListenOn.value
2428+
elementData.lastValue.get(triggerSpec).set(eltToListenOn, eltToListenOn.value)
24242429
})
24252430
}
24262431
forEach(eltsToListenOn, function(eltToListenOn) {
@@ -2462,13 +2467,14 @@ var htmx = (function() {
24622467
}
24632468
}
24642469
if (triggerSpec.changed) {
2465-
const eltToListenOnData = getInternalData(eltToListenOn)
2470+
const node = event.target
24662471
// @ts-ignore value will be undefined for non-input elements, which is fine
2467-
const value = eltToListenOn.value
2468-
if (eltToListenOnData.lastValue === value) {
2472+
const value = node.value
2473+
const lastValue = elementData.lastValue.get(triggerSpec)
2474+
if (lastValue.has(node) && lastValue.get(node) === value) {
24692475
return
24702476
}
2471-
eltToListenOnData.lastValue = value
2477+
lastValue.set(node, value)
24722478
}
24732479
if (elementData.delayed) {
24742480
clearTimeout(elementData.delayed)
@@ -2846,12 +2852,6 @@ var htmx = (function() {
28462852

28472853
triggerEvent(elt, 'htmx:beforeProcessNode')
28482854

2849-
// @ts-ignore value will be undefined for non-input elements, which is fine
2850-
if (elt.value) {
2851-
// @ts-ignore
2852-
nodeData.lastValue = elt.value
2853-
}
2854-
28552855
const triggerSpecs = getTriggerSpecs(elt)
28562856
const hasExplicitHttpAction = processVerbs(elt, nodeData, triggerSpecs)
28572857

test/attributes/hx-trigger.js

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ describe('hx-trigger attribute', function() {
6464
div.innerHTML.should.equal('Requests: 1')
6565
})
6666

67+
// This test and the next one should be kept in sync.
6768
it('changed modifier works along from clause with two inputs', function() {
6869
var requests = 0
6970
this.server.respondWith('GET', '/test', function(xhr) {
@@ -106,6 +107,92 @@ describe('hx-trigger attribute', function() {
106107
div.innerHTML.should.equal('Requests: 2')
107108
})
108109

110+
// This test and the previous one should be kept in sync.
111+
it('changed modifier counts each triggerspec separately', function() {
112+
var requests = 0
113+
this.server.respondWith('GET', '/test', function(xhr) {
114+
requests++
115+
xhr.respond(200, {}, 'Requests: ' + requests)
116+
})
117+
var input1 = make('<input type="text"/>')
118+
var input2 = make('<input type="text"/>')
119+
make('<div hx-trigger="click changed from:input" hx-target="#d1" hx-get="/test"></div>')
120+
make('<div hx-trigger="click changed from:input" hx-target="#d1" hx-get="/test"></div>')
121+
var div = make('<div id="d1"></div>')
122+
123+
input1.click()
124+
this.server.respond()
125+
div.innerHTML.should.equal('')
126+
input2.click()
127+
this.server.respond()
128+
div.innerHTML.should.equal('')
129+
130+
input1.value = 'bar'
131+
input2.click()
132+
this.server.respond()
133+
div.innerHTML.should.equal('')
134+
input1.click()
135+
this.server.respond()
136+
div.innerHTML.should.equal('Requests: 2')
137+
138+
input1.click()
139+
this.server.respond()
140+
div.innerHTML.should.equal('Requests: 2')
141+
input2.click()
142+
this.server.respond()
143+
div.innerHTML.should.equal('Requests: 2')
144+
145+
input2.value = 'foo'
146+
input1.click()
147+
this.server.respond()
148+
div.innerHTML.should.equal('Requests: 2')
149+
input2.click()
150+
this.server.respond()
151+
div.innerHTML.should.equal('Requests: 4')
152+
})
153+
154+
it('separate changed modifier works along from clause with two inputs', function() {
155+
var requests = 0
156+
this.server.respondWith('GET', '/test', function(xhr) {
157+
requests++
158+
xhr.respond(200, {}, 'Requests: ' + requests)
159+
})
160+
var input1 = make('<input type="text"/>')
161+
var input2 = make('<input type="text"/>')
162+
make('<div hx-trigger="click changed from:input:nth-child(1), click changed from:input:nth-child(2)" hx-target="#d1" hx-get="/test"></div>')
163+
var div = make('<div id="d1"></div>')
164+
165+
input1.click()
166+
this.server.respond()
167+
div.innerHTML.should.equal('')
168+
input2.click()
169+
this.server.respond()
170+
div.innerHTML.should.equal('')
171+
172+
input1.value = 'bar'
173+
input2.click()
174+
this.server.respond()
175+
div.innerHTML.should.equal('')
176+
input1.click()
177+
this.server.respond()
178+
div.innerHTML.should.equal('Requests: 1')
179+
180+
input1.click()
181+
this.server.respond()
182+
div.innerHTML.should.equal('Requests: 1')
183+
input2.click()
184+
this.server.respond()
185+
div.innerHTML.should.equal('Requests: 1')
186+
187+
input2.value = 'foo'
188+
input1.click()
189+
this.server.respond()
190+
div.innerHTML.should.equal('Requests: 1')
191+
input2.click()
192+
this.server.respond()
193+
div.innerHTML.should.equal('Requests: 2')
194+
})
195+
109196
it('once modifier works', function() {
110197
var requests = 0
111198
this.server.respondWith('GET', '/test', function(xhr) {

0 commit comments

Comments
 (0)