Skip to content

eldomagan/alpine-define-component

Repository files navigation

alpine-define-component

Structured component system for Alpine.js with parts/slots support and full TypeScript support.

Installation

npm install alpine-define-component

Quick Start

import Alpine from 'alpinejs';
import { defineComponent } from 'alpine-define-component';

const accordion = defineComponent({
  name: 'accordion',
  setup: (props) => ({
    value: props.value || [],

    toggle(id) {
      const isOpen = this.value.includes(id);
      this.value = isOpen ? this.value.filter((i) => i !== id) : [id];
    },

    isOpen(id) {
      return this.value.includes(id);
    },
  }),

  parts: {
    item(api, el, { value }) {
      return {
        'x-on:click': () => api.toggle(value),
        'x-bind:data-open': () => api.isOpen(value),
      };
    },
  },
});

Alpine.plugin(accordion);
Alpine.start();
<div x-accordion="{ value: [] }">
  <div x-accordion:item="'item-1'">Item 1</div>
  <div x-accordion:item="'item-2'">Item 2</div>
</div>

TypeScript

setup Helper (Optional)

For TypeScript users who want Alpine magics (this.$dispatch, this.$watch, etc.) typed in methods:

import { defineComponent, setup } from 'alpine-define-component';

const counter = defineComponent({
  name: 'counter',
  setup: setup((props: { count?: number }) => ({
    count: props.count ?? 0,

    increment() {
      this.count++;
      this.$dispatch('incremented');
    },
  })),
});

Scoped Parts

Use defineScope to create isolated reactive contexts for repeating parts (like tabs, accordion items, list items):

import { defineComponent, defineScope } from 'alpine-define-component';

const tabs = defineComponent({
  name: 'tabs',
  setup: (props) => ({
    activeTab: props.defaultTab || 'tab1',
    setTab(tab: string) {
      this.activeTab = tab;
    }
  }),

  parts: {
    item: defineScope({
      name: 'tabItem',
      setup: (api, el, { value }) => ({
        id: value,
        isActive: () => api.activeTab === value
      }),
      bindings: (api, scope) => ({
        'x-on:click': () => api.setTab(scope.id),
        'x-bind:class': () => ({ active: scope.isActive() })
      })
    })
  }
});
<div x-tabs="{ defaultTab: 'tab1' }">
  <!-- Each item has its own scope via $tabItem -->
  <button x-tabs:item="'tab1'" x-text="$tabItem.isActive() ? 'Active' : 'Tab 1'">
    Tab 1
  </button>
  <button x-tabs:item="'tab2'" x-text="$tabItem.isActive() ? 'Active' : 'Tab 2'">
    Tab 2
  </button>
</div>

Benefits:

  • Isolated reactive state per part instance
  • Access parent API and scope data together
  • Scope available as $scopeName magic in HTML
  • Perfect for lists, tabs, accordions, menu items, etc.

Scope Typing (TypeScript)

For full type safety with scopes, use the parts-as-function pattern with withScopes:

import { defineComponent, defineScope, setup } from 'alpine-define-component';

const accordion = defineComponent({
  name: 'accordion',
  setup: setup(() => ({
    openItems: [] as string[],
    toggle(itemId: string) { /* ... */ },
    isOpen(itemId: string) { /* ... */ },
  })),

  parts: ({ withScopes }) => withScopes<{
    $item: { id: string; isOpen: boolean; toggle: () => void };
  }>({
    item: defineScope({
      name: 'item',
      setup: (api, _, { value: itemId }) => ({
        id: itemId,
        isOpen: api.isOpen(itemId),
        toggle() { api.toggle(itemId); },
      }),
    }),

    header(api) {
      // api.$item is correctly typed (from withScopes)
      return {
        'x-on:click': () => api.$item.toggle(),
        'x-bind:class': () => ({ open: api.$item.isOpen }),
      };
    },
  }),
});

API

defineComponent(config)

defineComponent({
  name: string,                  // Component/directive name
  setup: (props, ctx) => {...},  // Returns component API
  parts?: {...}                  // Optional part handlers
})

defineScope(options)

Creates an isolated reactive scope for component parts.

defineScope({
  name: string,                        // Scope name (accessible as $name in template)
  setup: (api, el, ctx) => {...},      // Returns scope data
  bindings?: (api, scope) => {...}     // Optional Alpine bindings
})

Returns: Part handler function

setup(fn) (TypeScript)

Type helper for Alpine magics in methods.

Inspiration

License

MIT

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors