Vue how to implement a retry button in error boundary component - vue.js

I am trying to create a Vue error handling component following a similar pattern to the one documented here. It is basically a wrapper that uses the errorCaptured lifecycle hook to detect errors in its child components. If an error is caught, it hides the content passed into its slot and shows an error message instead. Pretty simple. Something like this:
ErrorBoundary.vue
<template>
<div>
<slot v-if="!err"></slot>
<h1 v-else>Oopsy</h1>
</div>
</template>
<script>
export default {
name: 'errorBoundary',
data(){
return {err: false};
},
errorCaptured(err){
this.err = true;
console.log(err);
}
}
</script>
The problem I have is that errorCaptured does not capture errors thrown in its own component's scope. For example, here nothing is caught and logged.
ErrorThrower.vue
<template>
<div>Some content</div>
</template>
<script>
export default {
name: 'errorThrower',
created(){
throw new Error();
},
errorCaptured(err){
console.log(err);
}
}
</script>
If the above example caught the error, then I could add a retry prop to the ErrorBoundary component and accept the content as a slot. Like this:
ErrorThrower.vue
<template>
<ErrorBoundary :onRetry="loadContent">
<div>Some content</div>
</ErrorBoundary>
</template>
<script>
export default {
name: 'errorThrower',
created(){
this.loadContent();
},
methods: {
loadContent(){
// make some call that fails
}
}
}
</script>
Unfortunately this doesn't work, due to the slotted content being on the same scope as the ErrorBoundary component.
Whereas if ErrorThrower is a child compoment, ErrorBoundary will log the error and display a message, because now ErrorThrower's error is caught by errorCaptured.
SomeParent.vue
<template>
<error-boundary>
<error-thrower/>
</error-boundary>
</template>
How can I include a retry button in ErrorBoundary's error message, that will retry some failed async operation if desired? Ideally I want to pass in the callback for the error component to retry on error.

Related

Cannot read properties of undefined (reading '$refs') vue js

Getting the Error Cannot read properties of undefined (reading '$refs') though I'm having a reference in the template. Does it mean I must use Vue mounted hook ?
<div class="input-wrapper">
<input type="text" id="phone" placeholder="(555) 555-5555" ref="input"/>
</div>
<script>
this.$refs.input.addEventListener('input', function () {
// some code
});
</script>
Inside root of <script> of a Vue component, in both Vue 2 and Vue 3, this is undefined:
<script>
console.log(this) // undefined
</script>
See it here.
Vue template refs can only be accessed inside any hook or method happening after the component has been mounted and before it is unmounted.
Which means the earliest you can reference this.$refs is inside mounted. And the latest is in beforeUnmount. And you can also access them in any hook or method happening between those two moments.
Considering you're attempting to add an event listener to a HTMLInputElement, I'd recommend using the v-on directive, which automatically adds the event listener on mount and removes it on unmount.
In your case:
<template>
<div class="input-wrapper">
<input
type="text"
id="phone"
placeholder="(555) 555-5555"
#input="myFn" />
</div>
</template>
<script>
export default {
methods: {
myFn(event) {
console.log(event)
}
}
}
</script>
As a side note, you should know that a regular function's this doesn't have access to the component context, unless it's an arrow function:
export default {
mounted() {
this.$refs.input.addEventListener('input', function() {
/*
* Here `this` is the context of the current function, you can't
* access methods, computed, data or props of the component.
* You'd need to make it an arrow function to access the component scope
*/
})
}
}
Whereas in any method (e.g: the above myFn), this is the component context, which has access to all component members.

Vue3 props access inside setup() returns nothing

I have a weird situation with my Vue3 component. I am trying to get a value from the props inside the setup() function. But this returns nothing.
<template>
<div class="border p-2 space-y-2">
<h2 class="text-center">Makers</h2>
{{ product.makers }}
</div>
</template>
<script>
import { ref, onMounted, toRef, toRefs } from 'vue'
import ProductStore from '../store/ProductStore'
export default {
props: {
product: Object
},
setup(props) {
console.log(props.product.makers)
},
}
</script>
The value is sent from another component like the one below
<ProductMakers :product="product"/>
But I always get undefined as my response. Any clue to resolve this problem? Am I missing something?
To my surprise, the template is always showing the value correctly. The problem is only inside the setup(). Any clue?
This means that product is reactively updated after the creation of component instance.
Updated value is supposed to be accessed in a watcher or a computed. In case a side effect like logging is needed, this is the case for a watcher:
setup(props) {
watchEffect(() => {
if (props.product.makers)
console.log(props.product.makers)
});
},

Vue props not passing correctly

I have a component
In the component I have
export default {
name: "Modal",
props: ['showFooter'],
}
Then in the template i have
<template>
<div class='modal'>
<div class='footer' v-if='showFooter'>This is the footer</div>
</div>
</template>
THe footer doesnt display at all, if i pass the prop or not. It just says
[Vue warn]: Property or method "showFooter" is not defined on the instance but referenced during render.
Whats the issue here??
Ive also just tried
<template>
<div class='modal'>
<div class='footer' v-if='showFoot'>This is the footer</div>
</div>
</template>
export default {
name: "Modal",
props: ['showFooter'],
data(){
return {
showFoot:this.showFooter
}
}
}
Then I get told showFoot isnt defined. Which is even wierder because Im defining it right there in the data!??
Even if I just remove the property totally, and just define it in data
export default {
name: "Modal",
data(){
return {
showFoot:true
}
}
}
I still get
[Vue warn]: Property or method "showFoot" is not defined on the instance but referenced during render.
So wierd and I cant for the life of me figure it out
Its almost like the template has no access to anything I define in the data() or props of the componenet. Never seen this before
i think so you are not passing props from parent component. Ideally you can pass it from parent component and receive it in Children.
<childComponent :yourPropNameInChild="anyDataFromParentYouWantToPass" />
here this ChildComponent is rendered in your parent component.
to receive it in children you can do
export default {
name: "Modal",
props: ['yourPropNameInChild'],
}
as of for your example
<modal :showFooter="showFooter" />
in your parent you have to write above line of code
and in your child you have to receive it as you are doing
also in your example you were missing script tag

VueJS: Is it really bad to mutate a prop directly even if I want it to ovewrite its value everytime it re-renders?

The question says it all. As an example, think of a component that can send messages, but depending on where you call this component, you can send a predefined message or edit it. So you would end with something like this:
export default {
props: {
message: {
type: String,
default: ''
}
},
methods: {
send() { insert some nice sending logic here }
}
}
<template>
<div>
<input v-model="message"></input>
<button #click="send">Send</button>
</div>
</template>
If I do this and try to edit the predefined message then Vue warns me to "Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders", but that's exactly the behaviour I'm searching for as the predefined message should return to being unedited if the user closes the component and opens it again.
I'm also not passing the prop to the father component, so the sending logic itself can be included in this same component.
It would still be considered bad practice? Why? How can I make it better? Thanks in advance!
A solution would be to assign the message you are passing as a prop to a variable in data and set this variable to the v-model instead.
<template>
<div>
<input v-model="message"></input>
<button #click="send">Send</button>
</div>
</template>
<script>
export default {
data(){
return{ message:this.msg
}
},
props: {
msg: {
type: String,
default: ''
}
},
methods: {
send() { use a bus to send yout message to other component }
}
}
</script>
If you are not passing the data to another component or from a component, you shouldn't be using props, you should use Vue's data object and data binding. This is for any component data that stays within itself, the component's local state. This can be mutated by you as well so for our example I would do something like:
export default {
data: function () {
return {
message: '',
}
},
methods: {
send() {
// insert some nice sending logic here
// when done reset the data field
this.data.message = '';
}
}
}
<template>
<div>
<input>{{ message }}</input>
<button #click="send">Send</button>
</div>
</template>
More info on props vs data with Vue

Using v-model and refs in a slot in Vue2

I have a component that takes a main <slot> from a form that is generated elsewhere in my application. I'm trying to use v-model on the form inputs but my vue component just spits out a warning about the properties not being defined, when in fact they are.
I admit it's a weird way of doing things, but it seems to be the easiest way for me to do this since my form is being generated by Symfony.
html:
<my-component>
<input ref="start" v-model="start"/>
</my-component>
my component:
<script>
export default {
data() {
start: null
},
mounted() {
console.log(this.$refs) // === {}; expected {"start":{...}}
}
}
</script>
<template>
<div>
<slot/>
... other stuff here
</div>
</template>
console log:
Property or method "start" is not defined on the instance but referenced during render
I cannot use $refs or v-model in the html. Am I doing something wrong? Or is this just not possible.
If you declare v-model="start" in the parent then it belongs to the parent and needs to be declared there. It looks like instead you declare it in the component instead as null.
If you reorder things it should work as you expect:
Parent:
<parent>
<input v-model="start" :start="start"/>
</parent>
<script>
export default {
data() {
start: null // Important to define start here if it's used in this component's html
}
}
</script>
Component:
<template>
<div>
<slot/>
... other stuff here
</div>
</template>
<script>
export default {
props: ['start'], // Receive the prop from the parent
data() {
},
mounted () {
console.log(this.start) // Should echo the value of the prop from the parent
}
}
</script>