vue3 composition api script setup, share click event with other components - vuejs3

I am using Vue3 (composition API with script setup) and I am trying to share a click event with other components.
<h1 #click="useLink">Header #</h1>
const useLink = (e) => {
let section = e.target.closest(".section");
if (router.currentRoute.value.hash !== "#" + section.id) {
router.push("#" + section.id);
}
};
Note: This method is repeated in few other components. ☹️
When a user clicks on h1, useLink() gets called and pushes that id to router which scrolls to position.
Thanks

A composable should export a function that returns some reusable logic (in this case, another function)
link.js
import { useRouter } from 'vue-router';
export function useLink() {
const router = useRouter();
function goToSection(e) {
const section = e.target.closest('.section');
if (router.currentRoute.value.hash !== '#' + section.id) {
router.push('#' + section.id);
}
}
return {
goToSection
};
}
Then in any component you need this reusable function: import the file, destructure the reusable function, and apply it to your #click handlers.
component.vue
<script setup>
import { useLink } from '#/composables/link';
const { goToSection } = useLink();
</script>
<template>
<div class="section" id="one">
.
.
.
<h1 #click="goToSection">Scroll to section div</h1>
</div>
</template>

Related

Vue3 Wait till Child Component is Mounted

I want to wait for a child component to mount before rendering a tooltip. While waiting, I will have a placeholder to display to the user.
An example component would be:
<template>
<div v-if="!mounted">Loading...</div>
<child-component></child-component>
</template>
<script>
export default defineComponent({
setup() {
const mounted = ref(false);
return {
mounted
}
}
});
</script>
After doing research, it looks like Vue2 supports life-cycle hooks on the components, which would change the above code to:
<template>
<div v-if="!mounted">Loading...</div>
<child-component #hook:mounted="mountedCheck"></child-component>
</template>
<script>
export default {
export default defineComponent({
setup() {
const mounted = ref(false);
const mountedCheck = () = {
mounted.value = true;
}
return {
mounted, mountedCheck
}
}
});
}
However, I cant seem to get the #hook:mounted to work. Is there something similar in Vue3, or am I missing something?
The syntax has changed in Vue3. Now you need to use #vue:mounted instead of #hook:mounted.
See Vue 3 Migration Guide - VNode Lifecycle Events for details
Also, keep in mind that some event names like destroyed and beforeDestroy have been renamed to unmounted and beforeUnmount respectively in Vue3
The child component needs to emit that hook
child:
<script>
export default defineComponent({
setup() {
onMounted(() => {
const { emit } = getCurrentInstance();
emit('mounted');
})
return {};
}
});
</script>
parent
<template>
<div v-if="!mounted">Loading...</div>
<child-component #mounted="mountedCheck"></child-component>
</template>

CodeMirror on Vue3 has a problem when setValue is kicked

I'm trying to use CodeMirror on Vue3 and the problem occurs when I call doc.setValue().
The Problem is following:
Cursor position is broken when doc.setValue() is called
CodeMirror throws an exception when continuing editing
The exception is here.
Uncaught TypeError: Cannot read property 'height' of undefined
at lineLength (codemirror.js:1653)
at codemirror.js:5459
at LeafChunk.iterN (codemirror.js:5623)
at Doc.iterN (codemirror.js:5725)
at Doc.iter (codemirror.js:6111)
at makeChangeSingleDocInEditor (codemirror.js:5458)
at makeChangeSingleDoc (codemirror.js:5428)
at makeChangeInner (codemirror.js:5297)
at makeChange (codemirror.js:5288)
at replaceRange (codemirror.js:5502)
How should I solve this?
~~~
Versions are CodeMirror: 5.61.1, Vue.js: 3.0.11
My code is following:
index.html
<div id="app"></div>
<script src="./index.js"></script>
index.js
import { createApp } from 'vue';
import App from './App';
const app = createApp(App);
app.mount('#app');
App.vue
<template>
<div>
<button #click="click">Push Me</button>
<textarea id="codemirror"></textarea>
</div>
</template>
<script>
import CodeMirror from 'codemirror/lib/codemirror.js';
import 'codemirror/lib/codemirror.css';
// import codemirror resources
import 'codemirror/addon/mode/overlay.js';
import 'codemirror/mode/markdown/markdown.js';
import 'codemirror/mode/gfm/gfm.js';
export default {
data () {
return {
cm: null
}
},
mounted () {
this.cm = CodeMirror.fromTextArea(document.getElementById('codemirror'), {
mode: 'gfm',
lineNumbers: true,
});
},
methods: {
click (event) {
this.cm.getDoc().setValue('foo\nbar');
}
}
}
</script>
Thanks.
UPDATES
First, this problem also occurs when I used replaceRange() with multiline.
Unfortunately, I couldn't find any solution. So I tried to find another way.
My solution is recreating Codemirror instance with a textarea that has new content.
It works well.
// Remove old editor
this.cm.toTextArea();
// Get textarea
const textarea = document.getElementById('codemirror');
// Set new content
textarea.value = 'foo\nbar';
// Create new editor
this.cm = CodeMirror.fromTextArea(textarea, { /** options */ });
I found a method, you can use toRaw to get the original Object from Proxy,and this method can be also used in monaco-editor
import { toRaw } from 'vue'
import CodeMirror from 'codemirror/lib/codemirror.js';
import 'codemirror/lib/codemirror.css';
// import codemirror resources
import 'codemirror/addon/mode/overlay.js';
import 'codemirror/mode/markdown/markdown.js';
import 'codemirror/mode/gfm/gfm.js';
export default {
data () {
return {
cm: null
}
},
mounted () {
this.cm = CodeMirror.fromTextArea(document.getElementById('codemirror'), {
mode: 'gfm',
lineNumbers: true,
});
},
methods: {
click (event) {
toRaw(this.cm).setValue('foo\nbar');
}
}
}
Another way,you don't have to define cm in data, just use this.cm
data () {
return {
//cm: null
}
},

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>

When recaptcha gets rendered, a transparent container overlaps all the ui and spans all the viewport space

I'm working with next.js and firebase.
I'm doing authentication with google and with phone number.
The problem I have happens when authenticating with phone number, when the recaptcha is rendered, I can't click it and move forward to the next steps in the auth flow, because a transparent container also is rendered and overlaps completly ui.
I don't know why this happens, I followed every step of the google guide:
Phone authentication
My page component is the following...
In the page component I'm rendering a different form, one if it's time to write the verification code sent via SMS and another which is the first in being showed, to write and send the phone number.
import AppHead from '../../components/head'
import { useEffect, useRef, useState } from 'react'
import { useTranslation } from "react-i18next"
import Select from '../../components/select'
import Nav from '../../components/nav'
import PhoneNumberInput from '../../components/phone-input'
import Dialog from '../../components/dialog'
import Footer from '../../components/footer'
import { isPossiblePhoneNumber } from 'react-phone-number-input'
export default function Home({ M, authService, libService }) {
const verificationCodeInput = useRef(null)
const [phoneNumber, setPhoneNumber] = useState("")
const [shouldVerify, setShouldVerify] = useState(false)
const [openDialog, setOpenDialog] = useState(false)
const [valid, setValid] = useState(2)
const [confirmationCode, setConfirmationCode] = useState('')
const { t, i18n } = useTranslation('common')
let verificationValidClass = ""
const onChangePhoneNumber = (value) => {
setPhoneNumber(value)
};
const onClickLoginWithPhone = async (evt) => {
evt.preventDefault()
try {
if (isPossiblePhoneNumber(phoneNumber)) {
const confirmationResult = await authService.signInWithPhoneNumber(phoneNumber, window.recaptchaVerifier)
window.confirmationResult = confirmationResult
setShouldVerify(true)
} else {
setOpenDialog(true)
}
} catch (error) {
console.log(error)
}
};
const phoneNumberForm = (
<form className="home__formLogin">
<PhoneNumberInput onChangePhoneNumber={onChangePhoneNumber} phoneNumber={phoneNumber} labelText={t("forms.auth.phoneFieldLabel")} />
<button data-target="phoneLoginModal" className="btn-large home__login-phone modal-trigger" onClick={onClickLoginWithPhone}>
<span className="material-icons home__sendIcon" aria-hidden="true">
send
</span>
{t("forms.auth.sendPhone")}
</button>
<div id="recaptcha-container">
</div>
</form>
)
if (valid === 0) {
verificationValidClass = "invalid"
} else if (valid === 1) {
verificationValidClass = "valid"
}
useEffect(() => {
const elems = document.querySelectorAll('.modal');
const instances = M.Modal.init(elems);
window.recaptchaVerifier = authService.getVerifier('recaptcha-container', (response) => setShouldVerify(true), () => console.log("captcha-expired"))
}, []);
return (
<>
<main className="main">
<div className="main__layout home">
<Nav top={true} content={pickLanguage} contentLg={pickLanguageLG} />
<AppHead title={t("pages.login_phone.meta-title")} description={t("metaDescription")} />
<header className="home__header">
<h1>{shouldVerify ? t("pages.home.titleCode") : "LOGIN"}</h1>
<p>{t("pages.login_phone.subtitle_1")} <br /> {t("pages.login_phone.subtitle_2")}</p>
</header>
<section className="home__login-options">
<h1 className="home__formTitle">{shouldVerify ? t("pages.home.code") : t("pages.home.loginHeading")}</h1>
{shouldVerify ? verificationCodeForm : phoneNumberForm}
<Dialog open={openDialog} onConfirm={onClickConfirmDialog} heading={t("components.dialog.wrongPhoneNumber")} confirmText={t("components.dialog.wrongPhoneNumberConfirm")}>
{t("components.dialog.wrongPhoneNumberContent")}
</Dialog>
</section>
</div>
</main>
<Footer />
</>
)
}
In the useEffect callback I create the verifier object and then in onClickLoginWithPhone I submit the phone number.
The function call authService.signInWithPhoneNumber(phoneNumber, window.recaptchaVerifier) is implemented as shown below:
export default function AuthService(authProvider,firebase,firebaseApp) {
return Object.freeze({
signInWithPhoneNumber,
getVerifier,
getCredentials
})
function getVerifier(id,solvedCallback,expiredCallback){
return new firebase.auth.RecaptchaVerifier("recaptcha-container",{
'size': 'normal',
'callback': solvedCallback,
'expired-callback': expiredCallback
});
}
//used when user signs in with phone number and need to introduce a verification code sended via SMS
async function getCredentials(verificationId,verificationCode){
const credential = firebase.auth.PhoneAuthProvider.credential(verificationId,verificationCode);
const userCredential = await firebase.auth(firebaseApp).signInWithCredential(credential);
return userCredential
}
async function signInWithPhoneNumber(phoneNumber, phoneVerifier) {
const confirmationResult = await firebase.auth(firebaseApp).signInWithPhoneNumber(`${phoneNumber}`, phoneVerifier)
return confirmationResult
}
}
The container that is displayed is the following:
The part highlighted in blue is the full element which appears when I submit the phone number, and the part highlighted in red is the div which spans all the viewport and dont let me click the captcha widget
I really don't know why this thing is displayed, I'm not manipulating the DOM when submit the phone number, so I guess this problem is related to using next with firebase or with firebase itself.
Note: t("<json-object-field>"), t is just a function which reads an specific json file (a different one depending on the language the page is being showed) which contains all the strings translated to an specific language and this function returns a translated string individually.
The captcha widget is rendered but the container overlaps it, the container is transparent.

Nextjs - link to '/' without refreshing

// import Link from "next/link";
<Link href="/">Home</Link>
A link to / refreshes the page. Is there anything I can do to stop the page from refreshing, whenever the user navigates to the homepage?
The docs states this: When linking between pages on websites, you use the HTML tag. In Next js you use the Link Component from next/link to wrap the tag. it allows you to do client-side navigation to a different page in the application.
Just do
<Link href="/">
<a>
Home
</a>
</Link>
Use this hook and you'll never have to use Link ever again. Just use regular HTML anchors and they'll work as you would expect.
// useNextClickHandler.ts
import type { Router } from 'next/router'
import { useEffect } from 'react'
/**
* Place this in a Next.js's _app.tsx component to use regular anchor elements
* instead of Next.js's <code>Link</code> component.
*
* #param router - the Next.js router
*/
export default function useNextClickHandler(router: Router): void {
useEffect(() => {
async function onClick(event: MouseEvent) {
// Only handle primary button click
if (event.button !== 0) {
return
}
// Use default handling of modifier+click events
if (
event.metaKey ||
event.ctrlKey ||
event.altKey ||
event.shiftKey
) {
return
}
const anchor = containingAnchor(event.target)
// Only handle anchor clicks
if (!anchor) {
return
}
// Use default handling of target="_blank" anchors
if (anchor.target === '_blank') {
return
}
// If the link is internal, prevent default handling
// and push the address (minus origin) to the router.
if (anchor.href.startsWith(location.origin)) {
event.preventDefault()
await router.push(anchor.href.substr(location.origin.length))
}
}
window.addEventListener('click', onClick)
return () => window.removeEventListener('click', onClick)
}, [router])
}
function containingAnchor(
target: EventTarget | null
): HTMLAnchorElement | undefined {
let parent = target
while (
parent instanceof HTMLElement &&
!(parent instanceof HTMLAnchorElement)
) {
parent = parent.parentElement
}
return parent instanceof HTMLAnchorElement ? parent : undefined
}
// _app.tsx
export default function App({
Component,
pageProps,
router,
}: AppProps): JSX.Element {
useNextClickHandler(router)
Other alternative is by using next/router and utilise onClick event.
import Router from "next/router";
<a onClick={() => Router.push("/")}">
Home
</a>

Resources