From a92f6f76e025667b93806635a5866653c7f6efaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Fri, 26 Feb 2021 22:20:39 +0100 Subject: [PATCH] Add real world example to docs --- README.md | 201 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 201 insertions(+) diff --git a/README.md b/README.md index 2600806..58434d5 100644 --- a/README.md +++ b/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). + +
+resources/js/app.ts + +```ts +declare global { + interface Window { + Alpine: any; + } +} + +import { component } from '@leanadmin/alpine-typescript'; +import Search from './search'; + +component('search', Search); + +import 'alpinejs'; +``` + +
+ +**`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 + +
+resources/js/search.js + +```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()); + } +} +``` + +
+ +**`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. + +
+page.blade.php + +```html +
+
+ + + +
+ +
+ +
+
+``` + +
+ +**`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