stimulus_rails_datatables 0.5.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +18 -0
- data/README.md +16 -1
- data/app/assets/javascripts/stimulus_rails_datatables/config.js +4 -1
- data/app/assets/javascripts/stimulus_rails_datatables/datatables_controller.js +7 -2
- data/app/assets/javascripts/stimulus_rails_datatables/filter_controller.js +154 -72
- data/app/helpers/stimulus_rails_datatables/datatable_helper.rb +3 -1
- data/app/helpers/stimulus_rails_datatables/filter_helper.rb +12 -2
- data/lib/generators/stimulus_rails_datatables/install/templates/datatables_config.js +4 -1
- data/lib/stimulus_rails_datatables/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6c52d575fdf1e4bf88eb1cdf3b581ed80ec4c6956b1d5f0c3a60b90f99d2e3f2
|
|
4
|
+
data.tar.gz: 9bf301decccb7e7490972346183d73fa25d659088383521ff8b825b6bc83ac5e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 61e84f397c85ad598f98b47f7451e454fa6fae7e8e707fee9b9281829feecab1e151be8a387d4ea19cad63ac3d71f3e3c301f756dc2f20f07f9b7948a0f830ea
|
|
7
|
+
data.tar.gz: 780dddf9239013c1fb8444a0d2ffeaab370829cfbffcd0827d23f5545f8e082d3740403b8e4854e52ed0185cf8eccd683decd3eb9d6353e2b8e327ac7113c73d
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,24 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.7.0] - 2026-09-25
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- `resetPaging` global config option (`window.datatablesConfig` or `setDatatablesConfig`) to go back to the first page when filters change; defaults to `false`
|
|
12
|
+
- `reset_paging` option for the `datatable_for` helper, which overrides the global `resetPaging` value per datatable
|
|
13
|
+
|
|
14
|
+
## [0.6.0] - 2026-08-03
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- Tom-Select support for remote filter selects via the `tomselect` option
|
|
18
|
+
- `multiple` option for remote filter selects, sending the selected values as array params (`filters[key][]=`)
|
|
19
|
+
- `set_values` remote option to preselect multiple values
|
|
20
|
+
- Summary label showing the number of selected items on Tom-Select multi-selects
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
- The `location` filter's barangay select is now a Tom-Select multi-select
|
|
24
|
+
- The filter controller imports `tom-select`, so the host app must pin it in its importmap
|
|
25
|
+
|
|
8
26
|
## [0.5.1] - 2026-07-28
|
|
9
27
|
|
|
10
28
|
### Changed
|
data/README.md
CHANGED
|
@@ -335,10 +335,25 @@ window.datatablesConfig = {
|
|
|
335
335
|
},
|
|
336
336
|
|
|
337
337
|
// Length menu options
|
|
338
|
-
lengthMenu: [[10, 25, 50, 100], [10, 25, 50, 100]]
|
|
338
|
+
lengthMenu: [[10, 25, 50, 100], [10, 25, 50, 100]],
|
|
339
|
+
|
|
340
|
+
// Go back to the first page when filters change (default: false)
|
|
341
|
+
resetPaging: true
|
|
339
342
|
}
|
|
340
343
|
```
|
|
341
344
|
|
|
345
|
+
### Resetting Pagination on Filter Change
|
|
346
|
+
|
|
347
|
+
By default, changing a filter reloads the datatable and keeps the current page. Set `resetPaging: true` in `window.datatablesConfig` to jump back to the first page for every datatable, or override it per table with `reset_paging:`:
|
|
348
|
+
|
|
349
|
+
```ruby
|
|
350
|
+
<%= datatable_for 'users-table', source: users_path, reset_paging: false do |dt| %>
|
|
351
|
+
<% dt.column :name %>
|
|
352
|
+
<% end %>
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
Leave `reset_paging:` out to use the global value. Restoring saved filters on page load never resets the page, so the page kept by `state_save` is not lost.
|
|
356
|
+
|
|
342
357
|
### Available Stimulus Controllers
|
|
343
358
|
|
|
344
359
|
The gem automatically registers the following Stimulus controllers:
|
|
@@ -30,7 +30,10 @@ let datatablesConfig = {
|
|
|
30
30
|
},
|
|
31
31
|
|
|
32
32
|
// Length menu options
|
|
33
|
-
lengthMenu: [[10, 25, 50, 100], [10, 25, 50, 100]]
|
|
33
|
+
lengthMenu: [[10, 25, 50, 100], [10, 25, 50, 100]],
|
|
34
|
+
|
|
35
|
+
// Go back to the first page when filters change (override per table with datatable_for reset_paging:)
|
|
36
|
+
resetPaging: false
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
// Function to let the app override defaults
|
|
@@ -17,7 +17,8 @@ export default class extends Controller {
|
|
|
17
17
|
pagingType: { type: String, default: 'simple_numbers' },
|
|
18
18
|
searching: { type: Boolean, default: true },
|
|
19
19
|
lengthChange: { type: Boolean, default: true },
|
|
20
|
-
responsive: { type: Boolean, default: true }
|
|
20
|
+
responsive: { type: Boolean, default: true },
|
|
21
|
+
resetPaging: Boolean
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
connect() {
|
|
@@ -89,7 +90,8 @@ export default class extends Controller {
|
|
|
89
90
|
const config = {
|
|
90
91
|
language: { ...defaultConfig.language, ...(userConfig.language || {}) },
|
|
91
92
|
layout: { ...defaultConfig.layout, ...(userConfig.layout || {}) },
|
|
92
|
-
lengthMenu: userConfig.lengthMenu || defaultConfig.lengthMenu
|
|
93
|
+
lengthMenu: userConfig.lengthMenu || defaultConfig.lengthMenu,
|
|
94
|
+
resetPaging: userConfig.resetPaging ?? defaultConfig.resetPaging
|
|
93
95
|
}
|
|
94
96
|
|
|
95
97
|
const responsiveValue = this.responsiveValue
|
|
@@ -107,6 +109,9 @@ export default class extends Controller {
|
|
|
107
109
|
responsive: this.responsiveValue,
|
|
108
110
|
language: config.language,
|
|
109
111
|
layout: config.layout,
|
|
112
|
+
// Not a DataTables option; the filter controller reads it back via datatable.init()
|
|
113
|
+
// Per-table value (datatable_for reset_paging:) wins over the global config
|
|
114
|
+
resetPaging: this.hasResetPagingValue ? this.resetPagingValue : !!config.resetPaging,
|
|
110
115
|
initComplete: function() {
|
|
111
116
|
if (responsiveValue === false) {
|
|
112
117
|
// Add overflow-x only to the table wrapper (not the whole layout) this is alternative of scrollX
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Disabling eslint since this is a library file and may not conform to all rules
|
|
3
3
|
|
|
4
4
|
import { Controller } from '@hotwired/stimulus'
|
|
5
|
+
import TomSelect from 'tom-select'
|
|
5
6
|
|
|
6
7
|
export default class extends Controller {
|
|
7
8
|
static targets = ['select', 'customFields']
|
|
@@ -13,6 +14,45 @@ export default class extends Controller {
|
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
connect() {
|
|
17
|
+
this.tomSelects = new Map()
|
|
18
|
+
this.remoteSelects = Array.from(
|
|
19
|
+
this.element.querySelectorAll('select[data-filter-remote-url-value]')
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
this.remoteSelects.forEach(select => {
|
|
23
|
+
if (select.dataset.filterTomselect !== 'true') return
|
|
24
|
+
|
|
25
|
+
const isMultiple = select.multiple
|
|
26
|
+
const ts = new TomSelect(select, {
|
|
27
|
+
maxItems: isMultiple ? null : 1,
|
|
28
|
+
placeholder: select.dataset.filterPlaceholder || 'Select',
|
|
29
|
+
create: false,
|
|
30
|
+
hideSelected: isMultiple ? false : undefined,
|
|
31
|
+
render: isMultiple ? { item: () => '<div class="ts-hidden-item"></div>' } : {}
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
if (isMultiple) {
|
|
35
|
+
const originalOnOptionSelect = ts.onOptionSelect.bind(ts)
|
|
36
|
+
ts.onOptionSelect = (evt, option) => {
|
|
37
|
+
const value = option.dataset.value
|
|
38
|
+
if (ts.items.includes(value)) {
|
|
39
|
+
evt.preventDefault()
|
|
40
|
+
ts.removeItem(value)
|
|
41
|
+
ts.refreshOptions(false)
|
|
42
|
+
} else {
|
|
43
|
+
originalOnOptionSelect(evt, option)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
this.updateSummaryLabel(select, ts)
|
|
47
|
+
ts.on('item_add', () => this.updateSummaryLabel(select, ts))
|
|
48
|
+
ts.on('item_remove', () => this.updateSummaryLabel(select, ts))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
this.tomSelects.set(select, ts)
|
|
52
|
+
|
|
53
|
+
if (select.dataset.filterDependsOn) ts.disable()
|
|
54
|
+
})
|
|
55
|
+
|
|
16
56
|
// begin restore; it is async but will dispatch filters:ready when done
|
|
17
57
|
this.restoreState()
|
|
18
58
|
|
|
@@ -29,16 +69,50 @@ export default class extends Controller {
|
|
|
29
69
|
// trigger datatable reload
|
|
30
70
|
this.reloadAppDatatable()
|
|
31
71
|
})
|
|
72
|
+
}
|
|
32
73
|
|
|
33
|
-
|
|
34
|
-
this.
|
|
74
|
+
disconnect() {
|
|
75
|
+
this.tomSelects?.forEach(ts => ts.destroy())
|
|
76
|
+
this.tomSelects?.clear()
|
|
77
|
+
}
|
|
35
78
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
79
|
+
// --- helpers: single source of truth for reading/writing a field, Tom-Select-aware ---
|
|
80
|
+
getFieldValue(el) {
|
|
81
|
+
const ts = this.tomSelects.get(el)
|
|
82
|
+
return ts ? ts.getValue() : el.value
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
isValueEmpty(value) {
|
|
86
|
+
return Array.isArray(value) ? value.length === 0 : !value
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
setFieldValue(el, value, silent = true) {
|
|
90
|
+
const ts = this.tomSelects.get(el)
|
|
91
|
+
if (ts) ts.setValue(value, silent)
|
|
92
|
+
else el.value = value
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
updateSummaryLabel(select, ts) {
|
|
96
|
+
const count = ts.items.length
|
|
97
|
+
const placeholder = select.dataset.filterPlaceholder || 'Select'
|
|
98
|
+
|
|
99
|
+
let label = ts.control.querySelector('.ts-summary-label')
|
|
100
|
+
if (!label) {
|
|
101
|
+
label = document.createElement('div')
|
|
102
|
+
label.className = 'ts-summary-label'
|
|
103
|
+
ts.control.prepend(label)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const hasSelection = count > 0
|
|
107
|
+
label.textContent = hasSelection ? `${count} Barangay${count > 1 ? 's' : ''} selected` : ''
|
|
108
|
+
label.style.display = hasSelection ? 'block' : 'none'
|
|
109
|
+
|
|
110
|
+
if (hasSelection) {
|
|
111
|
+
ts.control_input.style.setProperty('display', 'none', 'important')
|
|
112
|
+
} else {
|
|
113
|
+
ts.control_input.style.removeProperty('display')
|
|
114
|
+
ts.control_input.setAttribute('placeholder', placeholder)
|
|
115
|
+
}
|
|
42
116
|
}
|
|
43
117
|
|
|
44
118
|
toQuery(obj, prefix) {
|
|
@@ -47,7 +121,10 @@ export default class extends Controller {
|
|
|
47
121
|
for (const [key, value] of Object.entries(obj)) {
|
|
48
122
|
const fullKey = prefix ? `${prefix}[${key}]` : key
|
|
49
123
|
|
|
50
|
-
if (value
|
|
124
|
+
if (Array.isArray(value)) {
|
|
125
|
+
value.forEach(v => pairs.push(`${encodeURIComponent(fullKey)}[]=${encodeURIComponent(v)}`))
|
|
126
|
+
}
|
|
127
|
+
else if (value !== null && typeof value === 'object') {
|
|
51
128
|
pairs.push(this.toQuery(value, fullKey))
|
|
52
129
|
}
|
|
53
130
|
else if (value !== '' && value !== null && value !== undefined) {
|
|
@@ -59,7 +136,8 @@ export default class extends Controller {
|
|
|
59
136
|
}
|
|
60
137
|
|
|
61
138
|
// reloadAppDatatable reloads the datatable with current params
|
|
62
|
-
|
|
139
|
+
// pass resetPaging true/false to force it, otherwise the datatable's resetPaging setting is used
|
|
140
|
+
async reloadAppDatatable(resetPaging) {
|
|
63
141
|
var id = this.element.dataset.filterDatatableId
|
|
64
142
|
|
|
65
143
|
if (!id) {
|
|
@@ -70,17 +148,20 @@ export default class extends Controller {
|
|
|
70
148
|
const datatableUrl = datatable.ajax.url().split('?')[0]
|
|
71
149
|
const params = this.toQuery(this.currentParams())
|
|
72
150
|
|
|
73
|
-
datatable.
|
|
74
|
-
}
|
|
151
|
+
if (typeof resetPaging !== 'boolean') resetPaging = datatable.init()?.resetPaging === true
|
|
75
152
|
|
|
153
|
+
datatable.ajax.url(`${datatableUrl}?${params}`).load(null, resetPaging)
|
|
154
|
+
}
|
|
76
155
|
}
|
|
77
156
|
|
|
78
157
|
// async populate returns when options appended
|
|
79
158
|
async populate(select) {
|
|
159
|
+
const ts = this.tomSelects.get(select)
|
|
80
160
|
let url = select.dataset.filterRemoteUrlValue
|
|
81
161
|
const labelKey = select.dataset.filterLabelKey
|
|
82
162
|
const valueKey = select.dataset.filterValueKey
|
|
83
|
-
const
|
|
163
|
+
const setValues = (select.dataset.filterSetValues || select.dataset.filterSetValue || '')
|
|
164
|
+
.split(',').map(v => v.trim()).filter(Boolean)
|
|
84
165
|
|
|
85
166
|
url = decodeURIComponent(url).replace(/{(\w+)}/g, (_, key) => {
|
|
86
167
|
const input = this.element.querySelector(`[data-filter-field-name='${key}']`)
|
|
@@ -94,38 +175,54 @@ export default class extends Controller {
|
|
|
94
175
|
if (!response.ok) throw new Error(`Failed to fetch ${url}`)
|
|
95
176
|
const data = await response.json()
|
|
96
177
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
// If set_value matches, mark as selected
|
|
104
|
-
if (set_value && item[valueKey] == set_value) {
|
|
105
|
-
option.selected = true
|
|
178
|
+
if (ts) {
|
|
179
|
+
ts.clear(true) // clear current selection (silent — no change event)
|
|
180
|
+
ts.clearOptions() // wipe stale option list
|
|
181
|
+
if (!select.multiple) {
|
|
182
|
+
const placeholder = select.dataset.filterPlaceholder || 'All'
|
|
183
|
+
ts.addOption({ value: '', text: placeholder })
|
|
106
184
|
}
|
|
107
|
-
|
|
108
|
-
select.
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
185
|
+
ts.addOptions(data.map(item => ({ value: String(item[valueKey]), text: item[labelKey] })))
|
|
186
|
+
if (setValues.length) this.setFieldValue(select, select.multiple ? setValues : setValues[0])
|
|
187
|
+
ts.enable()
|
|
188
|
+
if (select.multiple) this.updateSummaryLabel(select, ts)
|
|
189
|
+
} else {
|
|
190
|
+
select.innerHTML = ''
|
|
191
|
+
|
|
192
|
+
const placeholder = select.dataset.filterPlaceholder || 'All'
|
|
193
|
+
const blankOption = document.createElement('option')
|
|
194
|
+
blankOption.value = ''
|
|
195
|
+
blankOption.textContent = placeholder
|
|
196
|
+
select.appendChild(blankOption)
|
|
197
|
+
|
|
198
|
+
data.forEach(item => {
|
|
199
|
+
const option = document.createElement('option')
|
|
200
|
+
option.value = item[valueKey]
|
|
201
|
+
option.textContent = item[labelKey]
|
|
202
|
+
if (setValues.includes(String(item[valueKey]))) option.selected = true
|
|
203
|
+
select.appendChild(option)
|
|
204
|
+
})
|
|
205
|
+
select.disabled = false
|
|
206
|
+
}
|
|
112
207
|
} catch (e) {
|
|
113
208
|
console.error('[Filter] fetch error:', e)
|
|
114
|
-
|
|
209
|
+
if (ts) ts.enable()
|
|
210
|
+
else select.disabled = false
|
|
115
211
|
}
|
|
116
212
|
}
|
|
117
213
|
|
|
118
214
|
resetSelect(select, disabled = true) {
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
215
|
+
const ts = this.tomSelects.get(select)
|
|
216
|
+
if (ts) {
|
|
217
|
+
ts.clear(true)
|
|
218
|
+
ts.clearOptions()
|
|
219
|
+
disabled ? ts.disable() : ts.enable()
|
|
220
|
+
} else {
|
|
221
|
+
const placeholder = select.dataset.filterPlaceholder || 'Select'
|
|
222
|
+
select.innerHTML = `<option value="">${placeholder}</option>`
|
|
223
|
+
select.value = ''
|
|
224
|
+
select.disabled = disabled
|
|
225
|
+
}
|
|
129
226
|
}
|
|
130
227
|
|
|
131
228
|
currentParams() {
|
|
@@ -133,20 +230,15 @@ export default class extends Controller {
|
|
|
133
230
|
const params = {}
|
|
134
231
|
|
|
135
232
|
this.element.querySelectorAll('[data-filter-field-name]').forEach(el => {
|
|
136
|
-
|
|
233
|
+
const value = this.getFieldValue(el)
|
|
234
|
+
if (!this.isValueEmpty(value)) params[el.dataset.filterFieldName] = value
|
|
137
235
|
})
|
|
138
236
|
|
|
139
|
-
|
|
140
|
-
if (v !== '' && v !== null && v !== undefined) acc[k] = v
|
|
141
|
-
return acc
|
|
142
|
-
}, {})
|
|
143
|
-
|
|
144
|
-
return { [rootKey]: clean }
|
|
237
|
+
return { [rootKey]: params }
|
|
145
238
|
}
|
|
146
239
|
|
|
147
240
|
saveState() {
|
|
148
241
|
const state = this.currentParams()
|
|
149
|
-
// deterministic storage key that datatable can read
|
|
150
242
|
try {
|
|
151
243
|
localStorage.setItem(`filterState:${this.filterDtId}`, JSON.stringify(state))
|
|
152
244
|
} catch (e) {
|
|
@@ -176,21 +268,13 @@ export default class extends Controller {
|
|
|
176
268
|
if (el && !el.dataset.filterRemoteUrlValue) el.value = value
|
|
177
269
|
})
|
|
178
270
|
|
|
179
|
-
|
|
180
|
-
this.selects = Array.from(this.element.querySelectorAll('select[data-filter-remote-url-value]'))
|
|
181
|
-
|
|
182
|
-
// find root remote selects (no depends_on)
|
|
183
|
-
const roots = this.selects.filter(s => !s.dataset.filterDependsOn)
|
|
271
|
+
const roots = this.remoteSelects.filter(s => !s.dataset.filterDependsOn)
|
|
184
272
|
|
|
185
|
-
// populate each root -> then recursively populate dependents and restore values
|
|
186
273
|
await Promise.all(roots.map(async (root) => {
|
|
187
274
|
await this.populate(root)
|
|
188
|
-
// after root options exist, restore saved root value (if any)
|
|
189
275
|
const sv = savedParams[root.dataset.filterFieldName]
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
if (sv && !set_value) root.value = sv
|
|
193
|
-
// cascade down children
|
|
276
|
+
const hasSetValue = root.dataset.filterSetValue || root.dataset.filterSetValues
|
|
277
|
+
if (sv && !hasSetValue) this.setFieldValue(root, sv)
|
|
194
278
|
await this.populateDependents(root, savedParams)
|
|
195
279
|
}))
|
|
196
280
|
|
|
@@ -201,52 +285,50 @@ export default class extends Controller {
|
|
|
201
285
|
const sd = this.element.querySelector('[data-filter-field-name="start_date"]')
|
|
202
286
|
if (sd) sd.value = savedParams['start_date']
|
|
203
287
|
}
|
|
204
|
-
|
|
205
288
|
if (savedParams['end_date']) {
|
|
206
289
|
const ed = this.element.querySelector('[data-filter-field-name="end_date"]')
|
|
207
290
|
if (ed) ed.value = savedParams['end_date']
|
|
208
291
|
}
|
|
209
292
|
}
|
|
210
293
|
|
|
211
|
-
// emit event
|
|
294
|
+
// emit event so any listener can catch
|
|
212
295
|
const payload = this.currentParams()
|
|
213
296
|
this.element.dispatchEvent(new CustomEvent('filters:ready', { detail: { params: payload }, bubbles: true }))
|
|
214
297
|
|
|
215
|
-
// also update deterministic storage (in case other code reads it)
|
|
216
298
|
try {
|
|
217
299
|
localStorage.setItem(`filterState:${this.filterDtId}`, JSON.stringify(payload))
|
|
218
300
|
} catch (e) {}
|
|
301
|
+
|
|
302
|
+
// restoring saved filters is not a filter change, so keep the page restored by stateSave
|
|
303
|
+
if (Object.keys(payload[rootKey] || {}).length > 0) {
|
|
304
|
+
this.reloadAppDatatable(false)
|
|
305
|
+
}
|
|
219
306
|
}
|
|
220
307
|
|
|
221
308
|
// recursively populate children of parent, restore each child's saved value, then recurse
|
|
222
309
|
async populateDependents(parent, savedParams = {}) {
|
|
223
|
-
this.selects = this.selects || Array.from(this.element.querySelectorAll('select[data-filter-remote-url-value]'))
|
|
224
310
|
const parentKey = parent.dataset.filterFieldName
|
|
225
|
-
const children = this.
|
|
311
|
+
const children = this.remoteSelects.filter(s => s.dataset.filterDependsOn === parentKey)
|
|
226
312
|
|
|
227
313
|
for (const child of children) {
|
|
228
314
|
this.resetSelect(child)
|
|
229
315
|
|
|
230
|
-
|
|
316
|
+
const parentValue = this.getFieldValue(parent)
|
|
317
|
+
if (this.isValueEmpty(parentValue)) {
|
|
231
318
|
await this.populateDependents(child, savedParams)
|
|
232
319
|
continue
|
|
233
320
|
}
|
|
234
321
|
|
|
235
|
-
// populate child using parent's current value substituted by populate()
|
|
236
322
|
await this.populate(child)
|
|
237
|
-
|
|
323
|
+
|
|
238
324
|
const childSaved = savedParams[child.dataset.filterFieldName]
|
|
239
|
-
const
|
|
325
|
+
const hasSetValue = child.dataset.filterSetValue || child.dataset.filterSetValues
|
|
326
|
+
if (childSaved && !hasSetValue) this.setFieldValue(child, childSaved)
|
|
240
327
|
|
|
241
|
-
if (childSaved && !set_value) {
|
|
242
|
-
child.value = childSaved
|
|
243
|
-
}
|
|
244
|
-
// recurse deeper
|
|
245
328
|
await this.populateDependents(child, savedParams)
|
|
246
329
|
}
|
|
247
330
|
}
|
|
248
331
|
|
|
249
|
-
// duration handler (unchanged)
|
|
250
332
|
durationChanged(event) {
|
|
251
333
|
const select = event.target
|
|
252
334
|
|
|
@@ -267,4 +349,4 @@ export default class extends Controller {
|
|
|
267
349
|
document.querySelectorAll('.customDurationField').forEach(el => el.remove())
|
|
268
350
|
}
|
|
269
351
|
}
|
|
270
|
-
}
|
|
352
|
+
}
|
|
@@ -8,6 +8,7 @@ module StimulusRailsDatatables
|
|
|
8
8
|
length_change = options.fetch(:length_change, true)
|
|
9
9
|
state_save = options.fetch(:state_save, true)
|
|
10
10
|
responsive = options.fetch(:responsive, true)
|
|
11
|
+
reset_paging = options[:reset_paging] # nil falls back to the global resetPaging config
|
|
11
12
|
columns = []
|
|
12
13
|
|
|
13
14
|
capture(DatatableBuilder.new(self, columns), &block)
|
|
@@ -22,7 +23,8 @@ module StimulusRailsDatatables
|
|
|
22
23
|
datatable_searching_value: searching,
|
|
23
24
|
datatable_length_change_value: length_change,
|
|
24
25
|
datatable_state_save_value: state_save,
|
|
25
|
-
datatable_responsive_value: responsive
|
|
26
|
+
datatable_responsive_value: responsive,
|
|
27
|
+
datatable_reset_paging_value: reset_paging
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
content_tag(:div, data: data) do
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
|
|
3
4
|
module StimulusRailsDatatables
|
|
4
5
|
module FilterHelper
|
|
5
6
|
def filter_for(datatable_id, root_key: 'filters', &)
|
|
@@ -39,6 +40,7 @@ module StimulusRailsDatatables
|
|
|
39
40
|
else
|
|
40
41
|
remote = options[:remote] || {}
|
|
41
42
|
depends_on = options[:depends_on]
|
|
43
|
+
multiple = options[:multiple] || false
|
|
42
44
|
|
|
43
45
|
attrs = {
|
|
44
46
|
filter_field_name: name,
|
|
@@ -47,16 +49,22 @@ module StimulusRailsDatatables
|
|
|
47
49
|
filter_value_key: remote[:value],
|
|
48
50
|
filter_depends_on: depends_on&.to_s,
|
|
49
51
|
filter_placeholder: remote[:placeholder] || 'Select an option',
|
|
50
|
-
filter_set_value: remote[:set_value]
|
|
52
|
+
filter_set_value: remote[:set_value],
|
|
53
|
+
filter_set_values: remote[:set_values],
|
|
54
|
+
filter_tomselect: options[:tomselect]
|
|
55
|
+
|
|
51
56
|
}
|
|
52
57
|
|
|
53
58
|
select_options = {
|
|
54
59
|
data: attrs,
|
|
55
|
-
class: html_class
|
|
60
|
+
class: html_class,
|
|
61
|
+
multiple: multiple
|
|
56
62
|
}
|
|
57
63
|
|
|
58
64
|
# Only disable if it depends on another field
|
|
59
65
|
select_options[:disabled] = true if depends_on.present?
|
|
66
|
+
field_name = "#{field_name}[]" if multiple
|
|
67
|
+
|
|
60
68
|
|
|
61
69
|
@view.select_tag(field_name, @view.options_for_select([]), **select_options)
|
|
62
70
|
end
|
|
@@ -94,6 +102,8 @@ module StimulusRailsDatatables
|
|
|
94
102
|
placeholder: 'All Barangays'
|
|
95
103
|
},
|
|
96
104
|
depends_on: :city_id,
|
|
105
|
+
multiple: true,
|
|
106
|
+
tomselect: true,
|
|
97
107
|
class: html_class
|
|
98
108
|
)
|
|
99
109
|
|
|
@@ -41,5 +41,8 @@ window.datatablesConfig = {
|
|
|
41
41
|
},
|
|
42
42
|
|
|
43
43
|
// Length menu options
|
|
44
|
-
lengthMenu: [[10, 25, 50, 100], [10, 25, 50, 100]]
|
|
44
|
+
lengthMenu: [[10, 25, 50, 100], [10, 25, 50, 100]],
|
|
45
|
+
|
|
46
|
+
// Go back to the first page when filters change (override per table with datatable_for reset_paging:)
|
|
47
|
+
resetPaging: false
|
|
45
48
|
}
|