Problem displaying components that use $route with vue-styleguidist - vue.js

I have some components that use this.$route internally, for various things. Some simplified examples would be, using this.$route.name to lookup a i18n string for the page:
<h1>New {{ $t($route.name + '.label') }}</h1>
or watching the $route and loading something:
watch: {
$route: {
handler: 'loadItem',
immediate: true
}
}
Whenever these components appear in vue-styleguidist, I get various errors in the browser console and nothing renders for the component example:
index.js?1ef8:1 [Vue warn]: Error in callback for immediate watcher "$route": "TypeError: Cannot read property 'id' of undefined"
found in
---> <MyComponent> at src/components/MyComponent.vue
<Anonymous>
<Root>
I am deliberately not importing vue-router or my routes objects into the styleguide, because this isn't supported - and it causes lots of problems with redirects, route guards, etc... which aren't relevant to the style guide.
What I really want to do, I think, is to mock this.$route inside the components when they appear in the styleguide - but I can't figure out how to do that.
Another option would be to pass the route information into the components as props. This would properly decouple the components from the router and fix the issue, but it will involve changes to every component and route.

So, I ended up refactoring all my components to pass in the route properties that they were using as properties. This properly decouple the components from the router and fix the issue, but it was a lot of work.

Related

Exposing functions on a Vue-defined Web Component (Custom Element)

Per the Vue docs, it's possible to build components in Vue (v3) and package them as native Web Components for use with any framework or none at all.
As I've already found, the gap between design models for Vue components and Web Components can make this complex and sometimes a straight-up bad idea (at what point is it better and more maintainable to just go ahead building fully-native components?)... But let's assume for a moment that it's necessary here.
My question - What's the best way to expose a function-like interface on a Vue-built Web Component (to parent nodes)?
The Vue doc discusses passing in reactive data via props/slots, and publishing CustomEvents from the components, but I don't see mention of
taking function calls (or at a stretch, events) from outside. As far as I can tell this is a pretty strong assumption that data and event flow on the rest of the app/page works in a very "Vue-like way"?
For now, my workaround on this is to look up the host element in onMounted() (as per this question) and just set whatever extra properties are required at that point (hoping they shouldn't be required before the Vue component mounts, because I'm not aware of any external events raised when Vue finishes mounting the custom element).
This way the function can still be defined in the context of, and access variables/etc from, the setup function - but can be called by other elements on the page that only have a reference to the element, not the Vue component.
Can't say I like it much though:
<template>
<div ref="someElInTemplate">...</div>
</template>
<script lang="ts">
interface MyCoolHTMLElement extends HTMLElement {
myCoolFunction: () => void;
}
</script>
<script setup lang="ts">
const someElInTemplate = ref<HTMLElement>();
function myCoolFunction() { }
onMounted(() => {
const hostNode = (
somElInTemplate.value?.getRootNode() as ShadowRoot | undefined
)?.host as MyCoolHTMLElement;
hostNode.myCoolFunction = myCoolFunction;
});
</script>

Avoid app logic that relies on enumerating keys on a component instance

in my complex Vue project I am getting this console warning:
[Vue warn]: Avoid app logic that relies on enumerating keys on a component instance. The keys will be empty in production mode to avoid performance overhead.
Unfortunately I can not find the reason for this warning just by the above message.
How can I track down the reason for this warning?
Check if your watching an entire route object anywhere in your code. Doing so throws that error (in my case).
Refer this vue documentation on watching routes
Accessing router and current route inside setup
The route object is a reactive object, so any of its properties can be watched and you should avoid watching the whole route object. In most scenarios, you should directly watch the param you are expecting to change.
Was able to fix this with the suggestion done by Glass Cannon.(https://stackoverflow.com/a/70205284/11787139)
To clarify and maybe help someone else: I was trying to send an Axios request to the server of which the data I sent through was composed of a direct component reference emitted by the component function.
Component
saveItem(){
this.saved = true;
setTimeout( this.resetState, 2500);
this.$emit('saveitem', this)
},
Parent
saveitem(e){
const data = {item : e}
axios.post(target, data, {headers . . .).then((response) => {}
})
The error disappeared when I instead fetched the index of the list item by doing so:
saveitem(e){
let item;
this.items.forEach( function(item, index, array) {
if(item.id == e.id) pointer = item
})
data.item = pointer
axios.post(target, data, {headers . . .).then((response) => {}
})
}
So I was also having this issue, but not for the reasons the accepted answer provided. It was occurring due to my Vuex store. After a lot of digging I discovered the cause was the presence of the "CreateLogger" plugin.
So if you're having this issue and it's not due to you watching an entire route, check if you're using the CreateLogger plugin in Vuex. That might be the culprit.
This happens for me when I pass this to a data object
data() {
return {
updateController: new UpdateController({
reportTo: this
})
}
}
This used to work fine with Vue 2 but causes this error in Vue 3.
Making this modification solved the problem for me.
data() {
return {
updateController: new UpdateController({
reportTo: () => this
})
}
}
I know this might be anti-pattern but I needed to inject partial reactivity to a non-reactive part of a JS library and this was the most not complicated way of achieving this that I can think of.
This happens to me when destructuring a ref without .value.
This was happening to me only in Firefox, and when I removed the Vue Dev Tools extension it stopped. After re-installing Vue dev tools it hasn't come back. Make sure you have the latest version of the Vue Dev Tools for your browser.

Vue/Nuxt: How to make a component be truly dynamic?

In order to use a dynamically-defined single page component, we use the component tag, thusly:
<component v-bind:is="componentName" :prop="someProperty"/>
...
import DynamicComponent from '#/components/DynamicComponent.vue';
...
components: {
DynamicComponent
},
props: {
componentName: String,
someProperty: null,
}
The problem is, this isn't really very dynamic at all, since every component we could ever possibly want to use here needs to be not only imported statically, but also registered in components.
We tried doing this, in order at least to avoid the need to import everything:
created() {
import(`#/components/${this.componentName}.vue`);
},
but of course this fails, as it seems that DynamicComponent must be defined before reaching created().
How can we use a component that is truly dynamic, i.e. imported and registered at runtime, given only its name?
From the documentation: Emphasis mine
<!-- Component changes when currentTabComponent changes -->
<component v-bind:is="currentTabComponent"></component>
In the example above, currentTabComponent can contain either:
the name of a registered component,
or a component’s options object
If currentTabComponent is a data property of your component you can simply import the component definition and directly pass it to the component tag without having to define it on the current template.
Here is an example where the component content will change if you click on the Vue logo.
Like this:
<component :is="dynamic" />
...
setComponentName() {
this.dynamic = () => import(`#/components/${this.componentName}.vue`);
},
Solution for Nuxt only
As of now its possible to auto-import components in Nuxt (nuxt/components). If you do so, you have a bunch of components ready to be registered whenever you use them in your vue template e.g.:
<MyComponent some-property="some-value" />
If you want to have truly dynamic components combined with nuxt/components you can make use of the way Nuxt prepares the components automagically. I created a package which enables dynamic components for auto-imported components (you can check it out here: #blokwise/dynamic).
Long story short: with the package you are able to dynamically import your components like this:
<NuxtDynamic :name="componentName" some-property="some-value" />
Where componentName might be 'MyComponent'. The name can either be statically stored in a variable or even be dynamically created through some API call to your backend / CMS.
If you are interested in how the underlying magic works you can checkout this article: Crank up auto import for dynamic Nuxt.js components
According to the official Documentation: Starting from v2.13, Nuxt can auto import your components when used in your templates, to activate this feature, set components: true in your configuration
you are talking about async components. You simply need to use the following syntax to return the component definition with a promise.
Vue.component('componentName', function (resolve, reject) {
requestTemplate().then(function (response) {
// Pass the component definition to the resolve callback
resolve({
template: response
})
});
})

Vue JS How to catch errors globally and display them in a top level component

I have set up Vue so that I have a top level AppLayout component which just includes a Navigation Menu component, the router-view and, which uses v-if to optionally display an ErrorDisplay component if the error data item is set. I set this from an err state variable in the Vuex store.
That is where I want to get to. However, I think the problem is more fundamental.
In a lower component, I have a submit function that gets called when I click the submit button. To test error handling I have put
throw new Error('Cannot Submit');
In my Main.js I have
handlers for window.orerror, window.addEventListner, Vue.config.errorhandler, Vue.config.warnhandler
All of these should just call the errHandler function, which just calls an action to update the err variable in the state. The hope being that this will then result in the ErrorDisplay component showing on my top level component.
However, I have console.log statements as the first statement in all the above handlers and in my errHandler function. None of these console.logs are getting executed.
In the Console in Chrome, I am just seeing
[vue warn]: Error in v-on handler: "Error: Cannot Submit"
So it is getting the text from my throw, but none of the error handlers seem to be capturing this?
Vue provides Global configuration config.errorHandler to capture error inside Vue components Globally.
As per Official Docs
Assign a handler for uncaught errors during component to render function and watchers. The handler gets called with the error and the Vue instance.
This is how it can be used:
Vue.config.errorHandler = function (err, vm, info) {
// handle error
// `info` is a Vue-specific error info, e.g. which lifecycle hook
// the error was found in. Only available in 2.2.0+
}
Official docs
Hope this helps!
Did more research and I think someone may have already raised a bug report with Vue for this
PR on Vue
https://github.com/vuejs/vue/pull/5709
So it looks like the problem is that the way that I am trying to test this isn't being caught.

Vue instance inside another Vue instance

I’m integrating Vue with a CMS called AEM thats works basically as component base system like Vue works too. But instead of having a webpack and imports of .vue files, every component on this CMS is a new Vue instance (new Vue({…})). So on my page I have a lot of Veu instances that communicate with each other using the same store (vuex).
This is actually working fine, but I have a scenario when I need a CMS component inside another. Since both this components are a unique vue instance and the “el” property from the parent includes the “el” from the child, the child component doesn’t work.
I know that this is not the expected use of this lib, but is there any way that I can tell or share the same “context” on both vue instances or even another approach for this scenario.
Thx,
Alexandre.
There should be only one instance of Vue.
I suggest you to create single empty Vue instance inside the body tag
All your existent Vue instances transform into components
Register all components in the root Vue instance
With this approach it will be fine to nest one component into another
You should use only one Vue instance as #shrpne mentioned.
If you keep instantiating Vue instances for every component, you'll run into issues while debugging or with component communication and overall this becomes very missy and you miss out on parent-child communication and inheritance provided by Vue.
I don't know about your Vue architecture, but I am currently working on a manual for working with Vue in AEM.
The basic premise is to use Vue's inline-template and vanilla-js, No typescript, nodeJS build, jsx or anything else at build time, just vanilla-js so that when your page is loaded and even before your js bundle is present, the DOM is already there, you just need to mount components by instantiating one Vue instance that will mount all components. This is also great for SEO (unless you plan to server-side render Vue components in java... which is possible theoretically, but good luck!)
Here is a sample AEM/Vue component:
<simple-counter inline-template>
<button v-bind:style="style" v-on:click="add">${properties.clicksText || 'clicks: '} {{ counter }}</button>
</simple-counter>
the JS:
notice how it does not have a template in the JS, because it's inlined above
Vue.component('simple-counter', {
data: function() {
return {
counter: 0,
style: {
color: 'red',
width: '200px'
}
}
},
methods: {
add: function() {
this.counter = this.counter + 1;
this.style.color = this.style.color == 'red' ? 'green' : 'red';
}
}
})
You can build more AEM components in this fashion, then at the end of your clientlib when all your Vue components have been registered, you can run:
new Vue({ el: '#app'})
This, off course, assumes that your page body or some other parent element has the id: app.
The second part of this, how do you enable re-mount of components after authoring dialog is submitted, you could just refresh the page.
I have a question about how we can re-mount components without refreshing the page here
The basic idea is to add an afteredit event to the component and run a new Vue instance only on the newly mutated component... still working on that
Solution:
Replace all new Vue(...) stuff into Vue.component(...) Vue.extend(...) etc for better interface management.
Only use ONE Vue instance witch is new Vue({...options})
Slice your vuex store into modules.
Google teacher knows everything.