From cca4c10bb8776e0bb1081ca7a117f78c60f1466b Mon Sep 17 00:00:00 2001 From: leastbad Date: Fri, 8 Jul 2022 19:00:09 -0400 Subject: [PATCH 1/4] README refresh, disconnect observer, observer options --- README.md | 95 +++++++++++++++++++++------ javascript/elements/futurism_utils.js | 8 ++- javascript/index.js | 7 +- lib/futurism/helpers.rb | 6 +- lib/tasks/futurism_tasks.rake | 7 +- 5 files changed, 94 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 7656511..34faa31 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,16 @@ # Futurism + [![Twitter follow](https://img.shields.io/twitter/follow/julian_rubisch?style=social)](https://twitter.com/julian_rubisch) + + [![All Contributors](https://img.shields.io/badge/all_contributors-15-orange.svg?style=flat-square)](#contributors-) + + Lazy-load Rails partials via CableReady -:rotating_light: *BREAKING CHANGE: With v1.0, futurism has been transferred to the [stimulusreflex](https://github.com/stimulusreflex) organization. Please update your npm package to `@stimulus_reflex/futurism` accordingly* :rotating_light: +:rotating*light: \_BREAKING CHANGE: With v1.0, futurism has been transferred to the [stimulusreflex](https://github.com/stimulusreflex) organization. Please update your npm package to `@stimulus_reflex/futurism` accordingly* :rotating_light: birmingham-museums-trust-GrvC6MI-z4w-unsplash Photo by Birmingham Museums Trust on Unsplash @@ -16,10 +21,13 @@ Lazy-load Rails partials via CableReady - [Facts](#facts) - [Browser Support](#browser-support) - [Usage](#usage) + - [Placeholders](#placeholders) + - [Tables and Lists](#tables-and-lists) - [API](#api) - [Resource](#resource) - [Explicit Partial](#explicit-partial) - [HTML Options](#html-options) + - [Observer Options](#observer-options) - [Eager Loading](#eager-loading) - [Bypassing](#bypassing) - [Broadcast Partials Individually](#broadcast-partials-individually) @@ -35,6 +43,7 @@ Lazy-load Rails partials via CableReady - [Contributors](#contributors) ## Facts + - only one dependency: CableReady - bundle size (without CableReady) is around [~2.46kB](https://bundlephobia.com/result?p=@stimulus_reflex/futurism@0.7.2) @@ -49,19 +58,38 @@ Lazy-load Rails partials via CableReady [Caniuse](https://www.caniuse.com/#search=custom%20elements) ## Usage -with a helper in your template + +Futurism provides the `futurize` helper. You can pass a single `ActiveRecord` or an `ActiveRecord::Relation`, just as you would call `render`: ```erb -<%= futurize @posts, extends: :div do %> +<%= futurize @posts do %> <% end %> ``` -custom ``s (in the form of a `
` or a `` are rendered. Those custom elements have an `IntersectionObserver` attached that will send a signed global id to an ActionCable channel (`FuturismChannel`) which will then replace the placeholders with the actual resource partial. +The helper will emit `` Web Component elements that have an `IntersectionObserver` attached. When the observer is triggered, it will send a signed global id to an Action Cable channel (`FuturismChannel`). The channel will then use CableReady to replace the placeholders with the actual resource partial. + +With Futurism, you can lazy load any class that has to_partial_path defined, which includes every Active Record and Active Model by default. + +### Placeholders -With that method, you could lazy load every class that has to_partial_path defined (ActiveModel has by default). +An import concept in Futurism is the placeholder content which is displayed before the computed content is requested from the server. -You can pass the placeholder as a block: +Pass the placeholder as a block: + +```erb +<%= futurize @posts do %> +
+<% end %> +``` + +![aa601dec1930151f71dbf0d6b02b61c9](https://user-images.githubusercontent.com/4352208/87131629-f768a480-c294-11ea-89a9-ea0a76ee06ef.gif) + +Optionally, you can omit the placeholder, which instructs Futurism to utilize [eager loading](#eager-loading). + +### Tables and Lists + +By default, `futurize` assumes that you are working with a _`div`-like_ element. Due to idiosyncracies in the HTML specification, you need to provide an `extends` option if you're replacing content inside of a `table` or list (`ul` or `ol`). ```erb <%= futurize @posts, extends: :tr do %> @@ -69,17 +97,25 @@ You can pass the placeholder as a block: <% end %> ``` -![aa601dec1930151f71dbf0d6b02b61c9](https://user-images.githubusercontent.com/4352208/87131629-f768a480-c294-11ea-89a9-ea0a76ee06ef.gif) +You will see that a `` is rendered instead of a `futurism-element`. -You can also omit the placeholder, which falls back to [eager loading](#eager-loading). +Similarly, you can replace an element in a list, resulting in an `
  • ` element: + +```erb +
      + <%= futurize @posts, extends: :li do %> + Loading... + <% end %> +
    +``` ## API -Currently there are two ways to call `futurize`, designed to wrap `render`'s behavior: +Currently there are two ways to call `futurize`, designed to mirror `render`'s behavior: ### Resource -You can pass a single `ActiveRecord` or an `ActiveRecord::Relation` to `futurize`, just as you would call `render`: +You can pass a single `ActiveRecord` or an `ActiveRecord::Relation` to `futurize`: ```erb <%= futurize @posts, extends: :tr do %> @@ -106,7 +142,7 @@ That way you get maximal flexibility when just specifying a single resource. Call `futurize` with a `partial` keyword: ```erb -<%= futurize partial: "items/card", locals: {card: @card}, extends: :div do %> +<%= futurize partial: "items/card", locals: {card: @card} do %>
    <% end %> ``` @@ -114,7 +150,7 @@ Call `futurize` with a `partial` keyword: You can also use the shorthand syntax: ```erb -<%= futurize "items/card", card: @card, extends: :div do %> +<%= futurize "items/card", card: @card do %>
    <% end %> ``` @@ -124,7 +160,7 @@ You can also use the shorthand syntax: Collection rendering is also possible: ```erb -<%= futurize partial: "items/card", collection: @cards, extends: :div do %> +<%= futurize partial: "items/card", collection: @cards do %>
    <% end %> ``` @@ -134,7 +170,7 @@ Collection rendering is also possible: You can also pass in the controller that will be used to render the partial. ```erb -<%= futurize partial: "items/card", collection: @cards, controller: MyController, extends: :div do %> +<%= futurize partial: "items/card", collection: @cards, controller: MyController do %>
    <% end %> ``` @@ -163,7 +199,20 @@ This will output the following: ``` +### Observer Options + +You can pass a hash of attribute/value pairs that will be passed to the IntersectionObserver constructor. + +```erb +<%= futurize @posts, observer_options: {rootMargin: "100px"} do %> +
    +<% end %> +``` + +One common use is to configure the observer to look ahead of the current viewable window to start loading partial content just before you scroll down to it. In many cases, this means that the user will never even be aware that the content they are seeing was lazy loaded. + ### Eager Loading + It may sound surprising to support eager loading in a lazy loading library :joy:, but there's a quite simple use case: Suppose you have some hidden interactive portion of your page, like a tab or dropdown. You don't want its content to block the initial page load, but once that is done, you occasionally don't want to wait for the element to become visible and trigger the `IntersectionObserver`, you want to lazy load its contents right after it's added to the DOM. @@ -188,9 +237,9 @@ In some rare cases, e.g. when combined with CableReady's async `updates_for` mec Internally, this works the same as [bypassing futurism in tests](#testing) - ### Broadcast Partials Individually -Futurism's default behavior is to `broadcast` partials as they are generated in batches: + +Futurism's default behavior is to `broadcast` partials as they are generated in batches: On the client side, `IntersectionObserver` events are triggered in a debounced fashion, so several `render`s are performed on the server for each of those events. By default, futurism will group those to a single `broadcast` call (to save server CPU time). @@ -207,7 +256,7 @@ For collections, however, you can opt into individual broadcasts by specifying ` For individual models or arbitrary collections, you can pass `record` and `index` to the placeholder block as arguments: ```erb -<%= futurize @post, extends: :div do |post| %> +<%= futurize @post do |post| %>
    <%= post.title %>
    <% end %> ``` @@ -229,6 +278,7 @@ For individual models or arbitrary collections, you can pass `record` and `index Once your futurize element has been rendered, the `futurize:appeared` custom event will be called. ## Installation + Add this line to your application's Gemfile: ```ruby @@ -236,6 +286,7 @@ gem 'futurism' ``` And then execute: + ```bash $ bundle ``` @@ -249,6 +300,7 @@ $ bin/rails futurism:install **! Note that the installer will run `yarn add @stimulus_reflex/futurism` for you !** ### Manual Installation + After `bundle`, install the Javascript library: There are a few ways to install the Futurism JavaScript client, depending on your application setup. @@ -281,12 +333,12 @@ import * as Futurism from '@stimulus_reflex/futurism' import consumer from './consumer' -Futurism.initializeElements() -Futurism.createSubscription(consumer) +Futurism.initialize(consumer) ``` ## Authentication -For authentication, you can rely on ActionCable identifiers, for example, if you use Devise: + +For authentication, you can rely on Action Cable identifiers, for example, if you use Devise: ```ruby module ApplicationCable @@ -303,6 +355,7 @@ end The [Stimulus Reflex Docs](https://docs.stimulusreflex.com/authentication) have an excellent section about all sorts of authentication. ## Testing + In Rails system tests there is a chance that flaky errors will occur due to Capybara not waiting for the placeholder elements to be replaced. To overcome this, add the flag ```ruby @@ -337,6 +390,7 @@ Futurism.configure do |config| end ``` + in config/initializers. ## Contributing @@ -387,6 +441,7 @@ yarn install --force 9. Create a new release on GitHub ([here](https://github.com/stimulusreflex/futurism/releases)) and generate the changelog for the stable release for it ## License + The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). ## Contributors ✨ diff --git a/javascript/elements/futurism_utils.js b/javascript/elements/futurism_utils.js index eed5c16..ec451e0 100644 --- a/javascript/elements/futurism_utils.js +++ b/javascript/elements/futurism_utils.js @@ -42,13 +42,19 @@ const observerCallback = (entries, observer) => { entries.forEach(async entry => { if (!entry.isIntersecting) return + observer.disconnect() await callWithRetry(dispatchAppearEvent(entry, observer)) }) } export const extendElementWithIntersectionObserver = element => { Object.assign(element, { - observer: new IntersectionObserver(observerCallback.bind(element), {}) + observer: new IntersectionObserver( + observerCallback.bind(element), + element.dataset.observerOptions + ? JSON.parse(element.dataset.observerOptions) + : {} + ) }) if (!element.hasAttribute('keep')) { diff --git a/javascript/index.js b/javascript/index.js index 44ca308..9086e9f 100644 --- a/javascript/index.js +++ b/javascript/index.js @@ -1,4 +1,9 @@ import { createSubscription } from './futurism_channel' import { initializeElements } from './elements' -export { createSubscription, initializeElements } +function initialize (consumer) { + initializeElements() + createSubscription(consumer) +} + +export { createSubscription, initializeElements, initialize } diff --git a/lib/futurism/helpers.rb b/lib/futurism/helpers.rb index cdbc5e9..93e9c1a 100644 --- a/lib/futurism/helpers.rb +++ b/lib/futurism/helpers.rb @@ -60,7 +60,7 @@ class WrappingFuturismElement include Futurism::MessageVerifier include Futurism::OptionsTransformer - attr_reader :extends, :placeholder, :html_options, :data_attributes, :model, :options, :eager, :broadcast_each, :controller + attr_reader :extends, :placeholder, :html_options, :observer_options, :data_attributes, :model, :options, :eager, :broadcast_each, :controller def initialize(extends:, placeholder:, options:) @extends = extends @@ -69,6 +69,7 @@ def initialize(extends:, placeholder:, options:) @broadcast_each = options.delete(:broadcast_each) @controller = options.delete(:controller) @html_options = options.delete(:html_options) || {} + @observer_options = options.delete(:observer_options) @data_attributes = html_options.fetch(:data, {}).except(:sgid, :signed_params) @model = options.delete(:model) @options = data_attributes.any? ? options.merge(data: data_attributes) : options @@ -80,7 +81,8 @@ def dataset sgid: model && model.to_sgid(expires_in: nil).to_s, eager: eager.presence, broadcast_each: broadcast_each.presence, - signed_controller: signed_controller + signed_controller: signed_controller, + observer_options: observer_options }) end diff --git a/lib/tasks/futurism_tasks.rake b/lib/tasks/futurism_tasks.rake index f9e5c96..d2cc9e1 100644 --- a/lib/tasks/futurism_tasks.rake +++ b/lib/tasks/futurism_tasks.rake @@ -28,11 +28,8 @@ namespace :futurism do lines.insert lines.index(matches.last).to_i + 1, "import consumer from '../channels/consumer'\n" end - initialize_line = lines.find { |line| line.start_with?("Futurism.initializeElements") } - lines << "Futurism.initializeElements()\n" unless initialize_line - - subscribe_line = lines.find { |line| line.start_with?("Futurism.createSubscription") } - lines << "Futurism.createSubscription(consumer)\n" unless subscribe_line + initialize_line = lines.find { |line| line.start_with?("Futurism.initialize(consumer)") } + lines << "Futurism.initialize(consumer)\n" unless initialize_line File.open(filepath, "w") { |f| f.write lines.join } end From e3e1158fe1e3424a3d809c557ca2e5cf1f9580a3 Mon Sep 17 00:00:00 2001 From: leastbad <38150464+leastbad@users.noreply.github.com> Date: Sun, 17 Jul 2022 14:21:56 -0400 Subject: [PATCH 2/4] Update lib/tasks/futurism_tasks.rake Co-authored-by: Julian Rubisch --- lib/tasks/futurism_tasks.rake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tasks/futurism_tasks.rake b/lib/tasks/futurism_tasks.rake index d2cc9e1..3771016 100644 --- a/lib/tasks/futurism_tasks.rake +++ b/lib/tasks/futurism_tasks.rake @@ -28,7 +28,7 @@ namespace :futurism do lines.insert lines.index(matches.last).to_i + 1, "import consumer from '../channels/consumer'\n" end - initialize_line = lines.find { |line| line.start_with?("Futurism.initialize(consumer)") } + initialize_line = lines.find { |line| line.start_with?("Futurism.initialize") } lines << "Futurism.initialize(consumer)\n" unless initialize_line File.open(filepath, "w") { |f| f.write lines.join } From 32d44c78ec7083e6cfaa5c804d0a47c90f43ce47 Mon Sep 17 00:00:00 2001 From: leastbad <38150464+leastbad@users.noreply.github.com> Date: Sun, 17 Jul 2022 14:29:52 -0400 Subject: [PATCH 3/4] Update lib/futurism/helpers.rb Co-authored-by: Julian Rubisch --- lib/futurism/helpers.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/futurism/helpers.rb b/lib/futurism/helpers.rb index 93e9c1a..c28e602 100644 --- a/lib/futurism/helpers.rb +++ b/lib/futurism/helpers.rb @@ -82,7 +82,7 @@ def dataset eager: eager.presence, broadcast_each: broadcast_each.presence, signed_controller: signed_controller, - observer_options: observer_options + observer_options: observer_options.presence }) end From 04ba83b468169d7490389f1169c3cc9c6569c6bd Mon Sep 17 00:00:00 2001 From: leastbad Date: Sun, 17 Jul 2022 14:30:02 -0400 Subject: [PATCH 4/4] split out observerOptions --- README.md | 2 +- javascript/elements/futurism_utils.js | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 34faa31..774dc61 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Lazy-load Rails partials via CableReady -:rotating*light: \_BREAKING CHANGE: With v1.0, futurism has been transferred to the [stimulusreflex](https://github.com/stimulusreflex) organization. Please update your npm package to `@stimulus_reflex/futurism` accordingly* :rotating_light: +:rotating_light: *BREAKING CHANGE: With v1.0, futurism has been transferred to the [stimulusreflex](https://github.com/stimulusreflex) organization. Please update your npm package to `@stimulus_reflex/futurism` accordingly* :rotating_light: birmingham-museums-trust-GrvC6MI-z4w-unsplash Photo by Birmingham Museums Trust on Unsplash diff --git a/javascript/elements/futurism_utils.js b/javascript/elements/futurism_utils.js index ec451e0..c2bebe9 100644 --- a/javascript/elements/futurism_utils.js +++ b/javascript/elements/futurism_utils.js @@ -48,12 +48,13 @@ const observerCallback = (entries, observer) => { } export const extendElementWithIntersectionObserver = element => { + const observerOptions = element.dataset.observerOptions + ? JSON.parse(element.dataset.observerOptions) + : {} Object.assign(element, { observer: new IntersectionObserver( observerCallback.bind(element), - element.dataset.observerOptions - ? JSON.parse(element.dataset.observerOptions) - : {} + observerOptions ) })