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

shuffle support functions to attributes.js #59

Closed
wants to merge 3 commits into from
Closed
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
83 changes: 83 additions & 0 deletions javascript/attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,93 @@ export const attributeValues = value => {
return value.split(' ').filter(v => v.trim().length)
}

// Extracts attributes from a DOM element.
//
export const extractElementAttributes = element => {
let attrs = Array.prototype.slice
.call(element.attributes)
.reduce((memo, attr) => {
memo[attr.name] = attr.value
return memo
}, {})

attrs.value = element.value
attrs.checked = !!element.checked
attrs.selected = !!element.selected
if (element.tagName.match(/select/i)) {
if (element.multiple) {
const checkedOptions = Array.prototype.slice.call(
element.querySelectorAll('option:checked')
)
attrs.values = checkedOptions.map(o => o.value)
} else if (element.selectedIndex > -1) {
attrs.value = element.options[element.selectedIndex].value
}
}
return attrs
}

// Finds an element based on the passed represention the DOM element's attributes.
//
// NOTE: This is the same set of attributes extrated via extractElementAttributes and forwarded to the server side reflex.
// SEE: stimulute()
// SEE: StimulusReflex::Channel#broadcast_morph
// SEE: StimulusReflex::Channel#broadcast_error
//
export const findElement = attributes => {
attributes = attributes || {}
let elements = []
if (attributes.id) {
elements = document.querySelectorAll(`#${attributes.id}`)
} else {
let selectors = []
for (const key in attributes) {
if (key.includes('.')) continue
if (key === 'value') continue
if (key === 'checked') continue
if (key === 'selected') continue
if (!Object.prototype.hasOwnProperty.call(attributes, key)) continue
selectors.push(`[${key}="${attributes[key]}"]`)
}
try {
elements = document.querySelectorAll(selectors.join(''))
} catch (error) {
console.log(
'StimulusReflex encountered an error identifying the Stimulus element. Consider adding an #id to the element.',
error,
attributes
)
}
}

const element = elements.length === 1 ? elements[0] : null
return element
}

// Returns the expected matching controller name for the passed reflex.
//
// matchingControllerName('ExampleReflex#do_stuff') // 'example'
//
export const matchingControllerName = reflex => {
return dasherize(underscore(reflex.split('#')[0].replace(/Reflex$/, '')))
}

// Finds the registered StimulusReflex controller for the passed element that matches the reflex.
// Traverses DOM ancestors starting with element until a match is found.
//
export const findReflexController = (application, element, reflex) => {
const name = matchingControllerName(reflex)
let controller
while (element && !controller) {
const controllers = attributeValues(element.dataset.controller)
if (controllers.includes(name)) {
const candidate = application.getControllerForElementAndIdentifier(
element,
name
)
if (candidate && candidate.StimulusReflex) controller = candidate
}
element = element.parentElement
}
return controller
}
109 changes: 18 additions & 91 deletions javascript/stimulus_reflex.js
Original file line number Diff line number Diff line change
@@ -1,100 +1,19 @@
import { Controller } from 'stimulus'
import ActionCable from 'actioncable'
import { camelize, dasherize, underscore } from 'inflected'
import { camelize } from 'inflected'
import CableReady from 'cable_ready'
import {
attributeValue,
attributeValues,
matchingControllerName
extractElementAttributes,
findElement,
findReflexController
} from './attributes'

// A reference to the Stimulus application registered with: StimulusReflex.initialize
//
let stimulusApplication

// Extracts attributes from a DOM element.
//
const extractElementAttributes = element => {
let attrs = Array.prototype.slice
.call(element.attributes)
.reduce((memo, attr) => {
memo[attr.name] = attr.value
return memo
}, {})

attrs.value = element.value
attrs.checked = !!element.checked
attrs.selected = !!element.selected
if (element.tagName.match(/select/i)) {
if (element.multiple) {
const checkedOptions = Array.prototype.slice.call(
element.querySelectorAll('option:checked')
)
attrs.values = checkedOptions.map(o => o.value)
} else if (element.selectedIndex > -1) {
attrs.value = element.options[element.selectedIndex].value
}
}
return attrs
}

// Finds an element based on the passed represention the DOM element's attributes.
//
// NOTE: This is the same set of attributes extrated via extractElementAttributes and forwarded to the server side reflex.
// SEE: stimulute()
// SEE: StimulusReflex::Channel#broadcast_morph
// SEE: StimulusReflex::Channel#broadcast_error
//
const findElement = attributes => {
attributes = attributes || {}
let elements = []
if (attributes.id) {
elements = document.querySelectorAll(`#${attributes.id}`)
} else {
let selectors = []
for (const key in attributes) {
if (key.includes('.')) continue
if (key === 'value') continue
if (key === 'checked') continue
if (key === 'selected') continue
if (!Object.prototype.hasOwnProperty.call(attributes, key)) continue
selectors.push(`[${key}="${attributes[key]}"]`)
}
try {
elements = document.querySelectorAll(selectors.join(''))
} catch (error) {
console.log(
'StimulusReflex encountered an error identifying the Stimulus element. Consider adding an #id to the element.',
error,
attributes
)
}
}

const element = elements.length === 1 ? elements[0] : null
return element
}

// Finds the registered StimulusReflex controller for the passed element that matches the reflex.
// Traverses DOM ancestors starting with element until a match is found.
//
const findReflexController = (element, reflex) => {
const name = matchingControllerName(reflex)
let controller
while (element && !controller) {
const controllers = attributeValues(element.dataset.controller)
if (controllers.includes(name)) {
const candidate = stimulusApplication.getControllerForElementAndIdentifier(
element,
name
)
if (candidate && candidate.StimulusReflex) controller = candidate
}
element = element.parentElement
}
return controller
}

// Invokes a lifecycle method on a StimulusReflex controller.
//
// - before
Expand All @@ -104,7 +23,7 @@ const findReflexController = (element, reflex) => {
//
const invokeLifecycleMethod = (stage, reflex, element) => {
if (!element) return
const controller = findReflexController(element, reflex)
const controller = findReflexController(stimulusApplication, element, reflex)
if (!controller) return

const reflexMethodName = reflex.split('#')[1]
Expand All @@ -115,18 +34,20 @@ const invokeLifecycleMethod = (stage, reflex, element) => {
? `${stage}${camelize(reflexMethodName)}`
: `${camelize(reflexMethodName, false)}${camelize(stage)}`

if (typeof controller[specificLifecycleMethodName] === 'function')
if (typeof controller[specificLifecycleMethodName] === 'function') {
setTimeout(
() =>
controller[specificLifecycleMethodName](element, element.reflexError),
1
)
if (typeof controller[genericLifecycleMethodName] === 'function')
}
if (typeof controller[genericLifecycleMethodName] === 'function') {
setTimeout(
() =>
controller[genericLifecycleMethodName](element, element.reflexError),
1
)
}
}

// Subscribes a StimulusReflex controller to an ActionCable channel and room.
Expand Down Expand Up @@ -243,22 +164,28 @@ const setupDeclarativeReflexes = () => {
const reflexes = attributeValues(element.dataset.reflex)
const actions = attributeValues(element.dataset.action)
reflexes.forEach(reflex => {
const controller = findReflexController(element, reflex.split('->')[1])
const controller = findReflexController(
stimulusApplication,
element,
reflex.split('->')[1]
)
let action
if (controller) {
action = `${reflex.split('->')[0]}->${controller.identifier}#__perform`
if (!actions.includes(action)) actions.push(action)
} else {
action = `${reflex.split('->')[0]}->stimulus-reflex#__perform`
if (!controllers.includes('stimulus-reflex'))
if (!controllers.includes('stimulus-reflex')) {
controllers.push('stimulus-reflex')
}
if (!actions.includes(action)) actions.push(action)
}
})
const controllerValue = attributeValue(controllers)
const actionValue = attributeValue(actions)
if (controllerValue)
if (controllerValue) {
element.setAttribute('data-controller', controllerValue)
}
if (actionValue) element.setAttribute('data-action', actionValue)
})
}
Expand Down