diff --git a/app/resources/views/form/components/selectAsyncDependent.blade.php b/app/resources/views/form/components/selectAsyncDependent.blade.php index 0d7761e0..21608e1c 100644 --- a/app/resources/views/form/components/selectAsyncDependent.blade.php +++ b/app/resources/views/form/components/selectAsyncDependent.blade.php @@ -32,6 +32,21 @@
@{{ form.$all }}
+ + + + +
@{{ form.$all }}
+ +
+ + + + +
@{{ form.$all }}
+ +
+ @endsection \ No newline at end of file diff --git a/app/resources/views/form/components/selectAsyncNested.blade.php b/app/resources/views/form/components/selectAsyncNested.blade.php index 0869963c..0caf7b1d 100644 --- a/app/resources/views/form/components/selectAsyncNested.blade.php +++ b/app/resources/views/form/components/selectAsyncNested.blade.php @@ -32,6 +32,20 @@
@{{ form.$all }}
+ + + + +
@{{ form.$all }}
+ +
+ + + + +
@{{ form.$all }}
+ +
@endsection \ No newline at end of file diff --git a/app/resources/views/navigation/nav.blade.php b/app/resources/views/navigation/nav.blade.php index d20930a3..d925d33d 100644 --- a/app/resources/views/navigation/nav.blade.php +++ b/app/resources/views/navigation/nav.blade.php @@ -11,6 +11,7 @@ AwayViaFacade AwayViaLink Lazy + Auth Confirm to two browse(function (Browser $browser) { + $browser->visit('/navigation/two') + ->waitForText('NavigationTwo') + ->click('@auth') + ->waitForRoute('navigation.one') + ->assertSee('NavigationOne'); + }); + } +} diff --git a/app/tests/Browser/Form/SelectDependentTest.php b/app/tests/Browser/Form/SelectDependentTest.php index b730f1d0..7c5e9c1b 100644 --- a/app/tests/Browser/Form/SelectDependentTest.php +++ b/app/tests/Browser/Form/SelectDependentTest.php @@ -112,4 +112,49 @@ public function it_restores_the_placeholder_on_choices_multiple_select($url) }); }); } + + /** + * @dataProvider dependentUrls + * + * @test + */ + public function it_can_select_the_first_remote_option($url) + { + $this->browse(function (Browser $browser) use ($url) { + $browser->visit($url) + ->waitForText('FormComponents') + ->waitUntilMissing('svg') + ->within('@select-first', function (Browser $browser) { + $browser + ->waitUntilMissing('svg') + ->assertSeeIn('@all', '{ "country": "BE", "province": "BE-VAN" }') + ->select('@country', 'NL') + ->waitUntilMissing('svg') + ->assertSeeIn('@all', '{ "country": "NL", "province": "NL-AW" }'); + }); + }); + } + + /** + * @dataProvider dependentUrls + * + * @test + */ + public function it_can_reset_the_select_option_on_a_remote_url_change($url) + { + $this->browse(function (Browser $browser) use ($url) { + $browser->visit($url) + ->waitForText('FormComponents') + ->waitUntilMissing('svg') + ->within('@select-reset', function (Browser $browser) { + $browser + ->waitUntilMissing('svg') + ->select('@province', 'BE-VAN') + ->assertSeeIn('@all', '{ "country": "BE", "province": "BE-VAN" }') + ->select('@country', 'NL') + ->waitUntilMissing('svg') + ->assertSeeIn('@all', '{ "country": "NL", "province": "" }'); + }); + }); + } } diff --git a/lib/Components/Select.vue b/lib/Components/Select.vue index 6e44bd55..0f0ad960 100644 --- a/lib/Components/Select.vue +++ b/lib/Components/Select.vue @@ -80,6 +80,18 @@ export default { required: false, default: null, }, + + selectFirstRemoteOption: { + type: Boolean, + required: false, + default: false, + }, + + resetOnNewRemoteUrl: { + type: Boolean, + required: false, + default: false, + }, }, emits: ["update:modelValue"], @@ -167,100 +179,121 @@ export default { }, methods: { - /* - * Loads the options from a remote URL. It removes all current options from the select - * element, and then adds the new options. If the components uses Choices.js, - * it will first destroy the instance and then re-initialize it. - */ - loadRemoteOptions() { - if(!this.remoteUrl) { - return; + async setOptionsFromRemote(data) { + // Cleanup previous choices instance. + this.destroyChoicesInstance(); + + if(this.resetOnNewRemoteUrl) { + this.$emit("update:modelValue", this.multiple ? [] : ""); } - this.loading = true; + let options = []; - Axios({ - url: this.remoteUrl, - method: "GET", - headers: { - Accept: "application/json", - }, - }) - .then((response) => { - // Cleanup previous choices instance. - this.destroyChoicesInstance(); + // Start with the the placeholder. + if(this.placeholder) { + options.push(this.placeholder); + } - let options = []; + // Normalize the response. + options = this.normalizeOptions(data, options); - // Start with the the placeholder. - if(this.placeholder) { - options.push(this.placeholder); - } + var index; + var currentOptionsCount = this.element.options.length - 1; - // Normalize the response. - options = this.normalizeOptions(this.remoteRoot ? get(response.data, this.remoteRoot) : response.data, options); + for(index = currentOptionsCount; index >= 0; index--) { + // Remove all current options. + this.element.remove(index); + } - var index; - var currentOptionsCount = this.element.options.length - 1; + let hasSelectedOption = false; - for(index = currentOptionsCount; index >= 0; index--) { - // Remove all current options. - this.element.remove(index); - } + forOwn(options, (option) => { + // Add the new options. + var optionElement = document.createElement("option"); - let hasSelectedOption = false; + optionElement.value = option.value; + optionElement.text = option.label; - forOwn(options, (option) => { - // Add the new options. - var optionElement = document.createElement("option"); + if(option.value === `${this.modelValue}` && option.value !== "") { + // The current value is in the new options, we use this later on + // to set the value on the select element and Choices instance. + hasSelectedOption = true; + } - optionElement.value = option.value; - optionElement.text = option.label; + if(option.disabled) { + optionElement.disabled = option.disabled; + } - if(option.value === `${this.modelValue}`) { - // The current value is in the new options, we use this later on - // to set the value on the select element and Choices instance. - hasSelectedOption = true; - } + if(option.placeholder) { + optionElement.placeholder = option.placeholder; + } - if(option.disabled) { - optionElement.disabled = option.disabled; - } + // Add the option to the select element. + this.element.appendChild(optionElement); + }); - if(option.placeholder) { - optionElement.placeholder = option.placeholder; - } - // Add the option to the select element. - this.element.appendChild(optionElement); - }); + if(!hasSelectedOption && this.selectFirstRemoteOption) { + const firstOption = this.placeholder ? options[1] : options[0]; - if(!hasSelectedOption) { - // The current value is not in the new options, we set the value to null. - this.$emit("update:modelValue", this.multiple ? [] : ""); - } + if(firstOption){ + this.$emit("update:modelValue", this.multiple ? [firstOption.value] : firstOption.value); + await this.$nextTick(); + } - if(this.choices) { - // Re-initialize the Choices instance. - return this.initChoices(this.element).then(() => { - this.loading = false; - }); - } + hasSelectedOption = true; + } - if(hasSelectedOption) { - // The current value is in the new options, we set the value on the select element. - this.element.value = this.modelValue; - } else { - // The current value is not in the new options, we set the value to null. - this.$nextTick(() => { - this.element.selectedIndex = 0; - }); - } + if(!hasSelectedOption) { + // The current value is not in the new options, we set the value to null. + this.$emit("update:modelValue", this.multiple ? [] : ""); + } + if(this.choices) { + // Re-initialize the Choices instance. + return this.initChoices(this.element).then(() => { this.loading = false; + }); + } + + if(hasSelectedOption) { + // The current value is in the new options, we set the value on the select element. + this.element.value = this.modelValue; + } else { + // The current value is not in the new options, we set the value to null. + this.$nextTick(() => { + this.element.selectedIndex = 0; + }); + } + }, + + /* + * Loads the options from a remote URL. It removes all current options from the select + * element, and then adds the new options. If the components uses Choices.js, + * it will first destroy the instance and then re-initialize it. + */ + loadRemoteOptions() { + if(!this.remoteUrl) { + return; + } + this.loading = true; + + + Axios({ + url: this.remoteUrl, + method: "GET", + headers: { + Accept: "application/json", + }, + }) + .then((response) => { + this.setOptionsFromRemote(this.remoteRoot ? get(response.data, this.remoteRoot) : response.data); }) .catch(() => { + this.setOptionsFromRemote([]); + }) + .finally(() => { this.loading = false; }); }, @@ -407,6 +440,10 @@ export default { // The Headless UI Dialog blocks the events on the Choices.js // instance, so we put an event listener on the portal root. vm.headlessListener = function(e) { + if(!vm.choicesInstance) { + return; + } + const isActive = vm.choicesInstance.dropdown.isActive; if(!isActive && e.target === selectElement) { diff --git a/resources/views/form/select.blade.php b/resources/views/form/select.blade.php index f86831b0..f7158e9f 100644 --- a/resources/views/form/select.blade.php +++ b/resources/views/form/select.blade.php @@ -10,6 +10,8 @@ :remote-root="@js($remoteRoot ?: null)" :option-value="@js($optionValue)" :option-label="@js($optionLabel)" + :select-first-remote-option="@js($selectFirstRemoteOption)" + :reset-on-new-remote-url="@js($resetOnNewRemoteUrl)" >