Vuetify3- Open tooltip on-click only - vuejs3

I've seen some examples of a button to copy data with a tooltip using vuetify2.
Out project is currently using vuetify3 version..
I've followed vuetify docs and here is my current component code:
<template>
<VTooltip :model-value="data.showTooltip">
<template v-slot:activator="{ props }">
<VBtn
icon
:color="properties.buttonColor"
:size="properties.size"
:elevation="elevation"
#click="copyData"
v-on:hover="data.showTooltip = false"
v-bind="props"
>
<VIcon>mdi-content-copy</VIcon>
</VBtn>
</template>
Copied to clipboard
</VTooltip>
</template>
<script setup lang="ts">
export interface CopyButtonProps {
size?: string;
elevation?: number | undefined;
data: string;
tooltipLocation?: string | undefined;
tooltipTimeoutMs?: number;
buttonColor?: string;
}
export interface CopyButtonData {
showTooltip: boolean;
}
const properties = withDefaults(defineProps<CopyButtonProps>(), {
size: "small",
elevation: 1,
tooltipLocation: "end",
buttonColor: "white transparent",
tooltipTimeoutMs: 1000,
});
const data = reactive<CopyButtonData>({
showTooltip: false,
});
function showTooltip() {
data.showTooltip = true;
setTimeout(() => {
data.showTooltip = false;
}, properties.tooltipTimeoutMs);
}
async function copyData(isTooltipActive: boolean) {
if (!properties.data) return;
await navigator.clipboard.writeText(properties.data);
showTooltip();
return true;
}
</script>
The code above makes the tooltip opens on click, but is also opening on hover, and I don't want this behavior. Do you have any suggestions to solve it?

:open-on-hover="false"

Related

Storybook Vue3 - Work with v-model in stories

I have a question regarding Storybook and Vue components with v-models. When writing a story for let's say an input component with a v-model i want a control reflecting the value of this v-model. Setting the modelValue from the control is no problem, but when using the component itself the control value stays the same. I am searching the web for a while now but i can't seem to find a solution for this.
A small example:
// InputComponent.vue
<template>
<input
type="text"
:value="modelValue"
#input="updateValue"
:class="`form-control${readonly ? '-plaintext' : ''}`"
:readonly="readonly"
/>
</template>
<script lang="ts">
export default {
name: "GcInputText"
}
</script>
<script lang="ts" setup>
defineProps({
modelValue: {
type: String,
default: null
},
readonly: {
type: Boolean,
default: false
}
});
const emit = defineEmits(['update:modelValue']);
const updateValue = (event: Event) => {
const target = event.target as HTMLInputElement;
emit('update:modelValue', target.value);
}
</script>
In Storybook:
Does anyone have a solution to make this working?
Thanks in advance!
In my case, I have a custom select input that uses a modelValue prop.
I tried this and worked for me:
at my-component.stories.js:
import { ref } from 'vue'
import MyComponent from './MyComponent.vue'
export default {
title: 'Core/MyComponent',
component: MyComponent,
argTypes: { }
}
const Template = (args) => ({
components: { MyComponent },
setup() {
let model = ref('Javascript')
const updateModel = (event) => model.value = event
return { args, model, updateModel }
},
template: '<my-component v-bind="args" :modelValue="model" #update:modelValue="updateModel" />'
})
export const Default = Template.bind({})
Default.args = {
options: [
'Javascript',
'PHP',
'Java'
]
}

Vue3 using Interface as Prop Types

I am having an issue using an Interface as a Vue3 Prop, the goal is to use the interface as a type check on the prop: (We are using Nuxt3 so alot of the imports are not listed below)
// IsProduct.ts
export interface IsProduct {
_id: string;
productName: string;
icon: string
}
// ProductDisplay.vue
<script lang="ts" setup>
import { IsProduct } from "./IsProduct";
interface Props {
product: IsProduct | string | null;
}
const {
product = null,
} = defineProps<Props>();
let productObj: IsProduct = ref(null);
let productRef: string = ref(null);
let displayText: string = ref("");
watch(
() => product,
() => {
initialize();
},
{ immediate: true }
);
function initialize() {
productRef = null;
productObj = null;
if (!product) return;
if (typeof product === "object") {
productRef = product._id;
productObj = product;
}
if (typeof product === "string") {
productRef = product;
productObj = productByRef[product]; //<-- productByRef goes to Pinia store
}
}
</script>
<template>
<span>
<span v-if="productObj">{{productObj.productName}} <fa-icon :icon="productObj.icon"></span>
</span>
</template>
// ProductList.vue
<script lang="ts" setup>
const { products } = useProductStore() // <-- list of products from Pinia store
</script>
<template>
<div>
<-- THIS WORKS because the _id is a string, no Vue warnings -->
<ProductDisplay :product="p._id" v-for="p of products" :key="p._id" />
<-- HAS VUE WARNINGS because the p is the full Object-->
<ProductDisplay :product="p" v-for="p of products" :key="p._id" />
</div>
</template>
Is there a way to have Vue understand that when a full Object is passed in that it uses the IsProduct interface?
I found a 'work around' for the example that I posted above:
// IsProduct.ts
export interface IsProduct {
_id: string;
productName: string;
icon: string
}
// ProductDisplay.vue
<script lang="ts" setup>
...
interface Props {
product: {_id: string} | string | null; <-- Change the Interface to an Object
}
...
function initialize() {
productRef = null;
productObj = null;
if (!product) return;
if (typeof product === "object") {
productRef = product._id;
productObj = product as IsProduct; <-- cast the product prop as IsProduct
}
}
...
</script>
Since the ProductDisplay.vue component is looking for either a Product Object or the string _id of a Product, in the Props I just defined an Object with _id of string since that is required. This tells the Vue compiler, 'hey this is either an object or a string' and there are no warnings in the devtools console.
Later in the code I check if this is an Object and if so, then cast the Object to the 'type of' IsProduct. This removes the Typescript warnings in VSCode.

How to correctly reference HTMLElement in VueJs component

I have simple component, that wraps text area. And I've another simple component, that renders a button. I want to set focus to text area when clicking the button.
This simplified example fails:
<template>
<MyCommand #resize="testResize" />
<TextArea ref="refElement" />
</template>
<script lang="ts">
// ...
export default defineComponent({
name: 'SimpleComponent',
setup(props, context) {
const refElement = ref<HTMLElement | null>(null)
const testResize = () => {
console.log('resize test')
if (refElement.value !== null) {
refElement.value.focus()
}
}
return {
refElement,
testResize,
}
}
</script>
TextArea is very simple component, some input normalization, oversimplified:
<template>
<textarea v-model.trim="value" />
</template>
I get "resize test" in console, so testResize method is running, but refElement is null.
When referencing component, not a HTML element, component type should be DefineComponent instead of HTMLElement.
Wrapped element could be referenced through $el property:
<template>
<MyCommand #resize="testResize" />
<TextArea ref="refElement" />
</template>
<script lang="ts">
// ...
export default defineComponent({
name: 'SimpleComponent',
setup(props, context) {
const refElement = ref<DefineComponent>()
const testResize = () => {
console.log('resize test')
if (refElement.value) {
refElement.value.$el.focus()
}
}
return {
refElement,
testResize,
}
}
</script>
I'm not sure if this is the "best practice", it looks to me as a hack. If anyone knows better solution, please comment.
You're missing the refElement in your return
return {
testResize, refElement
}
Update
If you are dealing with a component it becomes a bit trickier. while you can use refElement.value.$el, I'd say it's not a good idea. This will only work if the component has the first child the textarea. This will make for a brittle implementation, where if you need to change that at some point, it will break. IMHO, you're better off passing the ref as a prop to the child component. This is also not best practice, because you're supposed to pass props down and emit events up, but that that would be quite the overhead to implement. Passing ref as a prop comes with it's own issues though. If you have a ref in the template, it gets automagicaly converted from the ref/propxy to a value. To get around that, you can pass the prop in a function refElement: () => refElement in the setup(can't do it template). Of course, YMMV, but this is the path I'd chose.
const app = Vue.createApp({
setup(props, context) {
const refElement = Vue.ref(null)
const testResize = () => {
if (refElement.value !== null) {
refElement.value.focus()
}
}
return {
testResize,
refElement: () => refElement
}
}
});
app.component("text-area", {
template: `<textarea ref="taRef"></textarea></div></div>`,
props: {
textarearef: {
type: Function
}
},
setup(props) {
const taRef = props.textarearef()
return {
taRef
}
}
})
app.mount("#app");
<script src="https://unpkg.com/vue#3.0.3/dist/vue.global.prod.js"></script>
<div id="app">
<text-area :textarearef="refElement"></text-area>
<button #click="testResize">🦄</button>
</div>

Dynamic component in Vue3 Composition API

A simple working example of a Vue2 dynamic component
<template>
<div>
<h1>O_o</h1>
<component :is="name"/>
<button #click="onClick">Click me !</button>
</div>
</template>
<script>
export default {
data: () => ({
isShow: false
}),
computed: {
name() {
return this.isShow ? () => import('./DynamicComponent') : '';
}
},
methods: {
onClick() {
this.isShow = true;
}
},
}
</script>
Everything works, everything is great. I started trying how it would work with the Composition API.
<template>
<div>
<h1>O_o</h1>
<component :is="state.name"/>
<button #click="onClick">Click me !</button>
</div>
</template>
<script>
import {ref, reactive, computed} from 'vue'
export default {
setup() {
const state = reactive({
name: computed(() => isShow ? import('./DynamicComponent.vue') : '')
});
const isShow = ref(false);
const onClick = () => {
isShow.value = true;
}
return {
state,
onClick
}
}
}
</script>
We launch, the component does not appear on the screen, although no errors are displayed.
You can learn more about 'defineAsyncComponent' here
https://labs.thisdot.co/blog/async-components-in-vue-3
or on the official website
https://v3.vuejs.org/api/global-api.html#defineasynccomponent
import { defineAsyncComponent, defineComponent, ref, computed } from "vue"
export default defineComponent({
setup(){
const isShow = ref(false);
const name = computed (() => isShow.value ? defineAsyncComponent(() => import("./DynamicComponent.vue")): '')
const onClick = () => {
isShow.value = true;
}
}
})
Here is how you can load dynamic components in Vue 3. Example of dynamic imports from the icons collection inside /icons folder prefixed with "icon-".
BaseIcon.vue
<script>
import { defineComponent, shallowRef } from 'vue'
export default defineComponent({
props: {
name: {
type: String,
required: true
}
},
setup(props) {
// use shallowRef to remove unnecessary optimizations
const currentIcon = shallowRef('')
import(`../icons/icon-${props.name}.vue`).then(val => {
// val is a Module has default
currentIcon.value = val.default
})
return {
currentIcon
}
}
})
</script>
<template>
<svg v-if="currentIcon" width="100%" viewBox="0 0 24 24" :aria-labelledby="name">
<component :is="currentIcon" />
</svg>
</template>
You don't need to use computed or watch. But before it loads and resolved there is nothing to render, this is why v-if used.
UPD
So if you need to change components (icons in my case) by changing props use watchEffect as a wrapper around the import function.
watchEffect(() => {
import(`../icons/icon-${props.name}.vue`).then(val => {
currentIcon.value = val.default
})
})
Don't forget to import it from vue =)
The component should be added to components option then just return it name using the computed property based on the ref property isShow :
components:{
MyComponent:defineAsyncComponent(() => import("./DynamicComponent.vue"))
},
setup(){
const isShow = ref(false);
const name = computed (() => isShow.value ? 'MyComponent': '')
const onClick = () => {
isShow.value = true;
}
}
Instead of string you should provide Component
<script setup>
import Foo from './Foo.vue'
import Bar from './Bar.vue'
</script>
<template>
<component :is="Foo" />
<component :is="someCondition ? Foo : Bar" />
</template>

Webcomponents communicating by custom events cannot send data

I'm intrigued by webcomponents (and pretty new to javascript) and I am trying to set up a dummy microfrontend app. Ok, components are served by different web server (webpack devserver to be honest) and correctly displayed in the 'aggregator' page. I also add a customevent to send some text from a webcomponent to another and this is not working. Here is the sender webcomponent:
const tmplt = document.createElement('template');
tmplt.innerHTML = `
<div>
<label for="sendnpt">message:</label>
<input id="sendnpt" type="text"></input>
<button id="sendbtn">Send the message</button>
<div id="msglog"></div>
</div>
`;
var input = {};
window.customElements.define('team-zero', class TeamZero extends HTMLElement {
constructor() {
super();
this._shadowRoot = this.attachShadow({ 'mode': 'open' });
this._shadowRoot.appendChild(tmplt.content.cloneNode(true));
this.button = this._shadowRoot.getElementById('sendbtn');
input = this._shadowRoot.getElementById('sendnpt');
this.button.addEventListener('click', function(evt) {
var msg = {
bubbles: true,
detail: {}
};
msg.detail.name = input.value;
console.log('sending msg: ' + JSON.stringify(msg))
window.dispatchEvent(new CustomEvent('greet', msg));
});
}
connectedCallback() {
console.log('connected and ready!');
}
});
And this is the receiver:
const m = require('mithril');
window.customElements.define('team-one', class TeamOne extends HTMLElement {
constructor() {
super();
this._shadowRoot = this.attachShadow({ 'mode': 'open' });
window.addEventListener('greet', (msg) => {
console.log("a message arrives: " + JSON.stringify(msg));
});
}
connectedCallback() {
console.log('connected!');
m.mount(this._shadowRoot, {
view: function(vnode) {
return m("div", "team-0ne runs mithril!");
}
});
}
});
Problem is that event is emitted and received, but there is no detail in it. Why receiver component cannot log the text the other one is sending, what am I missing?
You can see the whole dummy project here.
Unlike default Events like click,
Custom Events require a composed:true to 'escape' shadowDOM
https://developer.mozilla.org/en-US/docs/Web/API/Event/composed
Note: besides data you can also pass Function references
<my-element id=ONE><button>Click One</button></my-element>
<my-element id=TWO><button>Click Two</button></my-element>
<script>
window.customElements.define('my-element', class extends HTMLElement {
constructor() {
super().attachShadow({mode:'open'}).innerHTML = `<slot></slot>`;
let report = (evt) =>
document.body.append(
document.createElement("br"),
`${this.id} received: ${evt.type} - detail: ${JSON.stringify(evt.detail)} `,
evt.detail.callback && evt.detail.callback(this.id) // not for 'click' event
);
window.addEventListener('greet', report); // Custom Event
window.addEventListener('click', report); // button click
this.onclick = () =>
window.dispatchEvent(new CustomEvent('greet', {
bubbles: true,
composed: true,
detail: {
fromid: this.id,
callback: this.callback.bind(this)
}
}));
}
callback(payload){
return `${this.id} executed callback function(${payload})`;
}
});
</script>
Also see: https://pm.dartus.fr/blog/a-complete-guide-on-shadow-dom-and-event-propagation/

Resources