feat(turbo): add turbo event helpers

This commit is contained in:
Paul Chavard 2022-04-14 20:46:28 +02:00
parent 69d5713c19
commit 0bd71ad51a
9 changed files with 152 additions and 1 deletions

View file

@ -22,3 +22,7 @@ a {
text-decoration: none; text-decoration: none;
} }
turbo-events {
display: none;
}

View file

@ -0,0 +1,35 @@
module TurboStreamHelper
def turbo_stream
TagBuilder.new(self)
end
class TagBuilder < Turbo::Streams::TagBuilder
def dispatch(type, detail)
append_all('turbo-events', partial: 'layouts/turbo_event', locals: { type: type, detail: detail })
end
def show(target, delay: nil)
dispatch('dom:mutation', { action: :show, target: target, delay: delay }.compact)
end
def show_all(targets, delay: nil)
dispatch('dom:mutation', { action: :show, targets: targets, delay: delay }.compact)
end
def hide(target, delay: nil)
dispatch('dom:mutation', { action: :hide, target: target, delay: delay }.compact)
end
def hide_all(targets, delay: nil)
dispatch('dom:mutation', { action: :hide, targets: targets, delay: delay }.compact)
end
def focus(target)
dispatch('dom:mutation', { action: :focus, target: target })
end
def focus_all(targets)
dispatch('dom:mutation', { action: :focus, targets: targets })
end
end
end

View file

@ -0,0 +1,91 @@
import { Controller } from '@hotwired/stimulus';
import invariant from 'tiny-invariant';
import { z } from 'zod';
type Detail = Record<string, unknown>;
export class TurboEventController extends Controller {
static values = {
type: String,
detail: Object
};
declare readonly typeValue: string;
declare readonly detailValue: Detail;
connect(): void {
this.globalDispatch(this.typeValue, this.detailValue);
this.element.remove();
}
private globalDispatch(type: string, detail: Detail): void {
this.dispatch(type, {
detail,
prefix: '',
target: document.documentElement
});
}
}
const MutationAction = z.enum(['show', 'hide', 'focus']);
type MutationAction = z.infer<typeof MutationAction>;
const Mutation = z.union([
z.object({
action: MutationAction,
delay: z.number().optional(),
target: z.string()
}),
z.object({
action: MutationAction,
delay: z.number().optional(),
targets: z.string()
})
]);
type Mutation = z.infer<typeof Mutation>;
addEventListener('dom:mutation', (event) => {
const detail = (event as CustomEvent).detail;
const mutation = Mutation.parse(detail);
mutate(mutation);
});
const Mutations: Record<MutationAction, (mutation: Mutation) => void> = {
hide: (mutation) => {
for (const element of findElements(mutation)) {
element.classList.add('hidden');
}
},
show: (mutation) => {
for (const element of findElements(mutation)) {
element.classList.remove('hidden');
}
},
focus: (mutation) => {
for (const element of findElements(mutation)) {
element.focus();
}
}
};
function mutate(mutation: Mutation) {
const fn = Mutations[mutation.action];
invariant(fn, `Could not find mutation ${mutation.action}`);
if (mutation.delay) {
setTimeout(() => fn(mutation), mutation.delay);
} else {
fn(mutation);
}
}
function findElements<Element extends HTMLElement = HTMLElement>(
mutation: Mutation
): Element[] {
if ('target' in mutation) {
const element = document.querySelector<Element>(`#${mutation.target}`);
invariant(element, `Could not find element with id ${mutation.target}`);
return [element];
} else if ('targets' in mutation) {
return [...document.querySelectorAll<Element>(mutation.targets)];
}
invariant(false, 'Could not find element');
}

View file

@ -18,6 +18,7 @@ import {
ReactController, ReactController,
registerComponents registerComponents
} from '../controllers/react_controller'; } from '../controllers/react_controller';
import { TurboEventController } from '../controllers/turbo_event_controller';
import '../new_design/dropdown'; import '../new_design/dropdown';
import '../new_design/form-validation'; import '../new_design/form-validation';
@ -94,6 +95,7 @@ Turbo.session.drive = false;
const Stimulus = Application.start(); const Stimulus = Application.start();
Stimulus.register('react', ReactController); Stimulus.register('react', ReactController);
Stimulus.register('turbo-event', TurboEventController);
// Expose globals // Expose globals
window.DS = window.DS || DS; window.DS = window.DS || DS;

View file

@ -0,0 +1,5 @@
%turbo-event{ data: {
controller: 'turbo-event',
turbo_event_type_value: type,
turbo_event_detail_value: detail.to_json
} }

View file

@ -41,3 +41,5 @@
= content_for(:footer) = content_for(:footer)
= yield :charts_js = yield :charts_js
%turbo-events

View file

@ -0,0 +1,6 @@
- if flash.any?
= turbo_stream.replace 'flash_messages', partial: 'layouts/flash_messages'
= turbo_stream.hide 'flash_messages', delay: 10000
- flash.clear
= yield

View file

@ -43,7 +43,8 @@
"use-debounce": "^5.2.0", "use-debounce": "^5.2.0",
"webpack": "^4.46.0", "webpack": "^4.46.0",
"webpack-cli": "^3.3.12", "webpack-cli": "^3.3.12",
"whatwg-fetch": "^3.0.0" "whatwg-fetch": "^3.0.0",
"zod": "^3.14.4"
}, },
"devDependencies": { "devDependencies": {
"@2fd/graphdoc": "^2.4.0", "@2fd/graphdoc": "^2.4.0",

View file

@ -14032,3 +14032,8 @@ zip-stream@^4.1.0:
archiver-utils "^2.1.0" archiver-utils "^2.1.0"
compress-commons "^4.1.0" compress-commons "^4.1.0"
readable-stream "^3.6.0" readable-stream "^3.6.0"
zod@^3.14.4:
version "3.14.4"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.14.4.tgz#e678fe9e5469f4663165a5c35c8f3dc66334a5d6"
integrity sha512-U9BFLb2GO34Sfo9IUYp0w3wJLlmcyGoMd75qU9yf+DrdGA4kEx6e+l9KOkAlyUO0PSQzZCa3TR4qVlcmwqSDuw==