In my case in desktop mode user can hover over the element to reveal the menu. User can also click the element that makes a request to the server.
In touchscreen mode, I would like the click to be revealing the menu instead of making a request.
It seems like impossible task to achieve unless I start going outside of vue and changing css directly from DOM level.
<script setup>
import { ref } from "vue";
const text = ref("whatever");
const isEditing = ref(false);
function sendToServer() {
console.log("Sending request of ", text.value, " to server.")
isEditing.value = false;
}
</script>
<template>
<div class="wrapper">
<span v-show="!isEditing" #click="sendToServer" #touchstart.prevent> {{ text }}</span>
<input v-show="isEditing" v-model="text">
<span #click="sendToServer" class="icon">➡️</span>
<span #click="isEditing = !isEditing" class="icon">✏️</span>
</div>
</template>
<style>
.icon {
opacity: 0;
}
.wrapper:hover .icon {
opacity: 1;
}
.wrapper > span {
cursor: pointer;
}
</style>
Vue SFC playground link
"It seems like impossible task to achieve unless I start going outside of vue and changing css directly from DOM level."
I would say it is impossible to achieve with CSS-only, but Vue (IMHO) seems cable and well suited to handle that.
Instead of using css :hover you will need to manage the visibility using state (isSelected in example)
then use #touchstart to toggle visibility - this will only be available to touch devices and mouse events #mouseover to show and #mouseout to hide.
there may be some more finessing to handle some edge cases, but this is how I'd implement it. For example, you may need a global touch event listener to hide when user clicks outside of the text.
<script setup>
import { ref } from "vue";
const text = ref("whatever");
const isEditing = ref(false);
const isSelected = ref(false);
function toggleIsSelected(){
isSelected.value = !isSelected.value
}
function unselect(){
isSelected.value = false;
}
function sendToServer() {
console.log("Sending request of ", text.value, " to server.")
isEditing.value = false;
}
</script>
<template>
<div class="wrapper">
<span v-show="!isEditing"
#click="sendToServer"
#touchstart=toggleIsSelected
#mouseover=toggleIsSelected
#mouseout=unselect
> {{ text }}</span>
<input v-show="isEditing" v-model="text">
<span v-show=isSelected #click="sendToServer" class="icon">➡️</span>
<span v-show=isSelected #click="isEditing = !isEditing" class="icon">✏️</span>
</div>
</template>
<style>
.wrapper > span {
cursor: pointer;
}
</style>
Related
When I detect a specific width via class binding to make a mobile responsive web and swap between templates using display:none, the console keeps giving me an error.
I'm having trouble using 2 YouTube templates. If you use only one YouTube, the error disappears.
Error type: ReferenceError: YT is not defined. YouTube.vue?6574:118:25
If replacing the class binding template I used is a problem, is there another way?
<template>
<div class="container">
<div
class="youtube_container"
:class="{'youtube_container on' : WidthActive}">
<h3 class="youtube_title">
desktop video
</h3>
<!-- add -->
<component
is="script"
id="youtube-iframe-js-api-script"
src="https://www.youtube.com/iframe_api"
/>
<YouTube
src="https://youtu.be/GQmO52f26Ws"
width="540"
height="360" />
</div>
<div
class="youtube_container_mobile"
:class="{'youtube_container_mobile on' : WidthmobileActive}">
<h3 class="youtube_title">
mobile video
</h3>
<YouTube
src="https://youtu.be/GQmO52f26Ws"
width="280"
height="300"
class="youtube_padding" />
</div>
</div>
</template>
<script setup>
import YouTube from 'vue3-youtube'
import { ref } from 'vue'
const WidthActive = ref(true)
const WidthPostion = ref(0)
//mobile
const WidthmobileActive = ref(false)
const WidthmobilePostion = ref(0)
window.addEventListener('resize', () => {
WidthmobileActive.value = WidthmobilePostion.value > 590
WidthmobilePostion.value = window.innerWidth
})
window.addEventListener('resize', () => {
WidthActive.value = WidthPostion.value < 591
WidthPostion.value = window.innerWidth
})
</script>
<style>
.youtube_container{
display: grid;
}
.youtube_container.on{
display: none;
}
.youtube_container_mobile{
display: grid;
}
.youtube_container_mobile.on{
display: none;
}
</style>
Update
Reversing ref(false) and ref(true) solved the issue.
This is a known issue apparently, maybe give a try to that one
<component
is="script"
id="youtube-iframe-js-api-script"
src="https://www.youtube.com/iframe_api"
/>
Above your <YouTube> component.
Or maybe try to give it 2 differents refs aka ref="youtube" and ref="youtube2", not sure.
Otherwise, add the following to an apex file
<script id="youtube-iframe-js-api-script" src="https://www.youtube.com/iframe_api"></script>
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.
In my Vue 3 application, I have nested components five levels deep. The top-level component, TopCom and the bottom-level component MostInnerCom both have a #keydown handler.
If MostInnerCom has focus and a key is pressed that MostInnerCom cannot handle, then that event shall be handled by TopCom. How can this be achieved?
I have created a very simple demo using nested divs instead of nested components. Please see this small demo in codepen.
Here is the relevant code snippet, which doesn't work:
Vue.createApp({
setup() {
const keycode = Vue.ref('')
function onKeydownForApp(e) {
keycode.value = '***'
}
function onKeydownForButton(e) {
if (e.code === 'KeyA') {
parent.dispatchEvent(new KeyboardEvent(e.type, e))
} else {
keycode.value = e.code
}
}
return {
keycode,
onKeydownForApp,
onKeydownForButton
}
}
}).mount('#app')
<script src="https://unpkg.com/vue#next"></script>
<main id="app">
<div #keydown="onKeydownForApp" tabindex="-1">
<input type=text>
<div>
<button #keydown.stop="onKeydownForButton">- K -</button>
keycode={{keycode}}
</div>
</div>
</main>
Global variable parent refers to window.parent, i.e. the parent document, not the parent HTML element. A solution would be to add an id attribute to the div that surrounds the button, e.g. <div id="parentDiv"> and then let parent refer to this div:
var parent = document.getElementById('parentDiv');
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.
In the following code, codePen demo here
child component emits a custom event changedMsg to parent which changes msg data property on the parent component. Not sure, why changedMsg does not work. It does not modify msg property of parent.
Note: In a single file setup this works, but not in the setup below which is using template tag. Not sure, why?
VueJS
var child = {
template: '#child',
props: ['parentMsg'],
methods: {
changeParentMsg() {
console.log(this.parentMsg)
this.parentMsg = 'Message was changed by CHILD'
this.$emit('changedMsg', this.parentMsg)
}
}
}
new Vue({
el: '#parent',
data() {
return {
msg: 'Hello World'
}
},
components: {
child
},
methods: {
changeMsg() {
this.msg = 'Changed Own Msg'
}
},
})
HTML
<div>
<h4>Parent</h4>
<p>{{ msg }}</p>
<button #click="changeMsg">Change Own Message</button>
<br>
<div class="child">
<h4>Child</h4>
<child :parentMsg="msg" #changedMsg= "msg = $event"></child>
</div>
</div>
</div>
<template id="child">
<div>
<button #click="changeParentMsg">Change Parnets msg</button>
</div>
</template>
CSS
#parent {
background-color: lightblue;
border: 2px solid;
width: 300px;
}
.child {
background-color: lightgray;
border: 2px solid;
margin-top: 20px
}
Thanks
Note: In a single file setup this works, but not in the setup below which is using template tag. Not sure, why?
This is explained in the docs:
Event Names
Unlike components and props, event names will never be used as variable or property names in JavaScript, so there’s no reason to use camelCase or PascalCase. Additionally, v-on event listeners inside DOM templates will be automatically transformed to lowercase (due to HTML’s case-insensitivity), so v-on:myEvent would become v-on:myevent – making myEvent impossible to listen to.
For these reasons, we recommend you always use kebab-case for event names.
So, it's not a good practice to make the logic on the listem event directive, i made this way and worked:
<child :parentMsg="msg" #changed-msg="msgChanged"></child>
</div>
</div>
on the child tag i changed the #changedMsg to kebab-case so now it's #changed-msg and made it call a function instead of do the logic on the child tag, function called msgChanged, so you need to create it on your methods section:
methods: {
changeMsg() {
this.msg = 'Changed Own Msg'
},
msgChanged(param) {
console.log(param)
}
}
hope that helps