From 1198add901c88e8215a24cc69aaf6c5ba8a2d194 Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Sun, 11 Feb 2024 18:31:06 +0100 Subject: [PATCH] Rewrite stream rendering to avoid client-side expanding * adding streams in client breaks things (e.g. autofocus) * some tasks need to be performed in one stream action to avoid flickering (e.g. table row substitution) --- app/helpers/application_helper.rb | 2 - app/javascript/application.js | 77 +++++++++++++++++++-- app/views/units/_form.html.erb | 8 ++- app/views/units/_form_close.html.erb | 5 +- app/views/units/edit.turbo_stream.erb | 6 +- app/views/units/new.turbo_stream.erb | 23 ++---- config/initializers/turbo_stream_actions.rb | 24 +++++-- 7 files changed, 107 insertions(+), 38 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 64c08b1..0b2ca77 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -103,8 +103,6 @@ module ApplicationHelper end def render_turbo_stream(partial, locals) - # TODO: extend with smth like "if outside of rendering, render; otherwise - # appendChild() template within current render" "Turbo.renderStreamMessage('#{j(render partial: partial, locals: locals)}'); return false;" end diff --git a/app/javascript/application.js b/app/javascript/application.js index cf3f536..3c3fe63 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -14,6 +14,7 @@ function beforeStreamRender(event) { document.addEventListener('turbo:before-stream-render', beforeStreamRender) */ +/* Turbo.session.streamMessageRenderer.appendFragment = async function (fragment) { for (let child of [...fragment.children]) { document.documentElement.appendChild(child) @@ -32,6 +33,38 @@ Turbo.session.streamMessageRenderer.appendFragment = async function (fragment) { } } +window.renderTurboStream = function (message) { + // Render if not already rendering, otherwise just append fragment to DOM + if (document.documentElement.getElementsByTagName("turbo-stream").length == 0) { + Turbo.renderStreamMessage(message) + } else { + Turbo.session.streamMessageRenderer + .appendFragment(Turbo.StreamMessage.wrap(message).fragment) + } +} +*/ + +Turbo.StreamElement.prototype.enableElement = function(element) { + element.removeAttribute("disabled") + element.removeAttribute("aria-disabled") + // 'tabindex' is not used explicitly, so removing it is safe + element.removeAttribute("tabindex") +} + +Turbo.StreamElement.prototype.removePreviousForm = function(form) { + const id = form.id + const row = document.getElementById(id + "_cached") + form.remove() + if (row) { + row.id = id + row.style.display = "revert" + } + if (form.hasAttribute("data-link-id")) { + const link = document.getElementById(form.getAttribute("data-link-id")) + this.enableElement(link) + } +} + Turbo.StreamActions.disable = function() { this.targetElements.forEach((e) => { @@ -42,12 +75,7 @@ Turbo.StreamActions.disable = function() { } Turbo.StreamActions.enable = function() { - this.targetElements.forEach((e) => { - e.removeAttribute("disabled") - e.removeAttribute("aria-disabled") - // 'tabindex' is not used explicitly, so removing it is safe - e.removeAttribute("tabindex") - }) + this.targetElements.forEach((e) => { this.enableElement(e) }) } Turbo.StreamActions.blur = function() { @@ -59,6 +87,43 @@ Turbo.StreamActions.focus = function() { this.targetElements[0].focus({focusVisible: true}) } +Turbo.StreamActions.prepend_form = function() { + this.targetElements.forEach((e) => { + [...e.getElementsByClassName("form")].forEach((f) => { + this.removePreviousForm(f) + }) + e.prepend(this.templateContent) + }) +} + +Turbo.StreamActions.after_form = function() { + this.targetElements.forEach((e) => { + [...e.parentElement?.getElementsByClassName("form")].forEach((f) => { + this.removePreviousForm(f) + }) + e.parentElement?.insertBefore(this.templateContent, e.nextSibling) + }) +} + +Turbo.StreamActions.replace_form = function() { + this.targetElements.forEach((e) => { + [...e.parentElement?.getElementsByClassName("form")].forEach((f) => { + this.removePreviousForm(f) + }) + e.style.display = "none" + e.id = e.id + "_cached" + e.parentElement?.insertBefore(this.templateContent, e.nextSibling) + }) +} + +Turbo.StreamActions.close_form = function() { + this.targetElements.forEach((e) => { + this.removePreviousForm(e.closest(".form")) + }) +} + +/* Turbo.StreamActions.click = function() { this.targetElements.forEach((e) => { e.click() }) } +*/ diff --git a/app/views/units/_form.html.erb b/app/views/units/_form.html.erb index 6b7d7f7..dce2f88 100644 --- a/app/views/units/_form.html.erb +++ b/app/views/units/_form.html.erb @@ -1,5 +1,7 @@ <%= fields_for @unit do |form| %> - + <%= tag.tr id: dom_id(@unit), class: "form", onkeydown: "processKey(event)", + data: {link_id: link_id} do %> + <%= form.text_field :symbol, form: :unit_form, required: true, autofocus: true, size: 12, maxlength: @unit.class.columns_hash['symbol'].limit, autocomplete: "off" %> @@ -19,8 +21,8 @@ <%= form.submit form: :unit_form %> <%= image_link_to t(:cancel), "close-circle-outline", units_path, class: 'dangerous', - name: :cancel, onclick: render_turbo_stream('form_close', {id: id}) %> + name: :cancel, onclick: render_turbo_stream('form_close', {link_id: link_id}) %> - + <% end %> <% end %> diff --git a/app/views/units/_form_close.html.erb b/app/views/units/_form_close.html.erb index 5226d99..3a9f2dd 100644 --- a/app/views/units/_form_close.html.erb +++ b/app/views/units/_form_close.html.erb @@ -1,3 +1,2 @@ -<%= @unit.persisted? ? turbo_stream.replace(@unit) : turbo_stream.remove(@unit) %> -<%= turbo_stream.enable id %> -<%= turbo_stream.focus id %> +<%= turbo_stream.close_form @unit %> +<%#= turbo_stream.focus link_id %> diff --git a/app/views/units/edit.turbo_stream.erb b/app/views/units/edit.turbo_stream.erb index e5df3f7..b796499 100644 --- a/app/views/units/edit.turbo_stream.erb +++ b/app/views/units/edit.turbo_stream.erb @@ -1,7 +1,7 @@ <%# TODO: make sure turbo_stream layout is used in new/edit %> -<%= turbo_stream.replace @unit, partial: 'form', locals: {id: dom_id(@unit, :edit)} %> - <%= turbo_stream.update :unit_form_frame do %> <%= form_with model: @unit, html: {id: :unit_form} do %> <% end %> -<% end unless @unit.errors.present? %> +<% end %> + +<%= turbo_stream.replace_form @unit, partial: 'form', locals: {link_id: dom_id(@unit, :edit)} %> diff --git a/app/views/units/new.turbo_stream.erb b/app/views/units/new.turbo_stream.erb index ff2d753..0c7b0d2 100644 --- a/app/views/units/new.turbo_stream.erb +++ b/app/views/units/new.turbo_stream.erb @@ -1,20 +1,9 @@ -<% options = {partial: 'form', locals: {id: dom_id(@unit.base || @unit, :add)}} %> +<% link_id = dom_id(@unit.base || @unit, :add) %> +<%= turbo_stream.disable link_id -%> -<% if @unit.errors.present? %> - <%= turbo_stream.replace @unit, **options -%> -<% else %> - <%= turbo_stream.disable options[:locals][:id] -%> - <%= turbo_stream.click_all 'tbody a[name=cancel]' -%> - <%#= turbo_stream.blur_all %> - - <% if @unit.base.nil? %> - <%= turbo_stream.prepend :units, **options -%> - <% else %> - <%= turbo_stream.after @unit.base, **options -%> - <% end %> - - <%= turbo_stream.update :unit_form_frame do %> - <%= form_with model: @unit, html: {id: :unit_form} do %> - <% end %> +<%= turbo_stream.update :unit_form_frame do %> + <%= form_with model: @unit, html: {id: :unit_form} do %> <% end %> <% end %> + +<%= turbo_stream.insert_form (@unit.base || :units), partial: 'form', locals: {link_id: link_id} %> diff --git a/config/initializers/turbo_stream_actions.rb b/config/initializers/turbo_stream_actions.rb index 34f0b88..8a07444 100644 --- a/config/initializers/turbo_stream_actions.rb +++ b/config/initializers/turbo_stream_actions.rb @@ -23,11 +23,27 @@ ActiveSupport.on_load :turbo_streams_tag_builder do action :focus, target, allow_inferred_rendering: false end - def click(target) - action :click, target, allow_inferred_rendering: false + #def click(target) + # action :click, target, allow_inferred_rendering: false + #end + + #def click_all(targets) + # action_all :click, targets, allow_inferred_rendering: false + #end + + def insert_form(target, content = nil, **rendering, &block) + if target.is_a? Symbol + action :prepend_form, target, content, **rendering, &block + else + action :after_form, target, content, **rendering, &block + end end - def click_all(targets) - action_all :click, targets, allow_inferred_rendering: false + def replace_form(target, content = nil, **rendering, &block) + action :replace_form, target, content, **rendering, &block + end + + def close_form(target) + action :close_form, target, allow_inferred_rendering: false end end