From 0bd71ad51a35f8f8c26282acd659d5df1269bf21 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Thu, 14 Apr 2022 20:46:28 +0200 Subject: [PATCH] feat(turbo): add turbo event helpers --- app/assets/stylesheets/custom_reset.scss | 4 + app/helpers/turbo_stream_helper.rb | 35 +++++++ .../controllers/turbo_event_controller.ts | 91 +++++++++++++++++++ app/javascript/packs/application.js | 2 + app/views/layouts/_turbo_event.html.haml | 5 + app/views/layouts/application.html.haml | 2 + .../layouts/application.turbo_stream.haml | 6 ++ package.json | 3 +- yarn.lock | 5 + 9 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 app/helpers/turbo_stream_helper.rb create mode 100644 app/javascript/controllers/turbo_event_controller.ts create mode 100644 app/views/layouts/_turbo_event.html.haml create mode 100644 app/views/layouts/application.turbo_stream.haml diff --git a/app/assets/stylesheets/custom_reset.scss b/app/assets/stylesheets/custom_reset.scss index b59355b03..e3486d5c1 100644 --- a/app/assets/stylesheets/custom_reset.scss +++ b/app/assets/stylesheets/custom_reset.scss @@ -22,3 +22,7 @@ a { text-decoration: none; } + +turbo-events { + display: none; +} diff --git a/app/helpers/turbo_stream_helper.rb b/app/helpers/turbo_stream_helper.rb new file mode 100644 index 000000000..40699c66e --- /dev/null +++ b/app/helpers/turbo_stream_helper.rb @@ -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 diff --git a/app/javascript/controllers/turbo_event_controller.ts b/app/javascript/controllers/turbo_event_controller.ts new file mode 100644 index 000000000..8f959e5a7 --- /dev/null +++ b/app/javascript/controllers/turbo_event_controller.ts @@ -0,0 +1,91 @@ +import { Controller } from '@hotwired/stimulus'; +import invariant from 'tiny-invariant'; +import { z } from 'zod'; + +type Detail = Record; + +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; +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; + +addEventListener('dom:mutation', (event) => { + const detail = (event as CustomEvent).detail; + const mutation = Mutation.parse(detail); + mutate(mutation); +}); + +const Mutations: Record 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( + mutation: Mutation +): Element[] { + if ('target' in mutation) { + const element = document.querySelector(`#${mutation.target}`); + invariant(element, `Could not find element with id ${mutation.target}`); + return [element]; + } else if ('targets' in mutation) { + return [...document.querySelectorAll(mutation.targets)]; + } + invariant(false, 'Could not find element'); +} diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index cedb6d332..79a11009e 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -18,6 +18,7 @@ import { ReactController, registerComponents } from '../controllers/react_controller'; +import { TurboEventController } from '../controllers/turbo_event_controller'; import '../new_design/dropdown'; import '../new_design/form-validation'; @@ -94,6 +95,7 @@ Turbo.session.drive = false; const Stimulus = Application.start(); Stimulus.register('react', ReactController); +Stimulus.register('turbo-event', TurboEventController); // Expose globals window.DS = window.DS || DS; diff --git a/app/views/layouts/_turbo_event.html.haml b/app/views/layouts/_turbo_event.html.haml new file mode 100644 index 000000000..e7ddff930 --- /dev/null +++ b/app/views/layouts/_turbo_event.html.haml @@ -0,0 +1,5 @@ +%turbo-event{ data: { + controller: 'turbo-event', + turbo_event_type_value: type, + turbo_event_detail_value: detail.to_json +} } diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 52f4934b4..ed9d71b76 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -41,3 +41,5 @@ = content_for(:footer) = yield :charts_js + + %turbo-events diff --git a/app/views/layouts/application.turbo_stream.haml b/app/views/layouts/application.turbo_stream.haml new file mode 100644 index 000000000..343964e1f --- /dev/null +++ b/app/views/layouts/application.turbo_stream.haml @@ -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 diff --git a/package.json b/package.json index 8d68f6920..7bef976e0 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,8 @@ "use-debounce": "^5.2.0", "webpack": "^4.46.0", "webpack-cli": "^3.3.12", - "whatwg-fetch": "^3.0.0" + "whatwg-fetch": "^3.0.0", + "zod": "^3.14.4" }, "devDependencies": { "@2fd/graphdoc": "^2.4.0", diff --git a/yarn.lock b/yarn.lock index bec7b1041..adbc387fe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14032,3 +14032,8 @@ zip-stream@^4.1.0: archiver-utils "^2.1.0" compress-commons "^4.1.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==