Reputation: 7007
I am wondering -- is there a way to create layouts and/or components with AlpineJS (so that I can keep my code DRY)? If not, are there any solutions that integrate with AlpineJS that add this functionality (without having to resort to a full framework like React or Vue)?
Upvotes: 4
Views: 3582
Reputation: 1288
As noted in previous answers, Alpinejs is designed to add some frontend reactivity to server-side templating engines.
It can be achieved using directives, but it is a bit complicated, and I don´t think is worth the effort.
Below I show an example of a small component I made to handle a checklist, purely made in Alpinejs (I removed some parts of the code for the sake of brevity, so we can address the most relevant parts):
Alpine.directive('checklist-dropdown', (el, { value, modifiers, expression }, { Alpine, effect, evaluate, evaluateLater, cleanup }) => {
var items = evaluate(items);
var tpl = `
<i x-show = 'checked_filters.length == 0' class = 'fa fa-filter'></i>
<i x-show = 'checked_filters.length > 0' class = 'fa fa-filter-circle-xmark' @click.stop = 'clear()'></i>
<span class = 'text'>Checklist</span>
<span class = 'badge' x-show = 'checked_filters.length > 0' x-text = 'checked_filters.length'></span>
<template x-teleport = 'body'>
<div class = 'dropdown-panel'
x-show = 'isOpen'
@click.outside = 'closePanel()'
x-transition.origin.top.left.duration.300ms
:style = '"top: " + (position.top + position.height) + "px; left: " + position.left + "px"'
>
<div class = 'dropdown-panel-scrollable' x-ref = 'scrollable'>
<template x-for = 'item in items'>
<div class = 'dropdown-panel-item' @click = 'toggleItem(item)'>
<input type = 'checkbox' :checked = 'isItemChecked(item)' />
<label x-text = 'itemDescription(item)'></label>
</div>
</template>
</div>
<div class = 'dropdown-panel-actions'>
<a class = 'button apply-button' @click = 'clickOnApplyButton()'>Aplicar</a>
</div>
</div>
</template>
`;
var data = {
items: items,
isOpen: false,
position: el.getBoundingClientRect(),
_checklist: [], //modeled checklist
checklist: [], //checklist within the component (can be modified before confirming)
label: $(el).attr('label'),
init(){
Alpine.effect(() => {
this.checklist = this._checklist.slice();
});
},
isItemChecked(item){
return this.checklist.indexOf(item) != -1;
},
itemDescription(item){
//var desc = description_expr.replace(regex_matches[4], 'item');
var localScope = {};
localScope[item_name] = item;
return evaluate(description_expr, {scope: localScope});
},
toggleItem(item){
var pos = this.checklist.indexOf(item);
if ( pos == -1 ) this.checklist.push(item);
else this.checklist.splice(pos, 1);
},
togglePanel(){
if ( this.isOpen ) this.closePanel();
else this.openPanel();
},
openPanel(){
this.position = el.getBoundingClientRect();
this.isOpen = true;
},
closePanel(){
this.isOpen = false;
setTimeout( () => {
$('.dropdown-panel-scrollable')[0].scrollTo(0, 0);
this.reset();
//x-ref not working inside template.
//this.$refs.scrollable.scrollTo(0, 0);
}, 250); //Wait until the transition is done.
},
apply(){
this._checklist = this.checklist.slice();
Alpine.nextTick( () => { //Needed to allow x-modelable to do its work
el.dispatchEvent(new CustomEvent('change', { detail: this._checklist }));
});
},
reset(){
this.checklist = this._checklist.slice();
},
clear(){
this._checklist = []; //If not present, causes isItemChecked to stop evaluating next time is open. Don´t know why.
this.checklist = [];
this.apply();
},
clickOnApplyButton(){
this.apply();
this.closePanel();
},
html: tpl
};
var reactiveData = Alpine.reactive(data);
var destroyScope = Alpine.addScopeToNode(el, reactiveData);
//reactiveData['init'] && evaluate(el, reactiveData['init'])
Alpine.bind(el, {'x-modelable': '_checklist'});
Alpine.bind(el, {'@click': 'togglePanel'});
Alpine.bind(el, {':class': '{active: checklist.length > 0}'});
Alpine.nextTick( () => {
Alpine.bind(el, {'x-html': 'html'});
});
evaluate(reactiveData['init']);
cleanup(() => {
destroyScope();
})
});
And this is how I use it:
<div
x-checklist-dropdown
x-model = 'checked_filters'
items = '["option1", "option2"]'
>
</div>
As you may have noticed, the data object is what you would usually place inside x-data.
To add this kind of reactivity to an arbitrary html node, we use these two lines:
var reactiveData = Alpine.reactive(data);
var destroyScope = Alpine.addScopeToNode(el, reactiveData);
Next, we need to bind certain properties
Alpine.bind(el, {'x-modelable': '_checklist'});
Alpine.bind(el, {'@click': 'togglePanel'});
Alpine.bind(el, {':class': '{active: checklist.length > 0}'});
Being the most interesting x-modelable. By adding x-modelable="_checklist" to the element, we bind its value to that of x-model, that´s how x-model = 'checked_filters'
works.
And that´s it, the rest of the lines, I think, are pretty much self descriptive.
Upvotes: 1
Reputation: 142
You could use x-component with named slot in vimesh ui
<head>
<script src="https://unpkg.com/@vimesh/ui"></script>
<script src="https://unpkg.com/alpinejs" defer></script>
</head>
<body>
<vui-card>
<template slot="title">TITLE</template>
This is content
<template slot="footer">Copyright</template>
</vui-card>
<template x-component="card">
<h1>
<slot name="title"></slot>
</h1>
<p>
<slot></slot>
</p>
<div>
<slot name="footer"></slot>
</div>
</template>
</body>
Upvotes: 1
Reputation: 179
If you only need small components you can use x-html
and x-data
:
<div x-data="{ message: '<p>Hello <strong>World!</strong></p>' }">
<span x-html="message"></span>
<span x-html="message"></span>
</div>
This would return:
Hello World!
Hello World!
You can read the docs here
Upvotes: 1
Reputation: 3888
Alpine.js tries to avoid templating as much as possible since it's designed to work in tandem with server-side templating or a static side generator.
The example per the Alpine.js docs to load HTML partials is to use x-init="fetch('/path/to/my/file.html).then(r => r.text()).then(html => $refs.someRef.innerHTML = html)"
(x-init
is just one spot where this could be done).
Upvotes: 2