Blur event is not working properly. It works if I click anywhere in the component except when clicking in the input field. If I click in the input field then outside the component, it won't trigger the blur event which closes the options list. How can I make the blur event on the outer div work after clicking on the input field and then clicking outside the component (* blur event should that be triggered if I click on the components list since it is still within the component, therefore I can't just place a blur event on the input field)
<template>
<div class="flex flex-col relative w-full">
<span v-if="label" class="font-jost-medium mb-2">{{ label }}</span>
<div>
<div #blur="showOptions = false" :tabindex="tabIndex">
<div
class="border border-[#EAEAEA] bg-white rounded-md flex flex-col w-full"
>
<div
v-if="selectedOptions.length"
class="flex flex-wrap px-4 py-2 border-b gap-2"
>
<div
v-for="option in selectedOptions"
class="border bg-secondary rounded-full py-1 px-2 flex items-center"
>
<span>{{ option.text }}</span>
<vue-feather
type="x"
class="h-3 w-3 ml-1.5 cursor-pointer"
#click="onDeleteOption(option)"
/>
</div>
</div>
<div
class="flex flex-row justify-end items-center px-4 cursor-pointer"
:class="selectedOptions.length ? 'py-2' : 'p-4'"
#click="showOptions = !showOptions"
>
<MagnifyingGlassIcon class="h-5 w-5 mr-2" />
<input
class="focus:outline-0 w-full"
type="text"
v-model="searchInput"
/>
<vue-feather type="chevron-down" class="h-5 w-5" />
</div>
</div>
<div v-if="showOptions && optionsMap.length" class="options-list">
<ul role="listbox" class="w-full overflow-auto">
<li
class="hover:bg-primary-light px-4 py-2 rounded-md cursor-pointer"
role="option"
v-for="option in optionsMap"
#mousedown="onOptionClick(option)"
>
{{ option.text }}
</li>
</ul>
</div>
<div
id="not-found"
class="absolute w-full italic text-center text-inactive-grey"
v-else-if="!optionsMap.length"
>
No records found
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType, ref, watch } from "vue";
import { IconNameTypes } from "#/types/enums/IconNameTypes";
import { AppIcon } from "#/components/base/index";
import { MagnifyingGlassIcon } from "#heroicons/vue/24/outline";
export default defineComponent({
name: "AppAutocomplete",
components: {
AppIcon,
MagnifyingGlassIcon,
},
props: {
modelValue: {
type: String,
},
label: {
type: String,
default: "",
},
tabIndex: {
type: Number,
default: 0,
},
options: {
type: Array as PropType<{ text: string; value: string }[]>,
required: true,
},
},
setup(props, { emit }) {
const showOptions = ref(false);
const optionsMap = ref(props.options);
const selectedOptions = ref<{ text: string; value: string }[]>([]);
const searchInput = ref("");
watch(searchInput, () => {
optionsMap.value = props.options.filter((option1) => {
return (
!selectedOptions.value.some((option2) => {
return option1.text === option2.text;
}) &&
option1.text.toLowerCase().includes(searchInput.value.toLowerCase())
);
});
sortOptionsMapList();
});
const onOptionClick = (option: { text: string; value: string }) => {
searchInput.value = "";
selectedOptions.value.push(option);
optionsMap.value = optionsMap.value.filter((option1) => {
return !selectedOptions.value.some((option2) => {
return option1.text === option2.text;
});
});
sortOptionsMapList();
emit("update:modelValue", option.value);
};
const onDeleteOption = (option: { text: string; value: string }) => {
selectedOptions.value = selectedOptions.value.filter((option2) => {
return option2.text !== option.text;
});
optionsMap.value.push(option);
sortOptionsMapList();
};
const sortOptionsMapList = () => {
optionsMap.value.sort(function (a, b) {
return a.text.localeCompare(b.text);
});
};
sortOptionsMapList();
document.addEventListener("click", () => {
console.log(document.activeElement);
});
return {
showOptions,
optionsMap,
searchInput,
selectedOptions,
IconNameTypes,
onOptionClick,
onDeleteOption,
};
},
});
</script>
<style scoped lang="scss">
.options-list,
#not-found {
box-shadow: 0 0 50px 0 rgb(19 19 28 / 12%);
#apply border border-[#EAEAEA] rounded-md p-4 mt-1 absolute bg-white z-10 w-full;
}
ul {
#apply max-h-52 #{!important};
}
</style>
blur is not an event that 'bubbles up' to outer elements, so it never reaches the parent div. What you want is focusout
<div #focusout="showOptions = false" :tabindex="tabIndex">
Related
I have a helper custom component that works as a multiple-choice selection. However, I am facing a problem with the ul list. I need to keep it open when the user selects one or more options. It shouldn't focus out at that time. If outside the component is clicked, it should focusout and close the list. Currently whenever a list is selected the div focuses out. I have tried to patch it but it is a buggy solution, sometimes the focusout event isn't firing. Any sort of suggestion is appreciated.
Thank you!
This is the template part of the component
<template>
<div
class="relative pb-6"
tabindex="0"
:class="{
'has-error': errorText,
success: !errorText
}"
#focusout="handleFocusOut"
>
<label
class="label block pt-0"
>
{{ label }}
</label>
<button
class="btn-primary flex w-full justify-between"
#click.prevent="onClick"
>
<span>
{{ displayedSelectedValue }}
</span>
<span class="ml-auto">
<ChevronUpIcon
v-if="showSelectOptions"
class="h-6 w-6"
/>
<ChevronDownIcon
v-else
class="h-6 w-6"
/>
</span>
</button>
<ul
v-show="showSelectOptions"
class="absolute bottom-16 z-10 max-h-72 w-full space-y-1 overflow-y-scroll
rounded border border-gray-300 bg-white px-4 py-2 shadow-xl"
>
<li
v-for="item in items"
:key="item"
>
<label
:for="item"
class="block cursor-pointer"
>
<input
:id="item"
type="checkbox"
class="cursor-pointer bg-gray-500"
:name="item"
:checked="isItemSelected(item)"
#input="onSelectItem(item)"
>
<span
class="mx-4"
>
{{ item }}
</span>
</label>
</li>
</ul>
<p
class="help-message absolute p-1 text-sm"
>
<template
v-if="errorText"
>
{{ errorText }}
</template>
</p>
</div>
</template>
and this is the script part of the component:
<script lang="ts" setup>
import {
ChevronDownIcon,
ChevronUpIcon,
XIcon,
} from '#heroicons/vue/outline';
import {
ref,
watch,
PropType,
computed,
} from 'vue';
const emits = defineEmits<{
(e: 'blur', value: string[]): void;
(e: 'select', value: string[]): void;
}>();
const props = defineProps({
label: {
type: String,
required: true,
},
multiple: {
type: Boolean,
default: false,
},
value: {
type: Array as PropType<string[]>,
required: true,
},
items: {
type: Array as PropType<string[]>,
required: true,
},
errorText: {
type: String,
default: '',
},
});
watch(() => props.value, () => {
selectedValue.value = props.value;
});
const showSelectOptions = ref(false);
const selectedValue = ref(props.value);
const isListClicked = ref(false);
const displayedSelectedValue = computed(() => selectedValue.value.join(', '));
let inputBlurTimeoutId: number | undefined;
function isItemSelected(item: string) {
return selectedValue.value.includes(item);
}
function onClick(e) {
if (inputBlurTimeoutId) {
window.clearTimeout(inputBlurTimeoutId);
}
showSelectOptions.value = !showSelectOptions.value;
}
function handleFocusOut() {
console.log('focus out');
if (inputBlurTimeoutId) {
window.clearTimeout(inputBlurTimeoutId);
}
inputBlurTimeoutId = window.setTimeout(() => {
if (isListClicked.value) {
isListClicked.value = false;
return;
}
showSelectOptions.value = false;
}, 1 * 500);
}
function onSelectItem(item: string) {
isListClicked.value = true;
const itemIndex = selectedValue.value.indexOf(item);
if (itemIndex !== -1) {
selectedValue.value.splice(itemIndex, 1);
} else {
selectedValue.value.push(item);
}
emits('select', selectedValue.value);
}
</script>
I have create a refinement component that sends data to its parent and it works correctly. after data goes to parent component, it will save to an array with useState. array value is ok when data from child component is incremental, but when the value from child component is decremented, the array value is not correct.
Here's the code:
Refinement.jsx
import { Fragment, useState } from "react";
const Refinement = (props) => {
let [fileTypeSelected, setFileTypeSelected] = useState([]);
let changingState = "";
const CheckBoxHandler = (event, content) => {
if (event) {
fileTypeSelected.push(content);
changingState = "Incremental"
}
else {
fileTypeSelected = fileTypeSelected.filter((c) => c !== content);
changingState = "Decremental"
}
}
return (
<Fragment>
<div className="w-full rounded-md overflow-hidden shadow-sm border">
<div className="flex justify-between items-center px-4 py-1 bg-biscay-700 text-white border-b">
<span className="font-semibold ">{props.RefineName}</span>
</div>
{
<div className={`px-4 py-2 w-full flex flex-col gap-2 transform transition-all duration-200 ${props.RefineValue.length <= 5 ? "" : "h-36 overflow-y-scroll scrollbar"} `}>
{
props.RefineValue.map(m => {
return (
<div className="text-sm flex justify-between items-center" key={m.id}>
<span>{m.displayName}</span>
<input className="accent-sky-500 w-4 h-4 rounded-md" onChange={(event) => {
CheckBoxHandler(event.target.checked, m.displayName);
props.onDataModified({
SearchCompnent: props.RefineName,
SearchItem: fileTypeSelected,
SearchState: changingState
});
}} type="checkbox" name="" id="" />
</div>
)
})
}
</div>
}
</div>
</Fragment>
)
}
export default Refinement;
App.jsx
import { Fragment, useState } from "react";
import Refinement from "../components/Refinement";
const App = () => {
const [fileFormatRefine, setfileFormatRefine] = useState([
{ id: 0, displayName: 'PDF' },
{ id: 1, displayName: 'DOCX' },
{ id: 2, displayName: 'PPTX' },
{ id: 3, displayName: 'XLSX' },
{ id: 4, displayName: 'TXT' },
{ id: 5, displayName: 'MP3' },
{ id: 6, displayName: 'MP4' },
{ id: 7, displayName: 'CS' },
]);
const [filterFormatModifiedDate, setFilterFormatModifiedDate] = useState([]);
let FileFormatQueryTemp = [];
const FileFormatModifiedEnvent = (enterdModifiedData) => {
const modifiedData = {
...enterdModifiedData
};
FileFormatQueryTemp = [];
for (let index = 0; index < modifiedData.SearchItem.length; index++) {
FileFormatQueryTemp.push([[modifiedData.SearchCompnent, modifiedData.SearchItem[index]]]);
}
setFilterFormatModifiedDate([]);
console.log(modifiedData.SearchItem.length);
setFilterFormatModifiedDate(FileFormatQueryTemp);
}
return (
<Fragment>
<div className="w-full lg:w-5/6 mx-auto flex gap-4 mt-16 px-4">
<div className="w-1/4 col-start-1 hidden lg:flex">
<form className="flex flex-col gap-4 w-full" id="Form" onSubmit={(event) => { event.preventDefault() }}>
<Refinement onDataModified={FileFormatModifiedEnvent} MinToShow={3} RefineValue={fileFormatRefine} RefineName="File Format"></Refinement>
<button className="h-8 text-white font-semibold text-sm rounded bg-red-600" onClick={() => { console.log(filterFormatModifiedDate.length) }}>Show Array Length</button>
</form>
</div>
<div className="lg:w-3/4 w-full flex flex-col gap-2">
<div className="w-full">
<h3 className="text-xs flex items-center font-semibold uppercase mb-2">Filters
<span className="h-4 w-4 ml-2 text-xs flex items-center justify-center rounded bg-gray-200 text-black">{filterFormatModifiedDate.length}</span>
</h3>
<div className="">
{
filterFormatModifiedDate.map(f => {
return (
<p key={f[0][1]} >{f[0]}</p>
)
})
}
</div>
</div>
</div>
</div>
</Fragment>
)
}
export default App;
if you wanna change fileTypeSelected state, you should do next:
setFileTypeSelected(prevArray => [...prevArray, content])
and in else scope:
setFileTypeSelected(prevValue => prevValue.filter((c) => c !== content));
You must always change state using his function, which comes inside array as second argument
let's consider this example
const [count,setCount] = useState(0)
setCount is the only function that can change count value
I'm trying to build an application in VueJS where I'm having some checkbox that gets selected on clicking.
My UI looks like this:
If we try to select/de-select individual checkbox it is working fine. If we check/uncheck via the select all button it working perfectly fine. But when we select all and try to uncheck with the individual checkbox it is slicing the actual checkbox list
As you can see in the above image I tried unchecking the RTY option and it got removed from the child component's variables array
My Concept:
I'm having a parent element where I'm passing the selections as props to this checkbox element:
<template>
<div>
<selection-component :selected_fields="selected_fields"></selection-component>
</div>
</template>
<script>
import selectionComponent from './../selection-component'
import {eventBus} from "../../../../../models/_events";
export default {
name: "MainLayout",
components: {
selectionComponent
},
data(){
return{
selected_fields:{},
}
},
created() {
//Fetch selections via API (axios) call
//capturing events from child components (selection-component)
eventBus.$on('admin_watercraft_filter', (data) => {
if(typeof data.var_name !== 'undefined') {
if(typeof data.data !== 'undefined' && data.data!=='' && data.data.length!==0)
this.selected_fields[data.var_name] = data.data;
else if(typeof this.selected_fields[data.var_name] !== 'undefined' || data.data === '' || data.data.length===0 )
delete this.selected_fields[data.var_name];
}
else this.selected_fields = data;
console.log(this.selected_fields);
});
eventBus.$on('admin_watercraft_create', (data) => {
if(typeof this.selected_fields[data.var_name] === 'undefined') {
this.selected_fields[data.var_name] = [];
this.selected_fields[data.var_name].push(data.data);
}
else this.selected_fields[data.var_name].push(data.data);
});
eventBus.$on('admin_watercraft_remove', (data) => {
var index = _.findIndex(this.selected_fields[data.var_name], (q) => {
return q.id === data.data.id
})
if(index > -1) this.selected_fields[data.var_name].splice(index, 1);
});
}
}
</script>
And I have child component selection-component as:
<template>
<div class="mt-3 lg:mt-0">
<div class="flex justify-between items-center mb-3">
<div class="flex items-center">
<input #click="selectAll($event)" :checked="selectAllChecked" type="checkbox" class="h-6 w-6 rounded-md bg-gray-200 border-none">
<div class="ml-2">
<div class="text-sm font-normal">Select Checkbox</div>
<div class="font-extralight text-gray-400" style="font-size: 11px">Assign by checking</div>
</div>
</div>
<div class="flex">
<div class="h-8 rounded-l-lg w-8 bg-gray-100 flex">
<div class="m-auto text-sm font-semibold ">$</div>
</div>
<input v-model="price" style="width: 70px" class="h-8 px-2 text-sm font-normal rounded-r-lg border focus:outline-none focus:border-gray-400">
</div>
</div>
<div class="">
<input v-model="search" type="search" class="text-sm font-normal px-4 py-2 my-3 border-none rounded-xl w-full bg-gray-100 focus:outline-none focus:bg-gray-200" placeholder="Search...">
<div class="overflow-y-auto h-40">
<div v-for="(ele, index) in tableData" class="flex items-center justify-between my-1 pr-3">
<div class="flex items-center">
<input :value="ele.id" #click="selectData(ele, $event)" :checked="ele.selected" type="checkbox" class="h-6 w-6 rounded-md bg-gray-200 border-none">
<div class="text-xs font-normal ml-2">{{ele.name}}</div>
</div>
<div class="flex">
<div class="h-8 rounded-l-lg w-10 bg-gray-100 flex">
<div class="m-auto text-sm font-semibold ">$</div>
</div>
<input v-model="ele.price" style="width: 75px" class="h-8 px-4 text-sm font-normal rounded-r-lg border focus:outline-none focus:border-gray-400">
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import {eventBus} from "../../../../../models/_events";
export default {
name: "SelectionComponent",
props:['selected_fields'],
data(){
return{
variables: [
{id:1, name:'ABC', price:''},
{id:2, name:'QWE', price:''},
{id:3, name:'RTY', price:''},
],
var_name:'source_file',
price:''
}
},
methods:{
selectData(ele, event) {
const dataset = {
var_name: this.var_name,
data: ele
}
if(event.target.checked) eventBus.$emit('admin_watercraft_create', dataset);
else eventBus.$emit('admin_watercraft_remove', dataset);
},
selectAll(event) {
let dataSet
if(event.target.checked) dataSet = {var_name: this.var_name, data: this.variables}
else dataSet = {var_name: this.var_name, data: []}
eventBus.$emit("admin_watercraft_filter", dataSet);
},
checked(element){
if(typeof this.selected_fields[this.var_name] === 'undefined') return false;
return this.selected_fields[this.var_name].some(el => el.id === element.id);
},
},
computed: {
tableData() {
if (typeof this.variables !== 'undefined' && this.variables.length) {
return this.variables.map(a => ({
name: a.name,
id: a.id,
price: a.price,
selected: this.checked(a)
}))
}
},
selectAllChecked(){
if(typeof this.selected_fields[this.var_name] === 'undefined') return false;
return this.variables.length === this.selected_fields[this.var_name].length
}
}
}
</script>
don't know why it is splicing the actual variable inside the child component. I need to keep the state of variables array defined inside the child component.
After getting results from api call to Google books i'd like to hide the description paragraphs and have a toggle button using the css class of hidden (tailwinds css). I'm currently just console.logging the elements on the "view description" button & I'm just not sure how to target a single element after looping through the nodeList with my toggleDesc() function
React SearchBar component
import React, { useState, useEffect } from 'react';
import { FontAwesomeIcon } from '#fortawesome/react-fontawesome';
import axios from 'axios';
import { faSearch } from '#fortawesome/free-solid-svg-icons';
import SearchResult from '../search-result/search-result.component';
const SearchBar = () => {
const [searchTerm, setSearchTerm] = useState('');
const [books, setBooks] = useState({ items: [] });
useEffect(() => {
async function fetchBooks() {
const newRes = await fetch(`${API_URL}?q=${searchTerm}`);
const json = await newRes.json();
const setVis = Object.keys(json).map(item => ({
...item, isDescVisible: 'false'
}))
setBooks(setVis);
}
fetchBooks();
}, []);
const toggleDesc = (id) => {
const newBooks = books.items.map(book => book.id === id ? {...book, isDescVisible: !book.isDescVisible} : book);
console.log(newBooks);
setBooks(newBooks);
}
const onInputChange = (e) => {
setSearchTerm(e.target.value);
};
let API_URL = `https://www.googleapis.com/books/v1/volumes`;
const fetchBooks = async () => {
// Ajax call to API via axios
const result = await axios.get(`${API_URL}?q=${searchTerm}`);
setBooks(result.data);
};
// Handle submit
const onSubmitHandler = (e) => {
// prevent browser from refreshing
e.preventDefault();
// call fetch books async function
fetchBooks();
};
// Handle enter press
const handleKeyPress = (e) => {
if (e.key === 'Enter') {
e.preventDefault();
fetchBooks();
}
}
return (
<div className="search-bar p-8">
<div className="bg-white flex items-center rounded-full shadow-xl">
<input
className="rounded-l-full w-full py-4 px-6 text-gray-700 leading-tight focus:outline-none"
id="search"
type="text"
placeholder="Try 'The Hunt For Red October by Tom Clancy' "
onChange={onInputChange}
value={searchTerm}
onKeyPress={handleKeyPress}
/>
<div className="p-4">
<button
onClick={onSubmitHandler}
className="bg-blue-800 text-white rounded-full p-2 hover:bg-blue-600 focus:outline-none w-12 h-12 flex items-center justify-center"
>
<FontAwesomeIcon icon={faSearch} />
</button>
</div>
</div>
<div className='result mt-8'>
<ul>
<SearchResult books={books} toggleDesc={toggleDesc} />
</ul>
</div>
</div>
);
};
export default SearchBar;
SearchResults Component
import React from 'react';
import { LazyLoadImage } from 'react-lazy-load-image-component';
import 'react-lazy-load-image-component/src/effects/blur.css';
import './search-result.styles.scss';
const SearchResult = ({ books, toggleDesc }) => {
return (
<div className="search-result mb-6">
{books.items !== undefined &&
books.items !== null &&
books.items.map((book, index) => {
return (
<div key={index} className="book-info mb-2">
<li className="ml-4">
<div className="flex">
<LazyLoadImage
className="book-img px-4 py-2"
effect="blur"
alt={`${book.volumeInfo.title} book`}
src={`http://books.google.com/books/content?id=${book.id}&printsec=frontcover&img=1&zoom=1&source=gbs_api`}
/>
<div className="flex-1">
<h3 className="text-2xl">{book.volumeInfo.title}</h3>
<div>
<p className="flex">
<button
onClick={() => toggleDesc(book.id)}
className="bg-blue-800 mt-2 text-gray-200 rounded hover:bg-blue-400 px-4 py-3 text-sm focus:outline-none"
type="button"
>
View Description
</button>
</p>
{book.isDescVisible &&
<div
className="block border px-4 py-3 my-2 text-gray-700 desc-content"
>
<p>{book.volumeInfo.description}</p>
</div>
}
</div>
<h3 className="text-xl text-blue-800 mt-2 p-2">
Average time to read:{' '}
</h3>
</div>
</div>
<hr />
</li>
</div>
);
})}
</div>
);
};
export default SearchResult;
console
You will have to add a property to each item of your books to handle the description visibility and change it when you click the button, this is a basic example
useEffect(()=> {
fetch(url).then(res => {
const newRes = res.map(item=> ({ ...item, isDescVisible: 'false' })) // <— add the new property to all items set to false
setBooks(newRes);
})
})
<p className='flex'>
<button
onClick={() => toggleDesc(book.id)} // <—- pass the id or the whole book
className='bg-blue-800 mt-2 text-gray-200 rounded hover:bg-blue-400 px-4 py-3 text-sm focus:outline-none'
type='button'
>
View Description
</button>
</p>
//here show the desc according to the value of the property you added, no need for the hidden class
{book.isDescVisible && <div className='block border px-4 py-3 my-2 text-gray-700 desc-content'>
<p>{book.volumeInfo.description}</p>
</div>}
This function needs to be on the parent where you are setting the state
const toggleDesc = (id) => {
const newBooks = books.map(book => book.id === id ? {...book, isDescVisible: !book.isDescVisible} : book); <-- toggle the property
setBooks(newBooks); <—- save it in the state again
};
I'm trying to build a component in VueJS with input field for file type. Here is my component code:
<template>
<div class="flex-col justify-start w-full">
<div class="mt-2 block uppercase tracking-wide text-gray-700 text-xs font-bold mb-2">{{ label }}</div>
<input
class="block appearance-none w-full bg-gray-200 border border-gray-200 text-gray-700 py-3 px-4 pr-8 rounded leading-tight focus:outline-none focus:bg-white focus:border-gray-500"
:class="errorDisplay ? 'border-red-500 focus:bg-white focus:border-red-500': ''"
type="file"
:value="value" #input="emitEvent($event)"
ref="input_file"
>
<span v-if="hint" class="text-xs text-gray-400 font-medium">{{ hint }}</span>
<span v-if="errorDisplay" class="text-xs text-pink-600 font-medium">{{ errorDisplay }}</span>
</div>
</template>
<script>
export default {
name: "InputFile",
props: {
label: String,
hint: {
type: String,
default: () => ''
},
error: {
type: Array,
default: () => []
},
placeholder: String,
value: Object,
},
methods: {
emitEvent(event) {
var reader = new FileReader();
reader.readAsDataURL(event.target.files[0]);
reader.onload = () => {
const docs = {
name: event.target.files[0].name,
size: event.target.files[0].size,
lastModifiedDate: event.target.files[0].lastModifiedDate,
base64: reader.result
};
console.log(docs);
this.$emit('input', docs)
};
}
},
computed: {
errorDisplay() {
if(this.error.length)
return this.error.join(', ');
else
return '';
}
}
}
</script>
And I'm calling my component as below:
<template>
<div class="flex items-center justify-start">
<div class="w-1/2 m-2 rounded-lg shadow-lg border b-gray-400 rounded flex flex-col justify-start items-start p-6 bg-white">
<div class="border-b -m-2 mb-3 px-6 py-2 flex-none w-full justify-start text-gray-700 font-semibold"> Base Controls </div>
<input-file
label="Upload file"
v-model="upload_file"
:error="errors['upload_file']"
>
</input-file>
<div class="mt-4 text-center">
<button #click="submit()" class="inline-block px-4 py-2 rounded-lg shadow-md bg-teal-500 hover:bg-teal-400 focus:outline-none focus:shadow-outline text-white text-sm tracking-wider font-semibold">Submit</button>
</div>
</div>
</div>
</template>
<script>
import InputFile from "../Elements/Forms/Inputs/File";
export default {
name: "Forms",
components: {
InputFile,
},
data() {
return {
upload_file: '',
errors: {},
}
},
methods: {
submit() {
//Submit code...
}
}
}
</script>
But I'm always getting an error:
Error in nextTick: "InvalidStateError: Failed to set the 'value' property on 'HTMLInputElement': This input element accepts a filename, which may only be programmatically set to the empty string."
I can see my event is getting emitted and upload_file has desired value set. To overcome this I made upload_file to object but this results in error and the component is also not shown. How can I fix this?
I believe the issue comes from trying to assign to the element's 'value' property (by binding it to prop.value)
When you're dealing with file-type elements, you can't write to the value property like you can with other types.
In your custom component's template, delete the binding, :value="value"
and in its script either:
delete the prop value: Object or,
if you need to assign the value prop for v-model compatibility, assign it to File. eg: value: File
note: This will work, but you'll get a Vue warning: 'type check failed' for an invalid prop when the component is called without a supplied file.
ie...
<template>
<div class="flex-col justify-start w-full">
<div class="mt-2 block uppercase tracking-wide text-gray-700 text-xs font-bold mb-2">{{ label }}</div>
<input
class="block appearance-none w-full bg-gray-200 border border-gray-200 text-gray-700 py-3 px-4 pr-8 rounded leading-tight focus:outline-none focus:bg-white focus:border-gray-500"
:class="errorDisplay ? 'border-red-500 focus:bg-white focus:border-red-500': ''"
type="file"
#input="emitEvent($event)"
ref="input_file"
>
<span v-if="hint" class="text-xs text-gray-400 font-medium">{{ hint }}</span>
<span v-if="errorDisplay" class="text-xs text-pink-600 font-medium">{{ errorDisplay }}</span>
</div>
</template>
<script>
export default {
name: "InputFile",
props: {
label: String,
hint: {
type: String,
default: () => ''
},
error: {
type: Array,
default: () => []
},
placeholder: String,
value: File,
},
methods: {
emitEvent(event) {
var reader = new FileReader();
reader.readAsDataURL(event.target.files[0]);
reader.onload = () => {
const docs = {
name: event.target.files[0].name,
size: event.target.files[0].size,
lastModifiedDate: event.target.files[0].lastModifiedDate,
base64: reader.result
};
console.log(docs);
this.$emit('input', docs)
};
}
},
computed: {
errorDisplay() {
if(this.error.length)
return this.error.join(', ');
else
return '';
}
}
}
</script>
should be ok.