{"id":32233597,"url":"https://github.com/tjb1982/hoquet","last_synced_at":"2026-02-19T23:02:08.559Z","repository":{"id":401737,"uuid":"11846554","full_name":"tjb1982/hoquet","owner":"tjb1982","description":"A tiny, minimal, platform-native, vanilla JavaScript web component library.","archived":false,"fork":false,"pushed_at":"2023-04-15T13:47:05.000Z","size":396,"stargazers_count":22,"open_issues_count":5,"forks_count":2,"subscribers_count":1,"default_branch":"master","last_synced_at":"2026-01-23T08:48:31.738Z","etag":null,"topics":["mixin","platform-native","vanilla-javascript","vanilla-js","web-components"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/tjb1982.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null}},"created_at":"2013-08-02T15:34:59.000Z","updated_at":"2025-08-07T03:02:40.000Z","dependencies_parsed_at":"2023-07-05T15:02:28.243Z","dependency_job_id":null,"html_url":"https://github.com/tjb1982/hoquet","commit_stats":{"total_commits":97,"total_committers":1,"mean_commits":97.0,"dds":0.0,"last_synced_commit":"7aac19ef1cd6dd45eb16b2ce09a6d7fd0c1a0d58"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/tjb1982/hoquet","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tjb1982%2Fhoquet","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tjb1982%2Fhoquet/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tjb1982%2Fhoquet/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tjb1982%2Fhoquet/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tjb1982","download_url":"https://codeload.github.com/tjb1982/hoquet/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tjb1982%2Fhoquet/sbom","scorecard":{"id":888213,"data":{"date":"2025-08-11","repo":{"name":"github.com/tjb1982/hoquet","commit":"7aac19ef1cd6dd45eb16b2ce09a6d7fd0c1a0d58"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":1.7,"checks":[{"name":"Dangerous-Workflow","score":-1,"reason":"no workflows found","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Code-Review","score":0,"reason":"Found 0/30 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Token-Permissions","score":-1,"reason":"No tokens found","details":null,"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Maintained","score":0,"reason":"0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Pinned-Dependencies","score":-1,"reason":"no dependencies found","details":null,"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"SAST","score":0,"reason":"no SAST tool detected","details":["Warn: no pull requests merged into dev branch"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Info: FSF or OSI recognized license: GNU General Public License v3.0: LICENSE:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'master'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"Vulnerabilities","score":0,"reason":"13 existing vulnerabilities detected","details":["Warn: Project is vulnerable to: GHSA-v6h2-p8h4-qcjw","Warn: Project is vulnerable to: GHSA-grv7-fg5c-xmjg","Warn: Project is vulnerable to: GHSA-3xgq-45jj-v275","Warn: Project is vulnerable to: GHSA-4q6p-r6v2-jvc5","Warn: Project is vulnerable to: GHSA-f8q6-p94x-37v3","Warn: Project is vulnerable to: GHSA-xvch-5gv4-984h","Warn: Project is vulnerable to: GHSA-qrpm-p2h7-hrv2","Warn: Project is vulnerable to: GHSA-mwcw-c2x4-8c55","Warn: Project is vulnerable to: GHSA-76c9-3jph-rj3q","Warn: Project is vulnerable to: GHSA-9wv6-86v2-598j","Warn: Project is vulnerable to: GHSA-g6ww-v8xp-vmwg","Warn: Project is vulnerable to: GHSA-c2qf-rxjj-qqgw","Warn: Project is vulnerable to: GHSA-76p7-773f-r4q5"],"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}}]},"last_synced_at":"2025-08-24T10:48:24.558Z","repository_id":401737,"created_at":"2025-08-24T10:48:24.558Z","updated_at":"2025-08-24T10:48:24.558Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29636040,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-19T22:32:43.237Z","status":"ssl_error","status_checked_at":"2026-02-19T22:32:38.330Z","response_time":117,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"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":["mixin","platform-native","vanilla-javascript","vanilla-js","web-components"],"created_at":"2025-10-22T12:31:24.202Z","updated_at":"2026-02-19T23:02:08.553Z","avatar_url":"https://github.com/tjb1982.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Hoquet\nA tiny, minimal, platform-native, vanilla JavaScript web component library.\n\nThe goal of this project is not necessarily to suggest using this particular library (although I use it in my own projects). Instead, the goal is as much to demostrate patterns you can use in your own projects using Web Components without having to use any framework/library.\n\n## `HTMLElement` mixin\nThe `Hoquet` mixin is the core of the library. It is designed to wrap any `class` that `extends` `HTMLElement`. It provides a small number of interfaces for dealing with template rendering, stylesheet construction, Shadow DOM, and attribute observation/reflection. Nothing that is provided is required. The template and stylesheets for each component are constructed only once, when the class is declared.\n\nThe following is a traditional todo app implementation using platform-native web components and the `Hoquet` mixin. It consists of a `TodoItem` component that subclasses `HTMLLIElement`, and can have three states (todo, doing, and done), and a `TodoApp` component that can hold any number of `TodoItem`s. See [`./example`](https://github.com/tjb1982/hoquet/tree/master/example) for the entire source. To run this demo with npm, run `npm run demo` in a terminal at the top level of this repository.\n\n```javascript\n// todo-item.js\nimport Hoquet from \"../mixin.js\";\nimport { template } from \"../utils.js\";\n\n\nconst states = [\"todo\", \"doing\", \"done\"];\n\nexport default class TodoItem extends Hoquet(HTMLLIElement, {\n    attributes: [\"state\", \"name\"],\n    template: template`\n        \u003cspan class=\"name\"\u003e\u003c/span\u003e\n        \u003cspan class=\"delete\"\u003ex\u003c/span\u003e\n    `,\n    /**\n     * HTMLLIElement doesn't support `attachShadow`\n     */\n    shadowy: false\n}) {\n    \n    constructor(name) {\n        super();\n        this.name = name;\n        this.state = states[0];\n    }\n\n    static states = states;\n\n    connectedCallback() {\n        this.render();\n\n        this.addEventListener(\"click\", e =\u003e {\n            if (e.target.classList.contains(\"delete\")) {\n                this.dispatch(\"item-deleted\");\n            } else {\n                this.toggleState()\n            }\n        });\n    }\n\n    dispatch(name) {\n        this.dispatchEvent(new CustomEvent(name, {\n            composed: true, bubbles: true, detail: this\n        }));\n    }\n\n    attributeChangedCallback(key, prev, curr) {\n        if (!this.rendered)\n            return;\n\n        if (key === \"state\") {\n            states.forEach(state =\u003e this.classList.remove(state));\n            this.classList.add(curr);\n            this.dispatch(\"item-state-changed\");\n        } else if (key === \"name\") {\n            const $name = this.querySelector(\".name\");\n            $name.innerText = curr;\n        }\n    }\n\n    toggleState() {\n        const currentStateIndex = states.indexOf(this.state);\n        this.state = states[\n            currentStateIndex \u003e= states.length - 1\n                ? 0 : currentStateIndex + 1\n        ];\n    }\n}\n\nwindow.customElements.define(\"todo-item\", TodoItem, {extends: \"li\"});\n```\n\n```javascript\n// todo-app.js\nimport Hoquet from \"../mixin.js\";\nimport { stylesheet, template } from \"../utils.js\";\n\nimport TodoItem from \"./todo-item.js\";\n\n\nconst capitalize = (x) =\u003e `${x[0].toUpperCase()}${x.substr(1)}`;\n\nconst styles = \"...\";\n\nclass TodoApp extends Hoquet(HTMLElement, {\n    template: template`\n        \u003cheader\u003e\n            \u003cdiv id=\"report\"\u003e\n            ${\n                TodoItem.states.map(\n                    state =\u003e (\n                        `${capitalize(state)}: \u003cspan id=\"${state}-count\"\u003e0\u003c/span\u003e`\n                    )\n                ).join(\", \")\n            }\n            \u003c/div\u003e\n            \u003cbutton id=\"clear-done\"\u003eClear done\u003c/button\u003e\n        \u003c/header\u003e\n        \u003cinput id=\"new-todo-input\" type=\"text\"\u003e\n        \u003cul id=\"list\"\u003e\u003c/ul\u003e\n    `,\n    stylesheets: [\n        stylesheet`${styles}`\n    ],\n    attributes: [\"placeholder\"]\n}) {\n\n    connectedCallback() {\n        this.render();\n        this.bind();\n        this.placeholder = this.placeholder || \"Default placeholder...\";\n    }\n\n    attributeChangedCallback(key, prev, curr) {\n        if (!this.rendered)\n            return;\n            \n        if (key === \"placeholder\") {\n            this.$[\"new-todo-input\"].placeholder = curr;\n        }\n    }\n\n    bind() {\n        this.addEventListener(\"keyup\", e =\u003e {\n            if (e.key === \"Enter\") {\n                const name = this.$[\"new-todo-input\"].value.trim();\n                if (name) {\n                    this.addItem(name);\n                }\n                this.$[\"new-todo-input\"].value = null;\n            }\n        });\n\n        this.addEventListener(\"item-state-changed\", e =\u003e {\n            this.updateReport();\n        });\n\n        this.addEventListener(\"item-deleted\", e =\u003e {\n            this.removeItem(e.detail);\n            this.updateReport();\n        });\n\n        this.$[\"clear-done\"].addEventListener(\"click\", e =\u003e {\n            this.clear(\"done\");\n        });\n    }\n\n    updateReport() {\n        TodoItem.states.forEach(state =\u003e {\n            this.$[`${state}-count`].innerText =\n                [...this.$[\"list\"].children].reduce(\n                    (count, $item) =\u003e count + ($item.state === state), 0\n                );\n        });\n    }\n\n    removeItem(item, update = false) {\n        this.$[\"list\"].removeChild(item);\n    }\n\n    addItem(item) {\n        this.$[\"list\"].appendChild(new TodoItem(item));\n    }\n\n    clear(state) {\n        [...this.$[\"list\"].children].forEach($item =\u003e {\n            if ($item.state === state) {\n                this.removeItem($item);\n            }\n        });\n        this.updateReport();\n    }\n}\n\nwindow.customElements.define(\"todo-app\", TodoApp);\n```\n\n```html\n\u003c!doctype html\u003e\n\u003chtml\u003e\n\u003chead\u003e\u003cscript type=\"module\" src=\"/example/todo-list.js\"\u003e\u003c/script\u003e\u003c/head\u003e\n\u003cbody\u003e\n        \u003ctodo-app placeholder=\"What do you want to do?\"\u003e\u003c/todo-app\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n```\n\nYou can see an example todo app using the web component mixin by running the \"demo\" script from the top level of the repo using npm/yarn.\n\n```bash\n$ npm install \u0026\u0026 npm run demo\n```\n\n\n\n## Templating DSL basic usage (optional)\n\n```javascript\nimport { render } from \"@pojagi/hoquet/hoquet\";\n\n\nconst things = [\"bread\", \"milk\", \"eggs\"];\nrender(\n  [\"ul\", {class: [\"things\", \"list\"]},\n   things.map(x =\u003e [\"li\", x])]\n);\n// \u003cul class=\"things list\"\u003e\u003cli\u003ebread\u003c/li\u003e\u003cli\u003emilk\u003c/li\u003e\u003cli\u003eeggs\u003c/li\u003e\u003c/ul\u003e\n\nrender(\n  [\"link\", {rel: \"stylesheet\", href: \"styles.css\"}]\n)\n// \u003clink rel=\"stylesheet\" href=\"styles.css\" /\u003e\n```\n\nWithin a class using `Hoquet` mixin:\n\n```javascript\nclass Foo extends Hoquet(HTMLElement) {\n    connectedCallback() {\n        this.things = [\"bread\", \"milk\", \"eggs\"];\n        this.render();\n    }\n\n    get template() {\n        return (\n            [\"div\", {class: \"container\"},\n             [\"ul\",\n              this.things.map(thing =\u003e [\"li\", thing])]]\n        );\n    }\n}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftjb1982%2Fhoquet","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftjb1982%2Fhoquet","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftjb1982%2Fhoquet/lists"}