Skip to content

Commit

Permalink
WIP - playing with Autocomplete + morph
Browse files Browse the repository at this point in the history
  • Loading branch information
weaverryan committed Feb 17, 2024
1 parent 5688875 commit 52bec73
Show file tree
Hide file tree
Showing 6 changed files with 121 additions and 24 deletions.
2 changes: 2 additions & 0 deletions src/Autocomplete/assets/dist/controller.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,6 @@ export default class extends Controller {
private onMutations;
private createOptionsDataStructure;
private areOptionsEquivalent;
private beforeMorphElement;
private beforeMorphAttribute;
}
53 changes: 44 additions & 9 deletions src/Autocomplete/assets/dist/controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ class default_1 extends Controller {
if (this.selectElement) {
this.originalOptions = this.createOptionsDataStructure(this.selectElement);
}
const parentElement = this.element.parentElement;
if (!parentElement) {
return;
}
parentElement.addEventListener('turbo:before-morph-element', this.beforeMorphElement.bind(this));
parentElement.addEventListener('turbo:before-morph-attribute', this.beforeMorphAttribute.bind(this));
this.initializeTomSelect();
}
initializeTomSelect() {
Expand All @@ -61,6 +67,12 @@ class default_1 extends Controller {
}
disconnect() {
this.stopMutationObserver();
const parentElement = this.element.parentElement;
if (!parentElement) {
return;
}
parentElement.removeEventListener('turbo:before-morph-element', this.beforeMorphElement.bind(this));
parentElement.removeEventListener('turbo:before-morph-attribute', this.beforeMorphAttribute.bind(this));
let currentSelectedValues = [];
if (this.selectElement) {
if (this.selectElement.multiple) {
Expand Down Expand Up @@ -156,15 +168,13 @@ class default_1 extends Controller {
}
}
onMutations(mutations) {
let changeDisabledState = false;
let requireReset = false;
if (this.tomSelect.isDisabled !== this.formElement.disabled) {
this.changeTomSelectDisabledState(this.formElement.disabled);
}
mutations.forEach((mutation) => {
switch (mutation.type) {
case 'attributes':
if (mutation.target === this.element && mutation.attributeName === 'disabled') {
changeDisabledState = true;
break;
}
if (mutation.target === this.element && mutation.attributeName === 'multiple') {
const isNowMultiple = this.element.hasAttribute('multiple');
const wasMultiple = mutation.oldValue === 'multiple';
Expand All @@ -178,13 +188,12 @@ class default_1 extends Controller {
});
const newOptions = this.selectElement ? this.createOptionsDataStructure(this.selectElement) : [];
const areOptionsEquivalent = this.areOptionsEquivalent(newOptions);
if (!areOptionsEquivalent || requireReset) {
const value = this.selectElement ? Array.from(this.selectElement.options || []).map((option) => option.value) : this.formElement.value;
const didValueChange = value !== this.tomSelect.getValue();
if (!areOptionsEquivalent || requireReset || didValueChange) {
this.originalOptions = newOptions;
this.resetTomSelect();
}
if (changeDisabledState) {
this.changeTomSelectDisabledState(this.formElement.disabled);
}
}
createOptionsDataStructure(selectElement) {
return Array.from(selectElement.options).map((option) => {
Expand All @@ -208,6 +217,32 @@ class default_1 extends Controller {
return (originalOptionsSet.size === newOptionsSet.size &&
[...originalOptionsSet].every((option) => newOptionsSet.has(option)));
}
beforeMorphElement(event) {
if (event.target.classList.contains('ts-wrapper')) {
event.preventDefault();
return;
}
if (event.target === this.element) {
const newOptions = this.selectElement ? this.createOptionsDataStructure(this.selectElement) : [];
if (this.areOptionsEquivalent(newOptions)) {
event.preventDefault();
return;
}
}
}
beforeMorphAttribute(event) {
if (event.target.tagName === 'LABEL') {
console.log('before morph attribute', event.detail);
}
if (event.target.tagName === 'LABEL' && ['for', 'id'].includes(event.detail.attributeName)) {
event.preventDefault();
return;
}
if (event.target === this.element && event.detail.attributeName === 'class') {
event.preventDefault();
return;
}
}
}
_default_1_instances = new WeakSet(), _default_1_getCommonConfig = function _default_1_getCommonConfig() {
const plugins = {};
Expand Down
78 changes: 66 additions & 12 deletions src/Autocomplete/assets/src/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,13 @@ export default class extends Controller {
if (this.selectElement) {
this.originalOptions = this.createOptionsDataStructure(this.selectElement);
}
// TODO - also listen on `live:before-morph-element`
const parentElement = this.element.parentElement;
if (!parentElement) {
return;
}
parentElement.addEventListener('turbo:before-morph-element', this.beforeMorphElement.bind(this));
parentElement.addEventListener('turbo:before-morph-attribute', this.beforeMorphAttribute.bind(this));

this.initializeTomSelect();
}
Expand Down Expand Up @@ -85,6 +92,13 @@ export default class extends Controller {
disconnect() {
this.stopMutationObserver();

const parentElement = this.element.parentElement;
if (!parentElement) {
return;
}
parentElement.removeEventListener('turbo:before-morph-element', this.beforeMorphElement.bind(this));
parentElement.removeEventListener('turbo:before-morph-attribute', this.beforeMorphAttribute.bind(this));

// TomSelect.destroy() resets the element to its original HTML. This
// causes the selected value to be lost. We store it.
let currentSelectedValues: string[] = [];
Expand Down Expand Up @@ -373,18 +387,15 @@ export default class extends Controller {
}

private onMutations(mutations: MutationRecord[]): void {
let changeDisabledState = false;
let requireReset = false;

if (this.tomSelect.isDisabled !== this.formElement.disabled) {
this.changeTomSelectDisabledState(this.formElement.disabled);
}

mutations.forEach((mutation) => {
switch (mutation.type) {
case 'attributes':
if (mutation.target === this.element && mutation.attributeName === 'disabled') {
changeDisabledState = true;

break;
}

if (mutation.target === this.element && mutation.attributeName === 'multiple') {
const isNowMultiple = this.element.hasAttribute('multiple');
const wasMultiple = mutation.oldValue === 'multiple';
Expand All @@ -401,14 +412,15 @@ export default class extends Controller {

const newOptions = this.selectElement ? this.createOptionsDataStructure(this.selectElement) : [];
const areOptionsEquivalent = this.areOptionsEquivalent(newOptions);
if (!areOptionsEquivalent || requireReset) {

// TODO: test this: look for changes in the "value" of the select element
const value = this.selectElement ? Array.from(this.selectElement.options || []).map((option) => option.value) : this.formElement.value;
const didValueChange = value !== this.tomSelect.getValue();

if (!areOptionsEquivalent || requireReset || didValueChange) {
this.originalOptions = newOptions;
this.resetTomSelect();
}

if (changeDisabledState) {
this.changeTomSelectDisabledState(this.formElement.disabled);
}
}

private createOptionsDataStructure(
Expand Down Expand Up @@ -443,4 +455,46 @@ export default class extends Controller {
[...originalOptionsSet].every((option) => newOptionsSet.has(option))
);
}

private beforeMorphElement(event: any) {
// TomSelect adds this element to the DOM. Keep it.
if (event.target.classList.contains('ts-wrapper')) {
event.preventDefault();

return;
}

if (event.target === this.element) {
const newOptions = this.selectElement ? this.createOptionsDataStructure(this.selectElement) : [];
if (this.areOptionsEquivalent(newOptions)) {
// prevent the <option> elements from being mutated, as this will
// change their order, which befuddles TomSelect (who changes the
// order of the <option> elements when you select them)
// this will, unfortunately, prevent any changes attributed on
// the select element from being updated.
event.preventDefault();

return;
}
}
}

private beforeMorphAttribute(event: any) {
if (event.target.tagName === 'LABEL') {
console.log('before morph attribute', event.detail);
}
// TomSelect changes the label "for". Keep that change.
if (event.target.tagName === 'LABEL' && ['for', 'id'].includes(event.detail.attributeName)) {
event.preventDefault();

return;
}

// TomSelect changes the root element's class. Keep that change.
if (event.target === this.element && event.detail.attributeName === 'class') {
event.preventDefault();

return;
}
}
}
4 changes: 4 additions & 0 deletions ux.symfony.com/src/Form/TimeForAMealForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Validator\Constraints\NotBlank;

class TimeForAMealForm extends AbstractType
{
Expand All @@ -23,6 +24,9 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
->add('foods', FoodAutocompleteField::class)
->add('name', TextType::class, [
'label' => 'What should we call this meal?',
'constraints' => [
new NotBlank(),
],
])
;
}
Expand Down
2 changes: 2 additions & 0 deletions ux.symfony.com/templates/base.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
<link rel="canonical" href="{{ meta.canonical }}">
{% endif %}
<meta name="view-transition" content="same-origin" />
<meta name="turbo-refresh-method" content="morph">
<meta name="turbo-refresh-scroll" content="preserve">
<link rel="icon" href="/favicon.ico" sizes="48x48">
<link rel="icon" href="/favicon.svg" sizes="any" type="image/svg+xml">
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
Expand Down
6 changes: 3 additions & 3 deletions ux.symfony.com/templates/ux_packages/autocomplete.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@

{% block demo_content %}
{# The frame is used just to keep the form submit all in one place: it feels nice #}
<turbo-frame id="autocomplete-demo-form">
<div id="autocomplete-demo-form">
{% for message in app.flashes('autocomplete_success') %}
<div class="alert alert-success" data-turbo-cache="false">{{ message }}</div>
{% endfor %}
Expand All @@ -41,7 +41,7 @@

{{ form_row(form.name) }}

<button type="submit" class="btn btn-primary">Let's Nom!</button>
<button type="submit" class="btn btn-primary" formnovalidate>Let's Nom!</button>
{{ form_end(form) }}
</turbo-frame>
</div>
{% endblock %}

0 comments on commit 52bec73

Please sign in to comment.