JavaScript conventions for all projects.
For styling and CSS class conventions, see → scss.md
For Vue-specific conventions, see → vue.md
JavaScript is responsible for behaviour only.
- CSS classes define styling — JavaScript must never select by CSS class
data-jsis the contract between JavaScript and HTML- CSS must never depend on
data-jsattributes - JavaScript must never depend on CSS class names
This strict separation means: renaming a CSS class never breaks JS, and
removing a data-js attribute never affects styling.
Formatting is defined in .editorconfig.
Fallback:
- indent_style: space
- indent_size: 2
- charset: utf-8
- end_of_line: lf
- insert_final_newline: true
- trim_trailing_whitespace: true
- Single Quotes als Standard für Strings
- Template Literals (
`) für Interpolation oder mehrzeilige Strings - Double Quotes nur, wenn der String selbst ein
'enthält (Vermeidung von Escaping)
const label = 'Menu öffnen' // korrekt
const message = `Hallo ${userName}` // korrekt – Interpolation
const html = "<div class='card'>" // korrekt – vermeidet Escaping
const label = "Menu öffnen" // falsch – Double Quotes ohne Grund- Semicolons sind Pflicht am Ende jeder Anweisung
- Grund: vermeidet ASI-Fallstricke (Automatic Semicolon Insertion), v. a. bei Zeilen, die mit
(,[oder`beginnen
const menuToggle = document.querySelector('[data-js="menuToggle"]');
async function loadData(url) {
const response = await fetch(url);
return response.json();
}- Always use
const— default for all declarations - Use
letonly when reassignment is required - Never use
var
const menuToggle = document.querySelector('[data-js="menuToggle"]') // default
let counter = 0 // only when reassignment follows
counter++
var label = 'old' // Wrong — never use varPrefer async/await over raw Promise chains or .then():
// Correct
async function loadData(url) {
const response = await fetch(url)
return response.json()
}
// Wrong
fetch(url).then(r => r.json()).then(data => { …
})- Use
lowerCamelCase - IDs are not the default JavaScript hook — use
data-jsinstead - Allowed only for:
- App roots (Vue)
- Anchor links
- External integrations that require an ID
Rule: if an ID is used in JavaScript, the variable name must match the ID value.
<div id="productFilterApp"></div>const productFilterApp = document.getElementById('productFilterApp')JavaScript always selects elements via data-js, never via CSS classes or IDs.
- Value:
lowerCamelCase - Value must match the JavaScript variable name — a single IDE search finds both
Exception for Vue components: inside Vue templates, do not use data-js
attributes or
document.querySelector. Use Vue template refs (ref="elementName") or event
bindings
(@click) instead.
<!-- Correct -->
<button class="sk-nav-toggle" data-js="menuToggle">
<!-- Wrong — CSS class used as JS hook -->
<button class="sk-nav-toggle js-menu-toggle">// Correct — same string as in the template
const menuToggle = document.querySelector('[data-js="menuToggle"]')
// Wrong — CSS class selector
document.querySelector('.sk-nav-toggle')PascalCasefor component names and file names- See
vue.mdfor all Vue-specific conventions
| Priority | Selector | When |
|---|---|---|
| 1 | data-js |
default for all JS interactions |
| 2 | ID | only when justified (Vue root, anchor, external lib) |
| 3 | CSS class | never |
| 4 | Structural selectors | never |
// Wrong — structural selector
document.querySelector('.container .row .card button')Default for all interactions. Use when:
- the interaction is isolated (toggle, scroll handler, modal trigger)
- no shared state is required
- a library like Bootstrap JS already handles it
Use when the UI is stateful and complex enough that Vanilla JS becomes hard to
maintain.
Vue is always a conscious decision — see vue.md for criteria and
conventions.
Default to plain JavaScript. Use TypeScript only for a standalone TYPO3
extension's own frontend logic when that logic is non-trivial (animations,
observers, calculations — not a one-off toggle), and the extension already
has (or is getting) its own Node-based build step to produce the shipped
Resources/Public assets.
Do not introduce TypeScript just because it "seems cleaner" for a trivial
script, and do not introduce a build step purely to enable TypeScript — the
build step must already be justified by the extension needing pre-minified,
dependency-free Resources/Public assets for standalone distribution.
When adopted, signal intent through the file extension and matching folder:
Resources/Private/JavaScript/CountUp.ts — source, type-checked
Resources/Public/JavaScript/CountUp.min.js — compiled + minified, committed
Minimal reference setup (esbuild transpiles .ts directly; tsc --noEmit is
used only for type-checking, not compilation):
// package.json (devDependencies)
{
"esbuild": "^0.25.0",
"typescript": "^5.7.0"
}// tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020", "DOM"],
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"noEmit": true,
"skipLibCheck": true
},
"include": ["Resources/Private/JavaScript/**/*.ts"]
}// build.mjs
import { build } from 'esbuild';
await build({
entryPoints: ['Resources/Private/JavaScript/CountUp.ts'],
outfile: 'Resources/Public/JavaScript/CountUp.min.js',
minify: true,
format: 'iife',
target: 'es2020',
});Add an npm script "typecheck": "tsc --noEmit" and run it before building —
esbuild silently strips types without checking them.
Do not move the TypeScript source itself out of the extension package
(e.g. into a consuming project's own Build/Default) — the compiled
Resources/Public assets must ship with the package either way for it to
work standalone in other projects, so keeping the source out of the package
only removes the ability for other consumers to read or adapt it, without
actually making the package smaller. A consuming project can still replace the
shipped assets as an optional customisation layer on top — by not including the
extension's own asset TypoScript and registering its own bundle instead, or by
pointing the corresponding TypoScript constant at a different file. Note that
templateRootPaths does not apply here: it resolves Fluid templates, not
Resources/Public assets.
- Initialize only after DOM is ready
- Guard for missing elements before initializing
ready(function () {
initPopovers()
initVideoPlayer()
})const element = document.querySelector('[data-js="something"]')
if (!element) return- Use
addEventListener - No inline handlers
- Do not bind events to unstable selectors
menuToggle?.addEventListener('click', () => {
// logic
})document.querySelectorAll('[data-js="accordionPanel"]').forEach((panel) => {
// logic
})Use Bootstrap's native attributes — do not reimplement with data-js:
<button data-bs-toggle="modal" data-bs-target="#confirmDialog">- Use a dedicated root element with an
lowerCamelCaseID - Pass data via
data-*attributes (rendered via Fluid in TYPO3) - One root element per app instance
<div id="productFilterApp" data-settings="..." data-items="...">
</div>See vue.md for mounting and component conventions.
- One entry point per large feature
- Do not load large scripts globally
- File names:
PascalCase
Build/Default/EntryPoints/ProductFilterApp.js
Register assets explicitly in Fluid — load only where needed:
<f:asset.script identifier="product-filter-app"
src="EXT:ot_febuild/.../ProductFilterApp.js"/>- Pair JS + CSS per feature
- Use consistent identifiers in
f:asset.*
- Provide data via
data-*attributes - Avoid global variables
- Prefer JSON-encoded data for complex structures
// Wrong — CSS class as JS hook
document.querySelector('.sk-nav-toggle')
// Wrong — ID used as generic JS hook
document.getElementById('button')<!-- Wrong — inline handler -->
<button onclick="toggleMenu()">- Do not load feature-specific scripts globally
- Do not use Vue for simple one-off interactions
| What | Convention | Example |
|---|---|---|
| ID | lowerCamelCase |
id="productFilterApp" |
| JS hook | data-js="lowerCamelCase" |
data-js="menuToggle" |
| Selector priority | data-js → ID → (never class) |
|
| JS variable | matches data-js value |
const menuToggle = ... |
| Bootstrap JS | data-bs-* |
data-bs-toggle="modal" |
| Vue component | PascalCase |
<ProductFilterApp /> |
| Entry point file | PascalCase |
ProductFilterApp.js |
| App root ID | lowerCamelCase |
productFilterApp |