Skip to content

Commit 8154f26

Browse files
committed
Support select elements
First, introduce the `PersistableElement` type to incorporate `<input>`, `<textarea>`, and `<select>` elements. Next, incorporate some special handling for `HTMLSelectElement`, since they don't have a `.defaultValue` property. Support for `HTMLSelectElement` instances is generalized for both single and `[multiple]` elements by looping over [HTMLSelectElement.selectedOptions][] while persisting, then setting [HTMLSelectElement.selected][] when restoring. [HTMLSelectElement.selectedOptions]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLSelectElement/selectedOptions [HTMLSelectElement.selected]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLOptionElement#instance_properties
1 parent bfb858e commit 8154f26

File tree

2 files changed

+94
-20
lines changed

2 files changed

+94
-20
lines changed

src/index.ts

+30-10
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,27 @@
11
// Last submitted HTMLFormElement that will cause a browser navigation.
22
let submittedForm: HTMLFormElement | null = null
33

4-
function shouldResumeField(field: HTMLInputElement | HTMLTextAreaElement, filter: StorageFilter): boolean {
4+
function shouldResumeField(field: PersistableElement, filter: StorageFilter): boolean {
55
return !!field.id && filter(field) && field.form !== submittedForm
66
}
77

8-
function valueIsUnchanged(field: HTMLInputElement | HTMLTextAreaElement): boolean {
9-
if (isHTMLCheckableInputElement(field)) {
8+
function valueIsUnchanged(field: PersistableElement): boolean {
9+
if (field instanceof HTMLSelectElement) {
10+
return true
11+
} else if (isHTMLCheckableInputElement(field)) {
1012
return field.checked !== field.defaultChecked
1113
} else {
1214
return field.value !== field.defaultValue
1315
}
1416
}
1517

16-
type StorageFilter = (field: HTMLInputElement | HTMLTextAreaElement) => boolean
18+
function isPersistableElement(node: Node | null): node is PersistableElement {
19+
return node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement || node instanceof HTMLSelectElement
20+
}
21+
22+
type PersistableElement = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement
23+
24+
type StorageFilter = (field: PersistableElement) => boolean
1725

1826
type PersistOptionsWithSelector = {
1927
scope?: ParentNode
@@ -63,13 +71,20 @@ export function persistResumableFields(id: string, options?: PersistOptions): vo
6371
const resumables = []
6472

6573
for (const el of elements) {
66-
if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
74+
if (isPersistableElement(el)) {
6775
resumables.push(el)
6876
}
6977
}
7078

71-
let fields = resumables.filter(field => shouldResumeField(field, storageFilter)).map(field => [field.id, field.value])
72-
79+
let fields = resumables
80+
.filter(field => shouldResumeField(field, storageFilter))
81+
.map(field => {
82+
if (field instanceof HTMLSelectElement) {
83+
return [field.id, Array.from(field.selectedOptions).map(option => option.value)]
84+
} else {
85+
return [field.id, field.value]
86+
}
87+
})
7388
if (fields.length) {
7489
try {
7590
const previouslyStoredFieldsJson = storage.getItem(key)
@@ -113,7 +128,7 @@ export function restoreResumableFields(id: string, options?: RestoreOptions): vo
113128

114129
if (!fields) return
115130

116-
const changedFields: Array<HTMLInputElement | HTMLTextAreaElement> = []
131+
const changedFields: PersistableElement[] = []
117132
const storedFieldsNotFound: string[][] = []
118133

119134
for (const [fieldId, value] of JSON.parse(fields)) {
@@ -125,8 +140,13 @@ export function restoreResumableFields(id: string, options?: RestoreOptions): vo
125140

126141
if (document.dispatchEvent(resumeEvent)) {
127142
const field = document.getElementById(fieldId)
128-
if (field && (field instanceof HTMLInputElement || field instanceof HTMLTextAreaElement)) {
129-
if (isHTMLCheckableInputElement(field)) {
143+
if (isPersistableElement(field)) {
144+
if (field instanceof HTMLSelectElement) {
145+
for (const option of field.options) {
146+
option.selected = value.includes(option.value)
147+
}
148+
changedFields.push(field)
149+
} else if (isHTMLCheckableInputElement(field)) {
130150
field.checked = !field.defaultChecked
131151
changedFields.push(field)
132152
} else if (field.value === field.defaultValue) {

test/test.js

+64-10
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@ describe('session-resume', function () {
1111
<input id="my-first-checkbox" type="checkbox" value="first-checkbox-value" class="js-session-resumable" />
1212
<input id="my-second-checkbox" type="checkbox" value="second-checkbox-value" class="js-session-resumable" />
1313
<input id="my-checked-checkbox" type="checkbox" value="checked-checkbox-value" class="js-session-resumable" checked />
14+
<select id="my-single-select-field" class="js-session-resumable">
15+
<option value="first">first</option>
16+
<option value="second">second</option>
17+
</select>
18+
<select id="my-multiple-select-field" class="js-session-resumable" multiple>
19+
<option value="first">first</option>
20+
<option value="second">second</option>
21+
</select>
1422
</form>
1523
`
1624
window.addEventListener('submit', sessionStorage.setForm, {capture: true})
@@ -23,7 +31,9 @@ describe('session-resume', function () {
2331
JSON.stringify([
2432
['my-first-field', 'test2'],
2533
['my-first-checkbox', 'first-checkbox-value'],
26-
['my-checked-checkbox', 'checked-checkbox-value']
34+
['my-checked-checkbox', 'checked-checkbox-value'],
35+
['my-single-select-field', ['second']],
36+
['my-multiple-select-field', ['first', 'second']]
2737
])
2838
)
2939
restoreResumableFields('test-persist')
@@ -33,6 +43,9 @@ describe('session-resume', function () {
3343
assert.equal(document.querySelector('#my-first-checkbox').checked, true)
3444
assert.equal(document.querySelector('#my-second-checkbox').checked, false)
3545
assert.equal(document.querySelector('#my-checked-checkbox').checked, false)
46+
assert.equal(document.querySelector('#my-single-select-field').value, 'second')
47+
assert.equal(document.querySelector('#my-multiple-select-field option[value=first]').selected, true)
48+
assert.equal(document.querySelector('#my-multiple-select-field option[value=second]').selected, true)
3649
})
3750

3851
it('uses a Storage object when provided as an option', function () {
@@ -46,11 +59,21 @@ describe('session-resume', function () {
4659
}
4760
}
4861

49-
fakeStorage.setItem('session-resume:test-persist', JSON.stringify([['my-first-field', 'test2']]))
62+
fakeStorage.setItem(
63+
'session-resume:test-persist',
64+
JSON.stringify([
65+
['my-first-field', 'test2'],
66+
['my-single-select-field', ['second']],
67+
['my-multiple-select-field', ['first', 'second']]
68+
])
69+
)
5070
restoreResumableFields('test-persist', {storage: fakeStorage})
5171

5272
assert.equal(document.querySelector('#my-first-field').value, 'test2')
5373
assert.equal(document.querySelector('#my-second-field').value, 'second-field-value')
74+
assert.equal(document.querySelector('#my-single-select-field').value, 'second')
75+
assert.equal(document.querySelector('#my-multiple-select-field option[value=first]').selected, true)
76+
assert.equal(document.querySelector('#my-multiple-select-field option[value=second]').selected, true)
5477
})
5578

5679
it('leaves unrestored values in session storage', function () {
@@ -71,7 +94,7 @@ describe('session-resume', function () {
7194

7295
// Some fields we want to restore are not always present in the DOM
7396
// and may be added later. We hold onto the values until they're needed.
74-
assert.deepEqual(JSON.parse(sessionStorage.getItem('session-resume:test-persist')), [
97+
assert.includeDeepMembers(JSON.parse(sessionStorage.getItem('session-resume:test-persist')), [
7598
['non-existant-field', 'test3']
7699
])
77100
})
@@ -127,17 +150,25 @@ describe('session-resume', function () {
127150
it('persist fields values to session storage by default', function () {
128151
document.querySelector('#my-first-field').value = 'test1'
129152
document.querySelector('#my-second-field').value = 'test2'
153+
document.querySelector('#my-single-select-field').value = 'first'
154+
document.querySelector('#my-multiple-select-field option[value=first]').selected = true
155+
document.querySelector('#my-multiple-select-field option[value=second]').selected = true
130156
persistResumableFields('test-persist')
131157

132158
assert.includeDeepMembers(JSON.parse(sessionStorage.getItem('session-resume:test-persist')), [
133159
['my-first-field', 'test1'],
134-
['my-second-field', 'test2']
160+
['my-second-field', 'test2'],
161+
['my-single-select-field', ['first']],
162+
['my-multiple-select-field', ['first', 'second']]
135163
])
136164
})
137165

138166
it('uses a Storage object when provided as an option', function () {
139167
document.querySelector('#my-first-field').value = 'test1'
140168
document.querySelector('#my-second-field').value = 'test2'
169+
document.querySelector('#my-single-select-field').value = 'second'
170+
document.querySelector('#my-multiple-select-field option[value=first]').selected = true
171+
document.querySelector('#my-multiple-select-field option[value=second]').selected = true
141172

142173
const fakeStorageBackend = {}
143174
const fakeStorage = {
@@ -153,34 +184,54 @@ describe('session-resume', function () {
153184

154185
assert.includeDeepMembers(JSON.parse(fakeStorage.getItem('session-resume:test-persist')), [
155186
['my-first-field', 'test1'],
156-
['my-second-field', 'test2']
187+
['my-second-field', 'test2'],
188+
['my-single-select-field', ['second']],
189+
['my-multiple-select-field', ['first', 'second']]
157190
])
158191
})
159192

160193
it('holds onto existing values in the store', function () {
161194
sessionStorage.setItem('session-resume:test-persist', JSON.stringify([['non-existant-field', 'test3']]))
162195
document.querySelector('#my-first-field').value = 'test1'
163196
document.querySelector('#my-second-field').value = 'test2'
197+
document.querySelector('#my-single-select-field').value = 'second'
198+
document.querySelector('#my-multiple-select-field option[value=first]').selected = true
199+
document.querySelector('#my-multiple-select-field option[value=second]').selected = true
164200

165201
persistResumableFields('test-persist')
166202

167203
assert.includeDeepMembers(JSON.parse(sessionStorage.getItem('session-resume:test-persist')), [
168204
['my-first-field', 'test1'],
169205
['my-second-field', 'test2'],
206+
['my-single-select-field', ['second']],
207+
['my-multiple-select-field', ['first', 'second']],
170208
['non-existant-field', 'test3']
171209
])
172210
})
173211

174212
it('replaces old values with the latest field values', function () {
175-
sessionStorage.setItem('session-resume:test-persist', JSON.stringify([['my-first-field', 'old data']]))
213+
sessionStorage.setItem(
214+
'session-resume:test-persist',
215+
JSON.stringify([
216+
['my-first-field', 'old data'],
217+
['my-second-field', 'old data'],
218+
['my-single-select-field', 'first'],
219+
['my-multiple-select-field', ['first', 'second']]
220+
])
221+
)
176222
document.querySelector('#my-first-field').value = 'test1'
177223
document.querySelector('#my-second-field').value = 'test2'
224+
document.querySelector('#my-single-select-field').value = 'second'
225+
document.querySelector('#my-multiple-select-field option[value=first]').selected = false
226+
document.querySelector('#my-multiple-select-field option[value=second]').selected = true
178227

179228
persistResumableFields('test-persist')
180229

181230
assert.includeDeepMembers(JSON.parse(sessionStorage.getItem('session-resume:test-persist')), [
182231
['my-first-field', 'test1'],
183-
['my-second-field', 'test2']
232+
['my-second-field', 'test2'],
233+
['my-single-select-field', ['second']],
234+
['my-multiple-select-field', ['second']]
184235
])
185236
})
186237

@@ -191,7 +242,9 @@ describe('session-resume', function () {
191242
sessionStorage.clear()
192243
persistResumableFields('test-persist', {selector: '#my-first-field'})
193244

194-
assert.deepEqual(JSON.parse(sessionStorage.getItem('session-resume:test-persist')), [['my-first-field', 'test1']])
245+
assert.includeDeepMembers(JSON.parse(sessionStorage.getItem('session-resume:test-persist')), [
246+
['my-first-field', 'test1']
247+
])
195248
})
196249

197250
it('scopes fields based on the scope: option', function () {
@@ -210,19 +263,20 @@ describe('session-resume', function () {
210263
sessionStorage.clear()
211264
persistResumableFields('test-persist', {scope: document.querySelector('form')})
212265

213-
assert.deepEqual(JSON.parse(sessionStorage.getItem('session-resume:test-persist')), [
266+
assert.includeDeepMembers(JSON.parse(sessionStorage.getItem('session-resume:test-persist')), [
214267
['my-first-field', 'test1'],
215268
['my-second-field', 'test2']
216269
])
217270
})
271+
218272
it('scopes fields based on the fields: option', function () {
219273
document.getElementById('my-first-field').value = 'test1'
220274
document.getElementById('my-second-field').value = 'test2'
221275

222276
sessionStorage.clear()
223277
persistResumableFields('test-persist', {fields: document.querySelectorAll('#my-second-field')})
224278

225-
assert.deepEqual(JSON.parse(sessionStorage.getItem('session-resume:test-persist')), [
279+
assert.includeDeepMembers(JSON.parse(sessionStorage.getItem('session-resume:test-persist')), [
226280
['my-second-field', 'test2']
227281
])
228282
})

0 commit comments

Comments
 (0)