Moshe
Moshe

Reputation: 7007

Creating Layouts & Components with AlpineJS

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

Answers (4)

sergio0983
sergio0983

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

comforx
comforx

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

TacoSnack
TacoSnack

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

Hugo
Hugo

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

Related Questions