I’m kinda new to Alpine.js and I have 3 selects in my blade view: make, model, engine. Options on model and engine are dynamic and being populated once the previous select has been changed (just to be able to select a vehicle with corresponding configuration):
<div class="md:col-span-5" x-data="{
selectedMake: '',
selectedModel: '',
models: [],
engines: [],
isLoading: false,
makeDisabled: false,
engineDisabled: false,
showAlert: false,
isFormValid: false,
fetchModels() {
fetchModels(this.selectedMake, this.models, this.isLoading, this.makeDisabled);
},
fetchEngines() {
fetchEngines(this.selectedModel, this.engines, this.isEngineLoading, this.engineDisabled)
},
resetEnginesSelector() {
this.selectedModel = '';
this.engines = [];
this.engineDisabled = true;
},
checkFormValidity() {
this.isFormValid = this.selectedMake !== '' && this.selectedModel !== '';
},
}"
x-init="$watch('selectedMake', () => { resetEnginesSelector(); checkFormValidity() }); $watch('selectedModel', () => checkFormValidity())"
>
<x-forms.select label="Make"
x-model="selectedMake"
@change="fetchModels"
x-bind:disabled="isLoading || makeDisabled"
id="make"
name="make"
:options="$makes"
required />
<select name="model"
id="model"
@change="fetchEngines"
x-model="selectedModel"
required
x-bind:disabled="Object.keys(models).length === 0 || isLoading"
class="form-control"
>
<option value="" selected>Please select</option>
<template x-for="model in models" :key="model.id">
<option x-bind:value="model.id" x-text="model.name"></option>
</template>
</select>
<select name="engine"
id="engine"
required
x-bind:disabled="Object.keys(engines).length === 0 || isLoading"
class="form-control"
>
<option value="" selected>Please select</option>
<template x-for="engine in engines" :key="engine.id">
<option x-bind:value="engine.id" x-text="engine.name"></option>
</template>
</select>
</div>
And in my <script>
tag, there is the following:
function fetchEngines(
selectedModel,
engines,
isLoading,
engineDisabled
) {
engineDisabled = true;
isLoading = true;
fetch(router.fetchEngines, {
method: 'post',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': token,
},
body: JSON.stringify({
model: selectedModel
})
})
.then(r => {
if (!r.ok) {
throw new Error('Network response was not ok')
}
return r.json();
})
.then(data => {
engines.splice(0, engines.length, ...data);
engineDisabled = false;
})
.finally(() => {
isLoading = false;
})
}
function fetchModels(
selectedMake,
models,
isLoading,
makeDisabled
) {
makeDisabled = true;
isLoading = true;
fetch(router.fetchModels, {
method: 'post',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': token,
},
body: JSON.stringify({
make: selectedMake
})
})
.then(r => {
if (!r.ok) {
throw new Error('Network response was not ok')
}
return r.json();
})
.then(data => {
models.splice(0, models.length, ...data);
makeDisabled = false;
})
.finally(() => {
isLoading = false;
})
}
So, what I am trying to achieve is: when the backend validation fails or any other backend exception is being thrown, I want to return back to the form and these 3 selects to be pre-populated and pre-checked with old values.
For this reason in my Laravel Controller, I am returning a redirect response: return back()->withInput()
.
I have tried to set the selectedMake
and selectedEngine
, by including old values in the x-data part:
selectedMake: '{{ old('make') }}',
selectedModel: '{{ old('model') }}',
selectedEngine: '{{ old('engine') }}',
....
But that didn’t work, since only the Make select has been selected, but Model and Engine selects were not pre-populated with options.
As ChatGPT suggested, I even created a separate function:
if (oldMake && oldModel) {
fetchModels(oldMake);
fetchEngines(oldModel);
}
and in x-init
of the first DIV I added the fetchModels(selectedMake); fetchEngines(selectedModel)
part. That didn’t work as well, since again – only Make select has been selected, but Model and Engine selects were not, since these are empty at that moment.
How can I achieve the desired behavior, so that all 3 selects are prefilled and preselected with old values?
P.S. As you can see, I have a Make select already pre-populated from backend, since Make list is static and for this reason, I created a blade component. For other 2 selects I used the regular HTML select with Alpine attributes.
P.P.S.: An idea came to me while writing this question: on my Backend, I could already pre-fetch the model and engines list and maybe send these 2 lists as a session variable: return back()->with(['models' => ...])->withInput();
and put the list somewhere in JS and then somehow preselect without AJAX request
Update 1
I have came up with a somehow almost working solution:
<div x-data="{
selectedMake: '{{ old('make') }}',
selectedModel: '{{ old('model') }}',
selectedEngine: '{{ old('engine') }}',
... // other code, unchanged
}">
<select name="model" id="model" @change="fetchEngines"
x-init="if (selectedModel !== '') (this.selectedMake, this.models, this.isLoading, this.makeDisabled)"
x-model="selectedModel"
required
x-bind:disabled="Object.keys(models).length === 0 || isLoading"
class="form-control"
>
<option value="" selected>Please select</option>
<template x-for="model in models" :key="model.id">
<option :value="model.id" :selected="model.id == this.selectedModel" x-text="model.name"></option>
</template>
</select>
In this case, the models
select is being pre-populated, but not yet pre-selected. What am I missing? I have tried x-bind:selected
and :selected
on the option
tag, but nothing seems to work here.