Handle Vue render errors locally - vue.js

I am using Vue (server side rendered) with mjml to generate emails.
So I have something (overly simplified) like:
<mjml><mj-body>Hello {{ User.Name }}</mj-body></mjml>
If the model doesn't define User then Vue throws an error and the whole output is lost.
What I want to the output to be along the lines:
<mjml><mj-body>Hello <error>'User' is undefined</error></mj-body></mjml>
I have implemented Vue.config.errorHandler but that just tells me about the error -- there is no rendered output.
Anyway to implement the equivalent of an error handler around each variable substitution?

If you are using Vue version >= 2.5, you can use errorCaptured to create an ErrorBoundary
const ErrorBoundary = {
name: 'ErrorBoundary',
data: () => ({
error: false,
msg: ''
}),
errorCaptured (err, vm, info) {
this.error = true
this.msg = `${err.stack}\n\nfound in ${info} of component`
},
render (h) {
return this.error
? h('pre', this.msg)
: this.$slots.default[0]
}
}
and use this in your component
<error-boundary>
<mjml><mj-body>Hello {{ User.Name }}</mj-body></mjml>
</error-boundary>
If the application has any javascript error, it will be displayed on UI
Example on codepen
If you want to have more user-friendly error, you can customize ErrorBoundary to have fallback to another component. You can find out in this tutorial
Another good example of using errorHandler

Related

Vue3: How to send params to a route without adding them to the url in Vue3?

In Vue3 is there a way to pass properties to a route without the values showing in the url?
I defined the route like this:
{
path: '/someRoute',
name: 'someRoute',
component: () => import('#/app/pages/SomeRoute.vue'),
props: (route) => {
...route.params
}, // <-- I've seen this method in Vue2 but in Vue3 the route.params is just empty here
}
I call the route like this:
<router-link :to="{ name: 'someRoute', params: { message: 'Some Message' } }">Some link</router-link>
When I change path into path: '/someRoute/:message', the message come through just fine but I just want to pass the message without it showing up in the url.
I've seen a couple of Vue2 examples that use this method (e.g. https://stackoverflow.com/a/50507329/1572330) but apparently they don't work in Vue3 anymore.
Also all the examples in the Vue3 docs (https://github.com/vuejs/vue-router/blob/dev/examples/route-props/app.js / https://v3.router.vuejs.org/guide/essentials/passing-props.html) pass on their values through the url itself so I'm not sure if it's even possible anymore.
Any thoughts on this would be helpfull.
Finally I found something about this in the changelog: https://github.com/vuejs/router/blob/main/packages/router/CHANGELOG.md#414-2022-08-22
Apparently it's no longer possible to send properties via params without the showing in the url. But fortunately they give some alternative suggestions.
The one that worked best for my situation was to use state: { ... } instead:
<router-link :to="{ name: 'someRoute', force: true, state: { message: 'Some Message' } }">Some link</router-link>
Now in the code of the page I read the property from the history.sate and put the value in whatever property I need it.
In case the url/route itself doesn't change you need to have an update hook and use force: true
public created() {
this.message = window.history.state.message;
}
public updated() {
this.message = window.history.state.message;
}
PS history.state has some limitations so in other situations one of the other suggestions from the changelog might work better

Why I'm Facing the error [Vue warn]: Error in render: "TypeError: Cannot read property 'name' of undefined"

This is a Laravel & Vue Js Project.
Everything works fine but why I'm facing [Vue warn]: Error in render: "TypeError: Cannot read property 'name' of undefined" type error.
My Vue File
<template>
<p>Sold By: {{product.user.name}}</p>
</template>
<script>
export default {
data(){
return {
product:{},
}
},
methods: {
loadData(){
axios.get('/api/'+this.$route.params.slug+'/product')
.then(response => {
this.product = response.data;
},
()=> {});
}
},
created(){
this.$Progress.start();
this.loadData();
this.$Progress.finish();
},
}
</script>
My Controller
public function getProduct($slug)
{
$product = Product::where('slug',$slug)->with('brand','category','subCategory','productImages','featureDescriptions','colors','variants','user')->first();
return response()->json($product, 200);
}
``
Now I want to show my User name in Vue file <p>Sold By: {{product.user.name}}</p>. It showing User Name With an error [Vue warn]: Error in render: "TypeError: Cannot read property 'name' of undefined". when I show the user data <p>Sold By: {{product.user}}</p>, It show user all data without error. Now how i show user name without error.
The error is self-explanatory: you're using {{product.user.name}} in the template. But before the product has returned from BE, product.user is undefined and therefore does not have a .name property.
The simplest fix would be to place a v-if on the <p>:
<p v-if="product.user">Sold By: {{product.user.name}}</p>
Another generic solution for this type of problem is to use a computed:
<template>
<p>Sold By: {{productUserName}}</p>
</template>
<script>
export default {
// ...
computed: {
productUserName() {
return this.product.user?.name || '';
}
}
// ...
}
</script>
You can read more about optional chaining operator (used above) (?.) here.
Because it's a fairly new addition to JavaScript, Vue doesn't currently support it in <template> tags (but it works in <script>).
Additional note: a common mistake is to add an additional data member instead of using the source of the error (product.user in this case) either directly or through a computed. This creates two problems:
it decouples product.user from rendering the <p>. Which means that if BE returns a product without a user, you'll still get the error, because you've set dataLoaded to true but the template still tries to read the property .name of user, which is falsy and therefore does not have a .name.
you create unnecessary boilerplate: anyone trying to understand or modify your code at a later time has to figure out the arbitrary connection between dataLoaded and product.user.
One of the reasons Vue is loved for is because it doesn't require boilerplate code, unlike other frameworks (i.e: Angular). Keep it that way! By using v-if="product.user" in the template, someone reading that code will immediately understand the rendering logic, without having to look at the component code. Decreasing the time needed to figure out the code on a regular basis will greatly decrease the time needed to modify it, should you (or someone else) ever need to. This results into more flexible, more scalable code. Less bugs, less time spent => more money.
This is happening because <p> is being rendered while product is still an empty object (product: {}).
You could use v-if to render only if product already has been loaded.
<template>
<p v-if="dataLoaded">Sold By: {{ product.user.name }}</p>
</template>
<script>
export default {
data() {
return {
product: {},
dataLoaded: false,
};
},
methods: {
loadData() {
axios.get("/api/" + this.$route.params.slug + "/product").then(
(response) => {
this.product = response.data;
this.dataLoaded = true;
},
() => {}
);
},
},
created() {
this.$Progress.start();
this.loadData();
this.$Progress.finish();
},
};
</script>

In vue.js, where can I place "app initialization" code?

Specifically, code that runs before the app actually loads. I'm using vuex and the first thing I want to do (regardless of what route the user is on) is to dispatch a getUser action to get currently user details from the API (or alternatively, redirect if not authenticated).
If I place it in my App.vue mounted component, I believe it might be too late? Don't children components load before parents?
If I get it right you want to do something before the application initialize. For that you can just perform async method in app initialization. Something like that as an example:
function initializeApp (vueCreated) {
return new Promise((resolve, reject) => {
switch (vueCreated) {
case false: // "prevue" initialization steps
console.log('vue not yet created, prevue steps happens')
// ...
setTimeout(_ => resolve(), 3500) // async call
break;
case true: // we can continue/prepare data for Vue
console.log('vue created, but waiting for next initialization steps and data')
// ...
setTimeout(_ => resolve('Mounted / shown when app ready'), 3500) // async call
}
})
}
initializeApp(false).then(_ => {
new Vue({
template: '#app',
data: {
content: null
},
async created () {
this.content = await initializeApp(true)
this.$mount('#app')
console.log('all inicialization steps done, data arrived, vue mounted')
}
})
})
I have found some article related to your question may be this help you out. Link
If you are using vue-router you can use beforeEach to prevent some routes of unauthenticated users.
You can read more here.
If you get stuck here provide code what you tried with router.
Also good example of using navigation guards.
Good luck!

How to watch on Route changes with Nuxt and asyncData

Hi everybody i'm trying to watch on route changes in my nuxt js app.
Here my middleware:
export default function ({ route }) {
return route; but i don't know what to write here
}
index.vue File
middleware: [routeReact]
i'm trying to write this:
app.context.route = route
but it says to me that app.context doesn't exist
Here's the point of my question i'm trying to update my data that gets from my api with axios on page if route changing
like this
this the page
i'm clicking link to next page :
but when i'm route to next page, nothing happens all data is the same:
here my asyncData code:
asyncData({ app }) {
return app.$axios.$get('apps/' + app.context.route.fullPath.replace(/\/categories\/?/, ''))
.then(res => {
return {
info: res.results,
nextPage: res.next,
prevPage: res.prev
};
})
}
Thanks for your help
First thing, context.route or it's alias this.$route is immutable object and should not be assigned a value.
Instead, we should use this.$router and it's methods for programmatic navigation or <nuxt-link> and <router-link>.
As I understand, you need to render the same route, but trigger asyncData hook in order to update component's data. Only route query is changed.
Correct way to navigate to the same page but with different data is to use link of such format:
<nuxt-link :to="{ name: 'index', query: { start: 420 }}"
Then you can use nuxt provided option watchQuery on page component and access that query inside asyncData as follows:
watchQuery: true,
asyncData ({ query, app }) {
const { start } = query
const queryString = start ? `?start=${start}` : ''
return app.$axios.$get(`apps/${queryString}`)
.then(res => {
return {
info: res.results,
nextPage: res.next,
prevPage: res.prev
}
})
},
This option does not require usage of middleware. If you want to stick to using middleware functions, you can add a key to layout or page view that is used. Here is an example of adding a key to default layout:
<nuxt :key="$route.fullPath" />
This will force nuxt to re-render the page, thus calling middlewares and hooks. It is also useful for triggering transitions when switching dynamic routes of the same page component.

VueJS - requiring and rendering a component programmatically

I have a bit of a specific usecase here. I have a large Vue app with lots of pages. Each page has a page-id to identify it on the server and load it's content. These ids are in the router path (parent-id/child-id/another-child-page-id). On the page components themselves I display a page title. Unfortunately, there is no correlation between the translation key of the page title to the page-id.
Now, instead of creating a mapping between page-ids and page title translations that I will have to maintain by hand, I figured I could lookup the route in the router config and require the component:
{
path: 'the-page',
name: 'ThePage',
component: resolve => require.ensure([], () => resolve(require('#/pages/ParentPage/ChildPage/ThePage')), 'parent-page')
}
This works by recursively walking through the routes to find the page-id, then loading the component in a promise:
// find the route definition
const route = findRouteByPath(file.areaName)
// load the route component
new Promise(resolve => { route.component(resolve) })
.then(component => {
console.log(component)
})
This gives me the component like so:
Object {
mixins: Array(1),
staticRenderFns: Array(0), __file: "/the/file/path.vue",
beforeCreate: Array(1),
beforeDestroy: Array(1),
components: Object,
mixins: Array(1),
render: function (),
// etc.
}
If I call the render function, I get an error:
Uncaught (in promise) TypeError: Cannot read property '_c' of undefined
What I want to do is programmatically render the component and grab the content of the slot page-title from the component.
Can anyone help with this?
Update 1
Vue.extend() then mounting seems to do what I want. The problem is, I get all kinds of errors because the pages depend on certain things being around, like the current route. I'm also a bit worried to trigger certain created() hooks that would trigger API calls and such. My current state is trying to get rid of mixins and hooks like so:
const strippedComponent = Object.assign(component, {
mixins: [],
components: {},
beforeCreate: []
})
const Constructor = Vue.extend(strippedComponent)
const vm = new Constructor({})
But I'm not quite there yet.
Update 2
I ended up abandoning this and bit the bullet by adding a meta: {} object to each route where I store things like the page title translation key and other good stuff.