From 03ad0c68d36b2ffc5d226b4a0acba4359eb0c6ec Mon Sep 17 00:00:00 2001 From: Sjors Baltus Date: Tue, 23 Nov 2021 11:30:15 +0100 Subject: [PATCH 1/2] Listen to cable-ready:after-update events and re-request the partial --- javascript/elements/futurism_element.js | 4 +++- javascript/elements/futurism_li.js | 4 +++- javascript/elements/futurism_table_row.js | 4 +++- javascript/elements/futurism_utils.js | 6 ++++++ 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/javascript/elements/futurism_element.js b/javascript/elements/futurism_element.js index 8dcf355..7197c7d 100644 --- a/javascript/elements/futurism_element.js +++ b/javascript/elements/futurism_element.js @@ -2,12 +2,14 @@ import { extendElementWithIntersectionObserver, - extendElementWithEagerLoading + extendElementWithEagerLoading, + extendElementWithCableReadyUpdatesFor } from './futurism_utils' export default class FuturismElement extends HTMLElement { connectedCallback () { extendElementWithIntersectionObserver(this) extendElementWithEagerLoading(this) + extendElementWithCableReadyUpdatesFor(this) } } diff --git a/javascript/elements/futurism_li.js b/javascript/elements/futurism_li.js index bba6979..58532c8 100644 --- a/javascript/elements/futurism_li.js +++ b/javascript/elements/futurism_li.js @@ -2,12 +2,14 @@ import { extendElementWithIntersectionObserver, - extendElementWithEagerLoading + extendElementWithEagerLoading, + extendElementWithCableReadyUpdatesFor } from './futurism_utils' export default class FuturismLI extends HTMLLIElement { connectedCallback () { extendElementWithIntersectionObserver(this) extendElementWithEagerLoading(this) + extendElementWithCableReadyUpdatesFor(this) } } diff --git a/javascript/elements/futurism_table_row.js b/javascript/elements/futurism_table_row.js index 3303c2f..58a98db 100644 --- a/javascript/elements/futurism_table_row.js +++ b/javascript/elements/futurism_table_row.js @@ -2,12 +2,14 @@ import { extendElementWithIntersectionObserver, - extendElementWithEagerLoading + extendElementWithEagerLoading, + extendElementWithCableReadyUpdatesFor } from './futurism_utils' export default class FuturismTableRow extends HTMLTableRowElement { connectedCallback () { extendElementWithIntersectionObserver(this) extendElementWithEagerLoading(this) + extendElementWithCableReadyUpdatesFor(this) } } diff --git a/javascript/elements/futurism_utils.js b/javascript/elements/futurism_utils.js index c2e6c1e..e4256e2 100644 --- a/javascript/elements/futurism_utils.js +++ b/javascript/elements/futurism_utils.js @@ -62,3 +62,9 @@ export const extendElementWithEagerLoading = element => { dispatchAppearEvent(element) } } + +export const extendElementWithCableReadyUpdatesFor = (element) => { + element.addEventListener('cable-ready:after-update', () => { + dispatchAppearEvent(element); + }); +} From 78b5bef847fe7ffac408e02e01124d726a0ee777 Mon Sep 17 00:00:00 2001 From: Sjors Baltus Date: Thu, 25 Nov 2021 10:34:46 +0100 Subject: [PATCH 2/2] Wrap futurism with CableReady#updates_for and instruct updates-for element to not morph but instead emit an event --- javascript/elements/futurism_li.js | 4 +- javascript/elements/futurism_table_row.js | 4 +- javascript/elements/futurism_utils.js | 19 +++++- javascript/futurism_channel.js | 12 ++-- lib/futurism/helpers.rb | 75 +++++++++++++++++++++-- lib/futurism/resolver/resources.rb | 37 ++++++++--- test/cable/channel_test.rb | 35 +++++++++++ test/helper/helper_test.rb | 24 ++++++++ 8 files changed, 184 insertions(+), 26 deletions(-) diff --git a/javascript/elements/futurism_li.js b/javascript/elements/futurism_li.js index 58532c8..bba6979 100644 --- a/javascript/elements/futurism_li.js +++ b/javascript/elements/futurism_li.js @@ -2,14 +2,12 @@ import { extendElementWithIntersectionObserver, - extendElementWithEagerLoading, - extendElementWithCableReadyUpdatesFor + extendElementWithEagerLoading } from './futurism_utils' export default class FuturismLI extends HTMLLIElement { connectedCallback () { extendElementWithIntersectionObserver(this) extendElementWithEagerLoading(this) - extendElementWithCableReadyUpdatesFor(this) } } diff --git a/javascript/elements/futurism_table_row.js b/javascript/elements/futurism_table_row.js index 58a98db..3303c2f 100644 --- a/javascript/elements/futurism_table_row.js +++ b/javascript/elements/futurism_table_row.js @@ -2,14 +2,12 @@ import { extendElementWithIntersectionObserver, - extendElementWithEagerLoading, - extendElementWithCableReadyUpdatesFor + extendElementWithEagerLoading } from './futurism_utils' export default class FuturismTableRow extends HTMLTableRowElement { connectedCallback () { extendElementWithIntersectionObserver(this) extendElementWithEagerLoading(this) - extendElementWithCableReadyUpdatesFor(this) } } diff --git a/javascript/elements/futurism_utils.js b/javascript/elements/futurism_utils.js index e4256e2..dd5552b 100644 --- a/javascript/elements/futurism_utils.js +++ b/javascript/elements/futurism_utils.js @@ -64,7 +64,20 @@ export const extendElementWithEagerLoading = element => { } export const extendElementWithCableReadyUpdatesFor = (element) => { - element.addEventListener('cable-ready:after-update', () => { - dispatchAppearEvent(element); - }); + if (element.dataset.updatesFor) { + if (element.hasAttribute('keep')) { + if (element.observer) element.observer.disconnect() + } + + element.addEventListener('cable-ready:after-update', (event) => { + const evt = new CustomEvent('futurism:appear', { + bubbles: true, + detail: { + target: element, + observer: null + } + }) + document.dispatchEvent(evt) + }); + } } diff --git a/javascript/futurism_channel.js b/javascript/futurism_channel.js index 35c3c27..5ffd3c2 100644 --- a/javascript/futurism_channel.js +++ b/javascript/futurism_channel.js @@ -16,6 +16,10 @@ const debounceEvents = (callback, delay = 20) => { } } +const targetResolver = (event) => { + return event.detail.target || event.target +} + export const createSubscription = consumer => { consumer.subscriptions.create('Futurism::Channel', { connected () { @@ -24,13 +28,13 @@ export const createSubscription = consumer => { 'futurism:appear', debounceEvents(events => { this.send({ - signed_params: events.map(e => e.target.dataset.signedParams), - sgids: events.map(e => e.target.dataset.sgid), + signed_params: events.map(e => targetResolver(e).dataset.signedParams), + sgids: events.map(e => targetResolver(e).dataset.sgid), signed_controllers: events.map( - e => e.target.dataset.signedController + e => targetResolver(e).dataset.signedController ), urls: events.map(_ => window.location.href), - broadcast_each: events.map(e => e.target.dataset.broadcastEach) + broadcast_each: events.map(e => targetResolver(e).dataset.broadcastEach) }) }) ) diff --git a/lib/futurism/helpers.rb b/lib/futurism/helpers.rb index 566f0a9..418de5b 100644 --- a/lib/futurism/helpers.rb +++ b/lib/futurism/helpers.rb @@ -68,10 +68,18 @@ def initialize(extends:, placeholder:, options:) @eager = options.delete(:eager) @broadcast_each = options.delete(:broadcast_each) @controller = options.delete(:controller) + @updates_for_object = options.delete(:updates_for) @html_options = options.delete(:html_options) || {} @data_attributes = html_options.fetch(:data, {}).except(:sgid, :signed_params) @model = options.delete(:model) + @wrapped_for_updates_for = options.delete(:wrapped_for_updates_for) + if @wrapped_for_updates_for + @html_options[:keep] = 'keep' + @data_attributes['updates-for'] = true + end @options = data_attributes.any? ? options.merge(data: data_attributes) : options + + warn "[Futurism] `updates_for` feature is not available for extends: :li or :tr elements." if [:tr, :li].include?(extends) end def dataset @@ -85,6 +93,53 @@ def dataset end def render + return render_updates_for if use_updates_for? + + render_tag + end + + def transformed_options + dump_options(options) + end + + private + + ############ + # TODO: Include CableReadyHelper + include CableReady::Compoundable + include CableReady::StreamIdentifier + include ActionView::Context + + def updates_for(*keys, url: nil, debounce: nil, only: nil, html_options: {}, &block) + options = build_options(*keys, html_options) + options[:url] = url if url + options[:debounce] = debounce if debounce + options[:only] = only if only + tag.updates_for(**options) { capture(&block) } + end + + private + + def build_options(*keys, html_options) + keys.select!(&:itself) + {identifier: signed_stream_identifier(compound(keys))}.merge(html_options) + end + ############ + + def render_updates_for + arguments = Array.wrap(@updates_for_object) + kwargs = arguments.last.is_a?(Hash) ? arguments.pop : {} + kwargs[:html_options] ||= {} + kwargs[:html_options][:data] ||= {} + kwargs[:html_options][:data]['ignore-morph'] = true + kwargs[:html_options][:data]['after-update-event-selector'] = 'futurism-element' + + updates_for(*arguments, **kwargs) do + render_tag + end + end + + def render_tag case extends when :li content_tag :li, placeholder, html_options.deep_merge({data: dataset, is: "futurism-li"}) @@ -95,14 +150,24 @@ def render end end - def transformed_options - dump_options(options) + def use_updates_for? + @updates_for_object.present? && ![:tr, :li].include?(extends) end - private - def signed_params - message_verifier.generate(transformed_options) + message_verifier.generate(transformed_options.merge(updates_for_params)) + end + + def updates_for_params + return {} unless use_updates_for? || @wrapped_for_updates_for + + { + wrap_for_updates_for: { + extends: extends, + html_options: html_options, + data_attributes: data_attributes, + } + } end def signed_controller diff --git a/lib/futurism/resolver/resources.rb b/lib/futurism/resolver/resources.rb index afd3a72..d86a1fc 100644 --- a/lib/futurism/resolver/resources.rb +++ b/lib/futurism/resolver/resources.rb @@ -15,20 +15,17 @@ def initialize(resource_definitions:, connection:, params:) def resolve resolved_models.zip(@resources_with_sgids).each do |model, resource_definition| - html = renderer_for(resource_definition: resource_definition).render(model) + options = options_from_resource(resource_definition) + html = render_html(model, resource_definition: resource_definition, render_exceptions: false) + html = wrapped_html(html, options) yield(resource_definition.selector, html, resource_definition.broadcast_each) end @resources_without_sgids.each do |resource_definition| options = options_from_resource(resource_definition) - renderer = renderer_for(resource_definition: resource_definition) - html = - begin - renderer.render(options) - rescue => exception - error_renderer.render(exception) - end + html = render_html(options, resource_definition: resource_definition) + html = wrapped_html(html, options) yield(resource_definition.selector, html, resource_definition.broadcast_each) end @@ -36,6 +33,30 @@ def resolve private + def wrapped_html(html, options) + wrap_for_updates_for = options.delete(:wrap_for_updates_for) + return html unless wrap_for_updates_for + + # Only wrap the element again if we were told to for the updates_for feature + options = options.dup + options.merge!(wrap_for_updates_for) + options[:wrapped_for_updates_for] = true + + extends = options.delete(:extends) + + Futurism::Helpers::WrappingFuturismElement.new(extends: extends, placeholder: html, options: options).render + end + + def render_html(model, render_exceptions: true, **kwargs) + return renderer_for(**kwargs).render(model) unless render_exceptions + + begin + renderer_for(**kwargs).render(model) + rescue => exception + error_renderer.render(exception) + end + end + def error_renderer ErrorRenderer.new end diff --git a/test/cable/channel_test.rb b/test/cable/channel_test.rb index ad4c221..a3aea35 100644 --- a/test/cable/channel_test.rb +++ b/test/cable/channel_test.rb @@ -103,6 +103,41 @@ class Futurism::ChannelTest < ActionCable::Channel::TestCase end end + test "broadcasts a rendered partial wrapped by a futurism element after receiving signed params" do + with_mocked_renderer do |mock_renderer| + post = Post.create title: "Lorem" + fragment = Nokogiri::HTML.fragment(futurize(partial: "posts/card", locals: {post: post}, extends: :div, wrap_for_updates_for: { extends: :div }, wrapped_for_updates_for: true) {}) + signed_params = fragment.children.first["data-signed-params"] + subscribe + + mock_renderer + .expect( + :render, + "", + [ + partial: "posts/card", + locals: {post: post}, + :wrap_for_updates_for => { + :extends => :div, + :html_options => { + :keep => "keep" + }, + :data_attributes => { + "updates-for" => true + } + }, + :data => { + "updates-for" => true + } + ] + ) + + perform :receive, {"signed_params" => [signed_params]} + + assert_mock mock_renderer + end + end + test "broadcasts a rendered partial after receiving the shorthand syntax" do with_mocked_renderer do |mock_renderer| post = Post.create title: "Lorem" diff --git a/test/helper/helper_test.rb b/test/helper/helper_test.rb index 26f0603..83dccca 100644 --- a/test/helper/helper_test.rb +++ b/test/helper/helper_test.rb @@ -174,6 +174,30 @@ def self.find(id) assert_includes element.children.first.children.first.text, "Lorem" end + test "allows to automatically wrap the futurism html element with a CableReady updates_for element" do + post = Post.create title: "Lorem" + + element = Nokogiri::HTML.fragment(futurize(post, updates_for: post, extends: :div) {}) + assert_equal "updates-for", element.children.first.name + assert_equal "futurism-element", element.children.first.children.first.name + end + + test "does not wrap the futurism html element with a CableReady updates_for element when using extends: :tr" do + post = Post.create title: "Lorem" + + element = Nokogiri::HTML.fragment(futurize(post, updates_for: post, extends: :tr) {}) + refute_equal "updates-for", element.children.first.name + assert_equal "tr", element.children.first.name + end + + test "does not wrap the futurism html element with a CableReady updates_for element when using extends: :li" do + post = Post.create title: "Lorem" + + element = Nokogiri::HTML.fragment(futurize(post, updates_for: post, extends: :li) {}) + refute_equal "updates-for", element.children.first.name + assert_equal "li", element.children.first.name + end + def verifier Futurism::MessageVerifier.message_verifier end