Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft: Listen to cable-ready:after-update events and re-request the partial #120

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion javascript/elements/futurism_element.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
19 changes: 19 additions & 0 deletions javascript/elements/futurism_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,22 @@ export const extendElementWithEagerLoading = element => {
dispatchAppearEvent(element)
}
}

export const extendElementWithCableReadyUpdatesFor = (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)
});
}
}
12 changes: 8 additions & 4 deletions javascript/futurism_channel.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ const debounceEvents = (callback, delay = 20) => {
}
}

const targetResolver = (event) => {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have to check if this is still needed or a relic of my try & fail & try again approach

return event.detail.target || event.target
}

export const createSubscription = consumer => {
consumer.subscriptions.create('Futurism::Channel', {
connected () {
Expand All @@ -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)
})
})
)
Expand Down
75 changes: 70 additions & 5 deletions lib/futurism/helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really happy with this variable name, any suggestions?

@html_options[:keep] = 'keep'
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needed to not add the IntersectionObserver (as it will re-request the partial over and over again when it is visible)

@data_attributes['updates-for'] = true
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instructing the javascript to listen for the CableReady cable-ready:after-update event. As we only want to refresh the partial if it has been rendered before.

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
Expand All @@ -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
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this and include it from CableReady once it's moved to a module.

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"})
Expand All @@ -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
Expand Down
37 changes: 29 additions & 8 deletions lib/futurism/resolver/resources.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,27 +15,48 @@ 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
end

private

def wrapped_html(html, options)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Name of this method can be improved

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
Expand Down
35 changes: 35 additions & 0 deletions test/cable/channel_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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,
"<tag></tag>",
[
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"
Expand Down
24 changes: 24 additions & 0 deletions test/helper/helper_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down