Repository navigation
26 Editor support
How to get completion and error reporting for fml in IntelliJ IDEA and in VS Code when the pages load it from a CDN, as most applications do.
The package carries everything an editor needs:
-
dist/ftl.d.mts,dist/ful.d.mts,dist/httpc.d.mtsanddist/fml.d.mtsare the type declarations of the public api.ful.d.mtsalso maps every tag to its class, sodocument.querySelector('ful-form')is aFormanddocument.createElement('ful-table')aTable, not a bareElement. -
dist/web-types.json(JetBrains) anddist/custom-elements.json(the community manifest) describe everyful-*element for html completion: its attributes, their values, its slots and its events.package.jsondeclares both, so IntelliJ finds them on its own in an installed package. -
dist/vscode.html-custom-data.jsonis the same element data in the format VS Code reads.
What an editor cannot see is a CDN url or an import map. A page that loads https://cdn.jsdelivr.net/npm/@optionfactory/fml@…/dist/ful.min.mjs and imports it as ful gives the editor nothing to read. The setup below installs the same version locally for the editor only, and tells it what the import map's names mean. The pages keep loading fml from the CDN: nothing changes at runtime.
Steps 1 to 3 are the same for both editors; step 4 is where they differ.
| editor | what it needs |
|---|---|
| IntelliJ IDEA Ultimate (or WebStorm) | steps 1, 2, 3. The Community edition has no JavaScript or web-types support. |
| VS Code | steps 1, 2, 3, and the html setting in step 4 |
At the root of the project, a package.json that exists only for the editor:
{
"name": "ide-config",
"version": "1.0.0",
"private": true,
"devDependencies": {
"@optionfactory/fml": "9.0.0"
}
}Pin the same version the pages load from the CDN, then run npm install. Add node_modules/ to .gitignore and commit package-lock.json, so every checkout gets the same version. Each person runs npm install once after cloning; IntelliJ offers to when it sees the lock file, VS Code does not.
Do not copy web-types.json or the declaration files into the project instead: a copy does not follow the version, and goes stale at the next upgrade.
Both editors read a jsconfig.json at the root of the project for the scripts. A page loading fml as ES modules through an import map imports ftl, ful and httpc by those bare names; paths tells the editor where their types are:
{
"compilerOptions": {
"module": "esnext",
"moduleResolution": "bundler",
"target": "es2022",
"checkJs": true,
"strict": false,
"noEmit": true,
"paths": {
"ftl": ["./node_modules/@optionfactory/fml/dist/ftl.d.mts"],
"ful": ["./node_modules/@optionfactory/fml/dist/ful.d.mts"],
"httpc": ["./node_modules/@optionfactory/fml/dist/httpc.d.mts"]
}
},
"include": ["src/main/webapp/assets/**/*.js"]
}-
includenames the application's own scripts: adjust the path to where they live. - The application's own import map names belong in
pathstoo, so its imports resolve and navigate. An import map with"app": "/assets/app.js"and"app/": "/assets/app/"becomes"app": ["./src/main/webapp/assets/app.js"]and"app/*": ["./src/main/webapp/assets/app/*"].
paths reaches the .js files include names, not a <script type="module"> written inside a template: there IntelliJ resolves a bare name the way node does, through node_modules, and finds no ftl, ful or httpc package. Install the same version under those names too, as npm aliases:
"devDependencies": {
"@optionfactory/fml": "9.0.0",
"ftl": "npm:@optionfactory/fml@9.0.0",
"ful": "npm:@optionfactory/fml@9.0.0",
"httpc": "npm:@optionfactory/fml@9.0.0"
}Each alias is the whole package, whose root declarations re-export ftl, httpc and ful: ftl. completes the three of them rather than ftl's names alone, a looser fit than the .js files get, but every name it offers exists.
The application's own import map names need a package of their own. A package.json in the folder the import map points at names it, and maps the subpaths the way the import map does:
{
"name": "app",
"private": true,
"type": "module",
"exports": {
".": "./app.js",
"./*": "./app/*"
}
}and the root package.json installs it with "app": "file:src/main/webapp/assets", which npm links rather than copies, so the editor reads the live sources. That package.json sits among the served assets: it is harmless there, and a build that packages the folder can leave it out.
A page loading the IIFE bundles through script tags uses the globals ftl, ful and httpc instead of imports. Each declaration file names its global, so listing them in include is enough, with no paths needed for them:
"include": [
"src/main/webapp/assets/**/*.js",
"node_modules/@optionfactory/fml/dist/ftl.d.mts",
"node_modules/@optionfactory/fml/dist/ful.d.mts",
"node_modules/@optionfactory/fml/dist/httpc.d.mts"
]checkJs makes the editor report type errors in the included files: a method a class does not have, a wrong argument, a misspelled property of a field or a form.
strict is set to false on purpose. Strict mode reports every parameter without a type and every value that could be null, which in plain JavaScript is most of them, and recent TypeScript versions turn it on unless the config says otherwise.
Expect some reports on existing code even so. Most are queries the checker cannot type: querySelector('[data-ref=save]') answers a plain Element, so .value or .reload() on it is reported. Querying by tag (querySelector('ful-table')) is typed through the tag map; for the others, a JSDoc cast says what the element is:
const table = /** @type {import('ful').Table} */ (form.querySelector('[data-ref=orders]'));If the reports are too many to act on, set checkJs to false: completion and navigation keep working, and only the error reporting goes.
An application that calls ful.AsyncEvents.mixInto(HTMLElement) at startup gives every element fireAsync, asyncOn and asyncOff. No library declaration can know the application did that, so the checker reports form.asyncOn(...) as a missing method. Declare it in a file of the application's own, types/fml-mixins.d.ts, and add "types/**/*.d.ts" to include:
export {};
declare global {
interface HTMLElement {
fireAsync(evt: Event, options?: { mode?: 'broadcast' | 'pipeline' | 'delegate' }): Promise<any>;
asyncOn(type: string, fn: (evt: any) => any, options?: AddEventListenerOptions): EventListener;
asyncOff(type: string, listener: EventListener, options?: EventListenerOptions): void;
}
}Nothing more to do: IntelliJ reads web-types.json from the installed package. Type <ful- in a template and the elements, their attributes and their values are offered, with the descriptions from the element reference.
VS Code does not look for element data in node_modules: point its html.customData setting at the package's file, in a .vscode/settings.json committed with the project:
{
"html.customData": ["./node_modules/@optionfactory/fml/dist/vscode.html-custom-data.json"]
}The path is relative to the folder VS Code opened, so open the project's root, the one holding node_modules. VS Code reads the file when the window opens: after npm install or an upgrade, run Developer: Reload Window.
The devDependency follows the CDN pin. An upgrade changes the urls and integrity hashes in the pages and the version in package.json, the ftl, ful and httpc aliases included, followed by npm install to update package-lock.json, in the same commit. Otherwise the editor completes against one version while the pages run another.
- The type checking covers the
.jsfilesincludenames. Scripts written inline in templates get less: IntelliJ completes them once the import map's names are installed as packages (above), VS Code gives them its plain JavaScript support only, without thejsconfig.jsonnames. Code worth checking is worth a.jsfile. -
data-tpl-*expressions and{{ }}are strings to the editor: they are neither completed nor checked. A typo in one shows when the template renders, as aRenderErrornaming the node.
ftl
httpc
ful
- Getting started
- Fields
- Forms
- Inputs
- Select
- Files
- Temporal inputs
- Tables
- Disclosures
- Navigation
- Async sections
- Timing and storage
- Theming
Extending
Reference
Project