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

Morph Modes: page, selector and nothing #211

Merged
merged 23 commits into from
Jul 4, 2020
Merged
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
1 change: 0 additions & 1 deletion docs/scoping.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,3 @@ This is especially important for 3rd-party elements such as ad tracking scripts,
{% hint style="danger" %}
Beware of Ruby gems that implicitly inject HTML into the body as it might be removed from the DOM when a Reflex is invoked. For example, consider the [intercom-rails gem](https://github.com/intercom/intercom-rails) which automatically injects the Intercom chat into the body. Gems like this often provide [instructions](https://github.com/intercom/intercom-rails#manually-inserting-the-intercom-javascript) for explicitly including their markup. We recommend using the explicit option whenever possible, so that you can wrap the content with `data-reflex-permanent`.
{% endhint %}

18 changes: 18 additions & 0 deletions javascript/lifecycle.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,24 @@ document.addEventListener(
true
)

document.addEventListener(
'stimulus-reflex:selector',
event => {
invokeLifecycleMethod('success', event.target)
dispatchLifecycleEvent('after', event.target)
},
true
)

document.addEventListener(
'stimulus-reflex:nothing',
event => {
invokeLifecycleMethod('success', event.target)
dispatchLifecycleEvent('after', event.target)
},
true
)

document.addEventListener(
'stimulus-reflex:error',
event => {
Expand Down
13 changes: 8 additions & 5 deletions javascript/log.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ function request (
element
) {
logs[reflexId] = new Date()
console.log(`\u2B95 ${target}`, {
console.log(`\u2B9C ${target}`, {
reflexId,
args,
stimulusControllerIdentifier,
Expand All @@ -21,7 +21,8 @@ function success (response, options = { halted: false }) {
const payloads = {}
const elements = {}
const { event, events } = response
const { reflexId, target, last } = event.detail.stimulusReflex || {}
const { reflexId, target, last, morphMode } =
event.detail.stimulusReflex || {}

if (events) {
Object.keys(events).map(selector => {
Expand All @@ -31,10 +32,11 @@ function success (response, options = { halted: false }) {
})
}

console.log(`\u2B05 ${target}`, {
console.log(`\u2B9E ${target}`, {
reflexId,
duration: `${new Date() - logs[reflexId]}ms`,
halted: options.halted,
morphMode,
elements,
payloads,
html
Expand All @@ -45,11 +47,12 @@ function success (response, options = { halted: false }) {
function error (response) {
const { event, element } = response || {}
const { detail } = event || {}
const { reflexId, target, error } = detail.stimulusReflex || {}
console.error(`\u2B05 ${target}`, {
const { reflexId, target, error, morphMode } = detail.stimulusReflex || {}
console.error(`\u2B9E ${target}`, {
reflexId,
duration: `${new Date() - logs[reflexId]}ms`,
error,
morphMode,
payload: event.detail.stimulusReflex,
element
})
Expand Down
48 changes: 37 additions & 11 deletions javascript/stimulus_reflex.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,12 @@ const createSubscription = controller => {
received: data => {
if (!data.cableReady) return
if (data.operations.morph && data.operations.morph.length) {
const urls = Array.from(
new Set(data.operations.morph.map(m => m.stimulusReflex.url))
)
if (urls.length !== 1 || urls[0] !== location.href) return
if (data.operations.morph[0].stimulusReflex) {
const urls = Array.from(
new Set(data.operations.morph.map(m => m.stimulusReflex.url))
)
if (urls.length !== 1 || urls[0] !== location.href) return
}
}
CableReady.perform(data.operations)
}
Expand All @@ -76,6 +78,7 @@ const extendStimulusController = controller => {
//
// - target - the reflex target (full name of the server side reflex) i.e. 'ReflexClassName#method'
// - element - [optional] the element that triggered the reflex, defaults to this.element
// - options - [optional] an object that contains at least one of attrs, reflexId, selectors
// - *args - remaining arguments are forwarded to the server side reflex method
//
stimulate () {
Expand All @@ -93,21 +96,33 @@ const extendStimulusController = controller => {
) {
return
}
const attrs = extractElementAttributes(element)
const options = {}
if (
args[0] &&
typeof args[0] == 'object' &&
Object.keys(args[0]).filter(key =>
['attrs', 'selectors', 'reflexId'].includes(key)
)
) {
const opts = args.shift()
Object.keys(opts).forEach(o => (options[o] = opts[o]))
}
const attrs = options['attrs'] || extractElementAttributes(element)
const reflexId = options['reflexId'] || uuidv4()
let selectors = options['selectors'] || getReflexRoots(element)
if (typeof selectors == 'string') selectors = [selectors]
const datasetAttribute = stimulusApplication.schema.reflexDatasetAttribute
const dataset = extractElementDataset(element, datasetAttribute)
const selectors = getReflexRoots(element)
const reflexId = uuidv4()
const data = {
target,
args,
url,
attrs,
dataset,
selectors,
reflexId,
permanent_attribute_name:
stimulusApplication.schema.reflexPermanentAttribute,
reflexId: reflexId
stimulusApplication.schema.reflexPermanentAttribute
}
const { subscription } = this.StimulusReflex

Expand Down Expand Up @@ -362,6 +377,7 @@ if (!document.stimulusReflexInitialized) {
const response = {
element,
event,
morphMode: promise && promise.morphMode,
data: promise && promise.data,
events: promise && promise.events
}
Expand All @@ -375,13 +391,16 @@ if (!document.stimulusReflexInitialized) {
if (debugging) Log.success(response)
})
document.addEventListener('stimulus-reflex:server-message', event => {
const { reflexId, attrs, serverMessage } = event.detail.stimulusReflex || {}
const { reflexId, attrs, serverMessage, morphMode } =
event.detail.stimulusReflex || {}
const { subject, body } = serverMessage
const element = findElement(attrs)
const promise = promises[reflexId]
const subjects = {
error: true,
halted: true
halted: true,
selector: true,
nothing: true
}

if (element && subject == 'error') element.reflexError = body
Expand All @@ -390,6 +409,7 @@ if (!document.stimulusReflexInitialized) {
data: promise && promise.data,
element,
event,
morphMode,
events: promise && promise.events,
toString: () => body
}
Expand All @@ -411,6 +431,12 @@ if (!document.stimulusReflexInitialized) {
case 'error':
Log.error(response)
break
case 'selector':
Log.success(response)
break
case 'nothing':
Log.success(response)
break
case 'halted':
Log.success(response, { halted: true })
break
Expand Down
5 changes: 5 additions & 0 deletions lib/stimulus_reflex.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@
require "stimulus_reflex/version"
require "stimulus_reflex/reflex"
require "stimulus_reflex/element"
require "stimulus_reflex/broadcaster"
require "stimulus_reflex/morph_mode"
require "stimulus_reflex/channel"
require "stimulus_reflex/morph_mode/nothing_morph_mode"
require "stimulus_reflex/morph_mode/page_morph_mode"
require "stimulus_reflex/morph_mode/selector_morph_mode"
require "generators/stimulus_reflex_generator"

module StimulusReflex
Expand Down
57 changes: 57 additions & 0 deletions lib/stimulus_reflex/broadcaster.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
module StimulusReflex
module Broadcaster
def self.included klass
klass.class_eval do
include CableReady::Broadcaster
end
end

def render_page_and_broadcast_morph(reflex, selectors, data = {})
html = render_page(reflex)
broadcast_morphs selectors, data, html if html.present?
end

def render_page(reflex)
reflex.controller.process reflex.url_params[:action]
reflex.controller.response.body
end

def broadcast_morphs(selectors, data, html)
document = Nokogiri::HTML(html)
selectors = selectors.select { |s| document.css(s).present? }
selectors.each do |selector|
cable_ready[stream_name].morph(
selector: selector,
html: document.css(selector).inner_html,
children_only: true,
permanent_attribute_name: data["permanent_attribute_name"],
stimulus_reflex: data.merge({
last: selector == selectors.last,
morph_mode: "page"
})
)
end
cable_ready.broadcast
end

def broadcast_message(subject:, body: nil, data: {})
message = {
subject: subject,
body: body
}

logger.error "\e[31m#{body}\e[0m" if subject == "error"

data[:morph_mode] = "page"
data[:server_message] = message
data[:morph_mode] = "selector" if subject == "selector"
data[:morph_mode] = "nothing" if subject == "nothing"

cable_ready[stream_name].dispatch_event(
name: "stimulus-reflex:server-message",
detail: {stimulus_reflex: data}
)
cable_ready.broadcast
end
end
end
50 changes: 6 additions & 44 deletions lib/stimulus_reflex/channel.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

class StimulusReflex::Channel < ActionCable::Channel::Base
include CableReady::Broadcaster
include StimulusReflex::Broadcaster

def stream_name
ids = connection.identifiers.map { |identifier| send(identifier).try(:id) || send(identifier) }
Expand All @@ -25,12 +25,13 @@ def receive(data)
reflex_name = reflex_name.end_with?("Reflex") ? reflex_name : "#{reflex_name}Reflex"
arguments = (data["args"] || []).map { |arg| object_with_indifferent_access arg }
element = StimulusReflex::Element.new(data)
permanent_attribute_name = data["permanent_attribute_name"]
params = data["params"] || {}

begin
begin
reflex_class = reflex_name.constantize.tap { |klass| raise ArgumentError.new("#{reflex_name} is not a StimulusReflex::Reflex") unless is_reflex?(klass) }
reflex = reflex_class.new(self, url: url, element: element, selectors: selectors, method_name: method_name, params: params)
reflex = reflex_class.new(self, url: url, element: element, selectors: selectors, method_name: method_name, permanent_attribute_name: permanent_attribute_name, params: params)
delegate_call_to_reflex reflex, method_name, arguments
rescue => invoke_error
reflex&.rescue_with_handler(invoke_error)
Expand All @@ -42,9 +43,10 @@ def receive(data)
broadcast_message subject: "halted", data: data
else
begin
render_page_and_broadcast_morph reflex, selectors, data
reflex.morph_mode.stream_name = stream_name
reflex.morph_mode.broadcast(reflex, selectors, data)
rescue => render_error
reflex&.rescue_with_handler(render_error)
reflex.rescue_with_handler(render_error)
message = exception_message_with_backtrace(render_error)
broadcast_message subject: "error", body: "StimulusReflex::Channel Failed to re-render #{url} #{message}", data: data
end
Expand Down Expand Up @@ -80,11 +82,6 @@ def delegate_call_to_reflex(reflex, method_name, arguments = [])
end
end

def render_page_and_broadcast_morph(reflex, selectors, data = {})
html = render_page(reflex)
broadcast_morphs selectors, data, html if html.present?
end

def commit_session(reflex)
store = reflex.request.session.instance_variable_get("@by")
store.commit_session reflex.request, reflex.controller.response
Expand All @@ -93,41 +90,6 @@ def commit_session(reflex)
logger.error "\e[31m#{message}\e[0m"
end

def render_page(reflex)
reflex.controller.process reflex.url_params[:action]
reflex.controller.response.body
end

def broadcast_morphs(selectors, data, html)
document = Nokogiri::HTML(html)
selectors = selectors.select { |s| document.css(s).present? }
selectors.each do |selector|
cable_ready[stream_name].morph(
selector: selector,
html: document.css(selector).inner_html,
children_only: true,
permanent_attribute_name: data["permanent_attribute_name"],
stimulus_reflex: data.merge(last: selector == selectors.last)
)
end
cable_ready.broadcast
end

def broadcast_message(subject:, body: nil, data: {})
message = {
subject: subject,
body: body
}

logger.error "\e[31m#{body}\e[0m" if subject == "error"

cable_ready[stream_name].dispatch_event(
name: "stimulus-reflex:server-message",
detail: {stimulus_reflex: data.merge(server_message: message)}
)
cable_ready.broadcast
end

def exception_message_with_backtrace(exception)
"#{exception} #{exception.backtrace.first}"
end
Expand Down
19 changes: 19 additions & 0 deletions lib/stimulus_reflex/morph_mode.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
module StimulusReflex
class MorphMode
include StimulusReflex::Broadcaster

attr_accessor :stream_name

def page?
false
end

def nothing?
false
end

def selector?
false
end
end
end
15 changes: 15 additions & 0 deletions lib/stimulus_reflex/morph_mode/nothing_morph_mode.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module StimulusReflex
class NothingMorphMode < MorphMode
def broadcast(reflex, selectors, data)
broadcast_message subject: "nothing", data: data
end

def to_sym
:nothing
end

def nothing?
true
end
end
end
Loading