How to set focus on input inside of v-if/v-else? - vue.js

I have an inline edit I'm working on, and when the input is active I want to set the focus on it. I think the issue though, is that it's within a v-if/v-else so the ref isn't being properly set.
I currently get this error:
Uncaught (in promise) TypeError: Cannot read properties of null (reading 'focus')
Relevant code:
<template>
<SlideUpTransition>
<q-btn
v-if="!isAdding"
flat
class="ans-new-vote noselect full-width q-mt-md"
color="primary"
:ripple="false"
label="+ Create New Answer"
#keydown="toggleIsAdding(true)"
#click="toggleIsAdding(true)" />
<q-input
v-else
ref="target"
v-model="newAnswer"
v-on-click-outside="() => toggleIsAdding(false)"
class="q-mt-md"
label="New Answer"
outlined
dense
#keyup.escape="handleEscape"
#keyup.enter="handleSubmitAnswer"
#keyup.tab="handleSubmitAnswer"
#blur="handleSubmitAnswer">
<template #append>
<q-btn
icon="bi-check-circle"
flat
:ripple="false"
color="primary"
#click="handleSubmitAnswer" />
</template>
</q-input>
</SlideUpTransition>
</template>
<script lang="ts" setup>
import { computed, ref, nextTick } from 'vue';
import { vOnClickOutside } from '#vueuse/components';
import SlideUpTransition from '#/components/transitions/SlideUpTransition.vue';
const isAdding = ref(false);
const target = ref<any>(null);
const newAnswer = ref('');
const toggleIsAdding = (val: boolean) => {
isAdding.value = val;
if (isAdding.value) {
nextTick(() => {
target.value.focus(); // <-- target.value is still null
});
}
};
</script
Is there a better way to handle focusing an input that isn't part of the DOM yet?

Since you are using quasar, I think the best way is to use either QInput's autofocus property, or QForm's autofocus.
Personally, I usually wrap a QInput in a QForm, and set autofocus on the input that I want to be focused. The added benefit is that you don't need to listen to #keyup.enter, but can instead use the form's #submit event (a little bit of future-proofing if you also add a submit button).

Related

Passing a prop to child component isn't working on Vue 3

I have this simple file:
<script setup>
import { ref } from 'vue'
import TheHeader from '#/components/_headerbar/TheHeader.vue'
import TheSidebar from '#/components/_sidebar/TheSidebar.vue'
const sidebarState = ref(false)
const onSidebarToggle = () => {
sidebarState.value = !sidebarState.value
}
</script>
<template>
<QLayout view="hHh lpR fFf">
<TheHeader #toggle-sidebar="onSidebarToggle" />
<TheSidebar :sidebar-state="sidebarState.value" />
<QPageContainer>
<RouterView v-slot="{ Component }">
<component :is="Component" />
</RouterView>
</QPageContainer>
</QLayout>
</template>
The sidebarState variable here updates just fine everytime the event toggle-sidebar is fired, but the prop that recieve its value never updates and I just don't know what is happening.
This is the TheSidebar.vue file:
<script setup>
const props = defineProps({
sidebarState: {
type: Boolean,
default: true
}
})
</script>
<template>
<QDrawer
:model-value="props.sidebarState"
show-if-above
side="left"
bordered
>
content
</QDrawer>
</template>
Debugging here I can tell the sidebarState prop from TheSidebar.vue file just never changes, even though the data prop of TheHeader.vue sidebarState changes just normally.
What am I doing wrong?
You shouldn't use .value in template with refs of top-level properties. The value is automatically unwrapped for you (note: make sure the toggle in the top left of the docs is switched from "Options" to "Composition" for link to correctly work).
Simply remove .value and your code should work
<TheSidebar :sidebar-state="sidebarState" />

ESLINT error 'props' is assigned a value but never used

I understand ESLINT helping to clean up my code and find it useful however I have found a scenario I am unable to resolve without and dummy console.log statement.
In the scenario, I receive a prop containing modelValue however only modelValue is updated in an event and ESLINT complains that props is defined but never accessed.
<template>
<q-header bordered class="bg-white text-black">
<q-toolbar>
<span class="lt-md">
<q-btn dense flat round icon="menu" #click="toggleLeftDrawer" />
</span>
</q-toolbar>
</q-header>
</template>
<script setup>
/*
emits
*/
const emit = defineEmits(['update:model-value'])
const toggleLeftDrawer = () => {
emit('update:model-value', !props.modelValue)
}
/*
props
*/
const props = defineProps({
modelValue: {
type: Boolean,
default: false
}
})
</script>
modelValue is a state passed down to this component, which from your shared snippet is not used anywhere as of right now.
The emits that you have in other places of your code are actual strings aka not the modelValue variable.
TLDR: modelValue != 'modelValue'

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 do I remove a Quasar q-input prop on button click?

I have a quasar q-input element that I want to enable and disable at the click of a button.
I have added a button to the input using v-slot:after but I don't know how to remove the disable prop when it is clicked.
P.S. I am using Quasar + Vue 3 + TypeScript.
<template>
<q-input
disable
type="text"
autocomplete="given-name"
label="First Name"
v-model="firstName"
>
<template v-slot:after>
<q-btn
label="Edit"
#click="handleClick"
/>
</template>
</q-input>
</template>
<script setup lang="ts">
import { ref, Ref } from 'vue';
const firstName: Ref<string | null> = ref(null);
const handleClick = () => {
//Remove the "disable" prop from the q-input element
}
</script>
In VUE you must think backwards.
Put in input :disable="myDisable"
Create the ref myDisable in the setup and put your initial value that you need true or false
In your handleClick change the value of myDisable =!myDisable

Vuetify v-dialog do not show in spite of value attribute equal to true

I am using vuex store state to show/hide Vuetify v-dialog in my NuxtJS app. Following are the code excerpt:
Vuex Store:
export const state = () => ({
dialogOpen: false
});
export const mutations = {
setDialogToOpen(state) {
state.dialogOpen = true;
},
setDialogToClosed(state) {
state.dialogOpen = false;
}
};
export const getters = {
isDialogOpen: state => {
return state.dialogOpen;
}
};
Dialog Component:
<v-dialog
v-model="isDialogOpen"
#input="setDialogToClosed"
max-width="600px"
class="pa-0 ma-0"
>
...
</v-dialog>
computed: {
...mapGetters("store", ["isDialogOpen"])
},
methods: {
...mapMutations({
setDialogToClosed: "store/setDialogToClosed"
})
}
This all works fine but when I redirect from one page to another page like below it stops working.
this.$router.push("/videos/" + id);
I hit browser refresh and it starts working again. Using the Chrome Vue dev tools, I can see the state is set correctly in the store as well as in the v-dialog value property as shown below
In Vuex store
In v-dialog component property
Yet the dialog is not visible. Any clue what is happening?
I am using NuxtJS 2.10.2 and #nuxtJS/Vuetify plugin 1.9.0
Issue was due to v-dialog not being wrapped inside v-app
My code was structured like this
default layout
<template>
<div>
<v-dialog
v-model="isDialogOpen"
#input="setDialogToClosed"
max-width="600px"
class="pa-0 ma-0"
>
<nuxt />
</div>
</template>
Below is the code for index page which replaces nuxt tag above at runtime.
<template>
<v-app>
<v-content>
...
</v-content>
</v-app>
</template>
So, in the final code v-dialog was not wrapped inside v-app. Moving v-app tag to default layout fixed it
<template>
<v-app>
<v-dialog
v-model="isDialogOpen"
#input="setDialogToClosed"
max-width="600px"
class="pa-0 ma-0"
>
<nuxt />
</v-app>
</template>