UNPKG

stimulus_reflex

Version:

Build reactive applications with the Rails tooling you already know and love.

259 lines (222 loc) 8.59 kB
import { Controller } from '@hotwired/stimulus' import './version_checker' import ActionCableTransport from './transports/action_cable' import App from './app' import Debug from './debug' import Deprecate from './deprecate' import IsolationMode from './isolation_mode' import Log from './log' import Reflex from './reflex' import ReflexData from './reflex_data' import Schema from './schema' import Transport from './transport' import { attributeValues } from './attributes' import { beforeDOMUpdate, afterDOMUpdate, routeReflexEvent } from './callbacks' import { dispatchLifecycleEvent } from './lifecycle' import { reflexes } from './reflexes' import { scanForReflexes, scanForReflexesOnElement } from './scanner' import { uuidv4, serializeForm, elementInvalid, getReflexElement, getReflexOptions, emitEvent } from './utils' // Default StimulusReflexController that is implicitly wired up as data-controller for any DOM elements // that have configured data-reflex. Note that this default can be overridden when initializing the application. // i.e. StimulusReflex.initialize(myStimulusApplication, MyCustomDefaultController) // export class StimulusReflexController extends Controller { constructor (...args) { super(...args) register(this) } } // Uniquely identify this browser tab in each Reflex const tabId = uuidv4() // Initializes StimulusReflex by registering the default Stimulus controller with the passed Stimulus application. // // - application - the Stimulus application // - options // * controller - [optional] the default StimulusReflexController // * consumer - [optional] the ActionCable consumer // * debug - [false] log all Reflexes to the console // * params - [{}] key/value parameters to send during channel subscription // * isolate - [false] restrict Reflex playback to the tab which initiated it // * deprecate - [true] show warnings regarding upcoming changes to the library // * transport - [optional] defaults to ActionCableTransport // const initialize = ( application, { controller, consumer, debug, params, isolate, deprecate, transport } = {} ) => { Transport.set(transport || ActionCableTransport) Transport.plugin.initialize(consumer, params) IsolationMode.set(!!isolate) App.set(application) Schema.set(application) App.app.register( 'stimulus-reflex', controller || StimulusReflexController ) Debug.set(!!debug) if (typeof deprecate !== 'undefined') Deprecate.set(deprecate) const observer = new MutationObserver(scanForReflexes) observer.observe(document.documentElement, { attributeFilter: [Schema.reflex, Schema.action], childList: true, subtree: true }) emitEvent('stimulus-reflex:initialized') } // Registers a Stimulus controller and extends it with StimulusReflex behavior // // controller - the Stimulus controller // options - [optional] configuration // const register = (controller, options = {}) => { const channel = 'StimulusReflex::Channel' controller.StimulusReflex = { ...options, channel } Transport.plugin.subscribe(controller) Object.assign(controller, { // Invokes a server side reflex method. // // - target - the reflex target (full name of the server side reflex) i.e. 'ReflexClassName#method' // - reflexElement - [optional] the element that triggered the reflex, defaults to this.element // - options - [optional] an object that contains at least one of attrs, id, selectors, resolveLate, serializeForm // - *args - remaining arguments are forwarded to the server side reflex method // stimulate () { const url = location.href const controllerElement = this.element const args = Array.from(arguments) const target = args.shift() || 'StimulusReflex::Reflex#default_reflex' const reflexElement = getReflexElement(args, controllerElement) if (elementInvalid(reflexElement)) { if (Debug.enabled) console.warn('Reflex aborted: invalid numeric input') return } const options = getReflexOptions(args) const reflexData = new ReflexData( options, reflexElement, controllerElement, this.identifier, Schema.reflexPermanent, target, args, url, tabId ) const id = reflexData.id // TODO: remove this in v4 controllerElement.reflexController = controllerElement.reflexController || {} controllerElement.reflexData = controllerElement.reflexData || {} controllerElement.reflexError = controllerElement.reflexError || {} controllerElement.reflexController[id] = this controllerElement.reflexData[id] = reflexData.valueOf() // END TODO: remove const reflex = new Reflex(reflexData, this) reflexes[id] = reflex this.lastReflex = reflex dispatchLifecycleEvent(reflex, 'before') setTimeout(() => { // TODO: in v4, params will be set on the reflex.data object const { params } = controllerElement.reflexData[id] || {} const check = reflexElement.attributes[Schema.reflexSerializeForm] if (check) { options['serializeForm'] = check.value !== 'false' } const form = reflexElement.closest(reflexData.formSelector) || document.querySelector(reflexData.formSelector) || reflexElement.closest('form') // TODO: remove this in v4 if (Deprecate.enabled && options['serializeForm'] === undefined && form) console.warn( `Deprecation warning: the next version of StimulusReflex will not serialize forms by default.\nPlease set ${Schema.reflexSerializeForm}=\"true\" on your Reflex Controller Element or pass { serializeForm: true } as an option to stimulate.` ) // END TODO: remove const formData = options['serializeForm'] === false ? '' : serializeForm(form, { element: reflexElement }) reflex.data = { ...reflexData.valueOf(), params, formData } // TODO: remove this in v4 controllerElement.reflexData[id] = reflex.data // END TODO: remove Transport.plugin.deliver(reflex) }) Log.request(reflex) return reflex.getPromise }, // Wraps the call to stimulate for any data-reflex elements. // This is internal and should not be invoked directly. __perform (event) { let element = event.target let reflex while (element && !reflex) { reflex = element.getAttribute(Schema.reflex) if (!reflex || !reflex.trim().length) element = element.parentElement } const match = attributeValues(reflex).find( reflex => reflex.split('->')[0] === event.type ) if (match) { event.preventDefault() event.stopPropagation() this.stimulate(match.split('->')[1], element) } } }) // Access the reflexes created by the current controller instance // reflexes is a Proxy to an object, keyed by id // this.reflexes.all and this.reflexes.last are scoped to this controller instance // Reflexes can also be scoped by stage eg. this.reflexes.queued if (!controller.reflexes) Object.defineProperty(controller, 'reflexes', { get () { return new Proxy(reflexes, { get: function (target, prop) { if (prop === 'last') return this.lastReflex return Object.fromEntries( Object.entries(target[prop]).filter( ([_, reflex]) => reflex.controller === this ) ) }.bind(this) }) } }) scanForReflexesOnElement(controller.element, controller) emitEvent('stimulus-reflex:controller-registered', { detail: { controller } }) } const useReflex = (controller, options = {}) => { register(controller, options) } document.addEventListener('cable-ready:after-dispatch-event', routeReflexEvent) document.addEventListener('cable-ready:before-inner-html', beforeDOMUpdate) document.addEventListener('cable-ready:before-morph', beforeDOMUpdate) document.addEventListener('cable-ready:after-inner-html', afterDOMUpdate) document.addEventListener('cable-ready:after-morph', afterDOMUpdate) document.addEventListener('readystatechange', () => { if (document.readyState === 'complete') { scanForReflexes() } }) export { initialize, register, useReflex, reflexes, scanForReflexes, scanForReflexesOnElement }