alpine.js $dispatch modal -> submit Form - javascript

I am using django and i have a base template where i defined a modal using alpine.js with $dispatch sender.
base.html:
<div x-data="modal()" class="mt-6" x-cloak>
<template x-on:show-modal.window="isOpenModal = $event.detail.show; modalHeader = $event.detail.modalHeader; modalData = showData($event.detail.modalData); "></template>
<div class="absolute z-50 top-0 left-0 w-full h-full flex items-center justify-center bg-black bg-opacity-50" x-show="isOpenModal">
<div class="z-50 text-left bg-gray-200 px-4 shadow-xl rounded-lg mx-2 md:max-w-lg md:p-6 lg:p-8 md:mx-0 h-auto " >
<div class="flex justify-between">
<h2 id="modalHeader" class="text-2xl" x-text="modalHeader"> </h2>
</div>
<div class="w-full border border-gray-600 mt-4" ></div>
<div id="modalContent" class="text-lg w-auto" > </div>
</div>
</div>
</div>
in script tags ....
function modal(){
return{
isOpenModal: false,
modalHeader:'',
modalData: '',
showData(data){
document.getElementById('modalContent').innerHTML = data
let fp = flatpickr(".pickerDate", {locale: "at", dateFormat: "d.m.Y"});
},
}
}
then in the other html which is extendet from the base.html i want to use the modal where i want to get with axios form data from the server and put it into the modal. This is working perfect. But i don't know how to realize the submit button ?
new.html
<div x-data="test()" #click="getCreateForm($dispatch)">
test click
</div>
this is the point where i go to function getCreateForm ....
function test(){
return{
getPatientCreateForm($dispatch){
axios.get("{% url 'user:createForm'%}")
.then(response => {
var modalHeader = response.data.header
var modalData = "<form id='createUser' class='' method='POST' action='' x-on:submit.prevent='?????????'> {% csrf_token %}" +response.data.seite.seite
$dispatch('show-modal', { show: true, modalHeader: modalHeader, modalData: modalData })
})
.catch(error => {
console.log(error);
})
}
}
}
The problem is when i put a sendForm function into the submit.prevent like: x-on:submit.prevent='sendForm()' alpine searches for the function on the base.html (where the modal is defined) and i don't want to implement a function there. I want by clicking the submit button that the data should be send with axios on the new.html (where i started with the getCreateForm ) and not at the base.html. Is that possible with alpine.js and $dispatch ? or is that impossible
Thanks for helping!
Martin

Related

How to apply onClick in the input

As you can this is a string js and I want to use onClick on the input whose view is being toggled by selecting the boolean value edit. Please suggest how to use onchnage here.
Already tried normal HTML onchange (not working)
onchange="${onchnage}"
Pls, suggest if you happen to know the answer.
export const DefaultNode = (d, selectedNodeIds, edit, fomatOptions, inputOnclick) => {
const mainData = d.data.data
return `<div style='background:${selectedNodeIds.length!==0 ? (selectedNodeIds.includes(d.data.id) ? `rgba(${ fomatOptions.nodeBg.r }, ${ fomatOptions.nodeBg.g }, ${ fomatOptions.nodeBg.b }, ${ fomatOptions.nodeBg.a })`: "#fff"): `rgba(${ fomatOptions.nodeBg.r }, ${ fomatOptions.nodeBg.g }, ${ fomatOptions.nodeBg.b }, ${ fomatOptions.nodeBg.a })`};
color:${selectedNodeIds.length!==0 ?(selectedNodeIds.includes(d.data.id) ?`rgba(${ fomatOptions.textColor.r }, ${ fomatOptions.textColor.g }, ${ fomatOptions.textColor.b }, ${ fomatOptions.textColor.a })`:'#000'): `rgba(${ fomatOptions.textColor.r }, ${ fomatOptions.textColor.g }, ${ fomatOptions.textColor.b }, ${ fomatOptions.textColor.a })`}'
class=${`"w-[250px] p-3 rounded-[15px] relative border-[3px] h-[140px] ${selectedNodeIds.includes(d.data.id)? 'drop-shadow-md' :"shadow"} ${ selectedNodeIds.includes(d.data.id) && fomatOptions.fontFamily.value}"`}>
<div class='flex justify-between w-full '>
<div class="">
${edit? `<input onclick='${inputOnclick}' class="fullName text-[13px] font-semibold" value="${mainData.name}"/>` : `<div class=" text-[13px] font-semibold">${mainData.name} </div>`}
<div class=" text-[11px] opacity-70 mt-0.5 font-medium">${mainData.position } </div>
<div class='mt-2'>
<div class=" text-[11px] opacity-70 mt-0.5 font-medium">${mainData.email } </div>
<div class=" text-[11px] opacity-70 mt-0.5 font-medium">${mainData.phone } </div>
</div>
</div>
<img class='w-10 h-10 mr-2 rounded-[10px]' src=${mainData.imgUrl} />
</div>
<div class='flex pt-4 justify-between items-center'>
<p class='text-[10px] font-medium uppercase bg-theme-gray px-2 text-black rounded-full py-0.5'>${mainData.department}</p>
<p class='text-[10px] font-medium uppercase mr-2'>${mainData.location}</p>
</div>
${((selectedNodeIds.includes(d.data.id))) ? `<div class="absolute left-4 -top-5 font-semibold text-[10px] p-1 bg-gray-400 text-white rounded-t-md">
Selected
</div>`: `<p></p>`}
</div>`
}
I believe what you're missing here is the different naming conventions for default HTML event listeners in React, not all of your code is here so I'm assuming you do not have a custom function called onchange, but in React its called onChange (or onClick, etc) so you're looking something like this for your code snippet.
onChange="${onchnage}"
Also double-check to make sure you have all your syntax and spelling correct. Also for writing better JSX for returning HTML elements you can write code like the following
return (
<div>
<p>Text here</p>
</div>
);

How to stop propagation with AlpineJS and Flatpickr?

I have a modal window for filters on my application. The filters modal has #click.outside="filters = false" so if the user clicks outside of the modal it will hide. Inside of that filters modal I have an option for choosing the minimum date for which I'm using Flatpickr.
The problem is when you click on the arrows to change the month - or the month or year at the top - the filters modal will hide.
I believe that I need to use e.stopPropagation or #click.prevent on the element but it hasn't worked in each spot that I've tried it.
How do I make it so that any clicks inside of the Flatpicker window doesn't propagate up and close the filters modal?
Here is my full code -
<div x-show="filters" #click.outside="filters = false" x-on:keydown.escape.window="filters = false" class="absolute shadow-lg z-40 mt-4">
<div x-trap="filters">
<div>
<label for="filter-date-min" class="block text-sm font-semibold leading-5 text-gray-700">Minimum Date</label>
<div class="mt-1 relative rounded-md shadow-sm">
<div x-data="{
value: '',
init() {
let picker = flatpickr(this.$refs.picker, {
dateFormat: 'Y-m-d',
defaultDate: this.value,
onChange: (date, dateString) => {
this.value = dateString
},
})
},
}">
<input id="filter-date-min" placeholder="MM/DD/YYYY" x-ref="picker" x-bind:value="value" class="flex-1 min-w-0 block w-full px-3 py-2 rounded-none rounded-r-md focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 border active" autocomplete="off">
</div>
</div>
</div>
</div>
</div>
A simple solution for this issue is to introduce a pickerOpen variable that monitors the Flatpickr popup's state via the onOpen and onClose hooks. Then only close the modal window when Flatkpickr popup is inactive.
<div x-data="{filters: false, pickerOpen: false}">
<div x-show="filters"
#click.outside="if (!pickerOpen) {filters = false}"
x-on:keydown.escape.window="if (!pickerOpen) {filters = false}"
class="absolute shadow-lg z-40 mt-4">
<div x-trap="filters">
<div>
<label for="filter-date-min" class="block text-sm font-semibold leading-5 text-gray-700">Minimum Date</label>
<div class="mt-1 relative rounded-md shadow-sm">
<div x-data="{
value: '',
init() {
let picker = flatpickr(this.$refs.picker, {
dateFormat: 'Y-m-d',
defaultDate: this.value,
onOpen: () => {this.pickerOpen = true},
onClose: () => {this.pickerOpen = false},
onChange: (date, dateString) => {
this.value = dateString
},
})
},
}">
<input id="filter-date-min" placeholder="MM/DD/YYYY" x-ref="picker" x-bind:value="value" autocomplete="off"
class="flex-1 min-w-0 block w-full px-3 py-2 rounded-none rounded-r-md focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 border active">
</div>
</div>
</div>
</div>
</div>
</div>
I don't believe AlpineJS is capable of doing this, so you can throw in some custom JS. You can add this code in a script tag right before the closing body tag:
[...document.getElementsByClassName("flatpickr-calendar")].forEach($el => {
$el.addEventListener("click", e => e.stopPropagation());
});
Once the user opens a Flatpickr popup, Flatpickr appends the calendar to the end of the body, not inside your div, so it's an "outside" click.
AlpineJS determines "outside" clicks by adding an event listener to window (or something similar), and when the event bubbles (propagates) up it tests whether the click target is the element that's requesting an outside click. If not, fire an event that there's an "outside" click.
What this code essentially does is, once there is a click on a Flatpickr popup calendar, we prevent the event from bubbling up to AlpineJS, so Alpine doesn't know there was any click on the window at all, thus #click.outside won't trigger when the calendar is clicked.

Vue Remove loop rendered component from DOM

I have image upload form for images to my website. When the user clicks on input images, he can choose multiple images. After selecting images, images are previewed and the user can select some meta info(Category, Type) about the image.
upload.vue
<template>
<div>
<div>
//Universal category select. this selection will apply to all comp.
<v-select placeholder="Select Category"
class="mt-2 md:w-1/2"
:options="category"
v-model="parentDesignCategory"
/>
<v-select
placeholder="Select Type"
class="mt-2 md:w-1/2"
:options="type"
v-model="parentDesignType"
/>
</div>
<input
type="file"
accept="image/*"
name="images"
#change="uploadImage"
id="images"
multiple
/>
<div class="flex flex-wrap">
<div class="md:w-1/2" v-for="(file, index) in files" :key="index">
<transition name="fade">
<AdminFileUpload
:file="file"
:type="type"
:category="category"
:parentDesignType="parentDesignType"
:parentDesignCategory="parentDesignCategory"
#delete-row="deleteThisRow(index)"
/>
</transition>
</div>
</div>
</div>
</template>
<script>
export default {
name: "admin",
// middleware: "auth",
data: function() {
return {
files: [],
parentDesignType: null,
parentDesignCategory: null,
type: ["1", "2", "3"],
category: ["a","b","c"
]
};
},
components: {},
methods: {
uploadImage(event) {
let file = event.target.files;
for (let i = 0; i < file.length; i++) {
this.files.push(file[i]);
}
},
deleteThisRow: function(index) {
this.files.splice(index, 1);
}
}
};
</script>
<style scoped>
.fade-enter-active {
transition: opacity 1.5s;
}
.fade-leave-active {
opacity: 0;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
</style>
And if all image falls in one category than a user can select one category from this page and all component follows this category.
fileUpload.vue Component
<template>
<div>
<div class="m-4">
<form
#submit.prevent="uploadImage"
class="flex flex-wrap w-full shadow-lg border border-black"
action="/upload"
>
<div class="w-full md:w-1/2 p-2">
<div class="relative pb-1/1">
<img :src="imageSrc" class="w-full absolute h-full" />
</div>
</div>
<div class="flex flex-col w-full md:w-1/2 p-2">
<v-select
placeholder="Select Category"
class="mt-2"
:options="category"
v-model="designCategory"
></v-select>
<v-select
placeholder="Select Type"
class="mt-2"
:options="type"
v-model="designType"
></v-select>
<input
placeholder="likes"
class="w-full text-black border-2 mt-2 p-3 rounded-lg focus:outline-none focus:shadow-outline"
type="number"
v-model="designLikes"
/>
<button
#click="removeSelf"
class="uppercase h-12 text-lg font-bold tracking-wide bg-primary text-gray-100 mt-2 p-3 rounded-lg w-full cursor-pointer"
type="button"
>
Cancel
</button>
<button
type="submit"
class="uppercase mt-2 h-16 text-xl font-bold tracking-wide bg-accent text-gray-100 p-3 rounded-lg w-full transition duration-300 hover:opacity-80 cursor-pointer"
>
Upload
</button>
</div>
</form>
</div>
</div>
</template>
<script>
import "vue-select/dist/vue-select.css";
export default {
name: "fileUpload",
middleware: "auth",
props: [
"file",
"type",
"category",
"parentDesignCategory",
"parentDesignType"
],
data() {
return {
designCategory: this.parentDesignCategory,
designType: this.parentDesignType,
designLikes: null
};
},
computed: {
imageSrc: function() {
return URL.createObjectURL(this.file);
}
},
created() {},
methods: {
async uploadImage() {
let formData = new FormData();
const config = {
headers: {
"content-type": "multipart/form-data"
}
};
formData.append("likes", this.designLikes);
formData.append("image", this.file);
formData.append("category", this.designCategory);
formData.append("type", this.designType);
await this.$axios
.post("upload", formData, config)
.then(response => {
this.progress = 0;
this.showToast("Photo Uploaded.", "success");
// Delete coomponent when upload complete
this.$emit("delete-row");
})
.catch(error => {
});
},
removeSelf: function() {
this.$emit("delete-row");
});
}
}
};
</script>
Now my first and main problem is when the user removes the component from the dom, it removes the component but the Selected category/type stays in the same position.
Suppose I chose 4 images. I set 2nd image category as "a". When I remove 1st image. 1st image gets removed and 2nd image comes at 1st place but the category selected "a" remains on position 2.
Now 2nd problem is if I chose the category for the universal component in the parent page before selecting images it applies to all components. but after selecting images, Universal select doesn't work.
3rd problem is transition doesn't work on any component.
Simple answer is - you have to set an unique ID. Here is how you can solve that:
Changings in the template:
First of all you need to set an id instead of using your index - setting an id makes it unique and that is what we need. So set your :key to file.id (we will create it in the script) and pass your file with deleteThisRow to your methods. Done!
<div class="md:w-1/2" v-for="file in files" :key="file.id">
//and change your index here to file here we will reference on the unique file we will create with the unique id we will set
#delete-row="deleteThisRow(file)"
Changings in the script: Set your id = null in data() - that your created id will not be undefined. After that go to your methods and set your id = i - now it's unique and could not be change anymore like your index could. Last thing you should do is to map over your files array and get the correct index which should be deleted with indexOf.
//in your data
data() {
return {
id: null,
}
},
//in your methods
methods: {
uploadImage(event) {
let file = event.target.files;
for (let i = 0; i < file.length; i++) {
this.files.push({image:file[i], id : i}); //here you set your id to an unique number! (could be this.id you have to try)
}
},
deleteThisRow: function(file) {
var indexDelete = this.files.map(x => {
return x.id;
}).indexOf(file.id);
this.files.splice(indexDelete, 1);
}
}
After all you have to pass your file.id to your child with following code:
<child :uniqueID="file.id"
:file="file.image">
and reference on this in your child with props
Hopefully I understood your question correct - than that should work out for your problem - please let me know if this works for you !
Additional Info: Please change all index-values to file.id - than everything is really unique.

filtering data with vue js

I have loaded a data from API and displayed here with VueJS. I have users information inside users[] array. I also have users with two types of plan: basic_plan and standard_plan. Currently it shows all users.
Now I want to apply filters equally to this example: https://codepen.io/marn/pen/jeyXKL?editors=0010
I also got an error filter not defined
Filters:
<input type="radio" v-model="selectedItems" value="All" /> All
<input type="radio" v-model="selectedItems" value="basic_plan" /> Basic
<ul
v-for="(user, index) in selectedUser.data"
:key="index"
class="watchers divide-y divide-gray-200"
>
<li class="py-4">
<div class="mx-4 flex space-x-3">
<span
class="inline-flex items-center justify-center h-8 w-8 rounded-full bg-gray-500"
>
</span>
<div class="flex-1 space-y-1">
<h3 class="text-sm font-medium">
{{ user.name }}
</h3>
<div class="flex items-center justify-between">
<p class="text-sm font-medium text-indigo-500">
{{ user.plan }}
</p>
</div>
</div>
</div>
</li>
</ul>
</div>
</template>
<script
export default {
data() {
return {
users: [],
selectedItems:"All"
};
},
created() {
this. users();
},
methods: {
users {
axios
.get('api/users')
.then(response => {
this.users = response.data;
}
},
computed: {
selectedUser: function() {
if(this.selectedItems ==="All"){
return this.users
}else{
return this.users.data.filter(function(item) {
console.log(item)
return item.plan === this.selectedItems;
});
}
}
}
};
</script>
when All is selected vue dev tool shows this
selectedUser:Object //OBJECT SHOWING
data:Array[10]
links:Object
meta:Object
but when basic radio is selected vue shows this
selectedUser:Array[1] //ARRAY SHOWING
0:Object
price:"10"
plan:"basic_planl"
If you want to filter out specific users you must apply the "filter" function to the users variable like this:
this.users.filter(...)
With this function you then can filter the users based on their plan like this:
this.users.filter((user) =>
user.plan === this.selectedItems;
});
For a modern approach I used an arrow function. And without using curly brackets the statement inside the function is returned by default, so that's why there is no "return" statement.
Try this way instead, as you are already using v-for in your HTML, you can conveniently filter out users without any more loops, if you are getting value as "basic_plan" in the "user.plan" key.
Also, I think that you should move your v-for to <li> tag instead of <ul> along with the validation on <ul> if there are no users in the array.
<template>
<div>
<input type="radio" v-model="selectedItems" value="All" /> All
<input type="radio" v-model="selectedItems" value="basic_plan" /> Basic
<ul v-if="selectedUser.data.length" class="watchers divide-y divide-gray-200">
<li v-for="(user, index) in selectedUser.data" :key="index" class="py-4">
<div v-if="filterUser(user)" class="mx-4 flex space-x-3">
<span class="inline-flex items-center justify-center h-8 w-8 rounded-full bg-gray-500"></span>
<div class="flex-1 space-y-1">
<h3 class="text-sm font-medium">
{{ user.name }}
</h3>
<div class="flex items-center justify-between">
<p class="text-sm font-medium text-indigo-500">
{{ user.plan }}
</p>
</div>
</div>
</div>
</li>
</ul>
</div>
</template>
<script>
export default {
data() {
return {
users: [],
selectedItems:"All"
};
},
methods: {
filterUser(user){
if(this.selectedItems === 'All'){
return true;
}
if(this.selectedItems === 'basic_plan'){
return this.selectedItems === user.plan;
}
}
},
}
</script>

Vue not showing categories after fetch it

Hi all I need some help with Vue rendering.
Im making Vue-Wordpress app, and im trying to get a list of categories for each post. Categories of every post backed from WP API as id of it. Im transfer categories ID as props in child element (prop "cats") and try to render it after fetching name. But on front-end im not see anything (in Vue dashboard i got list of categories names, sorry i can't post image with it)
<template>
<div class="bg-white border rounded border-black border-collapse">
<h2 class="font-bold text-xl p-2 hover:text-gray-600 cursor-pointer">{{ title }}</h2>
<div
class="w-full h-48 bg-cover bg-center"
:style="{ 'background-image': 'url(' + image + ')' }"
></div>
<div class="px-4 py-2">
<p class="text-gray-700 h-40 text-base" v-html="content"></p>
<div>
<span
v-for="(val, index) in categories"
:key="index"
class="inline-block bg-gray-200 rounded-full px-3 py-1 text-sm font-semibold text-gray-700 mr-2"
>{{val}}</span>
</div>
</div>
</div>
</template>
<script>
export default {
props: ["title", "content", "image", "cats"],
data() {
return {
categories: {}
};
},
created() {
this.getAll();
},
methods: {
getAll: function() {
this.$props.cats.forEach(r => this.getCatName(r));
},
getCatName: function(id) {
fetch(`http://rest-api.local/wp-json/wp/v2/categories/${id}`)
.then(r => r.json())
.then(res => (this.categories[id] = res.name));
}
}
};
</script>
Thank you so much, for help!
You have a reactivity caveat, so you should use this.$set function :
.then(res => (this.$set(this.categories,id,res.name)));
and the id property should be declared :
data() {
return {
categories: {
id:null
}
};
},

Categories