formstrap 0.4.5 → 0.4.7

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '0379b9b80ad9551b56d07884264fcc37132b72d5539976ec4034800a09adaeb2'
4
- data.tar.gz: bbd8d0892fce51fa07375b3e6a414e169344825f5da142c5a06ab87a7696424b
3
+ metadata.gz: 7b83b9b5c0b00d0d5b3d84a26ca44c16335321c28c6c3e183a84d51fd3c2c45b
4
+ data.tar.gz: 11d2ec48abdc5ffaaf223d8c3401f3e47a3e6e8d4de813590b8f2543a902ef4e
5
5
  SHA512:
6
- metadata.gz: '0788d10916489c6fabe8798a87ffa10a75d46a4e57a2cd8c73455cc64630414ebbdbcfe213041437b22d108df2a3fa986e3692e8ea46ba25fdc28bb068142a42'
7
- data.tar.gz: 3fba876a0ce0c7be868100726d60af6b1b5aae1b8af675e4861b9ba016405920b1ff35f537a12ce9aa10865fb1e599662170446d999a9bc6e69752f9cf2dc252
6
+ metadata.gz: 5cdfdd6829349c2e8e3821bfc3d7ab80b7414f125126d20e9158475e337a1bc7e974a83a0209f6f56e4e45cf115599498f25976a23b616cd9006e54be16a9042
7
+ data.tar.gz: 22c288a45528576ff1c91c76775003fb0ebeead2bb051c492e3923390e876b49fab4b936924e29b46b5484efd1fa43cfd15f3194fab63bf4e97ea59242d585c0
@@ -12,7 +12,10 @@ export default class extends Controller {
12
12
 
13
13
  connect () {
14
14
  this.validate()
15
- this.updateCount()
15
+
16
+ if (this.maxSelectedItems() !== 1) {
17
+ this.updateCount()
18
+ }
16
19
  }
17
20
 
18
21
  // Actions
@@ -27,11 +30,29 @@ export default class extends Controller {
27
30
  }
28
31
 
29
32
  inputChange (event) {
30
- this.handleIdsUpdate(event.target)
31
- this.updateCount()
33
+ if (this.maxSelectedItems() === 1) {
34
+ this.selectOneItem(event.target)
35
+ } else {
36
+ this.selectMultipleItems(event.target)
37
+ }
32
38
  }
33
39
 
34
40
  // Methods
41
+ selectOneItem (element) {
42
+ this.idsValue = []
43
+
44
+ for (const checkbox of this.idCheckboxTargets.filter(e => e.value !== element.value)) {
45
+ checkbox.checked = false
46
+ }
47
+
48
+ this.handleIdsUpdate(element)
49
+ }
50
+
51
+ selectMultipleItems (element) {
52
+ this.handleIdsUpdate(element)
53
+ this.updateCount()
54
+ }
55
+
35
56
  hidePlaceholder () {
36
57
  this.placeholderTarget.classList.add('d-none')
37
58
  }
@@ -54,7 +75,7 @@ export default class extends Controller {
54
75
  })
55
76
  }
56
77
 
57
- this.handleSearchdIdsUpdate()
78
+ this.handleSearchIdsUpdate()
58
79
  }
59
80
 
60
81
  itemTargetConnected (element) {
@@ -155,7 +176,7 @@ export default class extends Controller {
155
176
  this.countTarget.innerHTML = this.selectedItemsCount()
156
177
  }
157
178
 
158
- handleSearchdIdsUpdate () {
179
+ handleSearchIdsUpdate () {
159
180
  this.deleteSearchIdInputs()
160
181
  this.createSearchIdInputs()
161
182
  }
@@ -4,40 +4,159 @@ import I18n from '../config/i18n'
4
4
 
5
5
  export default class extends Controller {
6
6
  static values = {
7
- selected: Array
7
+ remoteUrl: String,
8
+ remoteValue: String,
9
+ remoteLabel: String,
10
+ remoteQueryParam: String
11
+ }
12
+
13
+ initialize () {
14
+ this.tomSelect = undefined
15
+ this.perPage = 24
8
16
  }
9
17
 
10
18
  connect () {
11
- if (this.element.hasAttribute('multiple') || this.element.dataset.tomSelect === 'true') {
12
- this.initTomSelect()
19
+ if (this.isMultiple() || this.isTomSelect() || this.isRemote()) {
20
+ this.tomSelect = this.initTomSelect()
21
+ }
22
+ }
23
+
24
+ disconnect () {
25
+ if (this.element.tomselect) {
26
+ this.element.tomselect.destroy()
13
27
  }
14
28
  }
15
29
 
16
30
  defaultOptions () {
17
31
  return {
18
- plugins: ['drag_drop', 'caret_position', 'input_autogrow'],
32
+ plugins: {
33
+ caret_position: {},
34
+ drag_drop: {},
35
+ input_autogrow: {}
36
+ },
19
37
  persist: false,
20
38
  create: true,
21
39
  render: this.renderOptions()[I18n.locale]
22
40
  }
23
41
  }
24
42
 
43
+ isMultiple () {
44
+ return this.element.hasAttribute('multiple')
45
+ }
46
+
47
+ isTomSelect () {
48
+ return this.element.dataset.tomSelect === 'true'
49
+ }
50
+
51
+ isRemote () {
52
+ return this.remoteUrlValue
53
+ }
54
+
55
+ setQueryParam (url, key, value) {
56
+ const urlObj = new URL(url)
57
+ const params = urlObj.searchParams
58
+
59
+ // Adds if not exists, updates if exists
60
+ params.set(key, value)
61
+
62
+ return urlObj.toString()
63
+ }
64
+
65
+ getQueryParam (url, key) {
66
+ const urlObj = new URL(url)
67
+ const params = urlObj.searchParams
68
+
69
+ return params.get(key)
70
+ }
71
+
72
+ firstUrl () {
73
+ return (query) => {
74
+ let url = `${this.remoteUrlValue}.json`
75
+ url = this.setQueryParam(url, this.remoteQueryParamValue, query)
76
+ url = this.setQueryParam(url, 'per_page', this.perPage)
77
+ url = this.setQueryParam(url, 'page', 1)
78
+ return url
79
+ }
80
+ }
81
+
82
+ load () {
83
+ return (query, callback) => {
84
+ let url = this.tomSelect.getUrl(query)
85
+
86
+ fetch(url)
87
+ .then(response => response.json())
88
+ .then(json => {
89
+ if (json.length === this.perPage) {
90
+ // Update page param for next call
91
+ const currentPage = parseInt(this.getQueryParam(url, 'page')) || 1
92
+ url = this.setQueryParam(url, 'page', currentPage + 1)
93
+ this.tomSelect.setNextUrl(query, url)
94
+ } else {
95
+ this.tomSelect.setNextUrl(query, undefined)
96
+ }
97
+
98
+ callback(json)
99
+ })
100
+ .catch(() => { callback() })
101
+ }
102
+ }
103
+
25
104
  renderOptions () {
26
105
  return {
27
106
  en: {
28
107
  option_create: function (data, escape) {
29
- return '<div class="create">Add <strong>' + escape(data.input) + '</strong>&hellip;</div>'
108
+ return `<div class="create">Add <strong>${escape(data.input)}</strong>&hellip;</div>`
30
109
  },
31
110
  no_results: function (data, escape) {
32
111
  return '<div class="no-results">No results found</div>'
112
+ },
113
+ loading_more: function (data, escape) {
114
+ return '<div class="loading-more-results">Loading more results ... </div>'
115
+ },
116
+ no_more_results: function (data, escape) {
117
+ return '<div class="no-more-results">No more results</div>'
33
118
  }
34
119
  },
35
120
  nl: {
36
121
  option_create: function (data, escape) {
37
- return '<div class="create">Voeg <strong>' + escape(data.input) + '</strong> toe &hellip;</div>'
122
+ return `<div class="create">Voeg <strong>${escape(data.input)}</strong> toe &hellip;</div>`
38
123
  },
39
124
  no_results: function (data, escape) {
40
125
  return '<div class="no-results">Geen resultaten gevonden</div>'
126
+ },
127
+ loading_more: function (data, escape) {
128
+ return '<div class="loading-more-results">Laad meer resultaten ... </div>'
129
+ },
130
+ no_more_results: function (data, escape) {
131
+ return '<div class="no-more-results">Geen resultaten meer</div>'
132
+ }
133
+ },
134
+ fr: {
135
+ option_create: function (data, escape) {
136
+ return `<div class="create">Ajouter <strong>${escape(data.input)}</strong>&hellip;</div>`
137
+ },
138
+ no_results: function (data, escape) {
139
+ return '<div class="no-results">Aucun résultat trouvé</div>'
140
+ },
141
+ loading_more: function (data, escape) {
142
+ return '<div class="loading-more-results">Chargement de plus de résultats ... </div>'
143
+ },
144
+ no_more_results: function (data, escape) {
145
+ return '<div class="no-more-results">Plus de résultats</div>'
146
+ }
147
+ },
148
+ de: {
149
+ option_create: function (data, escape) {
150
+ return `<div class="create">Hinzufügen <strong>${escape(data.input)}</strong>&hellip;</div>`
151
+ },
152
+ no_results: function (data, escape) {
153
+ return '<div class="no-results">Keine Ergebnisse gefunden</div>'
154
+ },
155
+ loading_more: function (data, escape) {
156
+ return '<div class="loading-more-results">Lade weitere Ergebnisse ... </div>'
157
+ },
158
+ no_more_results: function (data, escape) {
159
+ return '<div class="no-more-results">Keine weiteren Ergebnisse</div>'
41
160
  }
42
161
  }
43
162
  }
@@ -51,10 +170,29 @@ export default class extends Controller {
51
170
  const defaultOptions = this.defaultOptions()
52
171
  const options = {
53
172
  create: this.hasTags(),
54
- items: this.selectedValue
173
+ ...(this.isRemote() && {
174
+ plugins: {
175
+ caret_position: {},
176
+ drag_drop: {},
177
+ input_autogrow: {},
178
+ virtual_scroll: {}
179
+ },
180
+ valueField: this.remoteValueValue,
181
+ labelField: this.remoteLabelValue,
182
+ searchField: this.remoteLabelValue,
183
+ firstUrl: this.firstUrl(),
184
+ load: this.load(),
185
+ // Infinite options
186
+ maxOptions: null,
187
+ // Fetch first items when focused
188
+ onFocus: () => {
189
+ this.tomSelect.clearOptions()
190
+ this.tomSelect.setNextUrl('', this.firstUrl()(''))
191
+ this.tomSelect.load('')
192
+ }
193
+ })
55
194
  }
56
195
 
57
- /* eslint-disable no-new */
58
- new TomSelect(this.element, { ...defaultOptions, ...options })
196
+ return new TomSelect(this.element, { ...defaultOptions, ...options })
59
197
  }
60
198
  }
@@ -11289,7 +11289,9 @@ var media_modal_controller_default = class extends Controller {
11289
11289
  }
11290
11290
  connect() {
11291
11291
  this.validate();
11292
- this.updateCount();
11292
+ if (this.maxSelectedItems() !== 1) {
11293
+ this.updateCount();
11294
+ }
11293
11295
  }
11294
11296
  select() {
11295
11297
  this.dispatchSelectionEvent();
@@ -11300,7 +11302,21 @@ var media_modal_controller_default = class extends Controller {
11300
11302
  this.triggerFormSubmission();
11301
11303
  }
11302
11304
  inputChange(event) {
11303
- this.handleIdsUpdate(event.target);
11305
+ if (this.maxSelectedItems() === 1) {
11306
+ this.selectOneItem(event.target);
11307
+ } else {
11308
+ this.selectMultipleItems(event.target);
11309
+ }
11310
+ }
11311
+ selectOneItem(element) {
11312
+ this.idsValue = [];
11313
+ for (const checkbox of this.idCheckboxTargets.filter((e) => e.value !== element.value)) {
11314
+ checkbox.checked = false;
11315
+ }
11316
+ this.handleIdsUpdate(element);
11317
+ }
11318
+ selectMultipleItems(element) {
11319
+ this.handleIdsUpdate(element);
11304
11320
  this.updateCount();
11305
11321
  }
11306
11322
  hidePlaceholder() {
@@ -11321,7 +11337,7 @@ var media_modal_controller_default = class extends Controller {
11321
11337
  return element.value !== value;
11322
11338
  });
11323
11339
  }
11324
- this.handleSearchdIdsUpdate();
11340
+ this.handleSearchIdsUpdate();
11325
11341
  }
11326
11342
  itemTargetConnected(element) {
11327
11343
  this.updateItem(element.querySelector("input"));
@@ -11405,7 +11421,7 @@ var media_modal_controller_default = class extends Controller {
11405
11421
  updateCount() {
11406
11422
  this.countTarget.innerHTML = this.selectedItemsCount();
11407
11423
  }
11408
- handleSearchdIdsUpdate() {
11424
+ handleSearchIdsUpdate() {
11409
11425
  this.deleteSearchIdInputs();
11410
11426
  this.createSearchIdInputs();
11411
11427
  }
@@ -13397,35 +13413,134 @@ var repeater_controller_default = class extends Controller {
13397
13413
  // app/assets/javascripts/formstrap/controllers/select_controller.js
13398
13414
  var import_tom_select = __toESM(require_tom_select_complete());
13399
13415
  var select_controller_default = class extends Controller {
13416
+ initialize() {
13417
+ this.tomSelect = void 0;
13418
+ this.perPage = 24;
13419
+ }
13400
13420
  connect() {
13401
- if (this.element.hasAttribute("multiple") || this.element.dataset.tomSelect === "true") {
13402
- this.initTomSelect();
13421
+ if (this.isMultiple() || this.isTomSelect() || this.isRemote()) {
13422
+ this.tomSelect = this.initTomSelect();
13423
+ }
13424
+ }
13425
+ disconnect() {
13426
+ if (this.element.tomselect) {
13427
+ this.element.tomselect.destroy();
13403
13428
  }
13404
13429
  }
13405
13430
  defaultOptions() {
13406
13431
  return {
13407
- plugins: ["drag_drop", "caret_position", "input_autogrow"],
13432
+ plugins: {
13433
+ caret_position: {},
13434
+ drag_drop: {},
13435
+ input_autogrow: {}
13436
+ },
13408
13437
  persist: false,
13409
13438
  create: true,
13410
13439
  render: this.renderOptions()[i18n_default.locale]
13411
13440
  };
13412
13441
  }
13442
+ isMultiple() {
13443
+ return this.element.hasAttribute("multiple");
13444
+ }
13445
+ isTomSelect() {
13446
+ return this.element.dataset.tomSelect === "true";
13447
+ }
13448
+ isRemote() {
13449
+ return this.remoteUrlValue;
13450
+ }
13451
+ setQueryParam(url, key, value) {
13452
+ const urlObj = new URL(url);
13453
+ const params = urlObj.searchParams;
13454
+ params.set(key, value);
13455
+ return urlObj.toString();
13456
+ }
13457
+ getQueryParam(url, key) {
13458
+ const urlObj = new URL(url);
13459
+ const params = urlObj.searchParams;
13460
+ return params.get(key);
13461
+ }
13462
+ firstUrl() {
13463
+ return (query) => {
13464
+ let url = `${this.remoteUrlValue}.json`;
13465
+ url = this.setQueryParam(url, this.remoteQueryParamValue, query);
13466
+ url = this.setQueryParam(url, "per_page", this.perPage);
13467
+ url = this.setQueryParam(url, "page", 1);
13468
+ return url;
13469
+ };
13470
+ }
13471
+ load() {
13472
+ return (query, callback) => {
13473
+ let url = this.tomSelect.getUrl(query);
13474
+ fetch(url).then((response) => response.json()).then((json) => {
13475
+ if (json.length === this.perPage) {
13476
+ const currentPage = parseInt(this.getQueryParam(url, "page")) || 1;
13477
+ url = this.setQueryParam(url, "page", currentPage + 1);
13478
+ this.tomSelect.setNextUrl(query, url);
13479
+ } else {
13480
+ this.tomSelect.setNextUrl(query, void 0);
13481
+ }
13482
+ callback(json);
13483
+ }).catch(() => {
13484
+ callback();
13485
+ });
13486
+ };
13487
+ }
13413
13488
  renderOptions() {
13414
13489
  return {
13415
13490
  en: {
13416
13491
  option_create: function(data, escape) {
13417
- return '<div class="create">Add <strong>' + escape(data.input) + "</strong>&hellip;</div>";
13492
+ return `<div class="create">Add <strong>${escape(data.input)}</strong>&hellip;</div>`;
13418
13493
  },
13419
13494
  no_results: function(data, escape) {
13420
13495
  return '<div class="no-results">No results found</div>';
13496
+ },
13497
+ loading_more: function(data, escape) {
13498
+ return '<div class="loading-more-results">Loading more results ... </div>';
13499
+ },
13500
+ no_more_results: function(data, escape) {
13501
+ return '<div class="no-more-results">No more results</div>';
13421
13502
  }
13422
13503
  },
13423
13504
  nl: {
13424
13505
  option_create: function(data, escape) {
13425
- return '<div class="create">Voeg <strong>' + escape(data.input) + "</strong> toe &hellip;</div>";
13506
+ return `<div class="create">Voeg <strong>${escape(data.input)}</strong> toe &hellip;</div>`;
13426
13507
  },
13427
13508
  no_results: function(data, escape) {
13428
13509
  return '<div class="no-results">Geen resultaten gevonden</div>';
13510
+ },
13511
+ loading_more: function(data, escape) {
13512
+ return '<div class="loading-more-results">Laad meer resultaten ... </div>';
13513
+ },
13514
+ no_more_results: function(data, escape) {
13515
+ return '<div class="no-more-results">Geen resultaten meer</div>';
13516
+ }
13517
+ },
13518
+ fr: {
13519
+ option_create: function(data, escape) {
13520
+ return `<div class="create">Ajouter <strong>${escape(data.input)}</strong>&hellip;</div>`;
13521
+ },
13522
+ no_results: function(data, escape) {
13523
+ return '<div class="no-results">Aucun r\xE9sultat trouv\xE9</div>';
13524
+ },
13525
+ loading_more: function(data, escape) {
13526
+ return '<div class="loading-more-results">Chargement de plus de r\xE9sultats ... </div>';
13527
+ },
13528
+ no_more_results: function(data, escape) {
13529
+ return '<div class="no-more-results">Plus de r\xE9sultats</div>';
13530
+ }
13531
+ },
13532
+ de: {
13533
+ option_create: function(data, escape) {
13534
+ return `<div class="create">Hinzuf\xFCgen <strong>${escape(data.input)}</strong>&hellip;</div>`;
13535
+ },
13536
+ no_results: function(data, escape) {
13537
+ return '<div class="no-results">Keine Ergebnisse gefunden</div>';
13538
+ },
13539
+ loading_more: function(data, escape) {
13540
+ return '<div class="loading-more-results">Lade weitere Ergebnisse ... </div>';
13541
+ },
13542
+ no_more_results: function(data, escape) {
13543
+ return '<div class="no-more-results">Keine weiteren Ergebnisse</div>';
13429
13544
  }
13430
13545
  }
13431
13546
  };
@@ -13437,13 +13552,34 @@ var select_controller_default = class extends Controller {
13437
13552
  const defaultOptions = this.defaultOptions();
13438
13553
  const options = {
13439
13554
  create: this.hasTags(),
13440
- items: this.selectedValue
13555
+ ...this.isRemote() && {
13556
+ plugins: {
13557
+ caret_position: {},
13558
+ drag_drop: {},
13559
+ input_autogrow: {},
13560
+ virtual_scroll: {}
13561
+ },
13562
+ valueField: this.remoteValueValue,
13563
+ labelField: this.remoteLabelValue,
13564
+ searchField: this.remoteLabelValue,
13565
+ firstUrl: this.firstUrl(),
13566
+ load: this.load(),
13567
+ maxOptions: null,
13568
+ onFocus: () => {
13569
+ this.tomSelect.clearOptions();
13570
+ this.tomSelect.setNextUrl("", this.firstUrl()(""));
13571
+ this.tomSelect.load("");
13572
+ }
13573
+ }
13441
13574
  };
13442
- new import_tom_select.default(this.element, { ...defaultOptions, ...options });
13575
+ return new import_tom_select.default(this.element, { ...defaultOptions, ...options });
13443
13576
  }
13444
13577
  };
13445
13578
  __publicField(select_controller_default, "values", {
13446
- selected: Array
13579
+ remoteUrl: String,
13580
+ remoteValue: String,
13581
+ remoteLabel: String,
13582
+ remoteQueryParam: String
13447
13583
  });
13448
13584
 
13449
13585
  // app/assets/javascripts/formstrap/controllers/textarea_controller.js
@@ -36,7 +36,7 @@ module Formstrap
36
36
  end
37
37
 
38
38
  def attribute_with_id
39
- attribute_with_id = collection? ? "#{association_foreign_key}s" : foreign_key
39
+ attribute_with_id = collection? ? "#{singular_name}_ids" : foreign_key
40
40
 
41
41
  if attribute_with_id.nil?
42
42
  raise(AssociationDoesNotExistError, "Association attribute that was passed does not exist.")
@@ -46,11 +46,28 @@ module Formstrap
46
46
  end
47
47
 
48
48
  def collection
49
- association_class.all.map { |item| [item.to_s, item.id] }
49
+ if remote
50
+ collection_for_values
51
+ else
52
+ association_class.all.map { |item| [item.to_s, item.id] }
53
+ end
50
54
  end
51
55
 
52
56
  private
53
57
 
58
+ def collection_for_values
59
+ if collection?
60
+ form.object.send(attribute).map { |item| option_for_item[item] }
61
+ else
62
+ [option_for_item(form.object.send(attribute))]
63
+ end
64
+ end
65
+
66
+ def option_for_item(item)
67
+ return unless item
68
+ [item.send(remote[:label]), item.send(remote[:value])]
69
+ end
70
+
54
71
  def association_foreign_key
55
72
  reflection.association_foreign_key
56
73
  end
@@ -63,6 +80,10 @@ module Formstrap
63
80
  form.object.class.reflect_on_association(attribute)
64
81
  end
65
82
 
83
+ def singular_name
84
+ reflection.name.to_s.singularize
85
+ end
86
+
66
87
  def association_class
67
88
  reflection.klass
68
89
  end
@@ -85,6 +106,7 @@ module Formstrap
85
106
  tags: tags,
86
107
  controller: "select"
87
108
  },
109
+ remote: remote,
88
110
  multiple: tags,
89
111
  placeholder: placeholder
90
112
  }
@@ -92,7 +92,7 @@ module Formstrap
92
92
  if attached.is_a?(ActiveStorage::Attached::Many)
93
93
  form.object.send(nested_attribute).build
94
94
  else
95
- form.object.send("build_#{nested_attribute}")
95
+ form.object.send(:"build_#{nested_attribute}")
96
96
  end
97
97
  end
98
98
 
@@ -24,7 +24,7 @@ module Formstrap
24
24
  lang: I18n.locale,
25
25
  # button to control a block/line in the editor
26
26
  control: false,
27
- minHeight: '57px',
27
+ minHeight: "57px",
28
28
  theme: "light",
29
29
  # Popup when highlighting text
30
30
  context: false,
@@ -34,11 +34,11 @@ module Formstrap
34
34
  # Options in block/line popup
35
35
  control: [],
36
36
  # Options in format popup
37
- format: %w[text h1 h2 h3 h4],
37
+ format: %w[text h1 h2 h3 h4]
38
38
  },
39
39
  block: {
40
40
  # Outline block/line in the editor
41
- outline: false,
41
+ outline: false
42
42
  },
43
43
  buttons: {
44
44
  # Options when highlighting text
@@ -46,9 +46,9 @@ module Formstrap
46
46
  # Options in toolbar on the right
47
47
  extrabar: %w[],
48
48
  # Options in toolbar on the left
49
- toolbar: %w[format bold italic deleted list table link html],
49
+ toolbar: %w[format bold italic deleted list table link html]
50
50
  },
51
- plugins: %w[emoji linkstyles],
51
+ plugins: %w[emoji linkstyles]
52
52
  }.delete_if { |k, v| v.nil? }
53
53
  end
54
54
  end
@@ -9,7 +9,7 @@ module Formstrap
9
9
  include Formstrap::Wrappable
10
10
 
11
11
  def input_options
12
- keys = attributes - %i[append attribute collection float form input_group include_blank label prepend validate selected tags wrapper]
12
+ keys = attributes - %i[append attribute collection float form input_group include_blank label prepend validate selected tags wrapper remote]
13
13
  options = to_h.slice(*keys)
14
14
  default_input_options.deep_merge(options)
15
15
  end
@@ -35,9 +35,27 @@ module Formstrap
35
35
  private
36
36
 
37
37
  def default_options
38
- selected = attribute.nil? ? nil : form.object&.send(attribute)
39
38
  {
40
- selected: selected
39
+ selected: value
40
+ }
41
+ end
42
+
43
+ def value
44
+ attribute.nil? ? nil : form.object&.send(attribute)
45
+ end
46
+
47
+ def is_remote?
48
+ return false unless remote
49
+ remote.has_key?(:url)
50
+ end
51
+
52
+ def remote_options
53
+ return nil unless is_remote?
54
+ {
55
+ select_remote_url_value: remote[:url],
56
+ select_remote_value_value: remote&.dig(:value) || "name",
57
+ select_remote_label_value: remote&.dig(:label) || "id",
58
+ select_remote_query_param_value: remote&.dig(:query_param) || "search"
41
59
  }
42
60
  end
43
61
 
@@ -48,7 +66,7 @@ module Formstrap
48
66
  data: {
49
67
  tags: tags,
50
68
  controller: "select",
51
- "select_selected_value": select_options[:selected]
69
+ **remote_options
52
70
  },
53
71
  multiple: tags,
54
72
  placeholder: placeholder
@@ -14,7 +14,7 @@ module Formstrap
14
14
  options = {
15
15
  redactor: {
16
16
  context: !toolbar,
17
- extrabar: toolbar,
17
+ extrabar: toolbar
18
18
  }
19
19
  }
20
20
 
@@ -24,7 +24,7 @@ class ViewModel
24
24
 
25
25
  def initialize(hash = {})
26
26
  hash.each do |key, value|
27
- instance_variable_set("@#{key}", value)
27
+ instance_variable_set(:"@#{key}", value)
28
28
  end
29
29
  end
30
30
 
@@ -45,11 +45,11 @@ class ViewModel
45
45
  end
46
46
 
47
47
  def value_for(attribute)
48
- reserved_methods.include?(attribute) ? instance_variable_get("@#{attribute}") : send(attribute)
48
+ reserved_methods.include?(attribute) ? instance_variable_get(:"@#{attribute}") : send(attribute)
49
49
  end
50
50
 
51
51
  def method_missing(m, *args, &block)
52
- instance_variable_get("@#{m}")
52
+ instance_variable_get(:"@#{m}")
53
53
  end
54
54
 
55
55
  def respond_to_missing?
@@ -6,7 +6,21 @@
6
6
  # * +form+ - Form object
7
7
  #
8
8
  # ==== Optional parameters
9
+ # * +append+ - Display as input group with text on the right-hand side
9
10
  # * +collection+ - Values to create option tags for
11
+ # * +float+ - Use floating labels. Defaults to false
12
+ # * +hint+ - Informative text to assist with data input. HTML markup is allowed.
13
+ # * +label+ - Text to display inside label tag. Defaults to the attribute name. Set to false if you don"t want to show a label.
14
+ # * +plaintext+ - Render input as plain text.
15
+ # * +prepend+ - Display as input group with text on the left-hand side
16
+ # * +remote+ - Hash with all options for remote data fetching
17
+ # * +wrapper+ - Hash with all options for the surrounding html tag
18
+ #
19
+ # ==== Remote options
20
+ # * +url+ -- JSON endpoint to fetch data from
21
+ # * +value+ -- JSON attribute to use as the value for the option tag
22
+ # * +label+ -- JSON attribute to use as the label for the option tag
23
+ # * +query_param+ -- The query parameter used for searching the json endpoint, default: "search"
10
24
  #
11
25
  #
12
26
  # ==== Examples
@@ -14,17 +28,25 @@
14
28
  # <%= form_with do |form| %#>
15
29
  # <%= render "formstrap/association", form: form, attribute: :product %#>
16
30
  # <% end %#>
31
+ #
32
+ # Remote data
33
+ # <%= form_with do |form| %#>
34
+ # <%= render "formstrap/association", form: form, attribute: :product, remote: {url: admin_products_path, value: "id", label: "name"} %#>
35
+ # <% end %#>
17
36
 
18
37
  association = Formstrap::AssociationView.new(local_assigns)
19
-
20
38
  %>
21
39
 
22
- <%= render "formstrap/wrapper", association.wrapper_options do %>
23
- <%= render "formstrap/label", association.label_options if association.prepend_label? %>
24
- <%= render "formstrap/input_group", association.input_group_options do %>
25
- <%= form.select(association.attribute_with_id, formstrap: false, choices: association.collection, options: association.select_options, html_options: association.input_options) %>
26
- <% end %>
27
- <%= render "formstrap/validation", association.validation_options if association.validate? %>
28
- <%= render "formstrap/hint", association.hint_options if association.hint? %>
29
- <%= render "formstrap/label", association.label_options if association.append_label? %>
30
- <% end %>
40
+ <%= render(
41
+ "formstrap/select",
42
+ form: form,
43
+ attribute: association.attribute_with_id,
44
+ collection: association.collection,
45
+ wrapper: association.wrapper_options,
46
+ float: association.float,
47
+ hint: association.hint,
48
+ plaintext: association.plaintext,
49
+ prepend: association.prepend,
50
+ append: association.append,
51
+ remote: association.remote
52
+ ) %>
@@ -16,6 +16,13 @@
16
16
  # * +prepend+ - Display as input group with text on the left-hand side
17
17
  # * +tags+ - Allow options to be created dynamically. This will set the multiple attribute to true
18
18
  # * +wrapper+ - Hash with all options for the surrounding html tag
19
+ # * +remote+ - Hash with all options for remote data fetching
20
+ #
21
+ # ==== Remote options
22
+ # * +url+ -- JSON endpoint to fetch data from
23
+ # * +value+ -- JSON attribute to use as the value for the option tag
24
+ # * +label+ -- JSON attribute to use as the label for the option tag
25
+ # * +query_param+ -- The query parameter used for searching the json endpoint
19
26
  #
20
27
  # ==== References
21
28
  # https://headmin.dev/docs/forms/select
@@ -27,6 +34,11 @@
27
34
  # <%= form_with do |form| %#>
28
35
  # <%= render "formstrap/select", form: form, attribute: :color, collection: %w[red green blue] %#>
29
36
  # <% end %#>
37
+ #
38
+ # Remote data
39
+ # <%= form_with do |form| %#>
40
+ # <%= render "formstrap/select", form: form, attribute: :id, collection: [["Page 1", 1]], remote: {url: admin_pages_path, value: "id", label: "title"} %#>
41
+ # <% end %#>
30
42
 
31
43
  select = Formstrap::SelectView.new(local_assigns)
32
44
  %>
@@ -34,7 +46,13 @@
34
46
  <%= render "formstrap/wrapper", select.wrapper_options do %>
35
47
  <%= render "formstrap/label", select.label_options if select.prepend_label? %>
36
48
  <%= render "formstrap/input_group", select.input_group_options do %>
37
- <%= form.select(select.attribute, formstrap: false, choices: select.collection, options: select.select_options, html_options: select.input_options) %>
49
+ <%= form.select(
50
+ select.attribute,
51
+ formstrap: false,
52
+ choices: select.collection,
53
+ options: select.select_options,
54
+ html_options: select.input_options
55
+ ) %>
38
56
  <% end %>
39
57
  <%= render "formstrap/validation", select.validation_options if select.validate? %>
40
58
  <%= render "formstrap/hint", select.hint_options if select.hint? %>
@@ -59,7 +59,10 @@
59
59
  <% end %>
60
60
  <button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><%= t(".close") %></button>
61
61
  <button type="button" class="btn btn-primary" data-bs-dismiss="modal" data-action="click->media-modal#select" data-media-modal-target="selectButton">
62
- <%= t(".select") %> (<span data-media-modal-target="count">0</span><%= t(".maximum", count: max.to_i) if max.present? %>)
62
+ <%= t(".select") %>
63
+ <% unless max.present? && max.to_i == 1 %>
64
+ (<span data-media-modal-target="count">0</span><%= t(".maximum", count: max.to_i) if max.present? %>)
65
+ <% end %>
63
66
  </button>
64
67
  </div>
65
68
  </div>
@@ -1,6 +1,17 @@
1
- <div data-controller="nested-preview" data-nested-preview-url-value="<%= url %>">
1
+ <div
2
+ data-controller="nested-preview"
3
+ data-nested-preview-url-value="<%= url %>"
4
+ >
2
5
  <!-- Preview placeholder -->
3
- <div class="nested-preview-iframe-wrapper position-relative" role="button" data-nested-preview-target="iframeWrapper" data-bs-toggle="offcanvas" data-bs-target="#offcanvas-<%= form.options[:child_index] %>" aria-controls="offcanvasRight" data-turbo-cache="false">
6
+ <div
7
+ class="nested-preview-iframe-wrapper position-relative"
8
+ role="button"
9
+ data-nested-preview-target="iframeWrapper"
10
+ data-bs-toggle="offcanvas"
11
+ data-bs-target="#offcanvas-<%= form.options[:child_index] %>"
12
+ aria-controls="offcanvasRight"
13
+ data-turbo-temporary
14
+ >
4
15
  <iframe src="<%= url %>" class="pe-none" data-nested-preview-target="iframe"></iframe>
5
16
  <div data-nested-preview-target="loader" class="nested-preview-loader">
6
17
  <div class="spinner-grow text-secondary" role="status">
@@ -8,27 +19,36 @@
8
19
  </div>
9
20
  </div>
10
21
  </div>
11
-
12
22
  <!-- Preview fields wrapper -->
13
- <div class="offcanvas offcanvas-end nested-preview-offcanvas" tabindex="-1" id="offcanvas-<%= form.options[:child_index] %>" aria-labelledby="offcanvasRightLabel" data-nested-preview-target="offcanvas">
23
+ <div
24
+ class="offcanvas offcanvas-end nested-preview-offcanvas"
25
+ tabindex="-1"
26
+ id="offcanvas-<%= form.options[:child_index] %>"
27
+ aria-labelledby="offcanvasRightLabel"
28
+ data-nested-preview-target="offcanvas"
29
+ >
14
30
  <div class="offcanvas-header">
15
- <h5 class="offcanvas-title" id="offcanvasRightLabel"><%= t('.title', model: form.object.model_name.human) %></h5>
16
- <button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
31
+ <h5 class="offcanvas-title" id="offcanvasRightLabel"><%= t(".title", model: form.object.model_name.human) %></h5>
32
+ <button
33
+ type="button"
34
+ class="btn-close"
35
+ data-bs-dismiss="offcanvas"
36
+ aria-label="Close"
37
+ ></button>
17
38
  </div>
18
39
  <div class="offcanvas-body">
19
40
 
20
41
  <div class="alert alert-danger d-none" data-nested-preview-target="error">
21
42
  <%= t(".error") %>
22
43
  </div>
23
-
24
44
  <!-- Row content -->
25
45
  <div data-nested-preview-target="fields">
26
46
  <%= yield %>
27
47
  </div>
28
-
29
48
  <!-- Preview sync button -->
30
49
  <div class="btn btn-primary" data-action="click->nested-preview#update">
31
- <%= bootstrap_icon("arrow-repeat") %> <%= t('.button') %>
50
+ <%= bootstrap_icon("arrow-repeat") %>
51
+ <%= t(".button") %>
32
52
  </div>
33
53
  </div>
34
54
  </div>
@@ -11,7 +11,7 @@ module Formstrap
11
11
  render_input(:association, attribute, options)
12
12
  end
13
13
 
14
- def preview_button(url = nil, options = {}, &block)
14
+ def preview_button(url = nil, options = {}, &)
15
15
  default_options = {
16
16
  data: {
17
17
  controller: "preview",
@@ -19,7 +19,7 @@ module Formstrap
19
19
  }
20
20
  }
21
21
 
22
- @template.render("formstrap/link", form: self, url: url, options: default_options.deep_merge(options), &block)
22
+ @template.render("formstrap/link", form: self, url: url, options: default_options.deep_merge(options), &)
23
23
  end
24
24
 
25
25
  def checkbox(attribute, formstrap: true, **options)
@@ -114,8 +114,8 @@ module Formstrap
114
114
  end
115
115
  end
116
116
 
117
- def repeater_for(attribute, options = {}, &block)
118
- @template.render("formstrap/repeater", form: self, attribute: attribute, **options, &block)
117
+ def repeater_for(attribute, options = {}, &)
118
+ @template.render("formstrap/repeater", form: self, attribute: attribute, **options, &)
119
119
  end
120
120
 
121
121
  def redactor(attribute, formstrap: true, **options)
@@ -1,15 +1,15 @@
1
1
  module Formstrap
2
2
  module FormHelper
3
- def formstrap_form_for(record, options = {}, &block)
3
+ def formstrap_form_for(record, options = {}, &)
4
4
  # ToDo: Can we pass info about the view here (e.g. host, protocol ...)
5
5
  options = options.reverse_merge({builder: Formstrap::FormBuilder})
6
- form_for(record, options, &block)
6
+ form_for(record, options, &)
7
7
  end
8
8
 
9
- def formstrap_form_with(options = {}, &block)
9
+ def formstrap_form_with(options = {}, &)
10
10
  # ToDo: Can we pass info about the view here (e.g. host, protocol ...)
11
11
  options = options.reverse_merge({builder: Formstrap::FormBuilder})
12
- form_with(**options, &block)
12
+ form_with(**options, &)
13
13
  end
14
14
  end
15
15
  end
@@ -1,3 +1,3 @@
1
1
  module Formstrap
2
- VERSION = "0.4.5"
2
+ VERSION = "0.4.7"
3
3
  end
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frontierdotbe/formstrap",
3
- "version": "0.4.4",
3
+ "version": "0.4.7",
4
4
  "description": "Bootstrap-powered Form Helpers",
5
5
  "module": "app/assets/javascripts/formstrap.js",
6
6
  "main": "app/assets/javascripts/formstrap.js",
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: formstrap
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.5
4
+ version: 0.4.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jef Vlamings
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-11-21 00:00:00.000000000 Z
11
+ date: 2025-02-10 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: An extensive Bootstrap form library to power your Ruby On Rails application.
14
14
  email: