Merge pull request #8850 from tchak/feat-refactor-drop-downs
Simplifie l'implémentation des champs "liste d'options" et "listes d'options liées"
This commit is contained in:
commit
7bf31c6278
11 changed files with 52 additions and 137 deletions
|
@ -8,12 +8,12 @@
|
|||
|
||||
- if !@champ.mandatory?
|
||||
%label.blank-radio
|
||||
= @form.radio_button :value, ''
|
||||
= @form.radio_button :value, '', checked: @champ.value.blank? && !@champ.other?
|
||||
Non renseigné
|
||||
|
||||
- if @champ.drop_down_other?
|
||||
%label
|
||||
= @form.radio_button :value, Champs::DropDownListChamp::OTHER, checked: @champ.other_value_present?
|
||||
= @form.radio_button :value, Champs::DropDownListChamp::OTHER, checked: @champ.other?
|
||||
Autre
|
||||
- else
|
||||
= @form.select :value, @champ.options_without_empty_value_when_mandatory(@champ.options), { selected: @champ.selected }, required: @champ.required?, id: @champ.input_id, aria: { describedby: @champ.describedby_id }
|
||||
|
|
|
@ -1,2 +1,5 @@
|
|||
class EditableChamp::DropDownOtherInputComponent < EditableChamp::EditableChampBaseComponent
|
||||
def render?
|
||||
@champ.other?
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
.drop_down_other{ class: @champ.other_value_present? ? '' : 'hidden' }
|
||||
.drop_down_other
|
||||
.notice
|
||||
%label{ for: dom_id(@champ, :value_other) } Veuillez saisir votre autre choix
|
||||
= @form.text_field :value_other, maxlength: 200, size: nil, id: dom_id(@champ, :value_other), disabled: !@champ.other_value_present?
|
||||
= @form.text_field :value_other, maxlength: 200, size: nil, id: dom_id(@champ, :value_other)
|
||||
|
|
|
@ -30,11 +30,6 @@ class EditableChamp::EditableChampComponent < ApplicationComponent
|
|||
# This is an editable champ. Lets find what controllers it might need.
|
||||
controllers = ['autosave']
|
||||
|
||||
# This is a dropdown champ. Activate special behaviours it might have.
|
||||
if @champ.simple_drop_down_list? || @champ.linked_drop_down_list?
|
||||
controllers << 'champ-dropdown'
|
||||
end
|
||||
|
||||
controllers.join(' ')
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,2 +1,15 @@
|
|||
class EditableChamp::LinkedDropDownListComponent < EditableChamp::EditableChampBaseComponent
|
||||
private
|
||||
|
||||
def secondary_label
|
||||
secondary_label_text + secondary_label_mandatory
|
||||
end
|
||||
|
||||
def secondary_label_text
|
||||
@champ.drop_down_secondary_libelle.presence || "Valeur secondaire dépendant de la première"
|
||||
end
|
||||
|
||||
def secondary_label_mandatory
|
||||
@champ.mandatory? ? tag.span(' *', class: 'mandatory') : ''
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,16 +1,12 @@
|
|||
- if @champ.options?
|
||||
= @form.select :primary_value,
|
||||
@champ.primary_options,
|
||||
{},
|
||||
{ data: { secondary_options: @champ.secondary_options }, required: @champ.required?, id: @champ.input_id, aria: { describedby: @champ.describedby_id } }
|
||||
|
||||
.secondary{ class: @champ.has_secondary_options_for_primary? ? '' : 'hidden' }
|
||||
= @form.label :secondary_value, for: "#{@champ.input_id}-secondary" do
|
||||
- sanitize((@champ.drop_down_secondary_libelle.presence || "Valeur secondaire dépendant de la première") + (@champ.mandatory? ? tag.span(' *', class: 'mandatory') : ''))
|
||||
- if @champ.drop_down_secondary_description.present?
|
||||
.notice{ id: "#{@champ.describedby_id}-secondary" }= render SimpleFormatComponent.new(@champ.drop_down_secondary_description, allow_a: true)
|
||||
= @form.select :secondary_value,
|
||||
@champ.secondary_options[@champ.primary_value],
|
||||
{},
|
||||
{ data: { secondary: true }, disabled: !@champ.has_secondary_options_for_primary?, required: @champ.required?, id: "#{@champ.input_id}-secondary", aria: { describedby: "#{@champ.describedby_id}-secondary" } }
|
||||
= @form.hidden_field :secondary_value, value: '', disabled: @champ.has_secondary_options_for_primary?
|
||||
= @form.select :primary_value, @champ.primary_options, {}, required: @champ.required?, id: @champ.input_id, aria: { describedby: @champ.describedby_id }
|
||||
- if @champ.has_secondary_options_for_primary?
|
||||
.secondary
|
||||
= @form.label :secondary_value, for: "#{@champ.input_id}-secondary" do
|
||||
- sanitize(secondary_label)
|
||||
- if @champ.drop_down_secondary_description.present?
|
||||
.notice{ id: "#{@champ.describedby_id}-secondary" }
|
||||
= render SimpleFormatComponent.new(@champ.drop_down_secondary_description, allow_a: true)
|
||||
= @form.select :secondary_value, @champ.secondary_options[@champ.primary_value], {}, required: @champ.required?, id: "#{@champ.input_id}-secondary", aria: { describedby: "#{@champ.describedby_id}-secondary" }
|
||||
- else
|
||||
= @form.hidden_field :secondary_value, value: ''
|
||||
|
|
|
@ -1,101 +0,0 @@
|
|||
import {
|
||||
isSelectElement,
|
||||
isCheckboxOrRadioInputElement,
|
||||
show,
|
||||
hide,
|
||||
enable,
|
||||
disable
|
||||
} from '@utils';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ApplicationController } from './application_controller';
|
||||
|
||||
export class ChampDropdownController extends ApplicationController {
|
||||
connect() {
|
||||
this.on('change', (event) => this.onChange(event));
|
||||
}
|
||||
|
||||
private onChange(event: Event) {
|
||||
const target = event.target as HTMLInputElement;
|
||||
if (!target.disabled) {
|
||||
if (isSelectElement(target) || isCheckboxOrRadioInputElement(target)) {
|
||||
this.toggleOtherInput(target);
|
||||
this.toggleLinkedSelect(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private toggleOtherInput(target: HTMLSelectElement | HTMLInputElement) {
|
||||
const parent = target.closest('.editable-champ-drop_down_list');
|
||||
const inputGroup = parent?.querySelector<HTMLElement>('.drop_down_other');
|
||||
if (inputGroup) {
|
||||
const input = inputGroup.querySelector('input');
|
||||
if (input) {
|
||||
if (target.value == '__other__') {
|
||||
show(inputGroup);
|
||||
input.disabled = false;
|
||||
input.focus();
|
||||
} else {
|
||||
hide(inputGroup);
|
||||
input.disabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private toggleLinkedSelect(target: HTMLSelectElement | HTMLInputElement) {
|
||||
const secondaryOptions = target.dataset.secondaryOptions;
|
||||
if (isSelectElement(target) && secondaryOptions) {
|
||||
const parent = target.closest('.editable-champ-linked_drop_down_list');
|
||||
const secondary = parent?.querySelector<HTMLSelectElement>(
|
||||
'select[data-secondary]'
|
||||
);
|
||||
if (secondary) {
|
||||
const options = parseOptions(secondaryOptions);
|
||||
this.setSecondaryOptions(secondary, options[target.value]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private setSecondaryOptions(
|
||||
secondarySelectElement: HTMLSelectElement,
|
||||
options: string[]
|
||||
) {
|
||||
const wrapper = secondarySelectElement.closest('.secondary');
|
||||
const hidden = wrapper?.nextElementSibling as HTMLInputElement | null;
|
||||
|
||||
secondarySelectElement.innerHTML = '';
|
||||
|
||||
if (options.length) {
|
||||
disable(hidden);
|
||||
|
||||
if (secondarySelectElement.required) {
|
||||
secondarySelectElement.appendChild(makeOption(''));
|
||||
}
|
||||
for (const option of options) {
|
||||
secondarySelectElement.appendChild(makeOption(option));
|
||||
}
|
||||
|
||||
secondarySelectElement.selectedIndex = 0;
|
||||
enable(secondarySelectElement);
|
||||
show(wrapper);
|
||||
} else {
|
||||
hide(wrapper);
|
||||
disable(secondarySelectElement);
|
||||
enable(hidden);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const SecondaryOptions = z.record(z.string().array());
|
||||
|
||||
function parseOptions(options: string) {
|
||||
return SecondaryOptions.parse(JSON.parse(options));
|
||||
}
|
||||
|
||||
function makeOption(option: string) {
|
||||
const element = document.createElement('option');
|
||||
element.textContent = option;
|
||||
element.value = option;
|
||||
return element;
|
||||
}
|
|
@ -21,6 +21,7 @@
|
|||
# type_de_champ_id :integer
|
||||
#
|
||||
class Champs::DropDownListChamp < Champ
|
||||
store_accessor :value_json, :other
|
||||
THRESHOLD_NB_OPTIONS_AS_RADIO = 5
|
||||
OTHER = '__other__'
|
||||
delegate :options_without_empty_value_when_mandatory, to: :type_de_champ
|
||||
|
@ -44,7 +45,7 @@ class Champs::DropDownListChamp < Champ
|
|||
end
|
||||
|
||||
def selected
|
||||
other_value_present? ? OTHER : value
|
||||
other? ? OTHER : value
|
||||
end
|
||||
|
||||
def disabled_options
|
||||
|
@ -55,22 +56,28 @@ class Champs::DropDownListChamp < Champ
|
|||
drop_down_list_enabled_non_empty_options
|
||||
end
|
||||
|
||||
def other_value_present?
|
||||
drop_down_other? && value.present? && drop_down_list_options.exclude?(value)
|
||||
def other?
|
||||
drop_down_other? && (other || (value.present? && drop_down_list_options.exclude?(value)))
|
||||
end
|
||||
|
||||
def value=(value)
|
||||
if value != OTHER
|
||||
if value == OTHER
|
||||
self.other = true
|
||||
write_attribute(:value, nil)
|
||||
else
|
||||
self.other = false
|
||||
write_attribute(:value, value)
|
||||
end
|
||||
end
|
||||
|
||||
def value_other=(value)
|
||||
write_attribute(:value, value)
|
||||
if other?
|
||||
write_attribute(:value, value)
|
||||
end
|
||||
end
|
||||
|
||||
def value_other
|
||||
other_value_present? ? value : ""
|
||||
other? ? value : ""
|
||||
end
|
||||
|
||||
def in?(options)
|
||||
|
|
|
@ -503,7 +503,9 @@ class TypeDeChamp < ApplicationRecord
|
|||
when type_champs.fetch(:epci),
|
||||
type_champs.fetch(:communes),
|
||||
type_champs.fetch(:multiple_drop_down_list),
|
||||
type_champs.fetch(:dossier_link)
|
||||
type_champs.fetch(:dossier_link),
|
||||
type_champs.fetch(:linked_drop_down_list),
|
||||
type_champs.fetch(:drop_down_list)
|
||||
true
|
||||
else
|
||||
false
|
||||
|
|
|
@ -34,14 +34,14 @@ describe EditableChamp::EditableChampComponent, type: :component do
|
|||
it { expect(subject).to eq(data) }
|
||||
|
||||
context 'when a public dropdown champ' do
|
||||
let(:controllers) { ['autosave', 'champ-dropdown'] }
|
||||
let(:controllers) { ['autosave'] }
|
||||
let(:champ) { create(:champ_drop_down_list, dossier: dossier) }
|
||||
|
||||
it { expect(subject).to eq(data) }
|
||||
end
|
||||
|
||||
context 'when a private dropdown champ' do
|
||||
let(:controllers) { ['autosave', 'champ-dropdown'] }
|
||||
let(:controllers) { ['autosave'] }
|
||||
let(:champ) { create(:champ_drop_down_list, dossier: dossier, private: true) }
|
||||
|
||||
it { expect(subject).to eq(data) }
|
||||
|
@ -49,14 +49,14 @@ describe EditableChamp::EditableChampComponent, type: :component do
|
|||
end
|
||||
|
||||
context 'when a public dropdown champ' do
|
||||
let(:controllers) { ['autosave', 'champ-dropdown'] }
|
||||
let(:controllers) { ['autosave'] }
|
||||
let(:champ) { create(:champ_drop_down_list, dossier: dossier) }
|
||||
|
||||
it { expect(subject).to eq(data) }
|
||||
end
|
||||
|
||||
context 'when a private dropdown champ' do
|
||||
let(:controllers) { ['autosave', 'champ-dropdown'] }
|
||||
let(:controllers) { ['autosave'] }
|
||||
let(:champ) { create(:champ_drop_down_list, dossier: dossier, private: true) }
|
||||
|
||||
it { expect(subject).to eq(data) }
|
||||
|
|
|
@ -59,7 +59,7 @@ describe 'dropdown list with other option activated', js: true do
|
|||
scenario 'with a select and other, selecting a value save it (avoid hidden other_value to be sent)' do
|
||||
fill_individual
|
||||
|
||||
find(".drop_down_other input", visible: false)
|
||||
expect(page).not_to have_selector(".drop_down_other input")
|
||||
select("Autre")
|
||||
find(".drop_down_other input", visible: true)
|
||||
|
||||
|
|
Loading…
Reference in a new issue