name: folio-javascript description: >- JavaScript conventions for Folio: ES6+, StandardJS linting, DOM APIs, Folio.Api fetch wrappers, debounce/throttle, flash events, and when to use Stimulus. Use when writing or editing .js files, adding behavior to components, or when the user asks about JavaScript patterns in Folio.
JavaScript (Folio)
Language and style
- Use modern ES6+:
const/let, arrow functions, template literals, destructuring,async/await. - No
var, no jQuery in new code. - Lint with
npx standard --fix <path>(StandardJS — no semicolons, 2-space indent). SeeAGENTS.md.
When to use Stimulus
For any interactive component behavior (click handlers, form inputs, toggling classes, AJAX actions), use a Stimulus controller rather than standalone JS. See .skills/folio-stimulus/SKILL.md.
Plain JS (without Stimulus) is appropriate for:
- Global utilities and helpers under
app/assets/javascripts/folio/ - One-off scripts that don't belong to a component
- Library wrappers and polyfills
DOM APIs
Use native DOM — no jQuery:
document.querySelector/querySelectorAllelement.closest,classList,datasetaddEventListener/removeEventListener(in Stimulus controllers, preferdata-actioninstead — see folio-stimulus)insertAdjacentHTML,replaceWith,remove
HTTP / AJAX
Use window.Folio.Api (app/assets/javascripts/folio/api.js) instead of raw fetch or $.ajax:
window.Folio.Api.apiPatch(url, data).then((res) => {
// res.data available; flash already handled via flashMessageFromMeta
}).catch((res) => {
// res.message contains the error
})
Available: apiGet, apiPost, apiPatch, apiPut, apiDelete. They handle CSRF, JSON serialization, and automatically dispatch folio:flash from meta.flash — do not call flashMessageFromMeta again in .then.
Flash messages
Dispatch folio:flash events — do not call window.FolioConsole.Ui.Flash.* directly:
document.dispatchEvent(new CustomEvent('folio:flash', {
bubbles: true,
detail: { flash: { content: 'Saved', variant: 'success' } }
}))
Variants: success, danger, info, loader. Listener: app/components/folio/console/ui/flash_component.js.
Custom events
- Prefer
dispatchEvent(new CustomEvent('name', { bubbles: true, detail })). - In Stimulus controllers, use
this.dispatch('eventName', { detail })for controller-to-controller communication.
Cleanup — unbind and destroy
When adding event listeners, plugin instances, or observers, always provide a way to tear them down. Leaking listeners causes memory leaks and double-firing bugs.
- In Stimulus controllers, use
disconnect()to remove anything set up inconnect(). - In plain JS utilities, expose
bind/unbind(or similar) functions:
window.Folio.MyFeature = {
bind (container) {
container.addEventListener('click', this.handler)
},
unbind (container) {
container.removeEventListener('click', this.handler)
}
}
This applies to addEventListener, MutationObserver, IntersectionObserver, ResizeObserver, setInterval, third-party plugin instances, etc.
Global namespace
When exporting functionality for the rest of the app, use the global namespace — not ES module exports (the asset pipeline doesn't use a module bundler):
window.Folio = window.Folio || {}
window.Folio.Confirm = { confirm (callback) { /* ... */ } }
// Host app
window.MyApp = window.MyApp || {}
window.MyApp.Analytics = { track (event) { /* ... */ } }
Namespace under window.Folio.* for engine code, window.FolioConsole.* for console-specific engine code, and window.<AppName>.* for host app code.
Do not introduce random top-level const, let, function, or class declarations in component JavaScript. Component files are loaded into the shared asset scope, so helper state and constants must live inside the Stimulus controller or the owning namespace object. For Stimulus constants, prefer static getters/methods on the controller class or instance helper methods over file-scope globals.
Debounce and throttle
For rapidly firing callbacks (scroll, resize, input, mousemove), always debounce or throttle:
window.Folio.debounce(fn, wait = 150, immediate = false)— delays execution untilwaitms after the last call. Use for search inputs, resize layout recalculations.window.Folio.throttle(fn, delay = 100)— executes at most once perdelayms. Use for scroll handlers, drag tracking.
this.onScroll = window.Folio.throttle(() => { this.handleScroll() }, 100)
this.onResize = window.Folio.debounce(() => { this.handleResize() }, 200)
Source: app/assets/javascripts/folio/debounce.js, app/assets/javascripts/folio/throttle.js.
Testing
Testing strategy lives in .skills/folio-testing/SKILL.md.
For JavaScript behavior, use rendered DOM, component, integration/system, or
actual JavaScript behavior tests; do not rely on asset source-string assertions.
Reference
app/assets/javascripts/folio/api.js— API fetch wrappersapp/assets/javascripts/folio/debounce.js,throttle.js— timing utilitiesapp/components/folio/console/ui/flash_component.js— flash event listener- Stimulus conventions:
.skills/folio-stimulus/SKILL.md