{"id":13595111,"url":"https://github.com/archtechx/alpine-typescript","last_synced_at":"2025-06-19T08:35:31.947Z","repository":{"id":42471861,"uuid":"342588288","full_name":"archtechx/alpine-typescript","owner":"archtechx","description":"TypeScript support for Alpine.js","archived":false,"fork":false,"pushed_at":"2022-10-13T09:15:43.000Z","size":44,"stargazers_count":53,"open_issues_count":4,"forks_count":3,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-18T14:06:02.966Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/archtechx.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null},"funding":{"github":"stancl"}},"created_at":"2021-02-26T13:44:03.000Z","updated_at":"2025-04-14T18:19:34.000Z","dependencies_parsed_at":"2022-09-14T00:42:05.349Z","dependency_job_id":null,"html_url":"https://github.com/archtechx/alpine-typescript","commit_stats":null,"previous_names":["leanadmin/alpine-typescript"],"tags_count":25,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/archtechx%2Falpine-typescript","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/archtechx%2Falpine-typescript/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/archtechx%2Falpine-typescript/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/archtechx%2Falpine-typescript/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/archtechx","download_url":"https://codeload.github.com/archtechx/alpine-typescript/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":252395321,"owners_count":21741015,"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":[],"created_at":"2024-08-01T16:01:44.175Z","updated_at":"2025-05-04T20:31:28.838Z","avatar_url":"https://github.com/archtechx.png","language":"TypeScript","funding_links":["https://github.com/sponsors/stancl"],"categories":["TypeScript"],"sub_categories":[],"readme":"# TypeScript support for Alpine.js v2\n\nThis package provides full support for class components in Alpine.js using a thin TypeScript layer.\n\nIt's used like this:\n\n**Register a component**\n```ts\nimport DarkModeToggle from './darkModeToggle';\n\nAlpine.component('darkModeToggle', DarkModeToggle);\n```\n\n**Use it in a template**\n```html\n\u003cdiv x-data=\"Alpine.component('darkModeToggle')()\" x-init=\"init()\"\u003e\n    \u003cbutton type=\"button\" @click=\"switchTheme()\"\u003eSwitch theme\u003c/button\u003e\n\u003c/div\u003e\n```\n\n## Installation\n\n```\nnpm install --save-dev @leanadmin/alpine-typescript\n```\n\nThe package will automatically initialize itself when needed, i.e. when one of its components is used in the currently executed JS bundle.\n\nIf you'd like to initialize it manually, you can use:\n\n```ts\nimport { bootstrap } from '@leanadmin/alpine-typescript';\n\nbootstrap();\n```\n\nAdditionally, you can use the `addTitles()` function to add `x-title` attributes to all components. The class name of each component will be used as its title. This is useful if you're using tools like the [Alpine.js devtools](https://github.com/alpine-collective/alpinejs-devtools).\n\n```ts\nimport { addTitles } from '@leanadmin/alpine-typescript';\n\naddTitles();\n```\n\n## Usage\n\nYou can use a component by calling `Alpine.component('componentName')(arg1, arg2)`. If your component has no arguments, still append `()` at the end of the call.\n\nThe `component()` call itself returns a function for creating instances of the component. Invoking the function ensures that the component has a unique instance each time.\n\n```html\n\u003cdiv x-data=\"Alpine.component('darkModeToggle')()\" x-init=\"init()\"\u003e\n    \u003cbutton type=\"button\" @click=\"switchTheme()\"\u003eSwitch theme\u003c/button\u003e\n\u003c/div\u003e\n```\n\n```html\n\u003cdiv x-data=\"Alpine.component('searchableSelect')({ options: ['Foo', 'Bar'] })\" x-init=\"init()\"\u003e\n    \u003cdiv x-spread=\"options\"\u003e\n        ...\n    \u003c/div\u003e\n\u003c/div\u003e\n```\n\n## Creating components\n\nTo create a component, you need to create the component object and register it using one of the provided helpers.\n\nComponent objects can be:\n- classes\n- functions returning plain objects\n\nIn the context of plain objects, the wrapper function acts as a constructor that can pass initial data to the object.\n\n## Registering components\n\nA component can be registered like this:\n```ts\nimport ExampleComponent from './ExampleComponent';\nimport { component } from '@leanadmin/alpine-typescript';\n\ncomponent('example', ExampleComponent);\n```\n\nWhich will make it accessible using `Alpine.component('example')('foo', 'bar')`.\n\n**Note: You may notice that `Alpine.component()` can also be used to register components. However, it's better to avoid using it.** The reason for this is that `window.Alpine` might not yet be accessible when you're registering components, and if it is, it's possible that it's already evaluated some of the `x-data` attributes. `component()` is guaranteed to work. And of course, you can alias the import if you wish to use a different name.\n\nTo register multiple components, you can use the `registerComponents()` helper.\n\nThis can pair well with scripts that crawl your e.g. `alpine/` directory to register all components using their file names.\n\n```ts\n// alpine/index.js\n\nimport { registerComponents } from '@leanadmin/alpine-typescript';\n\nconst files = require.context('./', true, /.*.ts/)\n    .keys()\n    .map(file =\u003e file.substr(2, file.length - 5)) // Remove ./ and .ts\n    .filter(file =\u003e file !== 'index')\n    .reduce((files: { [name: string]: Function }, file: string) =\u003e {\n        files[file] = require(`./${file}.ts`).default;\n\n        return files;\n}, {});\n\nregisterComponents(files);\n```\n\n## Class components\n\nYou can create class components by extending `AlpineComponent` and exporting the class as `default`.\n\n`AlpineComponent` provides full IDE support for Alpine's magic properties. This means that you can use `this.$el`, `this.$nextTick(() =\u003e this.foo = this.bar)`, and more with perfect type enforcement.\n\n```ts\nimport { AlpineComponent } from '@leanadmin/alpine-typescript';\n\nexport default class DarkModeToggle extends AlpineComponent {\n    public theme: string|null = null;\n\n    /** Used for determining the transition direction. */\n    public previousTheme: string|null = null;\n\n    public browserTheme(): string {\n        return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';\n    }\n\n    public switchTheme(theme: string): void {\n        this.$nextTick(() =\u003e this.previousTheme = this.theme);\n\n        this.theme = theme;\n\n        window.localStorage.setItem('leanTheme', theme);\n\n        this.updateDocumentClass(theme);\n    }\n\n    // ...\n\n    public init(): void {\n        this.loadStoredTheme();\n        this.registerListener();\n    }\n}\n```\n\n## Plain object components\n\nTo register a plain object as an Alpine component, export a function that wraps the object like this:\n```ts\nexport default (foo: string, bar: number) =\u003e ({\n    foo,\n    bar,\n\n    someFunction() {\n        console.log(this.foo);\n    }\n})\n```\n\nThe function will serve as a \"constructor\" for the object, setting default values and anything else that's needed.\n\nNote that the `=\u003e ({` part is just syntactic sugar, you're free to use `return` if it's useful in your case:\n\n```ts\nexport default (foo: string, bar: number) =\u003e {\n    return {\n        foo,\n        bar,\n\n        // ...\n    }\n}\n```\n\n# Real-world example\n\nHere's a practical example that uses constructors, `init()`, refs, events, and includes dependencies the right way.\n\nThis example uses the Alpine component that we use for search on the [Lean documentation site](https://lean-admin.dev).\n\n\u003cdetails\u003e\n\u003csummary\u003eresources/js/app.ts\u003c/summary\u003e\n\n```ts\nimport { Alpine, component } from '@leanadmin/alpine-typescript';\n\ndeclare global {\n    interface Window {\n        Alpine: Alpine;\n    }\n}\n\nimport Search from './search';\n\ncomponent('search', Search);\n\nimport 'alpinejs';\n```\n\n\u003c/details\u003e\n\n**`app.ts` highlights:**\n- It's a good idea to declare the `Alpine` property on `Window` in case you need to use `window.Alpine`. The library provides an interface for this.\n- We initialize each component by calling `component()`\n- We import Alpine *after* this package\n\n\u003cdetails\u003e\n\u003csummary\u003eresources/js/search.ts\u003c/summary\u003e\n\n```ts\nimport { AlpineComponent } from '@leanadmin/alpine-typescript';\n\ntype AlgoliaIndex = {\n    search: Function,\n};\n\ntype Result = any;\n\nexport default class Search extends AlpineComponent {\n    search: string = '';\n    results: Result[] = [];\n\n    constructor(\n        public index: AlgoliaIndex,\n    ) {\n        super();\n    }\n\n    previousResult(): void {\n        let result = this.currentResult();\n\n        if (! result) {\n            if (this.results.length) {\n                // First result\n                this.getResult(0).focus();\n            } else if (this.search.length) {\n                // Re-fetch results\n                this.queryAlgolia();\n            }\n\n            return;\n        }\n\n        if (result.previousElementSibling instanceof HTMLElement \u0026\u0026 result.previousElementSibling.tagName === 'A') {\n            (result.previousElementSibling).focus();\n        } else {\n            // Last result\n            this.getResult(this.results.length - 1).focus();\n        }\n    };\n\n    nextResult(): void {\n        let result = this.currentResult();\n\n        if (! result) {\n            if (this.results.length) {\n                // First result\n                this.getResult(0).focus();\n            } else if (this.search.length) {\n                // Re-fetch results\n                this.queryAlgolia();\n            }\n\n            return;\n        }\n\n        if (result.nextElementSibling instanceof HTMLElement) {\n            result.nextElementSibling.focus();\n        } else {\n            // First result\n            this.getResult(0).focus();\n        }\n    };\n\n    getResult(index: number): HTMLElement {\n        return this.$refs.results.children[index + 1] as HTMLElement;\n    };\n\n    currentResult(): HTMLElement|null {\n        if (! this.$refs.results.contains(document.activeElement)) {\n            return null;\n        }\n\n        return document.activeElement as HTMLElement;\n    };\n\n    queryAlgolia(): void {\n        if (this.search) {\n            this.index.search(this.search, {\n                hitsPerPage: 3,\n            }).then(({ hits }) =\u003e {\n                this.results = hits.filter((hit: Result) =\u003e {\n                    // Remove duplicate results\n                    const occurances: any[] = hits.filter((h: Result) =\u003e h.hierarchy.lvl1 === hit.hierarchy.lvl1);\n\n                    return occurances.length === 1;\n                });\n\n                this.results.forEach((result: Result) =\u003e {\n                    // Clean displayed text\n                    if (result._highlightResult \u0026\u0026 result._highlightResult.content) {\n                        return result._highlightResult.content.value.replace(' ', '');\n                    }\n                });\n\n                if (this.results.length) {\n                    this.$nextTick(() =\u003e this.getResult(0).focus());\n                }\n            })\n        } else {\n            this.results = [];\n\n            this.$refs.search.focus();\n        }\n    };\n\n    init(): void {\n        this.$watch('search', () =\u003e this.queryAlgolia());\n    }\n}\n```\n\n\u003c/details\u003e\n\n**`search.ts` highlights:**\n- We `export default` the class\n- We have to call `super()` if we define a constructor\n- Sometimes, we have to use `as HTMLElement` because the DOM API can return `Element` which doesn't have methods like `focus()`\n- We define an `init()` method and can access magic properties there\n- We create helper types for consistency among parameters and return types, even if the type is `any` because we don't know much about the structure. Especially useful for API calls.\n\n\u003cdetails\u003e\n\u003csummary\u003epage.blade.php\u003c/summary\u003e\n\n```html\n\u003cdiv\n    class=\"relative w-full text-gray-400 focus-within:text-gray-600\"\n    x-data=\"Alpine.component('search')(\n        algoliasearch('\u003ctruncated key\u003e', '\u003ctruncated key\u003e').initIndex('lean-admin')\n    )\"\n    x-init=\"init\"\n    @click.away=\"results = []\"\n    @keydown.arrow-up.prevent=\"previousResult()\"\n    @keydown.arrow-down.prevent=\"nextResult()\"\n    @keydown=\"if (document.activeElement !== $refs.search \u0026\u0026 ! ['ArrowUp', 'ArrowDown', 'Enter', 'Tab' ].includes($event.key)) $refs.search.focus()\"\n\u003e\n    \u003cdiv class=\"pointer-events-none absolute inset-y-0 left-0 flex items-center\"\u003e\n        \u003csvg class=\"h-5 w-5\" fill=\"currentColor\" viewBox=\"0 0 20 20\"\u003e\n            \u003cpath fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z\"\u003e\u003c/path\u003e\n        \u003c/svg\u003e\n    \u003c/div\u003e\n    \u003cinput @keydown.s.away=\"\n        if (['s', '/'].includes($event.key)) {\n            $refs.search.focus();\n\n            if (! $refs.results.contains($event.target)) {\n                // Don't type the 's' or '/' unless it was within the search results.\n                $event.preventDefault();\n            }\n        }\n    \" x-ref=\"search\" x-model.debounce=\"search\" id=\"search\" class=\"block h-full w-full rounded-md py-2 pl-8 pr-3 text-gray-900 placeholder-gray-500 focus:outline-none focus:placeholder-gray-400 sm:text-sm\" placeholder=\"Search\" type=\"search\"\u003e\n    \u003cdiv id=\"search-results\" x-ref=\"results\" x-show=\"results.length\" class=\"max-w-full relative z-20 -mt-2 shadow-outline-purple bg-white\"\u003e\n        \u003ctemplate x-if=\"results\" x-for=\"result in results\"\u003e\n            ...\n        \u003c/template\u003e\n    \u003c/div\u003e\n\u003c/div\u003e\n```\n\n\u003c/details\u003e\n\n**`page.blade.php` highlights:**\n- We call the component in `x-data`\n- We use both the constructor and `init`. The constructor cannot access magic properties, `init` can.\n- We can still use Alpine syntax in the template with no issues\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Farchtechx%2Falpine-typescript","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Farchtechx%2Falpine-typescript","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Farchtechx%2Falpine-typescript/lists"}