How are existing reactive observables connected to Alpine.js?
The Dexie.js website lists a few examples with React and Svelte but how would I use Dexie.js liveQuery with Alpine.js? Is it as simple as passing the variable to x-data?
You cannot pass directly a liveQuery object to an Alpine.js property because it will lose reactivity. We need to create a small wrapper that updates Alpine.js data when a liveQuery returns new data. Here I provide a small example that uses a products table, the Alpine.js component just lists the products and there's a small form that can add new products to the DB.
Example database definition in db.js:
import Dexie from 'dexie'
export const db = new Dexie('myDatabase')
db.version(1).stores({
products: '++id, name, color',
})
In main.js we make db and liveQuery global:
import Alpine from 'alpinejs'
import { liveQuery } from "dexie"
window.liveQuery = liveQuery
import { db } from './db'
window.db = db
window.Alpine = Alpine
window.Alpine.start()
The example Alpine.js component:
<div x-data="productsComponent">
<div>
<input type="text" x-model="name" placeholder="Name" />
<input type="text" x-model="color" placeholder="Color" />
<button #click="add">Add product</button>
</div>
<div>
<h2>Products</h2>
<template x-for="p in products">
<div x-text="`ID: ${p.id} Name: ${p.name} Color: ${p.color}`"></div>
</template>
</div>
</div>
<script>
document.addEventListener('alpine:init', () => {
Alpine.data('productsComponent', () => ({
products: [],
name: '',
color: '',
observe(dataName, observable) {
const subscription = observable.subscribe({
next: val => {this[dataName] = val}
})
},
init() {
this.observe('products', liveQuery(() => db.products.toArray()))
},
async add() {
const id = await db.products.add({
name: this.name,
color: this.color,
})
this.name = ''
this.color = ''
}
}))
})
</script>
In the observe method we subscribe the specific liveQuery event and update the Alpine.js data when it changes.
Related
I have data that I am pulling from ajax. And I want this data display only when it is successfully pulled.
import Alpine from 'alpinejs'
import axios from 'axios'
const http = axios.create({
baseURL: 'url',
})
window.axios = http
window.Alpine = Alpine
document.addEventListener('alpine:init', () => {
Alpine.data('cart', () => ({
items: null,
init(){
window.axios.get('wc/store/cart')
.then(({data})=>{
this.items = data
console.log(this.items)
}).catch(error => {
console.log(error.toString())
})
},
}))
})
Alpine.start()
Now I am using this in my template
<div x-data="cart">
<template x-if="items">
<h1>Shopping Cart</h1>
<!-- display items here -->
</template
</div>
The thing is, the h1 element is displayed but not the data from ajax.
Am I doing anything wrong. I am pretty confidence this should work.
You're not displaying your items. Keep in mind that template tags require a single root element only.
<div x-data="card">
<template x-if="items">
<div>
<h1>Shopping Cart</h1>
<template x-for="item in items">
<div>
<h2 x-text="item.text"></h2>
</div>
</template>
</div>
</template>
</div>
I have created my own custom Modal plugin in vuejs to be added to my Laravel 8 app. The problem I am facing is opening the modal.
I have created the plugin in my app.js file
const Modal = {
install (Vue) {
this.event = new Vue()
Vue.prototype.$modal = {
show (modal, params = {}) {
Modal.event.$emit('show', modal, params)
},
$event: this.event
}
}
}
Vue.use(Modal)
I have created two vue components for my modal
<!-- AppModal //-->
<template>
<transition name="modal">
<div v-if="visible">
<div class="app-modal" #click.prevent="$modal.hide(name)"></div>
<div class="app-modal-inner">
close
<slot name="body" :params="params"/>
</div>
</div>
</transition>
</template>
<script>
export default {
name: "AppModal",
data () {
return {
params: {},
visible: false,
}
},
props: {
name: {
required: true,
type: String,
}
},
methods: {
setVisible () {
this.visible = true
},
setHidden () {
this.visible = false
}
},
beforeMount() {
this.$modal.$event.$on('show', (modal, params) => {
if (this.name !== modal) {
return
}
this.params = params
this.setVisible()
})
},
}
</script>
<!-- AppNonMemberRegisterModal //-->
<template>
<app-modal name="register">
<template slot="header">
<h1 class="text-lg-left text-4xl border border-b-2">Register Now</h1>
</template>
<template slot="body" slot-scope="{ params }">
<p>You need to register in order to share, comment and like on the site</p>
</template>
</app-modal>
</template>
<script>
import AppModal from "../AppModal";
export default {
name: "AppNonMemberRegisterModal",
components: { AppModal },
}
</script>
Where I am firing the event I import the AppNonMemberRegisterModal and I have the following click event: #click.prevent="$modal.show('register')".
When the following code is reached Modal.event.$emit('show', modal, params) I get the following error messages in my cnosole console.log(Modal)
Vue 3 removes the Event API (i.e., $on, $off, etc.). The migration guide recommends using tiny-emitter to create your own event bus. That example shows how to create a global bus, but it seems your plugin just needs a local bus, which you could create like this:
// eventBus.js
import Emitter from 'tiny-emitter'
export function createEventBus() {
const emitter = new Emitter()
return {
$on: (...args) => emitter.on(...args),
$once: (...args) => emitter.once(...args),
$off: (...args) => emitter.off(...args),
$emit: (...args) => emitter.emit(...args)
}
}
Then in your plugin, create a global with app.config.globalProperties, referring to the locally created event bus:
// myPlugin.js
import { createEventBus } from './eventBus'
export default {
install(app) {
const eventBus = createEventBus()
app.config.globalProperties.$modal = {
show (modal, params = {}) {
eventBus.$emit('show', modal, params)
},
$event: eventBus
}
}
}
And install it:
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import myPlugin from './myPlugin'
createApp(App).use(myPlugin).mount('#app')
Also be aware that your slot usage in AppRegisterModal.vue needs to be updated to the latest syntax (v-slot or # shorthand):
<app-modal name="register">
<!--
<template slot="header">
-->
<template #header>
<!--
<template slot="body" slot-scope="{ params }">
-->
<template #body="{ params }">
</app-modal>
demo
import React, { useState, useEffect } from "react";
import axios from "axios";
function App() {
const [contact, setContact] = useState({
fName: "",
lName: "",
email: ""
});
function handleClick() {
const res = axios.get("url");
}
useEffect(()=>{
handleClick();
})
return (
<div className="container">
<h1>
Hello {contact.fName} {contact.lName}
</h1>
<p>{contact.email}</p>
<input name="fName" placeholder={contact.fName} />
<input name="lName" placeholder={contact.lName} />
<input name="email" placeholder={contact.email} />
<button onClick={handleClick}>Submit</button>
</div>
);
}
export default App;
I set initial state with empty string but I am trying to update input attributes with data from external source whenever user clicks submit button.
I heard I need to use useEffect method to api call in react, but I have no idea where to start.
if you're going to update the data on the button click, then you can use a count mechanism, a separate variable to keep track of the count.
const [count, setCount] = useState(0);
<button onClick={() => setCount(count + 1 )}>Submit</button>
async function handleClick() {
const res = await axios.get("url");
setContact(res.data);
}
useEffect(() => {
handleClick();
}, [contact, count]);
I am validating a input field ( required , min length 3 ) with VeeValidate plugin.. it's working fine
but how I can avoid the onInput action to be called ( to avoid commit in store when the input becomes invalid ( as soon as input aria-invalid switch to false )
shortly said : Is there anyway to switch calling/not Calling onInput: 'changeTitle' when the input field aria-invalid is false/true ?
thanks for feedback
ChangeTitleComponent.vue
<template>
<div>
<em>Change the title of your shopping list here</em>
<input name="title" data-vv-delay="1000" v-validate="'required|min:3'" :class="{'input': true, 'is-danger': errors.has('required') }" :value="title" #input="onInput({ title: $event.target.value, id: id })"/>
<p v-show="errors.has('title')">{{ errors.first('title') }}</p>
</div>
</template>
<style scoped>
</style>
<script>
import { mapActions } from 'vuex'
import Vue from 'vue'
import VeeValidate from 'vee-validate'
Vue.use(VeeValidate)
export default {
props: ['title', 'id'],
methods: mapActions({ // dispatching actions in components
onInput: 'changeTitle'
})
}
</script>
vuex/actions.js
import * as types from './mutation_types'
import api from '../api'
import getters from './getters'
export default {
...
changeTitle: (store, data) => {
store.commit(types.CHANGE_TITLE, data)
store.dispatch('updateList', data.id)
},
updateList: (store, id) => {
let shoppingList = getters.getListById(store.state, id)
return api.updateShoppingList(shoppingList)
.then(response => {
return response
})
.catch(error => {
throw error
})
},
...
}
UPDATE
I tried to capture the input value with #input="testValidation) and check for a valid input value (required)
if valid ( aria-invalid: false) then I emit the input value, but the props are not updated in the parent component and the vuex action 'changeTitle' is not triggered
<template>
<div>
<em>Change the title of your shopping list here</em>
<input name="title" ref="inputTitle" data-vv-delay="1000" v-validate="'required'" :class="{'input': true, 'is-danger': errors.has('required') }" :value="title" #input="testValidation({ title: $event.target.value, id: id })"/>
<p v-show="errors.has('title')">{{ errors.first('title') }}</p>
</div>
</template>
<script>
import { mapActions } from 'vuex'
import Vue from 'vue'
import VeeValidate from 'vee-validate'
Vue.use(VeeValidate)
export default {
props: ['title', 'id'],
methods: {
testValidation: function (value) {
const ariaInvalid = this.$refs.inputTitle.getAttribute('aria-invalid')
if (ariaInvalid === 'false') {
this.$nextTick(() => {
this.$emit('input', value) // should change props in parent components
})
} else {
console.log('INVALID !') // do not change props
}
},
...mapActions({
onInput: 'changeTitle' // update store
})
}
}
</script>
like you access the errors collection in the VUE template, you can also access the same errors collection in your testValidation method
so replace
const ariaInvalid = this.$refs.inputTitle.getAttribute('aria-invalid')
with
const ariaInvalid = this.$validator.errors.has('title')
grtz
In resources/assets/js/app.js I have created eventHub Vue instance:
require('./bootstrap');
var eventHub = new Vue();
Vue.component('todos-list', require('./components/todos/TodoList.vue'));
Vue.component('todos-add', require('./components/todos/TodoAdd.vue'));
const app = new Vue({
el: '#app'
});
How can I use it in components that are created in separate .vue files?
For example, I also have two components:
todos-list located in /components/todos/TodoList.vue, which is used to fetch the data from server-side using vue-resource:
<template id="todos-list-template">
<div>
<ul v-if="todos.length > 0">
<li v-for="todo in todos">
{{ todo.title }}
</li>
</ul>
<p v-else>You don't hanve any Todos.</p>
</div>
</template>
<script>
export default {
template: '#todos-list-template',
data() {
return {
todos: {}
}
},
mounted() {
this.$http.get('api/vue/todos').then(function(response) {
this.todos = response.data;
});
},
methods: {
handleAddedTodo: function(new_todo) {
this.todos.push(new_todo);
}
},
created: function() {
eventHub.$on('add', this.handleAddedTodo);
},
destroyed: function() {
eventHub.$off('add', this.handleAddedTodo);
}
}
</script>
todos-add located in /components/todos/TodoAdd.vue which is used to add (save) the new 'todo' using vue-resource:
<template id="todos-add-template">
<div>
<form v-on:submit.prevent="addNewTodo()">
<div class="form-group">
<input v-model="newTodo.title" class="form-control" placeholder="Add a new Todo">
</div>
<div class="form-group">
<button>Add Todo</button>
</div>
</form>
</div>
</template>
<script>
export default {
template: '#todos-add-template',
data() {
return {
newTodo: { id: null, title: this.title, completed: false }
}
},
methods: {
addNewTodo() {
this.$http.post('api/vue/todos', { title: this.newTodo.title }).then(function(response) {
if (response.status == 201) {
eventHub.$emit('add', response.data);
this.newTodo = { id: null, title: this.title, completed: false }
}
}, function(response) {
console.log(response.status + ' - '+ response.statusText);
})
}
}
}
</script>
When I add (save) new 'todo' using the todos-add component (TodoAdd.vue) - I want to update data in todos-list component. If I understood well the documentation - for component communication we need to use centralized event hub. And that's what I tried - but I am getting the following error:
eventHub is not defined
I guess because it is defined in app.js, but how can I use in components that are created in separate .vue files?
You are getting this error because eventHub is actually not defined where you are using it. You have to export this from app.js and import in TodoAdd.vue.
in app.js:
var eventHub = new Vue()
exports.eventHub = eventHub
Add this code in TodoAdd.vue:
<script>
import eventHub from '/path/of/app'
export default {
template: '#todos-list-template',
This should make eventHub availble in TodoAdd.vue.
Edited
As the comment suggests, you may consider using vuex, as you have data whcih is being used across components, and going forward I see chances of getting it more complex, more event handlers and event listerners, which can quickly become a nightmare to manage.
var eventHub = new Vue()
You are getting this error because eventHub is actually not defined where you are using it. You have to export this from app.js file. Use this one
window.eventHub = new Vue()