Open file dialogbox in vue composition API - vue.js

I am trying to open the Select file dialog box when clicking on the button, It is possible using this.$refs.fileInput.click() in VUE, but this is not working in composition API.
Here is the code for reference: https://codepen.io/imjatin/pen/zYvGpBq
Script
const { ref, computed, watch, onMounted, context } = vueCompositionApi;
Vue.config.productionTip = false;
Vue.use(vueCompositionApi.default);
new Vue({
setup(context) {
const fileInput = ref(null);
const trigger = () => {
fileInput.click()
};
// lifecycle
onMounted(() => {
});
// expose bindings on render context
return {
trigger,fileInput
};
}
}).$mount('#app');
Template
<div id="app">
<div>
<div #click="trigger" class="trigger">Click me</div>
<input type="file" ref="fileInput"/>
</div>
</div>
Thank you.

Have you tried to access it using context.refs.fileInput.click();?
Don't forget that it's setup(props, context) and not setup(context).
Try my edit here: https://codepen.io/ziszo/pen/oNxbvWW
Good luck! :)

I'm working in Vue 3 CLI and have tried several different recommendations and found the following to be the most reliable.
<template>
<input class="btnFileLoad" type="file" ref="oFileHandler" #change="LoadMethod($event)" />
<button class="btn" #click="fileLoad">Load</button>
</template>
<script>
import {ref} from "vue";
export default{
setup(){
const hiddenFileElement = ref({})
return {hiddenFileElement }
}
methods:{
fileLoad() {
this.hiddenFileElement = this.$refs.oFileHandler;
this.hiddenFileElement.click();
},
}
}
</script>
<style>
.btn{ background-color:blue; color:white; }
.btnFileLoad{ display:none }
</style>
I also discovered in Chrome that if the call from the button element to the hidden file handler takes to long, an error message that reads "File chooser dialog can only be shown with a user activation." is displayed in the source view. By defining the hiddenFileElement in setup the problem went away.

Related

Passing a value via prop, editing it and saving the new value vue 3

I am trying to pass a value into a child component. The child component will then preform the save operation. The parent doesn't need to know anything about it. I am able to pass in the object but not save its updated form.
Parent
<template>
<div v-show="isOpened">
<EditModal #toggle="closeModal" #update:todo="submitUpdate($event)"
:updatedText="editText" :todo="modalPost" />
</div>
</template>
<script setup lang="ts">
import Post from "../components/Post.vue";
import { api } from "../lib/api";
import { ref } from "vue";
import { onMounted } from "vue-demi";
import EditModal from "../components/EditModal.vue";
const postArr = ref('');
const message = ref('');
let isOpened = ref(false);
let modalPost = ref('');
let editText = ref('');
function closeModal() {
isOpened.value = false
}
function openModal(value: string) {
isOpened.value = true
modalPost.value = value
}
// call posts so the table loads updated item
function submitUpdate(value: any) {
console.log("called update in parent " + value)
editText.value = value
posts()
}
</script>
Child EditModal
<template>
<div>
<div>
<textarea id="updateTextArea" rows="10" :value="props.todo.post"></textarea>
</div>
<!-- Modal footer -->
<div>
<button data-modal-toggle="defaultModal" type="button"
#click="update(props.todo.blogId, props.todo.post)">Save</button>
</div>
</div>
</template>
<script lang="ts" setup>
import { api } from "../lib/api";
import { reactive, ref } from "vue";
const props = defineProps({
todo: String,
updatedText: String,
})
const emit = defineEmits(
['toggle','update:todo']
);
function setIsOpened(value: boolean) {
emit('toggle', value);
}
function update(id: string, value: string) {
console.log('the value ' + value)
try {
api.updateBlog(id, value).then( res => {
emit('update:todo', value)
emit('toggle', false);
})
} catch (e) {
console.log('Error while updating post: '+ e)
}
}
</script>
I know the props are read only, therefore I tried to copy it I can only have one model.
I do not see the reason I should $emit to the parent and pass something to another variable to pass back to the child.
I am trying to pass in text to a modal component where it can edit the text and the child component saves it.
Advice?
First, your component should be named EditTodo no EditModal because you are not editing modal. All Edit components should rewrite props to new local variables like ref or reactive, so you can work on them, they won't be read only any more.
Child EditTodo.vue
<template>
<div>
<div>
<textarea id="updateTextArea" rows="10" v-model="data.todo.title"></textarea>
</div>
<div>
<button data-modal-toggle="defaultModal" type="button" #click="update()">Save</button>
</div>
</div>
</template>
<script setup lang="ts">
import { api } from "../lib/api";
import { reactive } from "vue";
const props = defineProps<{ id: number, todo: { title: string} }>()
const emit = defineEmits(['toggle']);
const data = reactive({ ...props })
// I assuming api.updateBlog will update data in database
// so job here should be done just need toogle false modal
// Your array with todos might be not updated but here is
// no place to do that. Well i dont know how your API works.
// Database i use will automaticly update arrays i updated objects
function update() {
try {
api.updateBlog(data.id, data.todo ).then(res => {
emit('toggle', false);
})
}
}
</script>
Parent
<template>
<div>
<BaseModal v-if="todo" :show="showModal">
<EditTodo :id="todo.id" :todo="todo.todo" #toggle="(value) => showModal = value"></EditTodo>
</BaseModal>
</div>
</template>
<script setup lang="ts">
const showModal = ref(false)
const todo = reactive({ id: 5, todo: { title: "Todo number 5"} })
</script>
I separated a modal object with edit form, so you can create more forms and use same modal. And here is a simple, not fully functional modal.
<template>
<div class="..."><slot></slot></div>
</template>
<script setup>
defineProps(['show'])
defineEmits(['toogle'])
</script>
You might want to close modal when user click somewhere outside of modal.

Vue Testing Library with NaiveUI

I'm using Vue 3, NaiveUI, Vitest + Vue Testing Library and got to the issue with testing component toggle on button click and conditional rendering.
Component TSample:
<template>
<n-button role="test" #click="show = !show" text size="large" type="primary">
<div data-testid="visible" v-if="show">visible</div>
<div data-testid="hidden" v-else>hidden</div>
</n-button>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
import { NButton } from 'naive-ui'
export default defineComponent({
name: 'TSample',
components: {
NButton
},
setup() {
const show = ref(true)
return {
show
}
}
})
</script>
The test case I have:
import { render, waitFor } from '#testing-library/vue'
import TSample from './TSample.vue'
import userEvent from '#testing-library/user-event'
describe('Tests TSample component', () => {
it('toggles between visible and hidden text inside the button', async () => {
const user = userEvent.setup()
const { getByText, queryByText, getByRole } = render(TSample)
expect(getByRole('test')).toBeInTheDocument()
expect(getByText(/visible/i)).toBeInTheDocument()
expect(queryByText('hidden')).not.toBeInTheDocument()
await user.click(getByRole('test'))
await waitFor(() => expect(queryByText(/hidden/i)).toBeInTheDocument()) <-- fails
})
})
The error:
<transition-stub />
<span
class="n-button__content"
>
<div
data-testid="visible"
>
visible
</div>
</span>
</button>
</div>
</body>
</html>...Error: expect(received).toBeInTheDocument()
received value must be an HTMLElement or an SVGElement.
Moreover in Testing Preview I get:
<button
class="n-button n-button--primary-type n-button--large-type mx-4"
tabindex="0"
type="button"
disabled="false"
role="test"
>
<transition-stub>
</transition-stub><span class="n-button__content">
<div data-testid="visible">visible</div>
</span>
</button>
a button, which makes it more confusing to me... Same situation happened when I replaced waitFor with nextTick from Vue, the component didn't do a toggle at all on click.
What works but isn't acceptable
When I changed the n-button to just button, the test passed and divs are toggled, but that's not the goal of this component. The component isn't supposed to be changed.
What I have tried:
I tried different approaches with reaching the div that contains hidden text. Either it was like above - queryByText/getByText or getByTestId, but test fails at the same point.
Also followed with similar approach shown at Testing Library - Disappearance Sample
but doesn't work in my case above.
What actually is going on and how can I test the change on click with such third-party components?
If more info is needed/something is missing, also let me know, I'll update the question.
Any suggestions, explanations - much appreciated.

How to test "errorComponent" in "defineAsyncComponent" in Vue?

I was learning about Async Components in Vue. Unfortunately in that documentation Vue did not show any example of using Async Components in the <template> part of a Vue SFC. So after searching on the web and reading some articles like this one and also this one, I tried to use this code to my Vue component:
<!-- AsyncCompo.vue -->
<template>
<h1>this is async component</h1>
<button #click="show = true">login show</button>
<div v-if="show">
<LoginPopup></LoginPopup>
</div>
</template>
<script>
import { defineAsyncComponent, ref } from 'vue';
import ErrorCompo from "#/components/ErrorCompo.vue";
const LoginPopup = defineAsyncComponent({
loader: () => import('#/components/LoginPopup.vue'),
/* -------------------------- */
/* the part for error handling */
/* -------------------------- */
errorComponent: ErrorCompo,
timeout: 10
}
)
export default {
components: {
LoginPopup,
},
setup() {
const show = ref(false);
return {
show,
}
}, // end of setup
}
</script>
And here is the code of my Error component:
<!-- ErrorCompo.vue -->
<template>
<h5>error component</h5>
</template>
Also here is the code of my Route that uses this component:
<!-- test.vue -->
<template>
<h1>this is test view</h1>
<AsyncCompo></AsyncCompo>
</template>
<script>
import AsyncCompo from '../components/AsyncCompo.vue'
export default {
components: {
AsyncCompo
}
}
</script>
And finally the code of my actual Async component called LoginPopup.vue that must be rendered after clicking the button:
<!-- LoginPopup.vue -->
<template>
<div v-if="show1">
<h2>this is LoginPopup component</h2>
<p>{{retArticle}}</p>
</div>
</template>
<script>
import { ref, onMounted } from 'vue';
export default {
setup() {
const getArticleInfo = async () => {
// wait 3 seconds to mimic API call
await new Promise(resolve => setTimeout(resolve, 3000));
const article = "my article"
return article
}
const show1 = ref(false);
const retArticle = ref(null);
onMounted(
async () => {
retArticle.value = await getArticleInfo();
show1.value = true;
}
);
return {
retArticle,
show1
}
}
}
</script>
When I comment the part below from AsyncCompo.vue everything works correctly and my component loads after 3s when I clicks the button:
errorComponent: ErrorCompo,
timeout: 10
But I want to test the error situation that Vue says in my component. I am not sure that my code implementation is absolutely true, but with code above when I use the errorComponent, I receive this warning and error in my console:
I also know that we could handle these situations with <Suspense> component, but because my goal is learning Async Components, I don't want to use them here. Could anyone please help me that how I can see and test my "error component" in the page? is my code wrong or I must do something intentionally to make an error? I don't know but some articles said that with decreasing timeout option I could see error component, but for me it gives that error.

vue test utils mount option data not working with compostion api

When i am learning the vue-test-utils from the offical site conditional-rendering.
I tried to change the option api to composition api.
It seems like the mount option data not working with the composition api.
Nav.vue Composition API test FAIL
<template>
<div>
<a id="profile" href="/profile">My Profile</a>
<a v-if="admin" id="admin" href="/admin">Admin</a>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const admin = ref(false)
</script>
Nav.vue Option API test PASS
<template>
<div>
<a id="profile" href="/profile">My Profile</a>
<a v-if="admin" id="admin" href="/admin">Admin</a>
</div>
</template>
<script>
export default {
data() {
return {
admin: false,
}
},
}
</script>
Nav.spec.js test
test('renders an admin link', () => {
const wrapper = mount(Nav, {
data() {
return {
admin: true
}
}
})
// Again, by using `get()` we are implicitly asserting that
// the element exists.
expect(wrapper.get('#admin').text()).toEqual('Admin')
})
I found a solution but I don't know if it's a good solution
test("renders an admin link", async () => {
const wrapper = mount(Nav);
wrapper.vm.admin = true;
await nextTick();
expect(wrapper.get("#admin").text()).toEqual("Admin");
});

Passing props to Vue root instance via attributes on element the app is mounted on

I am terribly new to Vue, so forgive me if my terminology is off. I have a .NET Core MVC project with small, separate vue pages. On my current page, I return a view from the controller that just has:
#model long;
<div id="faq-category" v-bind:faqCategoryId="#Model"></div>
#section Scripts {
<script src="~/scripts/js/faqCategory.js"></script>
}
Where I send in the id of the item this page will go grab and create the edit form for. faqCategory.js is the compiled vue app. I need to pass in the long parameter to the vue app on initialization, so it can go fetch the full object. I mount it with a main.ts like:
import { createApp } from 'vue'
import FaqCategoryPage from './FaqCategoryPage.vue'
createApp(FaqCategoryPage)
.mount('#faq-category');
How can I get my faqCategoryId into my vue app to kick off the initialization and load the object? My v-bind attempt seems to not work - I have a #Prop(Number) readonly faqCategoryId: number = 0; on the vue component, but it is always 0.
My FaqCategoryPAge.vue script is simply:
<script lang="ts">
import { Options, Vue } from "vue-class-component";
import { Prop } from 'vue-property-decorator'
import Card from "#/Card.vue";
import axios from "axios";
import FaqCategory from "../shared/FaqCategory";
#Options({
components: {
Card,
},
})
export default class FaqCategoryPage extends Vue {
#Prop(Number) readonly faqCategoryId: number = 0;
mounted() {
console.log(this.faqCategoryId);
}
}
</script>
It seems passing props to root instance vie attributes placed on element the app is mounting on is not supported
You can solve it using data- attributes easily
Vue 2
const mountEl = document.querySelector("#app");
new Vue({
propsData: { ...mountEl.dataset },
props: ["message"]
}).$mount("#app");
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app" data-message="Hello from HTML">
{{ message }}
</div>
Vue 3
const mountEl = document.querySelector("#app");
Vue.createApp({
props: ["message"]
}, { ...mountEl.dataset }).mount("#app");
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/3.0.0/vue.global.js"></script>
<div id="app" data-message="Hello from HTML">
{{ message }}
</div>
Biggest disadvantage of this is that everything taken from data- attributes is a string so if your component expects something else (Number, Boolean etc) you need to make conversion yourself.
One more option of course is pushing your component one level down. As long as you use v-bind (:counter), proper JS type is passed into the component:
Vue.createApp({
components: {
MyComponent: {
props: {
message: String,
counter: Number
},
template: '<div> {{ message }} (counter: {{ counter }}) </div>'
}
},
}).mount("#app");
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/3.0.0/vue.global.js"></script>
<div id="app">
<my-component :message="'Hello from HTML'" :counter="10" />
</div>
Just an idea (not a real problem)
Not really sure but it can be a problem with Props casing
HTML attribute names are case-insensitive, so browsers will interpret any uppercase characters as lowercase. That means when you're using in-DOM templates, camelCased prop names need to use their kebab-cased (hyphen-delimited) equivalents
Try to change your MVC view into this:
<div id="faq-category" v-bind:faq-category-id="#Model"></div>
Further to Michal LevĂ˝'s answer regarding Vue 3, you can also implement that pattern with a Single File Component:
app.html
<div id="app" data-message="My Message"/>
app.js
import { createApp } from 'vue';
import MyComponent from './my-component.vue';
const mountEl = document.querySelector("#app");
Vue.createApp(MyComponent, { ...mountEl.dataset }).mount("#app");
my-component.vue
<template>
{{ message }}
</template>
<script>
export default {
props: {
message: String
}
};
</script>
Or you could even grab data from anywhere on the parent HTML page, eg:
app.html
<h1>My Message</h1>
<div id="app"/>
app.js
import { createApp } from 'vue';
import MyComponent from './my-component.vue';
const message = document.querySelector('h1').innerText;
Vue.createApp(MyComponent, { message }).mount("#app");
my-component.vue
<template>
{{ message }}
</template>
<script>
export default {
props: {
message: String
}
};
</script>
To answer TheStoryCoder's question: you would need to use a data prop. My answers above demonstrate how to pass a value from the parent DOM to the Vue app when it is mounted. If you wanted to then change the value of message after it was mounted, you would need to do something like this (I've called the data prop myMessage for clarity, but you could also just use the same prop name message):
<template>
{{ myMessage }}
<button #click="myMessage = 'foo'">Foo me</button>
</template>
<script>
export default {
props: {
message: String
},
data() {
return {
myMessage: this.message
}
}
};
</script>
So I'm not at all familiar with .NET and what model does, but Vue will treat the DOM element as a placeholder only and it does not extend to it the same functionality as the components within the app have.
so v-bind is not going to work, even without the value being reactive, the option is not there to do it.
you could try a hack to access the value and assign to a data such as...
const app = Vue.createApp({
data(){
return {
faqCategoryId: null
}
},
mounted() {
const props = ["faqCategoryId"]
const el = this.$el.parentElement;
props.forEach((key) => {
const val = el.getAttribute(key);
if(val !== null) this[key] = (val);
})
}
})
app.mount('#app')
<script src="https://unpkg.com/vue#3.0.0-rc.11/dist/vue.global.prod.js"></script>
<div id="app" faqCategoryId="12">
<h1>Faq Category Id: {{faqCategoryId}}</h1>
</div>
where you get the value from the html dom element, and assign to a data. The reason I'm suggesting data instead of props is that props are setup to be write only, so you wouldn't be able to override them, so instead I've used a variable props to define the props to look for in the dom element.
Another option
is to use inject/provide
it's easier to just use js to provide the variable, but assuming you want to use this in an mvc framework, so that it is managed through the view only. In addition, you can make it simpler by picking the exact attributes you want to pass to the application, but this provides a better "framework" for reuse.
const mount = ($el) => {
const app = Vue.createApp({
inject: {
faqCategoryId: {
default: 'optional'
},
},
})
const el = document.querySelector($el)
Object.keys(app._component.inject).forEach(key => {
if (el.getAttribute(key) !== null) {
app.provide(key, el.getAttribute(key))
}
})
app.mount('#app')
}
mount('#app')
<script src="https://unpkg.com/vue#3.0.0-rc.11/dist/vue.global.prod.js"></script>
<div id="app" faqCategoryId="66">
<h1>Faq Category Id: {{faqCategoryId}}</h1>
</div>
As i tried in the following example
https://codepen.io/boussadjra/pen/vYGvXvq
you could do :
mounted() {
console.log(this.$el.parentElement.getAttribute("faqCategoryId"));
}
All other answers might be valid, but for Vue 3 the simple way is here:
import {createApp} from 'vue'
import rootComponent from './app.vue'
let rootProps = {};
createApp(rootComponent, rootProps)
.mount('#somewhere')