Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
95 changes: 75 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
# Futurism

[![Twitter follow](https://img.shields.io/twitter/follow/julian_rubisch?style=social)](https://twitter.com/julian_rubisch)

<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->

[![All Contributors](https://img.shields.io/badge/all_contributors-15-orange.svg?style=flat-square)](#contributors-)

<!-- ALL-CONTRIBUTORS-BADGE:END -->

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:

<img src="https://user-images.githubusercontent.com/4352208/88374198-9e6f3500-cd99-11ea-804b-0216ed320eff.jpg" alt="birmingham-museums-trust-GrvC6MI-z4w-unsplash" width="50%" align="center"/>
<span>Photo by <a href="https://unsplash.com/@birminghammuseumstrust?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Birmingham Museums Trust</a> on <a href="https://unsplash.com/s/photos/futurism?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a></span>
Expand All @@ -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)
Expand All @@ -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)

Expand All @@ -49,37 +58,64 @@ 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 %>
<!-- placeholder -->
<% end %>
```

custom `<futurism-element>`s (in the form of a `<div>` or a `<tr is="futurism-table-row">` 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 `<futurism-element>` 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 %>
<div class="spinner"></div>
<% 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 %>
<td class="placeholder"></td>
<% end %>
```

![aa601dec1930151f71dbf0d6b02b61c9](https://user-images.githubusercontent.com/4352208/87131629-f768a480-c294-11ea-89a9-ea0a76ee06ef.gif)
You will see that a `<tr is="futurism-table-row">` 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 `<li is="futurism-li">` element:

```erb
<ul>
<%= futurize @posts, extends: :li do %>
Loading...
<% end %>
</ul>
```

## 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 %>
Expand All @@ -106,15 +142,15 @@ 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 %>
<div class="spinner"></div>
<% end %>
```

You can also use the shorthand syntax:

```erb
<%= futurize "items/card", card: @card, extends: :div do %>
<%= futurize "items/card", card: @card do %>
<div class="spinner"></div>
<% end %>
```
Expand All @@ -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 %>
<div class="spinner"></div>
<% end %>
```
Expand All @@ -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 %>
<div class="spinner"></div>
<% end %>
```
Expand Down Expand Up @@ -163,7 +199,20 @@ This will output the following:
</tr>
```

### 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 %>
<div class="spinner"></div>
<% 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.
Expand All @@ -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).

Expand All @@ -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| %>
<div><%= post.title %></div>
<% end %>
```
Expand All @@ -229,13 +278,15 @@ 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
gem 'futurism'
```

And then execute:

```bash
$ bundle
```
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -337,6 +390,7 @@ Futurism.configure do |config|
end

```

in config/initializers.

## Contributing
Expand Down Expand Up @@ -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 ✨
Expand Down
8 changes: 7 additions & 1 deletion javascript/elements/futurism_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')) {
Expand Down
7 changes: 6 additions & 1 deletion javascript/index.js
Original file line number Diff line number Diff line change
@@ -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 }
6 changes: 4 additions & 2 deletions lib/futurism/helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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

Expand Down
7 changes: 2 additions & 5 deletions lib/tasks/futurism_tasks.rake
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down