{"id":18389045,"url":"https://github.com/steveblue/custom-elements","last_synced_at":"2026-05-05T11:37:28.977Z","repository":{"id":66258307,"uuid":"160003828","full_name":"steveblue/custom-elements","owner":"steveblue","description":"A project for building Web Components with TypeScript Decorators","archived":false,"fork":false,"pushed_at":"2019-01-13T05:11:51.000Z","size":508,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-12T04:58:42.481Z","etag":null,"topics":["custom-elements","customelements","decorators","javascript","shadow-dom","typescript","web-components","webcomponents"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/steveblue.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-12-02T01:41:03.000Z","updated_at":"2021-03-30T07:10:36.000Z","dependencies_parsed_at":"2023-02-21T09:00:27.405Z","dependency_job_id":null,"html_url":"https://github.com/steveblue/custom-elements","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/steveblue%2Fcustom-elements","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/steveblue%2Fcustom-elements/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/steveblue%2Fcustom-elements/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/steveblue%2Fcustom-elements/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/steveblue","download_url":"https://codeload.github.com/steveblue/custom-elements/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248519473,"owners_count":21117757,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["custom-elements","customelements","decorators","javascript","shadow-dom","typescript","web-components","webcomponents"],"created_at":"2024-11-06T01:39:54.988Z","updated_at":"2026-05-05T11:37:28.917Z","avatar_url":"https://github.com/steveblue.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# custom-elements\n\nA playground for building Web Components with TypeScript Decorators.\n\n### Problem\n\nWhen developing Web Components with Custom Elements API there is a lot of boilerplate that is repeated in all class declarations.\n\n\n### Solution\n\nProvide a Functional approach to encapsulate some of this logic into reusable Function, specifically a special kinda of higher order Function called a Decorator. Decorators are available in TypeScript. Decorators are used my libraries like Angular and Stencil. This approach can be applied to Custom Elements v1, giving the engineer a consistent interface for generating UI components.\n\nThese methods below define the `Component` decorator which uses the `compileTemplate` Function to compile a HTML template. `attachShadow` provides a Function for calling `attachShadow` to the new Element to give it Shadow DOM.\n\n```js\nfunction compileTemplate(elementMeta: ElementMeta, target: Function) {\n    target.prototype.elementMeta = elementMeta;\n    target.prototype.template = document.createElement('template');\n    target.prototype.template = `\u003cstyle\u003e${elementMeta.style}\u003c/style\u003e${elementMeta.template}`;\n};\n\nfunction Component(attributes: ElementMeta) {\n    return (target: any) =\u003e {\n        const customElement = function(...args: any[]){};\n        if (attributes !== undefined \u0026\u0026 attributes !== null) {\n            compileTemplate(attributes, target);\n        }\n        customElement.prototype = target.prototype;\n        return target;\n    };\n}\n\nfunction attachShadow(instance: any, options: any) {\n    const shadowRoot : ShadowRoot = instance.attachShadow(options || {});\n    const t = document.createElement('template');\n    t.innerHTML = instance.template;\n    shadowRoot.appendChild(t.content.cloneNode(true));\n}\n\n```\n\n\n### Examples\n\nSee the production output of these Components on [Github Pages](https://steveblue.github.io/custom-elements/).\n\nWith functions available in `src/decorators/component.ts` you can use the following syntax to create a Custom Element. In this example MyListComponent extends from HTMLElement making it an autonomous Custom Element. This means we can compile ShadowDOM inside the new Element by calling attachShadow and take advantage of slots so the user can define a custom template.\n\nIn our template we can create a custom list element that has a user selectable list item. Let's call it `my-list`. It also has a child `my-item` that is not shown in this example, but is another Component in the test library.\n\n```html\n\u003cmy-list\u003e\n\t\u003cul slot=\"menu\"\u003e\n\t\t\u003cli\u003e\n\t\t\t\u003cmy-item\u003e\u003cspan slot=\"msg\"\u003eMake\u003c/span\u003e\u003c/my-item\u003e\n\t\t\u003c/li\u003e\n\t\t\u003cli\u003e\n\t\t\t\u003cmy-item\u003e\u003cspan slot=\"msg\"\u003eCustom\u003c/span\u003e\u003c/my-item\u003e\n\t\t\u003c/li\u003e\n\t\t\u003cli\u003e\n\t\t\t\u003cmy-item\u003e\u003cspan slot=\"msg\"\u003eElements\u003c/span\u003e\u003c/my-item\u003e\n\t\t\u003c/li\u003e\n\t\t\u003cli\u003e\n\t\t\t\u003cmy-item\u003e\u003cspan slot=\"msg\"\u003eAccessible\u003c/span\u003e\u003c/my-item\u003e\n\t\t\u003c/li\u003e\n\t\u003c/ul\u003e\n\u003c/my-list\u003e\n```\n\nTo define the class attached to MyListComponent we can implement it like so with the Decorator in this repo:\n\n```js\nimport { Component, html, css, attachShadow, getSiblings, getElementIndex, Listen } from 'src/decorators/component';\n\nexport class CustomElement extends HTMLElement {\n\tconstructor() {\n\t\tsuper();\n\t\tattachShadow(this, { mode: 'open' });\n\t\tthis.onInit();\n\t}\n}\n\n@Component({\n\tselector: 'my-list',\n\ttemplate: html`\n\t\t\u003cslot name=\"menu\"\u003e\u003c/slot\u003e\n\t`,\n\tstyle: css`\n\t\t:host {\n\t\t\tdisplay: block;\n\t\t\tbackground: rgba(24, 24, 24, 1);\n\t\t\twidth: 200px;\n\t\t\theight: 200px;\n\t\t\tcolor: white;\n\t\t\tpadding: 1em;\n\t\t\tborder-radius: 8px;\n\t\t}\n\t`,\n})\nclass MyListComponent extends CustomElement {\n\tconstructor() {\n\t\tsuper();\n\t\tthis.currentIndex = 0;\n\t}\n\tdeactivateElement(elem: HTMLElement) {\n\t\telem.setAttribute('tabindex', '-1');\n\t\telem.querySelector('my-item').setAttribute('state', '');\n\t}\n\tactivateElement(elem: HTMLElement) {\n\t\telem.setAttribute('tabindex', '0');\n\t\telem.querySelector('my-item').setAttribute('state', '--selected');\n\t}\n\tconnectedCallback() {\n\t\tthis.setAttribute('tabindex', '0');\n\t}\n\t@Listen('focus')\n\tonFocus(ev: FocusEvent) {\n\t\t\tfor (let li of this.children[0].children) {\n\t\t\t\tif (li === this.children[0].children[this.currentIndex]) {\n\t\t\t\t\tthis.activateElement(li);\n\t\t\t\t} else {\n\t\t\t\t\tthis.deactivateElement(li);\n\t\t\t\t}\n\t\t\t\tli.addEventListener('click', (ev: MouseEvent) =\u003e {\n\t\t\t\t\tgetSiblings(li).forEach((elem: HTMLElement) =\u003e {\n\t\t\t\t\t\tthis.deactivateElement(elem);\n\t\t\t\t\t});\n\t\t\t\t\tthis.activateElement(li);\n\t\t\t\t\tthis.onSubmit(ev);\n\t\t\t\t});\n\t\t\t}\n\t}\n\t@Listen('keydown')\n\tonKeydown(ev: KeyboardEvent) {\n\t\t\tlet currentElement = this.querySelector('[tabindex]:not([tabindex=\"-1\"])');\n\t\t\tlet siblings = getSiblings(currentElement);\n\t\t\tthis.currentIndex = getElementIndex(currentElement);\n\t\t\tif (ev.keyCode === 13) {\n\t\t\t\tthis.onSubmit(ev);\n\t\t\t}\n\t\t\tif (ev.keyCode === 38) {\n\t\t\t\t// up\n\t\t\t\tif (this.currentIndex === 0) {\n\t\t\t\t\tthis.currentIndex = siblings.length - 1;\n\t\t\t\t} else {\n\t\t\t\t\tthis.currentIndex -= 1;\n\t\t\t\t}\n\t\t\t\tsiblings.forEach((elem: HTMLElement) =\u003e {\n\t\t\t\t\tif (getElementIndex(elem) === this.currentIndex) {\n\t\t\t\t\t\tthis.activateElement(elem);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tthis.deactivateElement(elem);\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t}\n\t\t\tif (ev.keyCode === 40) {\n\t\t\t\t// down\n\t\t\t\tif (this.currentIndex === siblings.length - 1) {\n\t\t\t\t\tthis.currentIndex = 0;\n\t\t\t\t} else {\n\t\t\t\t\tthis.currentIndex += 1;\n\t\t\t\t}\n\t\t\t\tsiblings.forEach((elem: HTMLElement) =\u003e {\n\t\t\t\t\tif (getElementIndex(elem) === this.currentIndex) {\n\t\t\t\t\t\tthis.activateElement(elem);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tthis.deactivateElement(elem);\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t}\n\t}\n\tonSubmit(event) {\n\t\tconsole.log(this, event);\n\t}\n}\n\ncustomElements.define('my-list', MyListComponent);\n```\n\nThe below example is a button that extends HTMLButtonElement. Since this is a customized built-in elements, MyButtonComponent extends from the native HTMLButtonElement, we cannot attach Shadow DOM. attachDOM compiles the template as the my-button innerHTML and places a style tag in the `\u003chead\u003e` to style the Element.\n\n```js\nimport { Component, html, css, attachDOM, attachStyle, Listen } from 'src/decorators/component';\n\nclass ButtonComponent extends HTMLButtonElement {\n\tconstructor() {\n\t\tsuper();\n\t\tattachDOM(this);\n\t\tattachStyle(this);\n\t\tthis.onInit();\n\t}\n}\n\n@Component({\n\tselector: 'my-button',\n\ttemplate: html`\n\t\t\u003cb\u003eClick me!\u003c/b\u003e\n\t`,\n\tstyle: css`\n\t\t:host {\n\t\t\tbackground: red;\n\t\t\tcursor: pointer;\n\t\t\tpadding: 10px;\n\t\t\tborder-radius: 30px;\n\t\t\tborder: 0 none;\n\t\t\tcolor: white;\n\t\t\ttext-decoration: none;\n\t\t}\n\t`,\n})\nclass MyButtonComponent extends ButtonComponent {\n\tconstructor() {\n\t\tsuper();\n\t}\n\t@Listen('click')\n\tonClick(event) {\n\t\tconsole.log(this, event);\n\t}\n}\n\ncustomElements.define('my-button', MyButtonComponent, { extends: 'button' });\n```\n\nIn a template somewhere...\n\n```html\n\u003cbutton is=\"my-button\"\u003e\u003c/button\u003e\n```\n\nWa la! A Custom Element that retains all the behaviors of a button, yet extends button to do other things. In this nieve implementation all that is changed is the style of the button. Creating a customized built in element like this will retain all the behaviors of the element that is extended.\n\n## Getting Started\n\n```\nnpm install\n```\n\n## Development\n\nThe dev build implements a watcher to compile and bundle the app on file change.\n\n```\nnpm start\n```\n\nRun the express server in a separate tab.\n\n```\nnode backend/server.js\n```\n\nor use a tool like live-server.\n\n```\ncd dist\nlive-server\n```\n\n## Production\n\nThe prod build minifies the test package and provides an entry point for using the Components defined in the library.\n\n```\nNODE_ENV=prod node index.js\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsteveblue%2Fcustom-elements","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsteveblue%2Fcustom-elements","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsteveblue%2Fcustom-elements/lists"}