feat(turbo): add turbo event helpers
This commit is contained in:
parent
69d5713c19
commit
0bd71ad51a
9 changed files with 152 additions and 1 deletions
|
@ -22,3 +22,7 @@ a {
|
||||||
|
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
turbo-events {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
35
app/helpers/turbo_stream_helper.rb
Normal file
35
app/helpers/turbo_stream_helper.rb
Normal 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
|
91
app/javascript/controllers/turbo_event_controller.ts
Normal file
91
app/javascript/controllers/turbo_event_controller.ts
Normal 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');
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
5
app/views/layouts/_turbo_event.html.haml
Normal file
5
app/views/layouts/_turbo_event.html.haml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
%turbo-event{ data: {
|
||||||
|
controller: 'turbo-event',
|
||||||
|
turbo_event_type_value: type,
|
||||||
|
turbo_event_detail_value: detail.to_json
|
||||||
|
} }
|
|
@ -41,3 +41,5 @@
|
||||||
= content_for(:footer)
|
= content_for(:footer)
|
||||||
|
|
||||||
= yield :charts_js
|
= yield :charts_js
|
||||||
|
|
||||||
|
%turbo-events
|
||||||
|
|
6
app/views/layouts/application.turbo_stream.haml
Normal file
6
app/views/layouts/application.turbo_stream.haml
Normal 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
|
|
@ -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",
|
||||||
|
|
|
@ -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==
|
||||||
|
|
Loading…
Reference in a new issue