How do I use Alpine.js to create a reusable component and display it? E.g., maybe I want to define an generic Alpine.js button component that changes text and color from parameters, then have my Alpine.js navbar component use the button component to show a login button.
Can I do this in pure client-side code, without relying on a server templating out all of the button HTML everywhere the button component is used?
Can I do this in pure client-side code, without relying on a server templating?
Yes, you can.
Alpine.js always will try to persuade you to use a server side templating engine.
But just like you, I don't let myself be persuaded:
<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>
Alpine.js contributer #ryangjchandler remarks that reusable templates are out of scope for Alpine.js:
The proposed [Alpine.js version 3] x-component directive will NOT have anything to do with templating or the markup for your component. Instead it will provide a way of writing more immediately reusable data sets & functions, whilst reducing the amount of directives you need to define in your markup.
If you need re-usable templates, I would consider using a server-side template engine or a more monolithic front end framework such as Vue or React. (link)
and
The functionality you are looking for is far out of the scope of Alpine. It's designed to work alongside your existing markup from the server or static files, not replace / component-ise your markup. (link)
With Alpine.js v3 and Global Alpine Components, you can use Alpine.component() to encapsulate this functionality.
<div x-data="dropdown">
...
</div>
<script>
Alpine.component('dropdown', () => ({
open: false,
toggle() { this.open = !this.open }
}))
</script>
use alpinejs-component
one same page by 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/alpinejs-component#1.x.x/dist/component.min.js"></script>
<script defer src="https://unpkg.com/alpinejs#3.x.x/dist/cdn.min.js"></script>
use url import html template:
<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/alpinejs-component#1.x.x/dist/component.min.js"></script>
<script defer src="https://unpkg.com/alpinejs#3.x.x/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>
install by npm :
npm i -D alpinejs-component
yarn add -D alpinejs-component
register plugin:
import Alpine from "alpinejs";
import component from "alpinejs-component";
Alpine.plugin(component);
window.Alpine = Alpine;
Alpine.start();
or use module in browser:
<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>
work well.
more info alpinejs-component
You can do this with Alpine.data and the documented approach for encapsulating directives with x-bind. The trick is to bind the x-html directive. In your HTML do this:
<div x-data="dropdown" x-bind="bind"></div>
In your 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>
`},
},
}));
})
JSFiddle here.
It's a little hacky as you are encapsulating all your nested content in a multiline HTML string bound in the x-html directive (though perhaps no more hacky than the alternative of cloning templates everywhere). Make sure you don't use the backtick character in the content. Nevertheless, the content can be nested as deeply as you like and can contain Alpine.js directives. You can initialise your component by declaring and passing parameters into Alpine.data. You could also bind x-modelable to expose any properties of your component as outputs.
If you prefer to use templates, maybe because your editor does better syntax highlighting when markup is not embedded in a string, you can combine this approach with templates. Here's an example that demonstrates x-modelable and the use of templates too. In effect, Alpine does your template cloning for you.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script defer src="https://unpkg.com/alpinejs#3.x.x/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>
JSFiddle here.
The x-component with native custom element in Vimesh UI (https://github.com/vimeshjs/vimesh-ui) is a more complete reusable component implementation :
<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>
I have a data attribute like so:
<div data-id="">
In my markdown file I have frontmatter variable like so:
id: rick
Now I want to pass that object into data-id which only lets me add string.
How to make like so:
<div data-id="id"> or <div data-id={{ id }}> or <div :data-id={{ id }}>
I use vuejs.
To bind attributes to an element (or component) you use
<div :data-id="id">
...
</div>
window.onload = () => {
new Vue({
el: '#app',
data () {
return {
id: 123
}
}
})
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.0/vue.js"></script>
<div id="app">
<div :data-id="id">
This is rendered as div with and data-id="123"
</div>
</div>
I have a simple problem but I can't find any reliable answer.
So here is what I want to do. The code is simplified for clarity.
<h1>Hello</h1>
<div id='app'>
<template>
<p>Here is the app: {{message}}</p>
#Html.Partial("_Component")
</template>
</div>
<script>
var vm = new Vue({
el: '#app',
data: {
message: "App works!"
}
});
</script>
And _Component.cshtml file:
<my-component inline-template>
<div>
<p><button v-on:click="counter += 1">Add One More Click</button></p>
<p>The button has been clicked {{ counter }} times</p>
</div>
</my-component>
<script>
Vue.component('my-component',
{
data: function () {
return {
counter: 1
}
}
});
</script>
Problem with this code is that my-component is not rendering. Overall, I'm working in project of asp.net mvc application and its required to use template tag.
If I remove this tag, the page is a mess until it's rendered.
Is there any solution or workaround to be able to see component in this case?
You declare the component in the wrong place. Should be like this
<h1>Hello</h1>
<div id='app'>
<template>
<p>Here is the app: {{message}}</p>
<my-component inline-template>
<div>
<p><button v-on:click="counter += 1">Add One More Click</button></p>
<p>The button has been clicked {{ counter }} times</p>
</div>
</my-component>
</template>
</div>
<script>
Vue.component('my-component', {
data: function () {
return {
counter: 1
}
}
});
var vm = new Vue({
el: '#app',
data: {
message: "App works!"
}
});
</script>
I have a Vue component in which I'm trying to do a v-for of an multi array json object passed by props, this object is dynamic and is populated by a parent method.
Here:
My problem is that in the component i'm seeing only the first object of the data:
but I have no error in console, so I don't understand what the problem is... must I do a watch in data?
Here is my code:
<lista-percorso :selezionati="il_tuo_percorso"></lista-percorso>
COMPONENT
Vue.component('lista-percorso', {
template:`
<div class="container" style="margin-top:30px;">
<template v-if="listaSelezionati.length>0">
<div class="row" v-for="(selezionato,index) in listaSelezionati">
<div>{{selezionato[index]}}</div>
</div>
</template>
<template v-else>
<h5>NON HAI SELEZIONATO NESSUN SERVIZIO</h5>
</template>
</div>
`,
props: ['selezionati'],
data: function(){
return{
listaSelezionati:this.selezionati
}
},
methods:{
}
});
Your data listaSelezionati is an Array of Arrays of Objects: [[{one:one}],[{two,two}]]
when you go this:
<div class="row" v-for="(selezionato,index) in listaSelezionati">
<div>{{selezionato[index]}}</div>
</div>
You are telling Vue to render the first item [{one:one}] and then the index in that item {one:one}. However, since they all appears to be arrays with length 1, you could do this:
<div class="row" v-for="(selezionato,index) in listaSelezionati">
<div>{{selezionato[0]}}</div>
</div>
I am developing an application and I am using Vue 2 as my javascript framework, I tried to declare some components and use them in my html pages
this is my html:
<!DOCTYPE html>
<html>
<head>
<title></title>
<link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.3.1/css/bulma.css" />
</head>
<body>
<div id="modal_element" >
<modal v-if="showModal" ></modal>
<button #click="showModal = true" >Show Modal</button>
</div>
<div id="root">
<ul>
<li v-for="task in incompeletedTasks" >
{{ task.description }}
</li>
</ul>
</div>
</body>
<script src="https://unpkg.com/vue#2.1.10/dist/vue.js" ></script>
<script src="main.js"></script>
<script src="modal.js" ></script>
<script>
let main_data = {
tasks : [
{ description : "Go to the store ", completed : true },
{ description : "Leave the store" , completed : false }
]
}
new Vue({
el : "#root",
data : main_data,
computed : {
incompeletedTasks() {
return this.tasks.filter(task => !task.completed);
}
}
});
and this the modal.js file:
Vue.component('modal',{
template : '<div id="modal_element">
<div class="modal is-active">
<div class="modal-background"></div>
<div class="modal-content box">
<p>
Some Modal Text here ...
</p>
</div>
<button class="modal-close" #click="showModal = false" >
</button>
</div>',
data : function(){
return {
showModal : false
};
}
});
new Vue({
el : '#modal_element',
});
but the modal is not displayed, and I am getting the following error in the chrome console
[Vue warn]: Property or method "showModal" is not defined on the instance
but referenced during render. Make sure to declare reactive data
properties in the data option.
Question:
what modification do I have to make to get the code working? and html page successfully displays modal?
I think there are a couple of things.
You are creating 2 vue instances in this example (#root and #modal-element), so the data will not be able to be shared unless you have some store. Much better to have just a single instance and put components in that.
You will need to pass the component into the vue instance in order for it to be aware of the component.
Here is an example with alot of the stuff trimmed out.
https://jsfiddle.net/Austio/vhgztp59/2/
The gist of it is
var component = ...createComponentStuff
new Vue({
...otherVueStuff,
components: [component]
})