Everyones going on an on about @click
, x-text
, and $store
, but what about the Alpine globals!? Let’s dive into some lesser known, but quite useful — dare I say powerful? — features of the Alpine API.
I maintain the
@types/alpinejs
package. I’ve worked hard to properly Type and even document the Alpine API. Even if you’re not using TypeScript, installing this package can give you helpful explanations of the API as you use these methods.
Alpine.reactive
interface Alpine {
reactive<T>(obj: T): T;
}
Underneath Alpine is the powerful @vue/reactivity
package. When you make new components, or stores, or add values to existing components, Alpine wraps them in reactive proxies. You’ve probably seen this when trying to log an object from a component.
Well, you can use Alpine.reactive
to make your own reactive objects! This is useful mainly for plugins that might want to hook into reactivity without waiting for values to exist in a component.
import Alpine from 'alpinejs';
const data = Alpine.reactive({ count: 0 });
Alpine.effect(() => {
console.log(data.count);
});
data.count++;
Alpine.effect
is also a way to register a reactive effect, likex-effect
does.
Bonus: Alpine.raw
interface Alpine {
raw<T>(obj: T): T;
}
To clean up those Proxies in those logs, Alpine.raw
does the opposite of reactive
! It lets you simply access the internal object without concern for proxy wrapper. Can really make console debugging a lot easier.
<button
type="button"
@click="
console.log(Alpine.raw(cartItems))
"></button>
Alpine.addRootSelector
interface Alpine {
addRootSelector(selectorCallback: () => string): void;
}
Ever make a fancy clean Headless Alpine component (like in @alpinejs/ui
) but not enjoy the process of adding the x-data
attribute to the elements, just to make them work? addRootSelector
is here to help!
It basically adds new selectors for Alpine to initialize as if they are x-data
, indicating the root of a new Alpine component. Just pass in a function that returns a css selector string.
import Alpine from 'alpinejs';
Alpine.addRootSelector(() => `[${Alpine.prefixed('dialog')}]`);
<dialog x-dialog>Alpine sure is cool!</dialog>
Alpine.prefixed
is a helper to transform the attribute name to respectAlpine.prefix
configuration.
Bonus: Alpine.addInitSelector
interface Alpine {
addInitSelector(selectorCallback: () => string): void;
}
Like addRootSelector
, but for elements that you only want to evaluate as standalone actions. This is like x-init
! x-init
can be used without being inside an x-data
to help run simple startup expressions.
Alpine.addScopeToNode
interface Alpine {
addScopeToNode(
node: Element,
data: Record<string, unknown>,
referenceNode?: Element,
): () => void;
}
addScopeToNode
is the JavaScript API equivalent to x-data
! It allows your code to add new data to an element, for directives or children to access.
The optionally passed in referenceNode
allows you to pass in a different node for the element to “inherit” from. This is useful for things like x-teleport
where the element is somewhere else, but reactively interacts with a different component tree!
import Alpine from 'alpinejs';
const el = document.querySelector('[x-data]');
const newPage = document.createElement('div');
Alpine.addScopeToNode(newPage, { page: 'home' }, el);
document.body.append(newPage);
Note:
addScopeToNode
returns a function that can be called to remove the scope from the element. This is useful for cleanup in Single Page Applications.
Bonus: Alpine.$data
interface Alpine {
$data(node: Element): void;
}
This gets the data context of the element! It’s the same way expressions access the data context. You get a flattened object of the entire data stack that expressions on that element can access.
import Alpine from 'alpinejs';
const el = document.querySelector('button');
el.addEventListener('click', () => {
if ('cart' in Alpine.$data(el)) console.log('Cart exists!');
else console.log('No cart here!');
});
Honourable Mentions
Alpine.throttle
interface Alpine {
throttle(callback: Function, wait: number): Function;
}
Sometimes you need to throttle
a function, but you aren’t using @click.throttle
. Well, you can use the same underlying functionality with Alpine.throttle
! Now you can have your functions run only once every so often.
import Alpine from 'alpinejs';
const throttledLog = Alpine.throttle(
(msg) => console.log('Message:', msg),
1000,
);
throttledLog('Hello'); // logs
throttledLog('World'); // doesn't log
It’s also FULLY type safe! So the type of function that goes in is the type of function that comes out!
Alpine.debounce
interface Alpine {
debounce(callback: Function, wait: number): Function;
}
Just like the above, for debounce
! Now you can have your functions run only after a certain amount of time has passed since the last call, always with just the last arguments passed in.
import Alpine from 'alpinejs';
const debouncedLog = Alpine.debounce(
(msg) => console.log('Latest Message:', msg),
1000,
);
debouncedLog('Hello'); // doesn't log
debouncedLog('World'); // logs.... later
Oh, and it’s type safe too!
Wrap Up
Theres a lot of amazing power in the Alpine API! While these aren’t all things you should be diving into regularly, they can be very useful for providing clean interfaces to highly reusable components. Let me know if there’s any you think are even better!