From 76058663b017c4f0598e3177f67b8cf46b1fcc18 Mon Sep 17 00:00:00 2001 From: Andrew Seier Date: Wed, 9 Oct 2024 15:12:58 -0700 Subject: [PATCH] =?UTF-8?q?Generate=20=E2=80=9Cx-element.d.ts=E2=80=9D=20f?= =?UTF-8?q?rom=20JSDoc=20comments.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, the “x-element.d.ts” file was hand-curated, which was prone to falling out of date. Because the TypeScript team manages JSDoc, it’s fairly straightforward to properly “type” your JS with JSDoc and output declarations which can be used by folks writing TypeScript — all of that without needing to actually author TypeScript ourselves.† Additionally, tools like JSR will build basic documentation for libraries which author JSDoc comments, so some wins there. Finally, to ensure best practices for our JSDocs, we enable some recommended rules from the eslint plugin. † One goal of “x-element” is to be _highly_ portable, it’s why no dependencies exist and why the “x-element.js” file can be used _verbatim_ without a build step. --- .github/workflows/test.yaml | 2 + CHANGELOG.md | 9 + deno.json | 15 +- eslint.config.js | 11 ++ etc/ready.d.ts | 9 + etc/ready.d.ts.map | 1 + etc/ready.js | 6 + package-lock.json | 178 +++++++++++++++++++- package.json | 16 +- tsconfig.json | 20 +++ x-element.d.ts | 327 ++++++++++++++++++++++++++++-------- x-element.d.ts.map | 1 + x-element.js | 274 +++++++++++++++++++++--------- 13 files changed, 714 insertions(+), 155 deletions(-) create mode 100644 etc/ready.d.ts create mode 100644 etc/ready.d.ts.map create mode 100644 tsconfig.json create mode 100644 x-element.d.ts.map diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 28e5a38..7abe24c 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -12,6 +12,8 @@ jobs: registry-url: 'https://registry.npmjs.org' - run: npm ci - run: npm run lint + - run: npm run type + - run: git diff --exit-code - run: npm start & - run: sleep 2 - run: npm test diff --git a/CHANGELOG.md b/CHANGELOG.md index 767260c..6426a32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - The `map` function now works with properties / attributes bound across template contexts (#179). +- The `x-element.d.ts` file now reflects the actual interface. Previously, it + has some issues (e.g., improper module export). + +### Changed + +- The `x-element.js` file is now “typed” via JSDoc. The validity of the JSDoc + comments are linted alongside the rest of the code and the annotations there + are exported into a generated `x-element.d.ts` file. Previously, that file was + hand-curated. ## [1.0.0] - 2024-02-29 diff --git a/deno.json b/deno.json index e576025..29c603c 100644 --- a/deno.json +++ b/deno.json @@ -1,5 +1,18 @@ { "name": "@netflix/x-element", "version": "1.0.0", - "exports": "./x-element.js" + "exports": { + "./x-element.js": "./x-element.js", + "./x-element.d.ts": "./x-element.d.ts", + "./etc/ready.js": "./etc/ready.js", + "./etc/ready.d.ts": "./etc/ready.d.ts" + }, + "include": [ + "./x-element.js", + "./x-element.d.ts", + "./x-element.d.ts.map", + "./etc/ready.js", + "./etc/ready.d.ts", + "./etc/ready.d.ts.map" + ] } diff --git a/eslint.config.js b/eslint.config.js index 64e103f..0e4975b 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,7 +1,9 @@ import globals from 'globals'; +import jsdoc from 'eslint-plugin-jsdoc'; import NetflixCommon from '@netflix/eslint-config'; export default [ + jsdoc.configs['flat/recommended'], { ...NetflixCommon, files: ['**/*.js'], @@ -12,6 +14,15 @@ export default [ 'demo/react/*', ], }, + { + files: ['x-element.js', 'etc/ready.js'], + plugins: { jsdoc }, + rules: { + 'jsdoc/require-param-description': 'off', + 'jsdoc/require-property-description': 'off', + 'jsdoc/require-returns-description': 'off', + }, + }, { ...NetflixCommon, files: ['demo/react/**/*.js'], diff --git a/etc/ready.d.ts b/etc/ready.d.ts new file mode 100644 index 0000000..6060ac4 --- /dev/null +++ b/etc/ready.d.ts @@ -0,0 +1,9 @@ +export default ready; +/** + * Await document completeness. Will likely be replaced by built-in apis. + * Check out https://github.com/Netflix/x-element/issues/65 for details. + * @param {Document} target + * @returns {Promise} + */ +declare function ready(target: Document): Promise; +//# sourceMappingURL=ready.d.ts.map \ No newline at end of file diff --git a/etc/ready.d.ts.map b/etc/ready.d.ts.map new file mode 100644 index 0000000..e1dfd5b --- /dev/null +++ b/etc/ready.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"ready.d.ts","sourceRoot":"","sources":["ready.js"],"names":[],"mappings":";AAAA;;;;;GAKG;AACH,+BAHW,QAAQ,GACN,OAAO,CAAC,GAAG,CAAC,CAgBxB"} \ No newline at end of file diff --git a/etc/ready.js b/etc/ready.js index 5b6113f..5e8f0ed 100644 --- a/etc/ready.js +++ b/etc/ready.js @@ -1,3 +1,9 @@ +/** + * Await document completeness. Will likely be replaced by built-in apis. + * Check out https://github.com/Netflix/x-element/issues/65 for details. + * @param {Document} target + * @returns {Promise} + */ const ready = target => { return new Promise(resolve => { if (target.readyState === 'complete') { diff --git a/package-lock.json b/package-lock.json index 6a112de..6ad443c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,8 +11,10 @@ "devDependencies": { "@netflix/eslint-config": "^3.0.0", "eslint": "^9.12.0", + "eslint-plugin-jsdoc": "^50.3.1", "puppeteer": "^23.5.3", - "tap-parser": "^18.0.0" + "tap-parser": "^18.0.0", + "typescript": "^5.6.3" }, "engines": { "node": "20.18.0", @@ -145,6 +147,21 @@ "node": ">=4" } }, + "node_modules/@es-joy/jsdoccomment": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.49.0.tgz", + "integrity": "sha512-xjZTSFgECpb9Ohuk5yMX5RhUEbfeQcuOp8IF60e+wyzWEF0M5xeSgqsfLtvPEX8BIyOX9saZqzuGPmZ8oWc+5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "comment-parser": "1.4.1", + "esquery": "^1.6.0", + "jsdoc-type-pratt-parser": "~4.1.0" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -306,6 +323,19 @@ "@eslint/js": "^8.21.0" } }, + "node_modules/@pkgr/core": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", + "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, "node_modules/@puppeteer/browsers": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.4.0.tgz", @@ -448,6 +478,16 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/are-docs-informative": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/are-docs-informative/-/are-docs-informative-0.0.2.tgz", + "integrity": "sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -673,6 +713,16 @@ "dev": true, "license": "MIT" }, + "node_modules/comment-parser": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.1.tgz", + "integrity": "sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -809,6 +859,13 @@ "is-arrayish": "^0.2.1" } }, + "node_modules/es-module-lexer": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", + "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==", + "dev": true, + "license": "MIT" + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -912,6 +969,32 @@ } } }, + "node_modules/eslint-plugin-jsdoc": { + "version": "50.4.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-50.4.1.tgz", + "integrity": "sha512-OXIq+JJQPCLAKL473/esioFOwbXyRE5MAQ4HbZjcp3e+K3zdxt2uDpGs3FR+WezUXNStzEtTfgx15T+JFrVwBA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@es-joy/jsdoccomment": "~0.49.0", + "are-docs-informative": "^0.0.2", + "comment-parser": "1.4.1", + "debug": "^4.3.6", + "escape-string-regexp": "^4.0.0", + "espree": "^10.1.0", + "esquery": "^1.6.0", + "parse-imports": "^2.1.1", + "semver": "^7.6.3", + "spdx-expression-parse": "^4.0.0", + "synckit": "^0.9.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0" + } + }, "node_modules/eslint-scope": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.1.0.tgz", @@ -1005,9 +1088,9 @@ } }, "node_modules/esquery": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", - "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -1435,6 +1518,16 @@ "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", "dev": true }, + "node_modules/jsdoc-type-pratt-parser": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.1.0.tgz", + "integrity": "sha512-Hicd6JK5Njt2QB6XYFS7ok9e37O8AYk3jTcppG4YVQnYjOemymvTcmc7OWsmq/Qqj5TdRFO5/x/tIPmBeRtGHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -1679,6 +1772,20 @@ "node": ">=6" } }, + "node_modules/parse-imports": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/parse-imports/-/parse-imports-2.2.1.tgz", + "integrity": "sha512-OL/zLggRp8mFhKL0rNORUTR4yBYujK/uU+xZL+/0Rgm2QE4nLO9v8PzEweSJEbMGKmDRjJE4R3IMJlL2di4JeQ==", + "dev": true, + "license": "Apache-2.0 AND MIT", + "dependencies": { + "es-module-lexer": "^1.5.3", + "slashes": "^3.0.12" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -1885,6 +1992,13 @@ "node": ">=8" } }, + "node_modules/slashes": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/slashes/-/slashes-3.0.12.tgz", + "integrity": "sha512-Q9VME8WyGkc7pJf6QEkj3wE+2CnvZMI+XJhwdTPR8Z/kWQRXi7boAWLDibRPyHRTUTPx5FaU7MsyrjI3yLB4HA==", + "dev": true, + "license": "ISC" + }, "node_modules/smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", @@ -1933,6 +2047,31 @@ "node": ">=0.10.0" } }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz", + "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.20", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.20.tgz", + "integrity": "sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/sprintf-js": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", @@ -2004,6 +2143,23 @@ "node": ">=8" } }, + "node_modules/synckit": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.2.tgz", + "integrity": "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, "node_modules/tap-parser": { "version": "18.0.0", "resolved": "https://registry.npmjs.org/tap-parser/-/tap-parser-18.0.0.tgz", @@ -2105,6 +2261,20 @@ "integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==", "dev": true }, + "node_modules/typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/unbzip2-stream": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", diff --git a/package.json b/package.json index 94d6462..b86bcc1 100644 --- a/package.json +++ b/package.json @@ -3,11 +3,12 @@ "description": "A dead simple starting point for custom elements.", "version": "1.0.0", "license": "Apache-2.0", - "repository": "https://github.com/Netflix/x-element", + "repository": "github:Netflix/x-element", "type": "module", - "main": "x-element.js", - "module": "x-element.js", - "types": "x-element.d.ts", + "exports": { + "./x-element.js": "./x-element.js", + "./x-element.d.ts": "./x-element.d.ts" + }, "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" @@ -17,19 +18,24 @@ "lint": "eslint --max-warnings=0 .", "lint-fix": "eslint --fix .", "test": "node test.js | tap-parser -l", + "type": "tsc", "bump": "./bump.sh" }, "files": [ "/x-element.js", + "/x-element.d.ts", + "/x-element.d.ts.map", "/demo", "/test", "/etc" ], "devDependencies": { "@netflix/eslint-config": "^3.0.0", + "eslint-plugin-jsdoc": "^50.3.1", "eslint": "^9.12.0", "puppeteer": "^23.5.3", - "tap-parser": "^18.0.0" + "tap-parser": "^18.0.0", + "typescript": "^5.6.3" }, "engines": { "node": "20.18.0", diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..4deec90 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + "files": [ + "./x-element.js", + "./etc/ready.js" + ], + "compilerOptions": { + "target": "ES2023", + "allowJs": true, + "checkJs": true, + "declaration": true, + "emitDeclarationOnly": true, + // Even though the docs suggest that “declarationMap” is a way to go from + // generated-js back to ts… it also seems to be the case that it will help + // IDEs get to the right _js_ when using types. See + // https://www.typescriptlang.org/docs/handbook/declaration-files/dts-from-js.html + "declarationMap": true, + "noEmitOnError": true, + "module": "NodeNext" // Approximates browser target. + } +} diff --git a/x-element.d.ts b/x-element.d.ts index 2424938..6aaf62e 100644 --- a/x-element.d.ts +++ b/x-element.d.ts @@ -1,70 +1,259 @@ -export class XElement extends HTMLElement { - readonly properties: { - [property: string]: { - type: any, - attribute: string, - input: string[], - compute: (...any) => any, - observe: (host: XElement, value: any, oldValue: any) => void, - reflect: boolean, - internal: boolean, - readOnly: boolean, - initial: any, - default: any, - } - }; - readonly listeners: { - [type: string]: ( - this: typeof XElement, - host: XElement, - event: Event - ) => any - }; - readonly internal: object; - render(): void; - listen( - element: EventTarget, - type: string, - callback: (this: typeof XElement, host: XElement, event: Event) => any, - options: AddEventListenerOptions - ): void; - unlisten( - element: EventTarget, - type: string, - callback: (this: typeof XElement, host: XElement, event: Event) => any, - options: AddEventListenerOptions - ): void; - dispatchError(error: Error): void; - - static readonly defaultTemplateEngine: { - render: (container: HTMLElement, result: any) => void, - html: (strings: TemplateStringsArray, ...any) => any, - svg: (strings: TemplateStringsArray, ...any) => any, - map: ( - items: any[], - identify: (item: any, index: number) => string, - callback: (item: any, index: number) => any - ) => any, - nullish: (value: any) => any, - - live: (value: any) => any, - unsafeHTML: (value: any) => any, - unsafeSVG: (value: any) => any, - ifDefined: (value: any) => any, - repeat: ( - items: any[], - identify: (item: any, index: number) => any, - callback?: (item: any, index: number) => any - ) => any, - }; - static readonly templateEngine: { - render: (container: HTMLElement, result: any) => void, - html: (strings: TemplateStringsArray, ...any) => any, - } - static readonly styles: [CSSStyleSheet] - static createRenderRoot(host: XElement): HTMLElement; - static template( - html: (strings: TemplateStringsArray, ...any) => any, - object - ): (properties: object, host: XElement) => any; +/** Base element class for creating custom elements. */ +export default class XElement extends HTMLElement { + /** + * Extends HTMLElement.observedAttributes to handle the properties block. + * @returns {string[]} + */ + static get observedAttributes(): string[]; + /** + * Default templating engine. Use "templateEngine" to override. + * @returns {{[key: string]: Function}} + */ + static get defaultTemplateEngine(): { + [key: string]: Function; + }; + /** + * Configured templating engine. Defaults to "defaultTemplateEngine". + * Override this as needed if x-element's default template engine does not + * meet your needs. A "render" method is the only required field. An "html" + * tagged template literal is expected, but not strictly required. + * @returns {{[key: string]: Function}} + */ + static get templateEngine(): { + [key: string]: Function; + }; + /** + * Declare an array of CSSStyleSheet objects to adopt on the shadow root. + * Note that a CSSStyleSheet object is the type returned when importing a + * stylesheet file via import attributes. + * ```js + * import importedStyle from './path-to.css' with { type: 'css' }; + * class MyElement extends XElement { + * static get styles() { + * const inlineStyle = new CSSStyleSheet(); + * inlineStyle.replaceSync(`:host { display: block; }`); + * return [importedStyle, inlineStyle]; + * } + * } + * ``` + * @returns {CSSStyleSheet[]} + */ + static get styles(): CSSStyleSheet[]; + /** + * Observe callback. + * @callback observeCallback + * @param {HTMLElement} host + * @param {any} value + * @param {any} oldValue + */ + /** + * A property value. + * @typedef {object} Property + * @property {any} [type] + * @property {string} [attribute] + * @property {string[]} [input] + * @property {Function} [compute] + * @property {observeCallback} [observe] + * @property {boolean} [reflect] + * @property {boolean} [internal] + * @property {boolean} [readOnly] + * @property {any|Function} [initial] + * @property {any|Function} [default] + */ + /** + * Declare watched properties (and related attributes) on an element. + * ```js + * static get properties() { + * return { + * property1: { + * type: String, + * }, + * property2: { + * type: Number, + * input: ['property1'], + * compute: this.computeProperty2, + * reflect: true, + * observe: this.observeProperty2, + * default: 0, + * } + * }; + * } + * ``` + * @returns {{[key: string]: Property}} + */ + static get properties(): { + [key: string]: { + type?: any; + attribute?: string; + input?: string[]; + compute?: Function; + observe?: (host: HTMLElement, value: any, oldValue: any) => any; + reflect?: boolean; + internal?: boolean; + readOnly?: boolean; + initial?: any | Function; + default?: any | Function; + }; + }; + /** + * Listen callback. + * @callback delegatedListenCallback + * @param {HTMLElement} host + * @param {Event} event + */ + /** + * Declare event handlers on an element. + * ```js + * static get listeners() { + * return { + * click: this.onClick, + * } + * } + *``` + * Note that listeners are added to the element's render root. Listeners are + * added during "connectedCallback" and removed during "disconnectedCallback". + * The arguments passed to your callback are always "(host, event)". + * @returns {{[key: string]: delegatedListenCallback}} + */ + static get listeners(): { + [key: string]: (host: HTMLElement, event: Event) => any; + }; + /** + * Customize shadow root initialization and optionally forgo encapsulation. + * E.g., setup focus delegation or return host instead of host.shadowRoot. + * @param {HTMLElement} host + * @returns {HTMLElement|ShadowRoot} + */ + static createRenderRoot(host: HTMLElement): HTMLElement | ShadowRoot; + /** + * Template callback. + * @callback templateCallback + * @param {object} properties + * @param {HTMLElement} host + */ + /** + * Setup template callback to update DOM when properties change. + * ```js + * static template(html, { nullish }) { + * return (href) => { + * return html`click me`; + * } + * } + * ``` + * @param {Function} html + * @param {{[key: string]: Function}} engine + * @returns {templateCallback} + */ + static template(html: Function, engine: { + [key: string]: Function; + }): (properties: object, host: HTMLElement) => any; + static "__#1@#analyzeConstructor"(constructor: any): void; + static "__#1@#validateProperties"(constructor: any, properties: any, entries: any): void; + static "__#1@#validateProperty"(constructor: any, key: any, property: any): void; + static "__#1@#validatePropertyAttribute"(constructor: any, key: any, property: any, attribute: any): void; + static "__#1@#propertyIsCyclic"(property: any, inputMap: any, seen?: Set): boolean; + static "__#1@#validateListeners"(constructor: any, listeners: any, entries: any): void; + static "__#1@#mutateProperty"(constructor: any, propertyMap: any, key: any, property: any): void; + static "__#1@#addPropertyInitial"(constructor: any, property: any): void; + static "__#1@#addPropertyDefault"(constructor: any, property: any): void; + static "__#1@#addPropertySync"(constructor: any, property: any): void; + static "__#1@#addPropertyReflect"(constructor: any, property: any): void; + static "__#1@#addPropertyCompute"(constructor: any, property: any): void; + static "__#1@#addPropertyObserve"(constructor: any, property: any): void; + static "__#1@#constructHost"(host: any): void; + static "__#1@#createInternal"(host: any): any; + static "__#1@#createProperties"(host: any): any; + static "__#1@#connectHost"(host: any): void; + static "__#1@#disconnectHost"(host: any): void; + static "__#1@#initializeHost"(host: any): boolean; + static "__#1@#upgradeOwnProperties"(host: any): void; + static "__#1@#getPreUpgradePropertyValue"(host: any, property: any): { + value: any; + found: boolean; + }; + static "__#1@#initializeProperty"(host: any, property: any): void; + static "__#1@#addListener"(host: any, element: any, type: any, callback: any, options: any): void; + static "__#1@#addListeners"(host: any): void; + static "__#1@#removeListener"(host: any, element: any, type: any, callback: any, options: any): void; + static "__#1@#removeListeners"(host: any): void; + static "__#1@#getListener"(host: any, listener: any): any; + static "__#1@#updateHost"(host: any): void; + static "__#1@#toPathString"(host: any): string; + static "__#1@#invalidateProperty"(host: any, property: any): Promise; + static "__#1@#getPropertyValue"(host: any, property: any): any; + static "__#1@#validatePropertyValue"(host: any, property: any, value: any): void; + static "__#1@#setPropertyValue"(host: any, property: any, value: any): void; + static "__#1@#serializeProperty"(host: any, property: any, value: any): any; + static "__#1@#deserializeProperty"(host: any, property: any, value: any): any; + static "__#1@#propertyHasAttribute"(property: any): boolean; + static "__#1@#getTypeName"(value: any): any; + static "__#1@#notNullish"(value: any): boolean; + static "__#1@#typeIsWrong"(type: any, value: any): boolean; + static "__#1@#camelToKebab"(camel: any): any; + static "__#1@#constructors": WeakMap; + static "__#1@#hosts": WeakMap; + static "__#1@#propertyKeys": Set; + static "__#1@#serializableTypes": Set; + static "__#1@#caseMap": Map; + static "__#1@#prototypeInterface": Set; + /** + * Extends HTMLElement.prototype.connectedCallback. + */ + connectedCallback(): void; + /** + * Extends HTMLElement.prototype.attributeChangedCallback. + * @param {string} attribute + * @param {string|null} oldValue + * @param {string|null} value + */ + attributeChangedCallback(attribute: string, oldValue: string | null, value: string | null): void; + /** + * Extends HTMLElement.prototype.adoptedCallback. + */ + adoptedCallback(): void; + /** + * Extends HTMLElement.prototype.disconnectedCallback. + */ + disconnectedCallback(): void; + /** + * Uses the result of your template callback to update your render root. + * + * This is called when properties update, but is exposed for advanced use cases. + */ + render(): void; + /** + * Listen callback. + * @callback listenCallback + * @param {Event} event + */ + /** + * Wrapper around HTMLElement.addEventListener. + * Advanced — use this only if declaring listeners statically is not possible. + * @param {EventTarget} element + * @param {string} type + * @param {listenCallback} callback + * @param {object} [options] + */ + listen(element: EventTarget, type: string, callback: (event: Event) => any, options?: object): void; + /** + * Wrapper around HTMLElement.removeEventListener. Inverse of "listen". + * @param {EventTarget} element + * @param {string} type + * @param {listenCallback} callback + * @param {object} [options] + */ + unlisten(element: EventTarget, type: string, callback: (event: Event) => any, options?: object): void; + /** + * Helper method to dispatch an "ErrorEvent" on the element. + * @param {Error} error + */ + dispatchError(error: Error): void; + /** + * For element authors. Getter and setter for internal properties. + * Note that you can set read-only properties from host.internal. However, you + * must get read-only properties directly from the host. + * @returns {object} + */ + get internal(): any; } +//# sourceMappingURL=x-element.d.ts.map \ No newline at end of file diff --git a/x-element.d.ts.map b/x-element.d.ts.map new file mode 100644 index 0000000..03c4b72 --- /dev/null +++ b/x-element.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"x-element.d.ts","sourceRoot":"","sources":["x-element.js"],"names":[],"mappings":"AAAA,uDAAuD;AACvD;IACE;;;OAGG;IACH,0CAGC;IAED;;;OAGG;IACH;;MAEC;IAED;;;;;;OAMG;IACH;;MAEC;IAED;;;;;;;;;;;;;;;OAeG;IACH,qCAEC;IAED;;;;;;OAMG;IAEH;;;;;;;;;;;;;OAaG;IAEH;;;;;;;;;;;;;;;;;;;;OAoBG;IACH;;mBAjCc,GAAG;wBACH,MAAM;oBACN,MAAM,EAAE;;6BAVX,WAAW,SACX,GAAG,YACH,GAAG;sBAWA,OAAO;uBACP,OAAO;uBACP,OAAO;sBACP,GAAG,WAAS;sBACZ,GAAG,WAAS;;MA0BzB;IAED;;;;;OAKG;IAEH;;;;;;;;;;;;;OAaG;IACH;8BAlBW,WAAW,SACX,KAAK;MAmBf;IAED;;;;;OAKG;IACH,8BAHW,WAAW,GACT,WAAW,GAAC,UAAU,CAIlC;IAED;;;;;OAKG;IAEH;;;;;;;;;;;;OAYG;IACH,wCAHW;QAAC,CAAC,GAAG,EAAE,MAAM,YAAW;KAAC,gBAdzB,MAAM,QACN,WAAW,SAkBrB;IA6ID,0DA6BC;IAGD,yFAqCC;IAED,iFA2FC;IAED,0GAMC;IAGD,wFAaC;IAED,uFAQC;IAGD,iGAiBC;IAGD,yEAUC;IAGD,yEAkBC;IAGD,sEAUC;IAGD,yEAaC;IAGD,yEAsBC;IAGD,yEAYC;IAGD,8CAyCC;IAGD,8CA6CC;IAID,gDA6BC;IAGD,4CAMC;IAED,+CAEC;IAED,kDAyBC;IAGD,qDAMC;IAGD;;;MAmBC;IAED,kEAiBC;IAED,kGAGC;IAED,6CAMC;IAED,qGAGC;IAED,gDAMC;IAED,0DAMC;IAED,2CAYC;IAGD,+CAeC;IAED,2EAiBC;IAED,+DAGC;IAED,iFAQC;IAED,4EAQC;IAED,4EAOC;IAED,8EAoBC;IAGD,4DAEC;IAED,4CAEC;IAED,+CAEC;IAED,2DAOC;IAED,6CAKC;IAED,mDAAqC;IACrC,4CAA8B;IAC9B,yCAA8I;IAC9I,kGAA+D;IAC/D,sCAA4B;IAC5B,+CAAqF;IA10BrF;;OAEG;IACH,0BAEC;IAED;;;;;OAKG;IACH,oCAJW,MAAM,YACN,MAAM,GAAC,IAAI,SACX,MAAM,GAAC,IAAI,QAOrB;IAED;;OAEG;IACH,wBAAoB;IAEpB;;OAEG;IACH,6BAEC;IAED;;;;OAIG;IACH,eAUC;IAED;;;;OAIG;IAEH;;;;;;;OAOG;IACH,gBALW,WAAW,QACX,MAAM,oBAPN,KAAK,oBASL,MAAM,QAoBhB;IAED;;;;;;OAMG;IACH,kBALW,WAAW,QACX,MAAM,oBAlCN,KAAK,oBAoCL,MAAM,QAoBhB;IAED;;;OAGG;IACH,qBAFW,KAAK,QAMf;IAED;;;;;OAKG;IACH,oBAEC;CA2sBF"} \ No newline at end of file diff --git a/x-element.js b/x-element.js index 5f52804..9004540 100644 --- a/x-element.js +++ b/x-element.js @@ -1,22 +1,28 @@ /** Base element class for creating custom elements. */ export default class XElement extends HTMLElement { - /** Extends HTMLElement.observedAttributes to handle the properties block. */ + /** + * Extends HTMLElement.observedAttributes to handle the properties block. + * @returns {string[]} + */ static get observedAttributes() { XElement.#analyzeConstructor(this); return [...XElement.#constructors.get(this).attributeMap.keys()]; } - /** Default templating engine. Use "templateEngine" to override. */ + /** + * Default templating engine. Use "templateEngine" to override. + * @returns {{[key: string]: Function}} + */ static get defaultTemplateEngine() { return TemplateEngine.interface; } /** * Configured templating engine. Defaults to "defaultTemplateEngine". - * * Override this as needed if x-element's default template engine does not * meet your needs. A "render" method is the only required field. An "html" * tagged template literal is expected, but not strictly required. + * @returns {{[key: string]: Function}} */ static get templateEngine() { return XElement.defaultTemplateEngine; @@ -26,47 +32,90 @@ export default class XElement extends HTMLElement { * Declare an array of CSSStyleSheet objects to adopt on the shadow root. * Note that a CSSStyleSheet object is the type returned when importing a * stylesheet file via import attributes. + * ```js + * import importedStyle from './path-to.css' with { type: 'css' }; + * class MyElement extends XElement { + * static get styles() { + * const inlineStyle = new CSSStyleSheet(); + * inlineStyle.replaceSync(`:host { display: block; }`); + * return [importedStyle, inlineStyle]; + * } + * } + * ``` + * @returns {CSSStyleSheet[]} */ static get styles() { return []; } + /** + * Observe callback. + * @callback observeCallback + * @param {HTMLElement} host + * @param {any} value + * @param {any} oldValue + */ + + /** + * A property value. + * @typedef {object} Property + * @property {any} [type] + * @property {string} [attribute] + * @property {string[]} [input] + * @property {Function} [compute] + * @property {observeCallback} [observe] + * @property {boolean} [reflect] + * @property {boolean} [internal] + * @property {boolean} [readOnly] + * @property {any|Function} [initial] + * @property {any|Function} [default] + */ + /** * Declare watched properties (and related attributes) on an element. - * - * static get properties() { - * return { - * property1: { - * type: String, - * }, - * property2: { - * type: Number, - * input: ['property1'], - * compute: this.computeProperty2, - * reflect: true, - * observe: this.observeProperty2, - * default: 0, - * } - * }; - * } + * ```js + * static get properties() { + * return { + * property1: { + * type: String, + * }, + * property2: { + * type: Number, + * input: ['property1'], + * compute: this.computeProperty2, + * reflect: true, + * observe: this.observeProperty2, + * default: 0, + * } + * }; + * } + * ``` + * @returns {{[key: string]: Property}} */ static get properties() { return {}; } + /** + * Listen callback. + * @callback delegatedListenCallback + * @param {HTMLElement} host + * @param {Event} event + */ + /** * Declare event handlers on an element. - * - * static get listeners() { - * return { - * click: this.onClick, - * } + * ```js + * static get listeners() { + * return { + * click: this.onClick, * } - * + * } + *``` * Note that listeners are added to the element's render root. Listeners are * added during "connectedCallback" and removed during "disconnectedCallback". - * * The arguments passed to your callback are always "(host, event)". + * @returns {{[key: string]: delegatedListenCallback}} */ static get listeners() { return {}; @@ -74,38 +123,59 @@ export default class XElement extends HTMLElement { /** * Customize shadow root initialization and optionally forgo encapsulation. - * * E.g., setup focus delegation or return host instead of host.shadowRoot. + * @param {HTMLElement} host + * @returns {HTMLElement|ShadowRoot} */ static createRenderRoot(host) { return host.attachShadow({ mode: 'open' }); } + /** + * Template callback. + * @callback templateCallback + * @param {object} properties + * @param {HTMLElement} host + */ + /** * Setup template callback to update DOM when properties change. - * - * static template(html, { nullish }) { - * return (href) => { - * return html`click me`; - * } + * ```js + * static template(html, { nullish }) { + * return (href) => { + * return html`click me`; * } + * } + * ``` + * @param {Function} html + * @param {{[key: string]: Function}} engine + * @returns {templateCallback} */ static template(html, engine) { // eslint-disable-line no-unused-vars return (properties, host) => {}; // eslint-disable-line no-unused-vars } - /** Standard instance constructor. */ + /** + * Standard instance constructor. + */ constructor() { super(); XElement.#constructHost(this); } - /** Extends HTMLElement.prototype.connectedCallback. */ + /** + * Extends HTMLElement.prototype.connectedCallback. + */ connectedCallback() { XElement.#connectHost(this); } - /** Extends HTMLElement.prototype.attributeChangedCallback. */ + /** + * Extends HTMLElement.prototype.attributeChangedCallback. + * @param {string} attribute + * @param {string|null} oldValue + * @param {string|null} value + */ attributeChangedCallback(attribute, oldValue, value) { const { attributeMap } = XElement.#constructors.get(this.constructor); // Authors may extend "observedAttributes". Optionally chain to account for @@ -113,10 +183,14 @@ export default class XElement extends HTMLElement { attributeMap.get(attribute)?.sync(this, value, oldValue); } - /** Extends HTMLElement.prototype.adoptedCallback. */ + /** + * Extends HTMLElement.prototype.adoptedCallback. + */ adoptedCallback() {} - /** Extends HTMLElement.prototype.disconnectedCallback. */ + /** + * Extends HTMLElement.prototype.disconnectedCallback. + */ disconnectedCallback() { XElement.#disconnectHost(this); } @@ -138,10 +212,19 @@ export default class XElement extends HTMLElement { } } + /** + * Listen callback. + * @callback listenCallback + * @param {Event} event + */ + /** * Wrapper around HTMLElement.addEventListener. - * * Advanced — use this only if declaring listeners statically is not possible. + * @param {EventTarget} element + * @param {string} type + * @param {listenCallback} callback + * @param {object} [options] */ listen(element, type, callback, options) { if (XElement.#typeIsWrong(EventTarget, element)) { @@ -164,9 +247,11 @@ export default class XElement extends HTMLElement { } /** - * Wrapper around HTMLElement.removeEventListener. - * - * Inverse of "listen". + * Wrapper around HTMLElement.removeEventListener. Inverse of "listen". + * @param {EventTarget} element + * @param {string} type + * @param {listenCallback} callback + * @param {object} [options] */ unlisten(element, type, callback, options) { if (XElement.#typeIsWrong(EventTarget, element)) { @@ -188,7 +273,10 @@ export default class XElement extends HTMLElement { XElement.#removeListener(this, element, type, callback, options); } - /** Helper method to dispatch an "ErrorEvent" on the element. */ + /** + * Helper method to dispatch an "ErrorEvent" on the element. + * @param {Error} error + */ dispatchError(error) { const { message } = error; const eventData = { error, message, bubbles: true, composed: true }; @@ -197,9 +285,9 @@ export default class XElement extends HTMLElement { /** * For element authors. Getter and setter for internal properties. - * * Note that you can set read-only properties from host.internal. However, you * must get read-only properties directly from the host. + * @returns {object} */ get internal() { return XElement.#hosts.get(this).internal; @@ -944,8 +1032,12 @@ class TemplateEngine { /** * Declare HTML markup to be interpolated. - * - * html`
${obj.content}
`; + * ```js + * html`
${obj.content}
`; + * ``` + * @param {string[]} strings + * @param {any[]} values + * @returns {any} */ static html(strings, ...values) { const reference = TemplateEngine.#createReference(); @@ -956,8 +1048,12 @@ class TemplateEngine { /** * Declare SVG markup to be interpolated. - * - * svg``; + * ```js + * svg``; + * ``` + * @param {string[]} strings + * @param {any[]} values + * @returns {any} */ static svg(strings, ...values) { const reference = TemplateEngine.#createReference(); @@ -968,8 +1064,9 @@ class TemplateEngine { /** * Core rendering entry point for x-element template engine. - * * Accepts a "container" element and renders the given "result" into it. + * @param {HTMLElement} container + * @param {any} resultReference */ static render(container, resultReference) { const state = TemplateEngine.#setIfMissing(TemplateEngine.#stateMap, container, () => ({})); @@ -991,11 +1088,13 @@ class TemplateEngine { /** * Updater to manage an attribute which may be undefined. - * * In the following example, the "ifDefined" updater will remove the * attribute if it's undefined. Else, it sets the key-value pair. - * - * html``; + * ```js + * html``; + * ``` + * @param {any} value + * @returns {any} */ static ifDefined(value) { const reference = TemplateEngine.#createReference(); @@ -1007,11 +1106,13 @@ class TemplateEngine { /** * Updater to manage an attribute which may not exist. - * * In the following example, the "nullish" updater will remove the * attribute if it's nullish. Else, it sets the key-value pair. - * - * html``; + * ```js + * html``; + * ``` + * @param {any} value + * @returns {any} */ static nullish(value) { const reference = TemplateEngine.#createReference(); @@ -1023,13 +1124,15 @@ class TemplateEngine { /** * Updater to manage a property which may change outside the template engine. - * * Typically, properties are declaratively managed from state and efficient * value checking is used (i.e., "value !== lastValue"). However, if DOM state * is expected to change, the "live" updater can be used to essentially change * this check to "value !== node[property]". - * - * html``; + * ```js + * html``; + * ``` + * @param {any} value + * @returns {any} */ static live(value) { const reference = TemplateEngine.#createReference(); @@ -1041,11 +1144,13 @@ class TemplateEngine { /** * Updater to inject trusted HTML into the DOM. - * * Use with caution. The "unsafeHTML" updater allows arbitrary input to be * parsed as HTML and injected into the DOM. - * - * html`
${unsafeHTML(obj.trustedMarkup)}
`; + * ```js + * html`
${unsafeHTML(obj.trustedMarkup)}
`; + * ``` + * @param {any} value + * @returns {any} */ static unsafeHTML(value) { const reference = TemplateEngine.#createReference(); @@ -1057,15 +1162,17 @@ class TemplateEngine { /** * Updater to inject trusted SVG into the DOM. - * * Use with caution. The "unsafeSVG" updater allows arbitrary input to be * parsed as SVG and injected into the DOM. - * - * html` - * - * ${unsafeSVG(obj.trustedMarkup)} - * - * `; + * ```js + * html` + * + * ${unsafeSVG(obj.trustedMarkup)} + * + * `; + * ``` + * @param {any} value + * @returns {any} */ static unsafeSVG(value) { const reference = TemplateEngine.#createReference(); @@ -1077,12 +1184,17 @@ class TemplateEngine { /** * Updater to manage a keyed array of templates (allows for DOM reuse). - * - * html` - *
    - * ${map(items, item => item.id, item => html`
  • ${item.value}
  • `)} - * - * `; + * ```js + * html` + *
      + * ${map(items, item => item.id, item => html`
    • ${item.value}
    • `)} + * + * `; + * ``` + * @param {any[]} items + * @param {Function} identify + * @param {Function} callback + * @returns {any} */ static map(items, identify, callback) { if (typeof identify !== 'function') { @@ -1094,8 +1206,14 @@ class TemplateEngine { return TemplateEngine.#mapOrRepeat(items, identify, callback, 'map'); } - /** Shim for prior "repeat" function. Use "map". */ - static repeat(value, identify, callback) { + /** + * Shim for prior "repeat" function. Use "map". + * @param {any[]} items + * @param {Function} identify + * @param {Function} [callback] + * @returns {any} + */ + static repeat(items, identify, callback) { if (arguments.length === 2) { callback = identify; identify = null; @@ -1105,9 +1223,13 @@ class TemplateEngine { } else if (typeof callback !== 'function') { throw new Error(`Unexpected repeat callback "${callback}" provided, expected a function.`); } - return TemplateEngine.#mapOrRepeat(value, identify, callback, 'repeat'); + return TemplateEngine.#mapOrRepeat(items, identify, callback, 'repeat'); } + /** + * Default template engine interface — what you get inside “template”. + * @returns {{[key: string]: Function}} + */ static get interface() { if (!TemplateEngine.#interface) { TemplateEngine.#interface = Object.freeze({