Merge pull request #8377 from tchak/use-coldwired
refactor(turbo): use @coldwired/actions
This commit is contained in:
commit
9db84cfd25
9 changed files with 100 additions and 115 deletions
|
@ -72,7 +72,8 @@ class Attachment::EditComponent < ApplicationComponent
|
||||||
id: input_id,
|
id: input_id,
|
||||||
aria: { describedby: champ&.describedby_id },
|
aria: { describedby: champ&.describedby_id },
|
||||||
data: {
|
data: {
|
||||||
auto_attach_url:
|
auto_attach_url:,
|
||||||
|
turbo_force: true
|
||||||
}.merge(has_file_size_validator? ? { max_file_size: } : {})
|
}.merge(has_file_size_validator? ? { max_file_size: } : {})
|
||||||
.merge(user_can_replace? ? { replace_attachment_target: "input" } : {})
|
.merge(user_can_replace? ? { replace_attachment_target: "input" } : {})
|
||||||
}.merge(has_content_type_validator? ? { accept: accept_content_type } : {})
|
}.merge(has_content_type_validator? ? { accept: accept_content_type } : {})
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
- row_id = @row.first.row_id
|
- row_id = "row-#{@row.first.row_id}"
|
||||||
.row{ id: row_id }
|
.row{ id: row_id }
|
||||||
- @row.each do |champ|
|
- @row.each do |champ|
|
||||||
= fields_for champ.input_name, champ do |form|
|
= fields_for champ.input_name, champ do |form|
|
||||||
|
|
|
@ -54,8 +54,9 @@ module TurboStreamHelper
|
||||||
action_all :morph, targets, content, **rendering, &block
|
action_all :morph, targets, content, **rendering, &block
|
||||||
end
|
end
|
||||||
|
|
||||||
def dispatch(type, detail = {})
|
def dispatch(type, detail = nil)
|
||||||
turbo_stream_simple_action_tag(:dispatch, 'event-type': type, 'event-detail': detail.to_json)
|
content = detail.present? ? tag.script(cdata_section(detail.to_json), type: 'application/json') : nil
|
||||||
|
action_all :append, 'head', tag.dispatch_event(content, type:)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -1,40 +1,91 @@
|
||||||
import { show, hide } from '@utils';
|
import { Actions } from '@coldwired/actions';
|
||||||
import { session as TurboSession } from '@hotwired/turbo';
|
import { parseTurboStream } from '@coldwired/turbo-stream';
|
||||||
|
import invariant from 'tiny-invariant';
|
||||||
|
import { session as TurboSession, type StreamElement } from '@hotwired/turbo';
|
||||||
|
|
||||||
import { ApplicationController } from './application_controller';
|
import { ApplicationController } from './application_controller';
|
||||||
|
|
||||||
|
type StreamRenderEvent = CustomEvent<{
|
||||||
|
render(streamElement: StreamElement): void;
|
||||||
|
}>;
|
||||||
|
|
||||||
export class TurboController extends ApplicationController {
|
export class TurboController extends ApplicationController {
|
||||||
static targets = ['spinner'];
|
static targets = ['spinner'];
|
||||||
|
|
||||||
declare readonly spinnerTarget: HTMLElement;
|
declare readonly spinnerTargets: HTMLElement[];
|
||||||
declare readonly hasSpinnerTarget: boolean;
|
|
||||||
|
|
||||||
#submitting = true;
|
#submitting = false;
|
||||||
|
#actions?: Actions;
|
||||||
|
|
||||||
|
// `actions` instrface exposes all available actions as methods and also `applyActions` method
|
||||||
|
// wich allows to apply a batch of actions. On top of regular `turbo-stream` actions we also
|
||||||
|
// expose `focus`, `enable`, `disable`, `show` and `hide` actions. Each action take a `targets`
|
||||||
|
// option (wich can be a CSS selector or a list of DOM nodes) and a `fragment` option (wich is a
|
||||||
|
// `DocumentFragment` and only required on "rendering" actions).
|
||||||
|
get actions() {
|
||||||
|
invariant(this.#actions, 'Actions not initialized');
|
||||||
|
return this.#actions;
|
||||||
|
}
|
||||||
|
|
||||||
connect() {
|
connect() {
|
||||||
|
this.#actions = new Actions({
|
||||||
|
element: document.documentElement,
|
||||||
|
schema: { forceAttribute: 'data-turbo-force', hiddenClassName: 'hidden' }
|
||||||
|
});
|
||||||
|
|
||||||
|
// actions#observe() is an interface over specialized mutation observers.
|
||||||
|
// They allow us to preserve certain HTML changes across mutations.
|
||||||
|
this.#actions.observe();
|
||||||
|
|
||||||
|
// setup spinner events
|
||||||
this.onGlobal('turbo:submit-start', () => this.startSpinner());
|
this.onGlobal('turbo:submit-start', () => this.startSpinner());
|
||||||
this.onGlobal('turbo:submit-end', () => this.stopSpinner());
|
this.onGlobal('turbo:submit-end', () => this.stopSpinner());
|
||||||
this.onGlobal('turbo:fetch-request-error', () => this.stopSpinner());
|
this.onGlobal('turbo:fetch-request-error', () => this.stopSpinner());
|
||||||
|
|
||||||
// prevent scroll on turbo form submits
|
// prevent scroll on turbo form submits
|
||||||
this.onGlobal('turbo:render', () => this.preventScrollIfNeeded());
|
this.onGlobal('turbo:render', () => this.preventScrollIfNeeded());
|
||||||
|
|
||||||
|
// reset state preserved for actions between pages
|
||||||
|
this.onGlobal('turbo:load', () => this.actions.reset());
|
||||||
|
|
||||||
|
// see: https://turbo.hotwired.dev/handbook/streams#custom-actions
|
||||||
|
this.onGlobal('turbo:before-stream-render', (event: StreamRenderEvent) => {
|
||||||
|
const fallbackToDefaultActions = event.detail.render;
|
||||||
|
event.detail.render = (streamElement: StreamElement) =>
|
||||||
|
this.renderStreamElement(streamElement, fallbackToDefaultActions);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
startSpinner() {
|
private renderStreamElement(
|
||||||
|
streamElement: StreamElement,
|
||||||
|
fallbackRender: (streamElement: StreamElement) => void
|
||||||
|
) {
|
||||||
|
switch (streamElement.action) {
|
||||||
|
// keep turbo default behavior to avoid risks going all in on coldwire
|
||||||
|
case 'replace':
|
||||||
|
case 'update':
|
||||||
|
fallbackRender(streamElement);
|
||||||
|
break;
|
||||||
|
case 'morph':
|
||||||
|
streamElement.setAttribute('action', 'replace');
|
||||||
|
this.actions.applyActions([parseTurboStream(streamElement)]);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.actions.applyActions([parseTurboStream(streamElement)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private startSpinner() {
|
||||||
this.#submitting = true;
|
this.#submitting = true;
|
||||||
if (this.hasSpinnerTarget) {
|
this.actions.show({ targets: this.spinnerTargets });
|
||||||
show(this.spinnerTarget);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
stopSpinner() {
|
private stopSpinner() {
|
||||||
this.#submitting = false;
|
this.#submitting = false;
|
||||||
if (this.hasSpinnerTarget) {
|
this.actions.hide({ targets: this.spinnerTargets });
|
||||||
hide(this.spinnerTarget);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
preventScrollIfNeeded() {
|
private preventScrollIfNeeded() {
|
||||||
if (this.#submitting && TurboSession.navigator.currentVisit) {
|
if (this.#submitting && TurboSession.navigator.currentVisit) {
|
||||||
TurboSession.navigator.currentVisit.scrolled = true;
|
TurboSession.navigator.currentVisit.scrolled = true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@ import * as Turbo from '@hotwired/turbo';
|
||||||
import { Application } from '@hotwired/stimulus';
|
import { Application } from '@hotwired/stimulus';
|
||||||
import '@gouvfr/dsfr/dist/dsfr.module.js';
|
import '@gouvfr/dsfr/dist/dsfr.module.js';
|
||||||
|
|
||||||
import '../shared/turbo-actions';
|
|
||||||
import '../shared/activestorage/ujs';
|
import '../shared/activestorage/ujs';
|
||||||
import '../shared/safari-11-empty-file-workaround';
|
import '../shared/safari-11-empty-file-workaround';
|
||||||
import '../shared/toggle-target';
|
import '../shared/toggle-target';
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import * as Turbo from '@hotwired/turbo';
|
import * as Turbo from '@hotwired/turbo';
|
||||||
import { Application } from '@hotwired/stimulus';
|
import { Application } from '@hotwired/stimulus';
|
||||||
|
|
||||||
import '../shared/turbo-actions';
|
|
||||||
import '../manager/fields/features';
|
import '../manager/fields/features';
|
||||||
import { registerControllers } from '../shared/stimulus-loader';
|
import { registerControllers } from '../shared/stimulus-loader';
|
||||||
|
|
||||||
|
@ -9,3 +8,7 @@ const application = Application.start();
|
||||||
registerControllers(application);
|
registerControllers(application);
|
||||||
|
|
||||||
Turbo.session.drive = false;
|
Turbo.session.drive = false;
|
||||||
|
|
||||||
|
addEventListener('DOMContentLoaded', () => {
|
||||||
|
document.body.setAttribute('data-controller', 'turbo');
|
||||||
|
});
|
||||||
|
|
|
@ -1,94 +0,0 @@
|
||||||
import { StreamActions, TurboStreamAction } from '@hotwired/turbo';
|
|
||||||
import morphdom from 'morphdom';
|
|
||||||
|
|
||||||
const hide: TurboStreamAction = function () {
|
|
||||||
this.targetElements.forEach((element: Element) => {
|
|
||||||
const delay = this.getAttribute('delay');
|
|
||||||
const hide = () => element.classList.add('hidden');
|
|
||||||
if (delay) {
|
|
||||||
setTimeout(hide, parseInt(delay, 10));
|
|
||||||
} else {
|
|
||||||
hide();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const show: TurboStreamAction = function () {
|
|
||||||
this.targetElements.forEach((element: Element) => {
|
|
||||||
const delay = this.getAttribute('delay');
|
|
||||||
const show = () => element.classList.remove('hidden');
|
|
||||||
if (delay) {
|
|
||||||
setTimeout(show, parseInt(delay, 10));
|
|
||||||
} else {
|
|
||||||
show();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const focus: TurboStreamAction = function () {
|
|
||||||
this.targetElements.forEach((element: HTMLInputElement) => element.focus());
|
|
||||||
};
|
|
||||||
const disable: TurboStreamAction = function () {
|
|
||||||
this.targetElements.forEach((element: HTMLInputElement) => {
|
|
||||||
element.disabled = true;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const enable: TurboStreamAction = function () {
|
|
||||||
this.targetElements.forEach((element: HTMLInputElement) => {
|
|
||||||
element.disabled = false;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const morph: TurboStreamAction = function () {
|
|
||||||
this.targetElements.forEach((element: Element) => {
|
|
||||||
let content: Element | DocumentFragment = this.templateContent;
|
|
||||||
|
|
||||||
// content.children ignores text node, the empty text nodes in particular
|
|
||||||
// so if templateContent contains an empty text node,
|
|
||||||
// we only keep the first element and happily morph it
|
|
||||||
if (content.children.length == 1) {
|
|
||||||
content = content.children[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
// morphom morphes if content is an element
|
|
||||||
// swaps if content if a documentFragment
|
|
||||||
morphdom(element, content, {
|
|
||||||
onBeforeElUpdated: function (fromEl, toEl) {
|
|
||||||
if (isTouchedInput(fromEl)) {
|
|
||||||
fromEl.removeAttribute('data-touched');
|
|
||||||
mergeInputValue(fromEl as HTMLInputElement, toEl as HTMLInputElement);
|
|
||||||
}
|
|
||||||
if (fromEl.isEqualNode(toEl)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const dispatch: TurboStreamAction = function () {
|
|
||||||
const type = this.getAttribute('event-type') ?? '';
|
|
||||||
const detail = this.getAttribute('event-detail');
|
|
||||||
const event = new CustomEvent(type, {
|
|
||||||
detail: JSON.parse(detail ?? '{}'),
|
|
||||||
bubbles: true
|
|
||||||
});
|
|
||||||
document.documentElement.dispatchEvent(event);
|
|
||||||
};
|
|
||||||
|
|
||||||
StreamActions['hide'] = hide;
|
|
||||||
StreamActions['show'] = show;
|
|
||||||
StreamActions['focus'] = focus;
|
|
||||||
StreamActions['disable'] = disable;
|
|
||||||
StreamActions['enable'] = enable;
|
|
||||||
StreamActions['morph'] = morph;
|
|
||||||
StreamActions['dispatch'] = dispatch;
|
|
||||||
|
|
||||||
function mergeInputValue(fromEl: HTMLInputElement, toEl: HTMLInputElement) {
|
|
||||||
toEl.value = fromEl.value;
|
|
||||||
toEl.checked = fromEl.checked;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isTouchedInput(element: HTMLElement): boolean {
|
|
||||||
return (
|
|
||||||
['INPUT', 'TEXTAREA', 'SELECT'].includes(element.tagName) &&
|
|
||||||
!!element.getAttribute('data-touched')
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,5 +1,7 @@
|
||||||
{
|
{
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@coldwired/actions": "^0.8.0",
|
||||||
|
"@coldwired/turbo-stream": "^0.8.0",
|
||||||
"@gouvfr/dsfr": "^1.7.2",
|
"@gouvfr/dsfr": "^1.7.2",
|
||||||
"@graphiql/plugin-explorer": "^0.1.11",
|
"@graphiql/plugin-explorer": "^0.1.11",
|
||||||
"@graphiql/toolkit": "^0.8.0",
|
"@graphiql/toolkit": "^0.8.0",
|
||||||
|
@ -30,7 +32,6 @@
|
||||||
"is-hotkey": "^0.2.0",
|
"is-hotkey": "^0.2.0",
|
||||||
"maplibre-gl": "^1.15.2",
|
"maplibre-gl": "^1.15.2",
|
||||||
"match-sorter": "^6.2.0",
|
"match-sorter": "^6.2.0",
|
||||||
"morphdom": "^2.6.1",
|
|
||||||
"patch-package": "^6.5.1",
|
"patch-package": "^6.5.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-coordinate-input": "^1.0.0",
|
"react-coordinate-input": "^1.0.0",
|
||||||
|
|
23
yarn.lock
23
yarn.lock
|
@ -511,6 +511,29 @@
|
||||||
"@babel/helper-validator-identifier" "^7.19.1"
|
"@babel/helper-validator-identifier" "^7.19.1"
|
||||||
to-fast-properties "^2.0.0"
|
to-fast-properties "^2.0.0"
|
||||||
|
|
||||||
|
"@coldwired/actions@^0.8.0":
|
||||||
|
version "0.8.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@coldwired/actions/-/actions-0.8.0.tgz#a5e92c0badbc37ffa7c9e70d2aa7982739fbba5f"
|
||||||
|
integrity sha512-IbqyC2ToDv1JWBVkk33ywDrftD34r3WBLokub7Tlon8xS6FQQVejwmgqIqiQ3iLE0jiXOqKz2an3Y1e69mJ55A==
|
||||||
|
dependencies:
|
||||||
|
"@coldwired/utils" "^0.4.1"
|
||||||
|
morphdom "^2.6.1"
|
||||||
|
tiny-invariant "^1.3.1"
|
||||||
|
|
||||||
|
"@coldwired/turbo-stream@^0.8.0":
|
||||||
|
version "0.8.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@coldwired/turbo-stream/-/turbo-stream-0.8.0.tgz#596202ecbca88f9b264d5055bedcb94298db626b"
|
||||||
|
integrity sha512-/ZKdAcgDkcH+YkW2RyxXGMM6OdpKp4nVpFGTYnor+4mR/jjEdnTRw+3wnZJQfgPzLMG39SXEmY4mp9xQM/qA3Q==
|
||||||
|
dependencies:
|
||||||
|
"@coldwired/actions" "^0.8.0"
|
||||||
|
"@coldwired/utils" "^0.4.1"
|
||||||
|
tiny-invariant "^1.3.1"
|
||||||
|
|
||||||
|
"@coldwired/utils@^0.4.1":
|
||||||
|
version "0.4.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@coldwired/utils/-/utils-0.4.1.tgz#50a615c81948754f4c7f118df05a44e078f28568"
|
||||||
|
integrity sha512-a673URA77AUBm8u5bWGtEmMi9oMv8HBidPT1GtRsaYma1W23Hd2Emn+oD6vUyGLfL+hJGT91w+Y2Y3x9BgLqwQ==
|
||||||
|
|
||||||
"@esbuild/android-arm64@0.16.14":
|
"@esbuild/android-arm64@0.16.14":
|
||||||
version "0.16.14"
|
version "0.16.14"
|
||||||
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.16.14.tgz#f02c9f0d43086ddf6ed2795b881ddf7990f74456"
|
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.16.14.tgz#f02c9f0d43086ddf6ed2795b881ddf7990f74456"
|
||||||
|
|
Loading…
Add table
Reference in a new issue