可重用的 Alpine.js 组件?

问题描述 投票:0回答:7

如何使用 Alpine.js 创建可重用组件并显示它?例如,也许我想定义一个通用的 Alpine.js 按钮组件来更改参数中的文本和颜色,然后让我的 Alpine.js 导航栏组件使用该按钮组件来显示登录按钮。

我可以在纯客户端代码中执行此操作,而不依赖于在使用按钮组件的所有地方模板化所有按钮 HTML 的服务器吗?

javascript alpine.js
7个回答
32
投票

我可以在纯客户端代码中执行此操作,而不依赖服务器模板吗?

是的,可以。

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>

19
投票

Alpine.js 贡献者 @ryangjchandler 评论可重用模板超出了 Alpine.js 的范围:

提议的 [Alpine.js 版本 3] x-component 指令与组件的模板或标记没有任何关系。相反,它将提供一种编写更立即可重用的数据集和函数的方法,同时减少您需要在标记中定义的指令数量。

如果您需要可重用的模板,我会考虑使用服务器端模板引擎或更单一的前端框架,例如 Vue 或 React。 (链接

您正在寻找的功能远远超出了 Alpine 的范围。它旨在与服务器或静态文件中的现有标记一起工作,而不是替换/组件化您的标记。 (链接


4
投票

使用

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>

工作顺利。

更多信息 alpinejs 组件


3
投票

您可以使用

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 在这里


3
投票
借助 Alpine.js v3 和全局 Alpine 组件,您可以使用 Alpine.component() 来封装此功能。

https://github.com/markmead/alpinejs-component

<div x-data="dropdown"> ... </div> <script> Alpine.component('dropdown', () => ({ open: false, toggle() { this.open = !this.open } })) </script>
    

1
投票
Vimesh UI 中带有原生自定义元素的 x 组件 (

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>


0
投票
我创建了一个可重用的自动完成组件,可能对某人有帮助

代码笔链接:

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>

© www.soinside.com 2019 - 2024. All rights reserved.