如何使用 Alpine.js 创建可重用组件并显示它?例如,也许我想定义一个通用的 Alpine.js 按钮组件来更改参数中的文本和颜色,然后让我的 Alpine.js 导航栏组件使用该按钮组件来显示登录按钮。
我可以在纯客户端代码中执行此操作,而不依赖于在使用按钮组件的所有地方模板化所有按钮 HTML 的服务器吗?
我可以在纯客户端代码中执行此操作,而不依赖服务器模板吗?
是的,可以。
Alpine.js 总是会尝试说服您使用服务器端模板引擎。
但就像你一样,我不会让自己被说服:
<template x-component="dropdown">
<div x-data="{ ...dropdown(), ...$el.parentElement.data() }">
<button x-on:click="open">Open</button>
<div x-show="isOpen()" x-on:click.away="close" x-text="content"></div>
</div>
</template>
<x-dropdown content="Content for my first dropdown"></x-dropdown>
<div> Random stuff... </div>
<x-dropdown content="Content for my second dropdown"></x-dropdown>
<x-dropdown></x-dropdown>
<script>
function dropdown() {
return {
show: false,
open() { this.show = true },
close() { this.show = false },
isOpen() { return this.show === true },
content: 'Default content'
}
}
// The pure client-side code
document.querySelectorAll('[x-component]').forEach(component => {
const componentName = `x-${component.getAttribute('x-component')}`
class Component extends HTMLElement {
connectedCallback() {
this.append(component.content.cloneNode(true))
}
data() {
const attributes = this.getAttributeNames()
const data = {}
attributes.forEach(attribute => {
data[attribute] = this.getAttribute(attribute)
})
return data
}
}
customElements.define(componentName, Component)
})
</script>
使用
alpinejs-component
cdn 的同一页面:
<div
x-data="{
people: [
{ name: 'John', age: '25', skills: ['JavaScript', 'CSS'] },
{ name: 'Jane', age: '30', skills: ['Laravel', 'MySQL', 'jQuery'] }
]
}"
>
<ul>
<template x-for="person in people">
<!-- use the person template to find the <template id="person"> element. -->
<x-component-wrapper x-component template="person" x-data="{ item: person }"></x-component-wrapper>
</template>
</ul>
</div>
<template id="person">
<li class="user-card">
<h2 x-text="item.name"></h2>
<p x-text="item.age"></p>
<ul>
<template x-for="skill in item.skills">
<li x-text="skill"></li>
</template>
</ul>
</li>
</template>
<script src="https://unpkg.com/[email protected]/dist/component.min.js"></script>
<script defer src="https://unpkg.com/[email protected]/dist/cdn.min.js"></script>
使用url导入html模板:
<div
x-data="{
people: [
{ name: 'John', age: '25', skills: ['JavaScript', 'CSS'] },
{ name: 'Jane', age: '30', skills: ['Laravel', 'MySQL', 'jQuery'] }
]
}"
>
<ul>
<template x-for="person in people">
<x-component-wrapper x-component url="/public/person.html" x-data="{ item: person }"></x-component-wrapper>
</template>
</ul>
</div>
<script src="https://unpkg.com/[email protected]/dist/component.min.js"></script>
<script defer src="https://unpkg.com/[email protected]/dist/cdn.min.js"></script>
person.html
:
<li class="user-card">
<h2 x-text="item.name"></h2>
<p x-text="item.age"></p>
<ul>
<template x-for="skill in item.skills">
<li x-text="skill"></li>
</template>
</ul>
</li>
通过 npm 安装:
npm i -D alpinejs-component
yarn add -D alpinejs-component
注册插件:
import Alpine from "alpinejs";
import component from "alpinejs-component";
Alpine.plugin(component);
window.Alpine = Alpine;
Alpine.start();
或在浏览器中使用模块:
<x-component-wrapper x-component template="dropdown" x-data="dropdown"></x-component-wrapper>
<x-component-wrapper x-component template="dropdown" x-data="dropdown"></x-component-wrapper>
<template id="dropdown">
<div @click="close" class="dropdown-toggle">
<button x-on:click="open">Open</button>
<div x-show="show" x-text="content"></div>
</div>
</template>
<script type="module">
import { default as Alpine } from 'https://cdn.skypack.dev/alpinejs'
import alpinejsComponent from 'https://cdn.skypack.dev/alpinejs-component'
function dropdown() {
return {
show: false,
open() {
console.log('open')
this.show = true
console.log(this.show)
},
close(event) {
const button = this.$el.querySelector('button')
const target = event.target
if (this.$el.contains(target) && !button.contains(target)) {
this.show = false
}
},
get isOpen() {
return this.show === true
},
content: 'Default content',
init() {
console.log(this.$el.parentElement)
console.log('dropdown --- init')
},
}
}
Alpine.data('dropdown', dropdown)
Alpine.plugin(alpinejsComponent)
Alpine.start()
</script>
工作顺利。
您可以使用
Alpine.data
以及使用 x-bind
封装指令的记录方法来完成此操作。诀窍是绑定 x-html
指令。在您的 HTML 中执行以下操作:
<div x-data="dropdown" x-bind="bind"></div>
在你的Javascript中:
document.addEventListener('alpine:init', () => {
Alpine.data('dropdown', () => ({
show: false,
bind: {
['x-html']() { return `
<button @click="show = !show">Click me!</button>
<div x-show="show">Hello World</div>
`},
},
}));
})
JSF 在这里。
这有点老套,因为您将所有嵌套内容封装在绑定在x-html
指令中的多行 HTML 字符串中(尽管也许并不比到处克隆模板的替代方案更老套)。确保内容中没有使用反引号字符。尽管如此,内容可以根据需要嵌套任意深度,并且可以包含 Alpine.js 指令。您可以通过声明参数并将参数传递到
Alpine.data
来初始化组件。您还可以绑定
x-modelable
以将组件的任何属性公开为输出。如果您更喜欢使用模板,也许是因为当标记未嵌入字符串时您的编辑器可以更好地进行语法突出显示,您可以将此方法与模板结合起来。这是一个演示
x-modelable
以及模板使用的示例。实际上,Alpine 会为您克隆模板。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script defer src="https://unpkg.com/[email protected]/dist/cdn.js"></script>
</head>
<body>
<div x-data="{clicked: false}">
<div>Clicked is <span x-text="clicked"></span></div>
<div x-data="dropdown" x-bind="bind" x-model="clicked"></div>
</div>
<template id="dropdown">
<button @click="show = !show">Click me!</button>
<div x-show="show">Hello World</div>
</template>
</body>
<script type="text/javascript">
document.addEventListener('alpine:init', () => {
Alpine.data('dropdown', () => ({
show: false,
bind: {
['x-modelable']: 'show',
['x-html']() { return document.querySelector('#dropdown').innerHTML},
},
}));
})
</script>
</html>
JSF 在这里。
https://github.com/vimeshjs/vimesh-ui) 是一个更完整的可重用组件实现:
<head>
<script src="https://unpkg.com/@vimesh/style" defer></script>
<script src="https://unpkg.com/@vimesh/ui"></script>
<script src="https://unpkg.com/alpinejs" defer></script>
</head>
<body x-cloak class="p-2" x-data="{name: 'Counter to rename', winner: 'Jacky'}">
Rename the 2nd counter : <input type="text" x-model="name" class="rounded-md border-2 border-blue-500">
<vui-counter x-data="{step: 1}" :primary="true" title="First" x-init="console.log('This is the first one')" owner-name="Tom"></vui-counter>
<vui-counter x-data="{step: 5}" :title="name + ' @ ' + $prop('owner-name')" owner-name="Frank"></vui-counter>
<vui-counter x-data="{step: 10, value: 1000}" :owner-name="winner">
<vui-counter-trigger></vui-counter-trigger>
</vui-counter>
<template x-component.unwrap="counter" :class="$prop('primary') ? 'text-red-500' : 'text-blue-500'"
x-data="{ step : 1, value: 0}" x-init="$api.init && $api.init()" title="Counter" owner-name="nobody">
<div>
<span x-text="$prop('title')"></span><br>
Owner: <span x-text="$prop('owner-name')"></span><br>
Step: <span x-text="step"></span><br>
Value : <span x-text="value"></span><br>
<button @click="$api.increase()"
class="inline-block rounded-lg bg-indigo-600 px-4 py-1.5 text-white shadow ring-1 ring-indigo-600 hover:bg-indigo-700 hover:ring-indigo-700">
Increase
</button>
<slot></slot>
</div>
<script>
return {
init() {
console.log(`Value : ${this.value} , Step : ${this.step}`)
},
increase() {
this.value += this.step
}
}
</script>
</template>
<template x-component="counter-trigger">
<button @click="$api.of('counter').increase()"
class="inline-block rounded-lg mt-2 bg-green-600 px-4 py-1.5 text-white shadow ring-1 ring-green-600 hover:bg-green-700 hover:ring-green-700">
Tigger from child element</button>
</template>
</body>
代码笔链接:
https://codepen.io/jatinnchhatbar/pen/mdoEJLp
<!-- Reusable autocomplete component start -->
<template id="autocomplete">
<style>
.highlighted {
background-color: lightgray;
}
</style>
<div class="relative z-10">
<input type="search" x-model="query" @input.debounce.150="search" @blur.debounce.300="handleBlur"
@focus="handleFocus" @keydown.enter.prevent="selectItem(items[highlightedIndex])"
@keydown.down.prevent="highlightItem('next')" @keydown.up.prevent="highlightItem('prev')"
:placeholder="placeholder" />
<ul x-show="showItem"
class="absolute list-none w-full z-20 bg-white text-slate-700 hover:highlighted shadow-sm max-h-60 overflow-y-auto">
<template x-for="(item, index) in items" :key="index">
<li class="cursor-pointer" :id="`item_${index}`" @mousedown="selectItem(item)"
:class="{ 'highlighted': index === highlightedIndex }" x-text="item[textprop]">
</li>
</template>
</ul>
</div>
</template>
<!-- Script for Reusable autocomplete component -->
<script>
document.addEventListener("alpine:init", () => {
Alpine.data("autocomplete", (data) => ({
showItem: false,
query: "",
id: "",
placeholder: "Search...",
items: [{ id: 'test', text: 'test' }],
idprop: 'id',
textprop: 'text',
watch: "",
onchange: "",
url: "",
highlightedIndex: -1,
...data,
bind: {
['x-modelable']: 'id',
['x-html']() {
return this.$el.innerHTML + document.querySelector('#autocomplete').innerHTML
}
},
init() {
if (this.watch) {
let watches = this.watch.split(',');
for (let watch in watches) {
let keyval = this.watch.split(':');;
this.$watch(keyval[1], (value) => {
this[keyval[0]] = value;
});
}
}
},
handleBlur() {
//dbounce 300 miliseconds
var empty = {}
empty[this.idprop] = '';
empty[this.textprop] = '';
if (this.query && this.items.length == 1 && this.items[0].id != this.id) {
this.selectItem(this.items[0]);
}
else {
if (this.query && this.items) {
//find query in items
let item = this.items.find(x => x.name == this.query);
if (!item && this.id) {
this.selectItem(empty);
}
if (this.query && !this.id) {
this.query = '';
}
}
else if (!this.query && this.id) {
this.selectItem(empty);
}
}
this.showItem = false
},
handleFocus() {
this.showItem = true;
},
selectItem(item) {
this.query = item[this.textprop];
this.id = item[this.idprop];
this.showItem = false;
if (this.onchange) {
//set timeout is for triggering after change
setTimeout(() => {
this[this.onchange](this.item);
});
}
},
highlightItem(direction) {
if (direction === "next" && this.highlightedIndex < this.items.length - 1) {
this.highlightedIndex++;
} else if (direction === "prev" && this.highlightedIndex > 0) {
this.highlightedIndex--;
}
document.querySelector(`#item_${this.highlightedIndex}`).scrollIntoView({
behavior: "smooth",
block: "nearest",
inline: "start"
});
},
async search(event) {
//execute 150 miliseconds debounce
if (!this.showItem)
this.showItem = true;
this.highlightedIndex = -1;
if (this.onsearch) {
//set timeout is for triggering after change is done
setTimeout(() => {
this[this.onsearch](this.query);
});
}
else {
let url = this.url + this.query;
let response = await $.get(url);
this.items = response;
}
},
}));
});
</script>
<!-- example usage:
<span
x-data="autocomplete({idprop:'id', textprop: 'name', items:countries, placeholder: 'Search Country...', onchange:'countrychanged', onsearch: 'searchcountry', watch:'items:countries'})"
x-bind="bind" x-model="country"> </span>
-->
<!-- Reusable autocomplete component end -->
<!-- Sample usage -->
<div x-data="main">
Below is the reusable Autocomplete Component
<span
x-data="autocomplete({idprop:'id', textprop: 'name', items:countries, placeholder: 'Search Country...', onchange:'countrychanged', onsearch: 'searchcountry', watch:'items:countries'})"
x-bind="bind" x-model="country"> </span>
Selected Country Code:
<span x-text="country"></span>
</div>
<script>
document.addEventListener("alpine:init", () => {
Alpine.data("main", () => ({
country: 'US',
allcountries: [
{ "id": "US", "name": "United States" },
{ "id": "CA", "name": "Canada" },
{ "id": "MX", "name": "Mexico" },
{ "id": "BR", "name": "Brazil" },
{ "id": "AR", "name": "Argentina" },
{ "id": "UK", "name": "United Kingdom" },
{ "id": "FR", "name": "France" },
{ "id": "DE", "name": "Germany" },
{ "id": "IT", "name": "Italy" },
{ "id": "ES", "name": "Spain" },
{ "id": "RU", "name": "Russia" },
{ "id": "CN", "name": "China" },
{ "id": "JP", "name": "Japan" },
{ "id": "KR", "name": "South Korea" },
{ "id": "IN", "name": "India" },
{ "id": "AU", "name": "Australia" },
{ "id": "ZA", "name": "South Africa" },
{ "id": "EG", "name": "Egypt" },
{ "id": "NG", "name": "Nigeria" },
{ "id": "KE", "name": "Kenya" }
],
countries: [
{ "id": "US", "name": "United States" },
{ "id": "CA", "name": "Canada" },
],
countrychanged(item) {
console.log(item);
},
searchcountry(q) {
setTimeout(() => {
//mimic async call
this.countries = this.allcountries.filter((item) => {
return item.name.toLowerCase().indexOf(q.toLowerCase()) > -1;
});
}, 100);
},
init() {
setTimeout(() => {
//test async loading top 10 countries
this.countries = this.allcountries.slice(0, 10);
}, 2000);
//get top 5 from this.allcountries
//try to get top 5 in init just for testing
this.countries = this.allcountries.slice(0, 5);;
//change it in init
}
}));
});
</script>
<script src="https://cdn.tailwindcss.com"></script>
<script defer src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn.min.js"></script>