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.
Related
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>
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
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
}
};
},
I have two components: Toggle.vue which is basically a button and a TestToggle.vue which has two Toggle components inside. I want to be able for the toggle elements to serve as a radio button of sorts: only one can be selected at a time.
It is supposed to look like this (only one button is active at a time):
However I can select two buttons:
which isn't right.
Toggle.vue:
<template>
<div class="rounded-full m-5 w-40
flex justify-center
p-2 cursor-pointer"
:class = "status ? 'bg-green-700
hover:bg-green-600' :
'bg-red-700
hover:bg-red-600'"
v-on:click="status = true">
<p>{{text}} : {{status}}</p>
</div>
</template>
<script>
export default {
props: {
text: {
type: String,
default: ''
},
status: {
type: Boolean,
default: false
}
}
}
</script>
TestToggle.vue:
<template>
<div>
<p>Active: {{activeTab}}</p>
<Toggle v-on:click = "activeTab = 1"
text="Toggle 1 "/>
<Toggle v-on:click = "activeTab = 2"
text = "Toggle 2"/>
</div>
</template>
<script>
import Toggle from '../test/Toggle.vue';
export default {
components: {Toggle},
data: function () {
return {
activeTab: 1
}
},
methods: {
}
}
</script>
I think I need to set status = false from TestToggle to Toggle when another Toggle is clicked? How do I do that? Or should I do it completely differently?
Another problem is that I can't update activeTab data property inside TestToggle component: it always shows 1...
EDIT:
I tried this code (as suggested in the answer), but it just doesn't work: the buttons don't react to clicks:
Toggle.vue:
<template>
<div class="rounded-full m-5 w-40
flex justify-center
p-2 cursor-pointer"
:class = "status ? 'bg-green-700 hover:bg-green-600' :
'bg-red-700 hover:bg-red-600'">
<p>{{text}} : {{status}}</p>
</div>
</template>
<script>
export default {
props: {
text: {
type: String,
default: ''
},
status: {
type: Boolean,
default: false
}
}
}
</script>
TestToggle.vue:
<template>
<div>
<p>Active: {{activeTab}}</p>
<Toggle v-on:click = "activeTab = 1"
text="Toggle 1 "
v-bind:status="activeTab === 1"/>
<Toggle v-on:click = "activeTab = 2"
text = "Toggle 2"
v-bind:status="activeTab === 2"/>
</div>
</template>
<script>
import Toggle from '.././toggle-so/Toggle.vue';
export default {
components: {Toggle},
data: function () {
return {
activeTab: 1
}
},
methods: {
}
}
</script>
In Toggle.vue, status is declared as a prop, so you should not modify it:
<template>
<div class="rounded-full m-5 w-40
flex justify-center
p-2 cursor-pointer"
:class = "status ? 'bg-green-700
hover:bg-green-600' :
'bg-red-700
hover:bg-red-600'"
<p>{{text}} : {{status}}</p>
</div>
</template>
but pass it to Toggle.vue from TestToggle.vue:
<template>
<div>
<p>Active: {{activeTab}}</p>
<Toggle v-on:click.native = "activeTab = 1"
text="Toggle 1 "
v-bind:status="activeTab === 1"/>
<Toggle v-on:click.native = "activeTab = 2"
text = "Toggle 2"
v-bind:status="activeTab === 2"/>
</div>
</template>
If you change the status in Toggle.vue, you make it independent of every other Toggle, but if you want a radio button behavior, each status is dependent of other statuses. That's why you need to manage if from the parent component.
You also need to use the native event modifier to listen to the div click of the children.
I made a simple JSFiddle to show a working example.
I currently have two active themes using Tailwind light-theme and dark-theme but I can't make it work within an external button component, just with the code and function inside the view.
This is just an example where the function inside the view is acceptable but I have a "real world" case where I need it working from the component, changing the class between dark/light mode.
Here is my "Home.vue" file importing ButtonA and ButtomB:
<template>
<div :class="theme" class="bg-background-primary">
<h1 class="text-4xl text-typo-primary">Test title</h1>
<!-- Inside Home.vue - WORKING -->
<button class="border border-gray-400 bg-blue-500 hover:bg-blue-700 text-white p-2 rounded" #click="toggleThemeOne()">Toggle Dark/Light</button>
<!-- Component with function outside - WORKING -->
<ButtonA msg="From component" #click.native="toggleThemeTwo()" />
<ButtonB msg="Full from component" />
</div>
</template>
<script>
// # is an alias to /src
import ButtonA from '#/components/ButtonA.vue'
import ButtonB from '#/components/ButtonB.vue'
export default {
name: 'Home',
components: {
ButtonA,
ButtonB
},
data() {
return {
theme: 'theme-light',
}
},
methods: {
toggleThemeOne() {
this.theme = this.theme === 'theme-light' ? 'theme-dark' : 'theme-light'
localStorage.setItem('theme', this.theme)
console.log('toggleThemeOne working');
console.log(this.theme)
},
toggleThemeTwo() {
this.theme = this.theme === 'theme-light' ? 'theme-dark' : 'theme-light'
localStorage.setItem('theme', this.theme)
console.log('toggleThemeTwo working');
console.log(this.theme)
},
}
}
</script>
Home.vue has a working button that's changing the theme
ButtonA
It has the HTML only and the function applied on the component
<template>
<div>
<button class="border border-gray-400 bg-blue-500 hover:bg-blue-700 text-white p-2 rounded"> {{ msg }} </button>
</div>
</template>
<script>
export default {
name: "ButtonComp",
props: [
'msg'
]
}
</script>
ButtonB
<template>
<div>
<button
class="border border-gray-400 bg-blue-500 hover:bg-blue-700 text-white p-2 rounded"
#click="toggleThemeTree()"
> {{ msg }} </button>
</div>
</template>
<script>
export default {
name: "ButtonComp",
props: [
'msg'
],
methods: {
toggleThemeTree() {
this.theme = this.theme === 'theme-light' ? 'theme-dark' : 'theme-light'
localStorage.setItem('theme', this.theme)
console.log('toggleThemeTree working');
console.log(this.theme)
},
},
}
</script>
This is the one that's not working. the function should change the :class on Home.vue but I only get the values on the console and the :class isn't working.
I did try with $emit and computed property before but It didn't work.
You should pass theme to ButtonB component in Home.vue:
<ButtonB msg="Full from component" :theme.sync="theme" />
Then in ButtonB component, emit the value back to parent on click:
<script>
export default {
name: "ButtonComp",
props: [
'msg',
'theme'
],
methods: {
toggleThemeTree() {
let theme = this.theme === 'theme-light' ? 'theme-dark' : 'theme-light' // Do not change this.theme directly
localStorage.setItem('theme', theme)
this.$emit('update:theme', theme)
},
},
}
</script>