diff --git a/app/components/editable_champ/decimal_number_component/decimal_number_component.html.haml b/app/components/editable_champ/decimal_number_component/decimal_number_component.html.haml index a3efb88f4..1eca76fae 100644 --- a/app/components/editable_champ/decimal_number_component/decimal_number_component.html.haml +++ b/app/components/editable_champ/decimal_number_component/decimal_number_component.html.haml @@ -1 +1 @@ -= @form.text_field(:value, input_opts(id: @champ.input_id, aria: { describedby: @champ.describedby_id }, required: @champ.required?, pattern: "-?[0-9]+([\.,][0-9]{1,3})?", inputmode: :decimal)) += @form.text_field(:value, input_opts(id: @champ.input_id, aria: { describedby: @champ.describedby_id }, required: @champ.required?, pattern: "-?[0-9]+([\.,][0-9]{1,3})?", inputmode: :decimal, data: { controller: 'format decimal-number-input', format: :decimal })) diff --git a/app/components/editable_champ/integer_number_component/integer_number_component.html.haml b/app/components/editable_champ/integer_number_component/integer_number_component.html.haml index bab39525f..117e412f9 100644 --- a/app/components/editable_champ/integer_number_component/integer_number_component.html.haml +++ b/app/components/editable_champ/integer_number_component/integer_number_component.html.haml @@ -1 +1 @@ -= @form.text_field(:value, input_opts(id: @champ.input_id, aria: { describedby: @champ.describedby_id }, pattern: "[0-9]*", inputmode: :numeric, required: @champ.required?)) += @form.text_field(:value, input_opts(id: @champ.input_id, aria: { describedby: @champ.describedby_id }, pattern: "[0-9]*", inputmode: :numeric, required: @champ.required?, data: { controller: 'format', format: :integer })) diff --git a/app/javascript/controllers/decimal_number_input_controller.ts b/app/javascript/controllers/decimal_number_input_controller.ts new file mode 100644 index 000000000..74746e52c --- /dev/null +++ b/app/javascript/controllers/decimal_number_input_controller.ts @@ -0,0 +1,35 @@ +import { ApplicationController } from './application_controller'; + +export class DecimalNumberInputController extends ApplicationController { + connect() { + const value = this.inputElement.value; + + if (value) { + this.formatValue(value); + } + } + + formatValue(value: string) { + const number = parseFloat(value); + + if (isNaN(number)) { + return; + } + + this.inputElement.value = number.toLocaleString(); + this.emitInputEvent(); // trigger format controller + } + + private get inputElement(): HTMLInputElement { + return this.element as HTMLInputElement; + } + + private emitInputEvent() { + const event = new InputEvent('input', { + bubbles: true, + cancelable: true + }); + + this.inputElement.dispatchEvent(event); + } +} diff --git a/app/javascript/controllers/format_controller.ts b/app/javascript/controllers/format_controller.ts index aed58a6cf..439bbe35b 100644 --- a/app/javascript/controllers/format_controller.ts +++ b/app/javascript/controllers/format_controller.ts @@ -21,6 +21,13 @@ export class FormatController extends ApplicationController { const target = event.target as HTMLInputElement; target.value = this.formatInteger(target.value); }); + break; + case 'decimal': + this.on('input', (event) => { + const target = event.target as HTMLInputElement; + target.value = this.formatDecimal(target.value); + }); + break; } } @@ -39,4 +46,14 @@ export class FormatController extends ApplicationController { private formatInteger(value: string) { return value.replace(/[^\d]/g, ''); } + + private formatDecimal(value: string) { + // Le séparateur de décimales est toujours après le séparateur de milliers (un point ou une virgule). + // S'il n'y a qu'un seul séparateur, on considère que c'est celui des décimales. + // S'il n'y en a pas, ça n'a pas d'effet. + const decimalSeparator = + value.lastIndexOf(',') > value.lastIndexOf('.') ? ',' : '.'; + + return value.replace(new RegExp(`[^\\d${decimalSeparator}]`, 'g'), ''); + } } diff --git a/app/models/champs/decimal_number_champ.rb b/app/models/champs/decimal_number_champ.rb index 9d67ee267..267ce6bca 100644 --- a/app/models/champs/decimal_number_champ.rb +++ b/app/models/champs/decimal_number_champ.rb @@ -1,4 +1,5 @@ class Champs::DecimalNumberChamp < Champ + before_validation :format_value validates :value, numericality: { allow_nil: true, allow_blank: true, @@ -17,6 +18,12 @@ class Champs::DecimalNumberChamp < Champ private + def format_value + return if value.blank? + + self.value = value.tr(",", ".") + end + def processed_value return if invalid? diff --git a/spec/controllers/users/dossiers_controller_spec.rb b/spec/controllers/users/dossiers_controller_spec.rb index 49441e50b..c85c9b477 100644 --- a/spec/controllers/users/dossiers_controller_spec.rb +++ b/spec/controllers/users/dossiers_controller_spec.rb @@ -694,6 +694,36 @@ describe Users::DossiersController, type: :controller do it { expect(first_champ.reload.value).to eq('beautiful value') } it { expect(response).to have_http_status(:ok) } end + + context 'decimal number champ separator' do + let (:procedure) { create(:procedure, :published, types_de_champ_public: [{ type: :decimal_number }]) } + let (:submit_payload) do + { + id: dossier.id, + dossier: { + champs_public_attributes: { first_champ.id => { id: first_champ.id, value: value } } + } + } + end + + context 'when spearator is dot' do + let(:value) { '3.14' } + + it "saves the value" do + subject + expect(first_champ.reload.value).to eq('3.14') + end + end + + context 'when spearator is comma' do + let(:value) { '3,14' } + + it "saves the value" do + subject + expect(first_champ.reload.value).to eq('3.14') + end + end + end end describe '#update en_construction' do diff --git a/spec/system/users/brouillon_spec.rb b/spec/system/users/brouillon_spec.rb index 10b9604e9..f20ef195f 100644 --- a/spec/system/users/brouillon_spec.rb +++ b/spec/system/users/brouillon_spec.rb @@ -146,7 +146,9 @@ describe 'The user' do let(:simple_procedure) { create(:procedure, :published, :for_individual, types_de_champ_public: [ - { mandatory: true, libelle: 'texte obligatoire' }, { mandatory: false, libelle: 'texte optionnel' }, { mandatory: false, libelle: "nombre", type: :integer_number } + { mandatory: true, libelle: 'texte obligatoire' }, { mandatory: false, libelle: 'texte optionnel' }, + { mandatory: false, libelle: "nombre entier", type: :integer_number }, + { mandatory: false, libelle: "nombre décimal", type: :decimal_number } ]) } @@ -174,17 +176,24 @@ describe 'The user' do expect(page).to have_current_path(merci_dossier_path(user_dossier)) end - scenario 'validates invalid number', js: true, retry: 3 do + scenario 'numbers champs formatting', js: true, retry: 3 do log_in(user, simple_procedure) fill_individual - # Check an incomplete dossier can be saved as a draft, even when mandatory fields are missing - fill_in('nombre', with: 'environ 300') - wait_for_autosave + fill_in('nombre entier', with: '300 environ') + wait_until { + champ_value_for('nombre entier') == '300' + } - within ".fr-message--error" do - expect(page).to have_content("doit être un nombre entier") - end + fill_in('nombre décimal', with: '123 456,78') + wait_until { + champ_value_for('nombre décimal') == '123456.78' + } + + fill_in('nombre décimal', with: '1,234.56') + wait_until { + champ_value_for('nombre décimal') == '1234.56' + } end scenario 'extends dossier experation date more than one time, ', js: true, retry: 3 do