mirror of
https://github.com/archtechx/alpine-typescript.git
synced 2025-12-11 22:34:03 +00:00
Add real world example to docs
This commit is contained in:
parent
91791366bd
commit
a92f6f76e0
1 changed files with 201 additions and 0 deletions
201
README.md
201
README.md
|
|
@ -166,3 +166,204 @@ export default (foo: string, bar: number) => {
|
|||
}
|
||||
}
|
||||
```
|
||||
|
||||
# Real-world example
|
||||
|
||||
Here's a practical example that uses constructors, `init()`, refs, events, and includes dependencies in the right way.
|
||||
|
||||
This example uses the Alpine component that we use for search on the [Lean documentation site](https://lean-admin.dev).
|
||||
|
||||
<details>
|
||||
<summary>resources/js/app.ts</summary>
|
||||
|
||||
```ts
|
||||
declare global {
|
||||
interface Window {
|
||||
Alpine: any;
|
||||
}
|
||||
}
|
||||
|
||||
import { component } from '@leanadmin/alpine-typescript';
|
||||
import Search from './search';
|
||||
|
||||
component('search', Search);
|
||||
|
||||
import 'alpinejs';
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
**`app.ts` highlights:**
|
||||
- It's a good idea to declare the `Alpine` property on `Window` in case you need to use `window.Alpine`
|
||||
- We initialize each component by calling `component()`
|
||||
- We import Alpine *after* this package
|
||||
|
||||
<details>
|
||||
<summary>resources/js/search.js</summary>
|
||||
|
||||
```ts
|
||||
import { AlpineComponent } from '@leanadmin/alpine-typescript';
|
||||
|
||||
type AlgoliaIndex = {
|
||||
search: Function,
|
||||
};
|
||||
|
||||
type Result = any;
|
||||
|
||||
export default class Search extends AlpineComponent {
|
||||
search: string = '';
|
||||
results: Result[] = [];
|
||||
|
||||
constructor(
|
||||
public index: AlgoliaIndex,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
previousResult(): void {
|
||||
let result = this.currentResult();
|
||||
|
||||
if (! result) {
|
||||
if (this.results.length) {
|
||||
// First result
|
||||
this.getResult(0).focus();
|
||||
} else if (this.search.length) {
|
||||
// Re-fetch results
|
||||
this.queryAlgolia();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.previousElementSibling instanceof HTMLElement && result.previousElementSibling.tagName === 'A') {
|
||||
(result.previousElementSibling).focus();
|
||||
} else {
|
||||
// Last result
|
||||
this.getResult(this.results.length - 1).focus();
|
||||
}
|
||||
};
|
||||
|
||||
nextResult(): void {
|
||||
let result = this.currentResult();
|
||||
|
||||
if (! result) {
|
||||
if (this.results.length) {
|
||||
// First result
|
||||
this.getResult(0).focus();
|
||||
} else if (this.search.length) {
|
||||
// Re-fetch results
|
||||
this.queryAlgolia();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.nextElementSibling instanceof HTMLElement) {
|
||||
result.nextElementSibling.focus();
|
||||
} else {
|
||||
// First result
|
||||
this.getResult(0).focus();
|
||||
}
|
||||
};
|
||||
|
||||
getResult(index: number): HTMLElement {
|
||||
return this.$refs.results.children[index + 1] as HTMLElement;
|
||||
};
|
||||
|
||||
currentResult(): HTMLElement {
|
||||
if (! this.$refs.results.contains(document.activeElement)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return document.activeElement as HTMLElement;
|
||||
};
|
||||
|
||||
queryAlgolia(): void {
|
||||
if (this.search) {
|
||||
this.index.search(this.search, {
|
||||
hitsPerPage: 3,
|
||||
}).then(({ hits }) => {
|
||||
this.results = hits.filter((hit: Result) => {
|
||||
// Remove duplicate results
|
||||
const occurances: any[] = hits.filter((h: Result) => h.hierarchy.lvl1 === hit.hierarchy.lvl1);
|
||||
|
||||
return occurances.length === 1;
|
||||
});
|
||||
|
||||
this.results.forEach((result: Result) => {
|
||||
// Clean displayed text
|
||||
if (result._highlightResult && result._highlightResult.content) {
|
||||
return result._highlightResult.content.value.replace(' ', '');
|
||||
}
|
||||
});
|
||||
|
||||
if (this.results.length) {
|
||||
this.$nextTick(() => this.getResult(0).focus());
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this.results = [];
|
||||
|
||||
this.$refs.search.focus();
|
||||
}
|
||||
};
|
||||
|
||||
init(): void {
|
||||
this.$watch('search', () => this.queryAlgolia());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
**`search.js` highlights:**
|
||||
- We `export default` the class
|
||||
- We have to call `super()` if we define a constructor
|
||||
- Sometimes, we have to use `as HTMLElement` because the DOM API can return `Element` which doesn't have methods like `focus()`
|
||||
- We define an `init()` method and can access magic properties there
|
||||
- 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.
|
||||
|
||||
<details>
|
||||
<summary>page.blade.php</summary>
|
||||
|
||||
```html
|
||||
<div
|
||||
class="relative w-full text-gray-400 focus-within:text-gray-600"
|
||||
x-data="Alpine.component('search')(
|
||||
algoliasearch('<truncated key>', '<truncated key>').initIndex('lean-admin')
|
||||
)"
|
||||
x-init="init"
|
||||
@click.away="results = []"
|
||||
@keydown.arrow-up.prevent="previousResult()"
|
||||
@keydown.arrow-down.prevent="nextResult()"
|
||||
@keydown="if (document.activeElement !== $refs.search && ! ['ArrowUp', 'ArrowDown', 'Enter', 'Tab' ].includes($event.key)) $refs.search.focus()"
|
||||
>
|
||||
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center">
|
||||
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path 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"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<input @keydown.s.away="
|
||||
if (['s', '/'].includes($event.key)) {
|
||||
$refs.search.focus();
|
||||
|
||||
if (! $refs.results.contains($event.target)) {
|
||||
// Don't type the 's' or '/' unless it was within the search results.
|
||||
$event.preventDefault();
|
||||
}
|
||||
}
|
||||
" 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">
|
||||
<div id="search-results" x-ref="results" x-show="results.length" class="max-w-full relative z-20 -mt-2 shadow-outline-purple bg-white">
|
||||
<template x-if="results" x-for="result in results">
|
||||
...
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
**`page.blade.php` highlights:**
|
||||
- We call the component in `x-data`
|
||||
- We use both the constructor and `init`. The constructor cannot access magic properties, `init` can.
|
||||
- We can still use Alpine syntax in the template with no issues
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue