Skip to content

Commit d142431

Browse files
committed
[Live] Smart Rendering! Mix/match live components with external JavaScript
1 parent fc8b5db commit d142431

32 files changed

+4463
-262
lines changed

src/Autocomplete/assets/dist/controller.d.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
import { Controller } from '@hotwired/stimulus';
22
import TomSelect from 'tom-select';
3+
export interface AutocompletePreConnectOptions {
4+
options: any;
5+
}
6+
export interface AutocompleteConnectOptions {
7+
tomSelect: TomSelect;
8+
options: any;
9+
}
310
export default class extends Controller {
411
#private;
512
static values: {
@@ -23,11 +30,20 @@ export default class extends Controller {
2330
readonly hasPreloadValue: boolean;
2431
readonly preloadValue: string;
2532
tomSelect: TomSelect;
33+
private mutationObserver;
34+
private isObserving;
2635
initialize(): void;
2736
connect(): void;
2837
disconnect(): void;
38+
private getMaxOptions;
2939
get selectElement(): HTMLSelectElement | null;
3040
get formElement(): HTMLInputElement | HTMLSelectElement;
3141
private dispatchEvent;
3242
get preload(): string | boolean;
43+
private resetTomSelect;
44+
private changeTomSelectDisabledState;
45+
private updateTomSelectPlaceholder;
46+
private startMutationObserver;
47+
private stopMutationObserver;
48+
private onMutations;
3349
}

src/Autocomplete/assets/dist/controller.js

Lines changed: 133 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,13 @@ class default_1 extends Controller {
2727
constructor() {
2828
super(...arguments);
2929
_default_1_instances.add(this);
30+
this.isObserving = false;
3031
}
3132
initialize() {
32-
this.element.setAttribute('data-live-ignore', '');
33-
if (this.element.id) {
34-
const label = document.querySelector(`label[for="${this.element.id}"]`);
35-
if (label) {
36-
label.setAttribute('data-live-ignore', '');
37-
}
33+
if (!this.mutationObserver) {
34+
this.mutationObserver = new MutationObserver((mutations) => {
35+
this.onMutations(mutations);
36+
});
3837
}
3938
}
4039
connect() {
@@ -47,11 +46,15 @@ class default_1 extends Controller {
4746
return;
4847
}
4948
this.tomSelect = __classPrivateFieldGet(this, _default_1_instances, "m", _default_1_createAutocomplete).call(this);
49+
this.startMutationObserver();
5050
}
5151
disconnect() {
52-
this.tomSelect.revertSettings.innerHTML = this.element.innerHTML;
52+
this.stopMutationObserver();
5353
this.tomSelect.destroy();
5454
}
55+
getMaxOptions() {
56+
return this.selectElement ? this.selectElement.options.length : 50;
57+
}
5558
get selectElement() {
5659
if (!(this.element instanceof HTMLSelectElement)) {
5760
return null;
@@ -79,6 +82,123 @@ class default_1 extends Controller {
7982
}
8083
return this.preloadValue;
8184
}
85+
resetTomSelect() {
86+
if (this.tomSelect) {
87+
this.stopMutationObserver();
88+
this.tomSelect.clearOptions();
89+
this.tomSelect.settings.maxOptions = this.getMaxOptions();
90+
this.tomSelect.sync();
91+
this.startMutationObserver();
92+
}
93+
}
94+
changeTomSelectDisabledState(isDisabled) {
95+
this.stopMutationObserver();
96+
if (isDisabled) {
97+
this.tomSelect.disable();
98+
}
99+
else {
100+
this.tomSelect.enable();
101+
}
102+
this.startMutationObserver();
103+
}
104+
updateTomSelectPlaceholder() {
105+
const input = this.element;
106+
let placeholder = input.getAttribute('placeholder') || input.getAttribute('data-placeholder');
107+
if (!placeholder && !this.tomSelect.allowEmptyOption) {
108+
const option = input.querySelector('option[value=""]');
109+
if (option) {
110+
placeholder = option.textContent;
111+
}
112+
}
113+
if (placeholder) {
114+
this.stopMutationObserver();
115+
this.tomSelect.control_input.setAttribute('placeholder', placeholder);
116+
this.startMutationObserver();
117+
}
118+
}
119+
startMutationObserver() {
120+
if (!this.isObserving) {
121+
this.mutationObserver.observe(this.element, {
122+
childList: true,
123+
subtree: true,
124+
attributes: true,
125+
characterData: true,
126+
});
127+
this.isObserving = true;
128+
}
129+
}
130+
stopMutationObserver() {
131+
if (this.isObserving) {
132+
this.mutationObserver.disconnect();
133+
this.isObserving = false;
134+
}
135+
}
136+
onMutations(mutations) {
137+
const addedOptionElements = [];
138+
const removedOptionElements = [];
139+
let hasAnOptionChanged = false;
140+
let changeDisabledState = false;
141+
let changePlaceholder = false;
142+
mutations.forEach((mutation) => {
143+
switch (mutation.type) {
144+
case 'childList':
145+
if (mutation.target instanceof HTMLOptionElement) {
146+
if (mutation.target.value === '') {
147+
changePlaceholder = true;
148+
break;
149+
}
150+
hasAnOptionChanged = true;
151+
break;
152+
}
153+
mutation.addedNodes.forEach((node) => {
154+
if (node instanceof HTMLOptionElement) {
155+
if (removedOptionElements.includes(node)) {
156+
removedOptionElements.splice(removedOptionElements.indexOf(node), 1);
157+
return;
158+
}
159+
addedOptionElements.push(node);
160+
}
161+
});
162+
mutation.removedNodes.forEach((node) => {
163+
if (node instanceof HTMLOptionElement) {
164+
if (addedOptionElements.includes(node)) {
165+
addedOptionElements.splice(addedOptionElements.indexOf(node), 1);
166+
return;
167+
}
168+
removedOptionElements.push(node);
169+
}
170+
});
171+
break;
172+
case 'attributes':
173+
if (mutation.target instanceof HTMLOptionElement) {
174+
hasAnOptionChanged = true;
175+
break;
176+
}
177+
if (mutation.target === this.element && mutation.attributeName === 'disabled') {
178+
changeDisabledState = true;
179+
break;
180+
}
181+
break;
182+
case 'characterData':
183+
if (mutation.target instanceof Text && mutation.target.parentElement instanceof HTMLOptionElement) {
184+
if (mutation.target.parentElement.value === '') {
185+
changePlaceholder = true;
186+
break;
187+
}
188+
hasAnOptionChanged = true;
189+
}
190+
}
191+
});
192+
if (hasAnOptionChanged || addedOptionElements.length > 0 || removedOptionElements.length > 0) {
193+
this.resetTomSelect();
194+
}
195+
if (changeDisabledState) {
196+
this.changeTomSelectDisabledState((this.formElement.disabled));
197+
}
198+
if (changePlaceholder) {
199+
this.updateTomSelectPlaceholder();
200+
}
201+
}
82202
}
83203
_default_1_instances = new WeakSet(), _default_1_getCommonConfig = function _default_1_getCommonConfig() {
84204
const plugins = {};
@@ -103,10 +223,6 @@ _default_1_instances = new WeakSet(), _default_1_getCommonConfig = function _def
103223
onItemAdd: () => {
104224
this.tomSelect.setTextboxValue('');
105225
},
106-
onInitialize: function () {
107-
const tomSelect = this;
108-
tomSelect.wrapper.setAttribute('data-live-ignore', '');
109-
},
110226
closeAfterSelect: true,
111227
};
112228
if (!this.selectElement && !this.urlValue) {
@@ -115,12 +231,12 @@ _default_1_instances = new WeakSet(), _default_1_getCommonConfig = function _def
115231
return __classPrivateFieldGet(this, _default_1_instances, "m", _default_1_mergeObjects).call(this, config, this.tomSelectOptionsValue);
116232
}, _default_1_createAutocomplete = function _default_1_createAutocomplete() {
117233
const config = __classPrivateFieldGet(this, _default_1_instances, "m", _default_1_mergeObjects).call(this, __classPrivateFieldGet(this, _default_1_instances, "m", _default_1_getCommonConfig).call(this), {
118-
maxOptions: this.selectElement ? this.selectElement.options.length : 50,
234+
maxOptions: this.getMaxOptions(),
119235
});
120236
return __classPrivateFieldGet(this, _default_1_instances, "m", _default_1_createTomSelect).call(this, config);
121237
}, _default_1_createAutocompleteWithHtmlContents = function _default_1_createAutocompleteWithHtmlContents() {
122238
const config = __classPrivateFieldGet(this, _default_1_instances, "m", _default_1_mergeObjects).call(this, __classPrivateFieldGet(this, _default_1_instances, "m", _default_1_getCommonConfig).call(this), {
123-
maxOptions: this.selectElement ? this.selectElement.options.length : 50,
239+
maxOptions: this.getMaxOptions(),
124240
score: (search) => {
125241
const scoringFunction = this.tomSelect.getScoreFunction(search);
126242
return (item) => {
@@ -183,9 +299,11 @@ _default_1_instances = new WeakSet(), _default_1_getCommonConfig = function _def
183299
}, _default_1_mergeObjects = function _default_1_mergeObjects(object1, object2) {
184300
return Object.assign(Object.assign({}, object1), object2);
185301
}, _default_1_createTomSelect = function _default_1_createTomSelect(options) {
186-
this.dispatchEvent('pre-connect', { options });
302+
const preConnectPayload = { options };
303+
this.dispatchEvent('pre-connect', preConnectPayload);
187304
const tomSelect = new TomSelect(this.formElement, options);
188-
this.dispatchEvent('connect', { tomSelect, options });
305+
const connectPayload = { tomSelect, options };
306+
this.dispatchEvent('connect', connectPayload);
189307
return tomSelect;
190308
};
191309
default_1.values = {

0 commit comments

Comments
 (0)