PageSourceSearch

https://railsblocks.com/assets/controllers/nested_form_controller-…4d12397365b235b0f73371cae12b5a937804c972.js

js railsblocks.com collected 2026-09-25 20:23:44 UTC 3,497 bytes, 145 lines download raw bytes

1import { Controller } from "@hotwired/stimulus";
2
3// Connects to data-controller="nested-form"
4export default class extends Controller {
5  static targets = ["container", "template", "item", "button", "maxHint"];
6  static values = {
7    index: Number,
8    maxFields: {
9      type: Number,
10      default: 5,
11    },
12    minItems: {
13      type: Number,
14      default: 1,
15    },
16    maxHintText: String,
17  };
18
19  connect() {
20    if (!this.hasIndexValue) {
21      this.indexValue = this.itemTargets.length;
22    }
23
24    this.updateButtonStates();
25  }
26
27  add(event) {
28    event.preventDefault();
29
30    if (this.maxReached()) {
31      return;
32    }
33
34    const content = this.templateTarget.innerHTML.replace(/NEW_RECORD/g, this.indexValue);
35    this.containerTarget.insertAdjacentHTML("beforeend", content);
36    this.indexValue += 1;
37
38    const newItem = this.itemTargets[this.itemTargets.length - 1];
39    const focusableField = newItem?.querySelector(
40      "input:not([type='hidden']):not([disabled]), textarea:not([disabled]), select:not([disabled])",
41    );
42
43    if (focusableField) {
44      requestAnimationFrame(() => focusableField.focus());
45    }
46
47    this.dispatchLifecycleEvent("add");
48    this.updateButtonStates();
49  }
50
51  remove(event) {
52    event.preventDefault();
53
54    const item = event.currentTarget.closest('[data-nested-form-target="item"]');
55
56    if (!item) {
57      return;
58    }
59
60    if (!this.canRemoveItem()) {
61      return;
62    }
63
64    const destroyInput = item.querySelector('input[name*="[_destroy]"]');
65
66    if (item.dataset.newRecord === "true" || !destroyInput) {
67      item.remove();
68      this.dispatchLifecycleEvent("remove");
69      this.updateButtonStates();
70      return;
71    }
72
73    destroyInput.value = "1";
74    item.classList.add("hidden");
75
76    this.dispatchLifecycleEvent("remove");
77    this.updateButtonStates();
78  }
79
80  activeItemCount() {
81    return this.itemTargets.filter((item) => !item.classList.contains("hidden")).length;
82  }
83
84  canRemoveItem() {
85    return this.activeItemCount() > this.minItemsValue;
86  }
87
88  updateButtonStates() {
89    this.updateAddButtonState();
90    this.updateRemoveButtonsState();
91    this.updateMaxHintState();
92  }
93
94  updateAddButtonState() {
95    const addButton = this.addButtonElement;
96    if (!addButton) {
97      return;
98    }
99
100    addButton.disabled = this.maxReached();
101  }
102
103  updateRemoveButtonsState() {
104    const disableRemove = !this.canRemoveItem();
105    this.removeButtons.forEach((button) => {
106      button.disabled = disableRemove;
107    });
108  }
109
110  get addButtonElement() {
111    if (this.hasButtonTarget) {
112      return this.buttonTarget;
113    }
114
115    return this.element.querySelector('[data-action~="nested-form#add"]');
116  }
117
118  get removeButtons() {
119    return Array.from(this.element.querySelectorAll('[data-action~="nested-form#remove"]'));
120  }
121
122  updateMaxHintState() {
123    if (!this.hasMaxHintTarget) {
124      return;
125    }
126
127    const showHint = this.maxReached();
128    this.maxHintTarget.classList.toggle("hidden", !showHint);
129
130    if (this.hasMaxHintTextValue && this.maxHintTextValue.length > 0) {
131      this.maxHintTarget.textContent = this.maxHintTextValue;
132      return;
133    }
134
135    this.maxHintTarget.textContent = `Maximum of ${this.maxFieldsValue} items reached.`;
136  }
137
138  maxReached() {
139    return this.maxFieldsValue > 0 && this.activeItemCount() >= this.maxFieldsValue;
140  }
141
142  dispatchLifecycleEvent(actionName) {
143    this.element.dispatchEvent(new CustomEvent(`rails-nested-form:${actionName}`, { bubbles: true }));
144  }
145};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.