1
0
Fork 0
mirror of https://github.com/archtechx/alpine-typescript.git synced 2025-12-11 22:34:03 +00:00

initial commit

This commit is contained in:
Samuel Štancl 2021-02-26 14:43:39 +01:00
commit 1260642e07
5 changed files with 298 additions and 0 deletions

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Samuel Štancl
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

160
README.md Normal file
View file

@ -0,0 +1,160 @@
# TypeScript support for Alpine.js
This package comes with a light TypeScript layer which provides full support for class components in Alpine.js.
It's used like this:
**Register a component**
```ts
import { DarkModeToggle }
Alpine.component('DarkModeToggle', DarkModeToggle);
```
**Use it the template**
```html
<div x-data="Alpine.component('DarkModeToggle')()" x-init="init()">
<button type="button" @click="switchTheme()">Switch theme</button>
</div>
```
## Installation
```
npm install --save-dev github:leanadmin/alpine-typescript
```
```ts
// todo
```
## Usage
You can get a component by calling `Alpine.component('component-name')(arg1, arg2)`. If your component has no arguments, still append the `()` after the call.
The `component()` call itself returns a function that creates an instance of the component. Invoking the function ensures that the component has a unique instance each time.
```html
<div x-data="Alpine.component('DarkModeToggle')()" x-init="init()">
<button type="button" @click="switchTheme()">Switch theme</button>
</div>
```
```html
<div x-data="Alpine.component('SearchableSelect')({ options: ['Foo', 'Bar'] })" x-init="init()">
<div x-spread="options">
...
</div>
</div>
```
## Creating components
To create a component, you need to create the component object and register it using one of the provided helpers.
Component objects can be:
- functions returning plain objects
- classes
In the context of plain objects, the wrapper function acts as a constructor that can pass initial data to the object.
## Registering components
A component can be registered like this:
```ts
import { ExampleComponent } from './ExampleComponent';
import { component } from '@leanadmin/alpine-typescript';
component('example', ExampleComponent);
```
Which will make it accessible using `Alpine.component('example')('foo', 'bar)`.
**Note: It's better to avoid using `Alpine.component('example', ExampleComponent)`** even if it might work in some cases. 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.
To register multiple components, you can use the `registerComponents()` helper.
This can pair well with scripts that crawl your e.g. `alpine/` directory to register all components using their file names.
```ts
import { registerComponents } from '@leanadmin/alpine-typescript';
const files = require.context('./', true, /.*.ts/)
.keys()
.map(file => file.substr(2, file.length - 5)) // Remove ./ and .ts
.filter(file => file !== 'index')
.reduce((files: { [name: string]: Function }, file: string) => {
files[file] = require(`./${file}.ts`).default;
return files;
}, {});
registerComponents(files);
```
## Class components
You can create class components by extending `AlpineComponent` and exporting the class as `default`.
The `AlpineComponent` provides IDE support for Alpine's magic properties. This means that you can use `this.$el`, `this.$nextTick(() => this.foo = this.bar)`, and more with full type support.
```ts
import { AlpineComponent } from '@leanadmin/alpine-typescript';
export default class DarkModeToggle extends AlpineComponent {
public theme: string|null = null;
/** Used for determining the transition direction. */
public previousTheme: string|null = null;
public browserTheme(): string {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
public switchTheme(theme: string): void {
this.$nextTick(() => this.previousTheme = this.theme);
this.theme = theme;
window.localStorage.setItem('leanTheme', theme);
this.updateDocumentClass(theme);
}
// ...
public init(): void {
this.loadStoredTheme();
this.registerListener();
}
}
```
## Plain object components
To register a plain object as an Alpine component, return a function that wraps the object like this:
```ts
export default (foo: string, bar: number) => ({
foo,
bar,
someFunction() {
console.log(this.foo);
}
})
```
The function will serve as a "constructor" for the object, setting default values and anything else you might need.
Note that the `=> ({` part is just syntactic sugar, you're free to use `return` if it's useful in your case:
```ts
export default (foo: string, bar: number) => {
return {
foo,
bar,
// ...
}
}
```

28
package.json Normal file
View file

@ -0,0 +1,28 @@
{
"name": "@leanadmin/alpine-typescript",
"version": "0.1.0",
"description": "TypeScript support for Alpine.js",
"main": "src/index.js",
"repository": {
"type": "git",
"url": "git+https://github.com/LeanAdmin/alpine-typescript.git"
},
"files": [
"src/index.ts"
],
"scripts": {
"build": "npx mix --production",
"prepublishOnly": "npm run-script build"
},
"keywords": [
"alpine",
"alpine.js",
"typescript"
],
"author": "Samuel Štancl <samuel.stancl@gmail.com>",
"license": "MIT",
"bugs": {
"url": "https://github.com/LeanAdmin/alpine-typescript/issues"
},
"homepage": "https://github.com/LeanAdmin/alpine-typescript#readme"
}

81
src/index.ts Normal file
View file

@ -0,0 +1,81 @@
declare global {
interface Window {
AlpineComponents: { [name: string]: ComponentConstructor };
Alpine: any;
deferLoadingAlpine: Function;
}
}
type ComponentConstructor = (...args: any[]) => object;
export class AlpineComponent {
/** Retrieve the root component DOM node. */
$el?: Element;
/** Retrieve DOM elements marked with x-ref inside the component. */
$refs?: { [name: string]: Element };
/** Retrieve the native browser "Event" object within an event listener. */
$event?: Event;
/** Create a CustomEvent and dispatch it using .dispatchEvent() internally. */
$dispatch?: (event: string, data: object) => void;
/** Execute a given expression AFTER Alpine has made its reactive DOM updates. */
$nextTick?: (callback: () => void) => void;
/** Will fire a provided callback when a component property you "watched" gets changed. */
$watch?: (property: string, callback: (value: any) => void) => void;
}
export function registerComponents(components: { [name: string]: Function }): { [name: string]: ComponentConstructor } {
Object.entries(components).forEach(([name, file]) => {
component(name, file);
});
return window.AlpineComponents;
}
export function component(name: string, component: Function = null): ComponentConstructor {
if (! component) {
return window.AlpineComponents[name];
}
if (component['prototype'] instanceof AlpineComponent) {
component = convertClassToAlpineConstructor(component);
}
// @ts-ignore
window.AlpineComponents[name] = component;
}
export function convertClassToAlpineConstructor(component: any): ComponentConstructor {
return function (...args: any[]) {
let instance: AlpineComponent = new component(...args);
// Copy methods
const methods = Object.getOwnPropertyNames(
Object.getPrototypeOf(instance)
)
.reduce((obj, method) => {
obj[method] = instance[method];
return obj;
}, {});
// Copy properties
return Object.assign(methods, instance);
}
}
export default () => {
window.AlpineComponents = {};
const deferrer = window.deferLoadingAlpine || function (callback: CallableFunction) { callback() };
window.deferLoadingAlpine = function (callback: Function) {
window.Alpine.component = component;
deferrer(callback);
}
}

8
tsconfig.json Normal file
View file

@ -0,0 +1,8 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es6",
"lib": ["ES2017", "DOM"]
},
"include": ["src/**/*"]
}