Merge pull request #7239 from tchak/refactor-form-input

refactor form inputs to use turbo
This commit is contained in:
Paul Chavard 2022-05-04 14:15:46 +02:00 committed by GitHub
commit 7e3a73ad18
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 96 additions and 67 deletions

View file

@ -31,5 +31,13 @@ module TurboStreamHelper
def focus_all(targets)
dispatch('dom:mutation', { action: :focus, targets: targets })
end
def disable(target)
dispatch('dom:mutation', { action: :disable, target: target })
end
def enable(target)
dispatch('dom:mutation', { action: :enable, target: target })
end
end
end

View file

@ -38,7 +38,7 @@ export function useFeatureCollection(
features: callback(features)
}));
httpRequest(url)
.js()
.turbo()
.catch(() => null);
},
[url, setFeatureCollection]

View file

@ -22,4 +22,19 @@ export class ApplicationController extends Controller {
target: document.documentElement
});
}
protected on<HandlerEvent extends Event = Event>(
eventName: string,
handler: (event: HandlerEvent) => void
): void {
const disconnect = this.disconnect;
const callback = (event: Event): void => {
handler(event as HandlerEvent);
};
this.element.addEventListener(eventName, callback);
this.disconnect = () => {
this.element.removeEventListener(eventName, callback);
disconnect.call(this);
};
}
}

View file

@ -18,7 +18,7 @@ export class TurboEventController extends ApplicationController {
}
}
const MutationAction = z.enum(['show', 'hide', 'focus']);
const MutationAction = z.enum(['show', 'hide', 'focus', 'enable', 'disable']);
type MutationAction = z.infer<typeof MutationAction>;
const Mutation = z.union([
z.object({
@ -55,6 +55,16 @@ const Mutations: Record<MutationAction, (mutation: Mutation) => void> = {
for (const element of findElements(mutation)) {
element.focus();
}
},
disable: (mutation) => {
for (const element of findElements<HTMLInputElement>(mutation)) {
element.disabled = true;
}
},
enable: (mutation) => {
for (const element of findElements<HTMLInputElement>(mutation)) {
element.disabled = false;
}
}
};

View file

@ -0,0 +1,22 @@
import { httpRequest } from '@utils';
import { ApplicationController } from './application_controller';
export class TurboInputController extends ApplicationController {
static values = {
url: String
};
declare readonly urlValue: string;
connect(): void {
this.on('input', () => this.debounce(this.load, 200));
}
private load(): void {
const target = this.element as HTMLInputElement;
const url = new URL(this.urlValue, document.baseURI);
url.searchParams.append(target.name, target.value);
httpRequest(url.toString()).turbo();
}
}

View file

@ -8,7 +8,6 @@ import * as Turbo from '@hotwired/turbo';
import '../shared/activestorage/ujs';
import '../shared/remote-poller';
import '../shared/safari-11-file-xhr-workaround';
import '../shared/remote-input';
import '../shared/franceconnect';
import '../shared/toggle-target';
import '../shared/ujs-error-handling';
@ -19,6 +18,7 @@ import {
} from '../controllers/react_controller';
import { TurboEventController } from '../controllers/turbo_event_controller';
import { GeoAreaController } from '../controllers/geo_area_controller';
import { TurboInputController } from '../controllers/turbo_input_controller';
import '../new_design/dropdown';
import '../new_design/form-validation';
@ -96,6 +96,7 @@ const Stimulus = Application.start();
Stimulus.register('react', ReactController);
Stimulus.register('turbo-event', TurboEventController);
Stimulus.register('geo-area', GeoAreaController);
Stimulus.register('turbo-input', TurboInputController);
// Expose globals
window.DS = window.DS || DS;

View file

@ -1,21 +0,0 @@
import { delegate, fire, debounce } from '@utils';
const remote = 'data-remote';
const inputChangeSelector = `input[${remote}], textarea[${remote}]`;
// This is a patch for ujs remote handler. Its purpose is to add
// a debounced input listener.
function handleRemote(event) {
const element = this;
if (isRemote(element)) {
fire(element, 'change', event);
}
}
function isRemote(element) {
const value = element.getAttribute(remote);
return value && value !== 'false';
}
delegate('input', inputChangeSelector, debounce(handleRemote, 200));

View file

@ -1,9 +0,0 @@
<%= render_flash(timeout: 5000, fixed: true) %>
<%= render_to_element("##{@champ.input_group_id} .geo-areas",
partial: 'shared/champs/carte/geo_areas',
locals: { champ: @champ, editing: true }) %>
<% if @focus %>
<%= fire_event('map:feature:focus', { bbox: @champ.bounding_box }.to_json) %>
<% end %>

View file

@ -0,0 +1,4 @@
= turbo_stream.update dom_id(@champ, :geo_areas), partial: 'shared/champs/carte/geo_areas', locals: { champ: @champ, editing: true }
- if @focus
= turbo_stream.dispatch 'map:feature:focus', bbox: @champ.bounding_box

View file

@ -1,3 +0,0 @@
<%= render_to_element("##{@champ.input_group_id} .help-block",
partial: 'shared/champs/dossier_link/help_block',
locals: { id: @linked_dossier_id }) %>

View file

@ -0,0 +1 @@
= turbo_stream.update dom_id(@champ, :help_block), partial: 'shared/champs/dossier_link/help_block', locals: { id: @linked_dossier_id }

View file

@ -1,3 +0,0 @@
<%= render_to_element("##{@champ.input_group_id} .siret-info",
partial: 'shared/champs/siret/etablissement',
locals: { siret: @siret, etablissement: @etablissement }) %>

View file

@ -0,0 +1 @@
= turbo_stream.update dom_id(@champ, :siret_info), partial: 'shared/champs/siret/etablissement', locals: { siret: @siret, etablissement: @etablissement }

View file

@ -1,4 +1,4 @@
= form.password_field :password, autofocus: true, autocomplete: 'off', placeholder: 'Mot de passe', data: { remote: test_complexity, url: show_password_complexity_path }
= form.password_field :password, autofocus: true, autocomplete: 'off', placeholder: 'Mot de passe', data: { controller: test_complexity ? 'turbo-input' : false, turbo_input_url_value: show_password_complexity_path }
- if test_complexity
#complexity-bar.password-complexity

View file

@ -1,3 +0,0 @@
<%= render_to_element('#complexity-label', partial: 'label', outer: true) %>
<%= render_to_element('#complexity-bar', partial: 'bar', outer: true) %>
<%= raw("document.querySelector('#submit-password').disabled = #{@score < @min_complexity || @length < @min_length};") %>

View file

@ -0,0 +1,6 @@
= turbo_stream.replace 'complexity-label', partial: 'label'
= turbo_stream.replace 'complexity-bar', partial: 'bar'
- if @score < @min_complexity || @length < @min_length
= turbo_stream.disable 'submit-password'
- else
= turbo_stream.enable 'submit-password'

View file

@ -1,4 +1,4 @@
= react_component("MapEditor", featureCollection: champ.to_feature_collection, url: champs_carte_features_path(champ), options: champ.render_options)
.geo-areas
.geo-areas{ id: dom_id(champ, :geo_areas) }
= render partial: 'shared/champs/carte/geo_areas', locals: { champ: champ, editing: true }

View file

@ -5,7 +5,7 @@
placeholder: "Numéro de dossier",
autocomplete: 'off',
required: champ.mandatory?,
data: { remote: true, url: champs_dossier_link_path(champ.id) }
data: { controller: 'turbo-input', turbo_input_url_value: champs_dossier_link_path(champ.id) }
.help-block
.help-block{ id: dom_id(champ, :help_block) }
= render partial: 'shared/champs/dossier_link/help_block', locals: { id: champ.value }

View file

@ -2,11 +2,11 @@
id: champ.input_id,
aria: { describedby: champ.describedby_id },
placeholder: champ.libelle,
data: { remote: true, debounce: true, url: champs_siret_path(champ.id), spinner: true },
data: { controller: 'turbo-input', turbo_input_url_value: champs_siret_path(champ.id) },
required: champ.mandatory?,
pattern: "[0-9]{14}",
title: "Le numéro de SIRET doit comporter exactement 14 chiffres"
.spinner.right.hidden
.siret-info
.siret-info{ id: dom_id(champ, :siret_info) }
- if champ.etablissement.present?
= render partial: 'shared/dossiers/editable_champs/etablissement_titre', locals: { etablissement: champ.etablissement }

View file

@ -117,7 +117,7 @@ describe Champs::CarteController, type: :controller do
render_views
before do
get :index, params: params, format: :js, xhr: true
get :index, params: params, format: :turbo_stream
end
context 'without focus' do
@ -126,7 +126,7 @@ describe Champs::CarteController, type: :controller do
end
it 'updates the list' do
expect(response.body).not_to include("DS.fire('map:feature:focus'")
expect(response.body).not_to include("map:feature:focus")
expect(response.status).to eq 200
end
end
@ -140,7 +140,8 @@ describe Champs::CarteController, type: :controller do
end
it 'updates the list and focuses the map' do
expect(response.body).to include("DS.fire('map:feature:focus'")
expect(response.body).to include(ActionView::RecordIdentifier.dom_id(champ, :geo_areas))
expect(response.body).to include("map:feature:focus")
expect(response.status).to eq 200
end
end

View file

@ -27,33 +27,33 @@ describe Champs::DossierLinkController, type: :controller do
context 'when the dossier exist' do
before do
get :show, params: params, format: :js, xhr: true
get :show, params: params, format: :turbo_stream
end
it 'renders the procedure name' do
expect(response.body).to include('Dossier en brouillon')
expect(response.body).to include(procedure.libelle)
expect(response.body).to include(procedure.organisation)
expect(response.body).to include("##{champ.input_group_id} .help-block")
expect(response.body).to include(ActionView::RecordIdentifier.dom_id(champ, :help_block))
end
end
context 'when the dossier does not exist' do
let(:dossier_id) { '13' }
before do
get :show, params: params, format: :js, xhr: true
get :show, params: params, format: :turbo_stream
end
it 'renders error message' do
expect(response.body).to include('Ce dossier est inconnu')
expect(response.body).to include("##{champ.input_group_id} .help-block")
expect(response.body).to include(ActionView::RecordIdentifier.dom_id(champ, :help_block))
end
end
end
context 'when user is not connected' do
before do
get :show, params: { champ_id: champ.id }, format: :js, xhr: true
get :show, params: { champ_id: champ.id }, format: :turbo_stream
end
it { expect(response.code).to eq('401') }

View file

@ -37,7 +37,7 @@ describe Champs::SiretController, type: :controller do
end
context 'when the SIRET is empty' do
subject! { get :show, params: params, format: :js, xhr: true }
subject! { get :show, params: params, format: :turbo_stream }
it 'clears the etablissement and SIRET on the model' do
champ.reload
@ -46,15 +46,14 @@ describe Champs::SiretController, type: :controller do
end
it 'clears any information or error message' do
expect(response.body).to include("##{champ.input_group_id} .siret-info")
expect(response.body).to include('innerHTML = ""')
expect(response.body).to include(ActionView::RecordIdentifier.dom_id(champ, :siret_info))
end
end
context 'when the SIRET is invalid' do
let(:siret) { '1234' }
subject! { get :show, params: params, format: :js, xhr: true }
subject! { get :show, params: params, format: :turbo_stream }
it 'clears the etablissement and SIRET on the model' do
champ.reload
@ -71,7 +70,7 @@ describe Champs::SiretController, type: :controller do
let(:siret) { '82161143100015' }
let(:api_etablissement_status) { 503 }
subject! { get :show, params: params, format: :js, xhr: true }
subject! { get :show, params: params, format: :turbo_stream }
it 'clears the etablissement and SIRET on the model' do
champ.reload
@ -88,7 +87,7 @@ describe Champs::SiretController, type: :controller do
let(:siret) { '00000000000000' }
let(:api_etablissement_status) { 404 }
subject! { get :show, params: params, format: :js, xhr: true }
subject! { get :show, params: params, format: :turbo_stream }
it 'clears the etablissement and SIRET on the model' do
champ.reload
@ -106,7 +105,7 @@ describe Champs::SiretController, type: :controller do
let(:api_etablissement_status) { 200 }
let(:api_etablissement_body) { File.read('spec/fixtures/files/api_entreprise/etablissements.json') }
subject! { get :show, params: params, format: :js, xhr: true }
subject! { get :show, params: params, format: :turbo_stream }
it 'populates the etablissement and SIRET on the model' do
champ.reload
@ -119,7 +118,7 @@ describe Champs::SiretController, type: :controller do
end
context 'when user is not signed in' do
subject! { get :show, params: { champ_id: champ.id }, format: :js, xhr: true }
subject! { get :show, params: { champ_id: champ.id }, format: :turbo_stream }
it { expect(response.code).to eq('401') }
end

View file

@ -4,7 +4,7 @@ describe PasswordComplexityController, type: :controller do
{ user: { password: 'moderately complex password' } }
end
subject { get :show, format: :js, params: params, xhr: true }
subject { get :show, format: :turbo_stream, params: params }
it 'computes a password score' do
subject
@ -27,8 +27,8 @@ describe PasswordComplexityController, type: :controller do
it 'renders Javascript that updates the password complexity meter' do
subject
expect(response.body).to include('#complexity-label')
expect(response.body).to include('#complexity-bar')
expect(response.body).to include('complexity-label')
expect(response.body).to include('complexity-bar')
end
end
end