Disable attribute inheritance globally in Vue3 - vue.js

In Vue3 - is there any way to globally disable Attribute Inheritance? I know that it's possible to set inheritAttrs to false when registering / creating a new component but what if I want to generally disallow this kind of behavior without having to change / add inheritAttrs: false to every single component I create?
A related question is Vue JS Non prop attributes and Disabling Attribute Inheritance - but it's only about if it's possible, not how you can do this globally...
The reason I want to disable it is that I want to achieve the same kind of behavior as in React / Angular - forwarding any props without receiving any error / warnings leads to inconsistency / unexpected behavior (possibly - especially with properties such as class and other native HTML Attributes).
A workaround we currently have is to import and re-export any component and "pre-processing" them:
import * as components from './components.ts'; // all components are re-exported with names
export * from './components.ts';
// Disable attribute-inheritance for every single component
Object.values(components).forEach((v) => (v.inheritAttrs = false));

It's technically possible to set inheritAttrs=false for every component by using a global mixin:
// main.js
import { createApp } from 'vue'
createApp(App)
.mixin({ inheritAttrs: false }) 👈
.mount('#app')
However, you should be aware that this does not cause Vue to emit warnings/errors when trying to set nonexistent props. Any such usage is silently ignored.
demo

We ended up writing a vite plugin to avoid mixins (since we're using vue3 and mixins are only there for backwards compatibility) to automatically inject { inheritAttrs: false }:
import { Plugin } from 'vite';
const DISABLED_INHERIT_SCRIPT = `
<script lang="ts">
export default { inheritAttrs: false };
</script>
`;
export const disableAttributeInheritance = (disable = true): Plugin => ({
name: 'DisableInheritAttributesPlugin',
transform: (src, id) => ({
code:
id.endsWith('.vue') && disable
? `${src}\n\n${DISABLED_INHERIT_SCRIPT}`
: src,
map: null
})
});
and use it like that:
export default defineConfig({
plugins: [
disableAttributeInheritance(),
vue()
]
});

Related

How to use VueRouter with Storybook

I'm trying to write a story for a component that references this.$route.params. I'm not sure how to synthetically define this.$route in the context of a story. I think the solution is to use decorators, but all the examples in the docs focus on rendering, like adding a wrapping <div> etc. I'm not sure how to inject values.
I also found this project which appears designed for this exact situation, but it hasn't been maintained in years and README references outdated syntax that doesn't match modern versions of Storybook, so I don't think it's an option.
Here's what doesn't work:
import AssetShow from '../app/javascript/src/site/components/assets/Show'
export default {
title: 'Site/AssetShow',
component: AssetShow,
parameters: {
}
};
export const CustomerSpotlight = () => ({
components: { AssetShow },
template: '<AssetShow />',
});
import Vue from 'vue'
import VueRouter from 'vue-router'
import StoryRouter from 'storybook-vue-router';
CustomerSpotlight.decorators = [
(story) => {
Vue.use(VueRouter)
return {
components: { story },
template: '<story />'
}
}
];
The component I'm writing the story for has this:
mounted() {
axios.get(`.../bla/${this.$route.params.id}.json`)
},
...which causes Storybook to throw this error: TypeError: Cannot read properties of undefined (reading 'params')
I suppose that your intention is to do something with the story's component based on the route parameters?
If that is the case, then I don't think you need to define the route.params within the story context. I suggest either keeping that code within the component itself, or create an option within the story for the user to simulate adding parameters to the path. Which you can simply have as an input text / select field that you send down to the component as a prop.

How to correctly set up Cypress 10, Vue2, Vuetify, Composition API for component testing?

The guide is quite confusing and obviously not correct when trying to set up Cypress 10 for component testing with Vue2 and Vuetify with composition API. There's lots of errors of unknown tags, things returned from setup() aren't accessible, spread operators where there shouldn't be, imports that don't work etc. What's the correct way to set things up so testing works?
You need to set up Vuetify as regular, to the global Vue object. Then in the mount you need to give the Vuetify object to the mount function so it can be found by the components. When using Composition API that also needs to be set up regularly to the global instance (unlike Vuetify it also works in the local instance, if you want).
Then mount the component inside a v-appso it should work properly and pass arugments around.
So component.ts file will include this:
import { mount } from 'cypress/vue2'
import Vuetify from 'vuetify'
import VueCompositionAPI from '#vue/composition-api';
import Vue from 'vue'
import { VApp } from 'vuetify/lib/components/VApp';
Vue.use(Vuetify);
Vue.use(VueCompositionAPI);
Cypress.Commands.add('mount', (component, args) => {
args.vuetify = new Vuetify(yourVuetifyOptions);
return mount({ render: (h) => h(VApp, [h(component, args)]) }, args);
})
When using the mount just do:
cy.mount(myComponent, { props: {someProp: 123 } });
If you need to set up plugins for the local Vue instance in the test they need to be set in args.extensions.plugins, the guide seems to mention globals but that is incorrect.
cy.mount(myComponent, { props: {someProp: 123 }, extensions: { plugins: [MyPlugin] } });
Note that I'm using args for both settings parameters for mount and also for the component, if needed those two can be separated. But there shouldn't be much clashing of properties and attributes so this works.
Also the props/attributes/etc for the component must be given as they're given to createElement, not mount (so props instead of propsData etc).

Importing a Vue library in nuxt.js via plugins

Any idea how I'm going to use this plugin? https://github.com/DimanVorosh/vue-json-rpc-websocket/blob/e2199d89dc15f50e57e7c5c70adfd95e5ceb5cda/src/wsMain.js
I see that it is auto registering with vue but I can't use it in nuxt.
I created the plugins/vue-json-rpc-websocket.client.js, registered in nuxt.config.js as
'~/plugins/vue-json-rpc-websocket.client.js'
but I have no idea what to write in the inject method and IF I have to do it to make it work. this.$socket is undefined in component.
import Vue from 'vue'
import JRPCWS from 'vue-json-rpc-websocket'
Vue.use(JRPCWS, 'wss://bsc-ws-node.nariox.org:443', {
reconnectEnabled: true,
reconnectInterval: 5000,
reconnectAttempts: 3
})
// do I need this?
export default ({ app }, inject) => {
// Inject $hello(msg) in Vue, context and store.
// inject('hello', msg => console.log(`Hello ${msg}!`))
}
also, any idea how can I ENV the 'wss://bsc-ws-node.nariox.org:443' string?
Totally working on my side with the package that you're using and your given configuration. No need to inject anything so far!
Here is a fresh repo created for the example: https://github.com/kissu/so-nuxt-json-rpc-websocket
The below screenshot is using a console.log(this.$socket) in a mounted hook in /pages/index.vue but you can also use $vm0 and access the instance directly from the devtools after selecting the root component (in the screenshot too).
For the env variables part, you can create an .env file at the root of your directory like this
WS_URL="wss://echo.websocket.org"
// nuxt.config.js
export default {
publicRuntimeConfig: {
wsUrl: process.env.WS_URL,
},
}
Then, use this variable in your plugin like this
import Vue from 'vue'
import JRPCWS from 'vue-json-rpc-websocket'
export default ({ $config: { wsUrl } }) => {
Vue.use(JRPCWS, wsUrl, {
reconnectEnabled: true,
reconnectInterval: 5000,
reconnectAttempts: 3
})
}

How to integrate inertiaJS with quasar framework?

I would like to integrate intertiaJS into my Quasar app so that I can communicate with my Laravel backend. My problem now is that the general stuff is taken over by the Quasar CLI, which is good in principle, but in this case it takes away my entry point as described at https://inertiajs.com/client-side-setup:
import { createApp, h } from 'vue'
import { App, plugin } from '#inertiajs/inertia-vue3'
const el = document.getElementById('app')
createApp({
render: () => h(App, {
initialPage: JSON.parse(el.dataset.page),
resolveComponent: name => require(`./Pages/${name}`).default,
})
}).use(plugin).mount(el)
My thought is that I could use a boot file like the offered in Quasar (https://quasar.dev/quasar-cli/boot-files), but I have to admit that I don't have the right approach.
When I look at the app.js that is automatically generated, I see that nothing special happens in the rendering:
/**
* THIS FILE IS GENERATED AUTOMATICALLY.
* DO NOT EDIT.
*
* You are probably looking on adding startup/initialization code.
* Use "quasar new boot <name>" and add it there.
* One boot file per concern. Then reference the file(s) in quasar.conf.js > boot:
* boot: ['file', ...] // do not add ".js" extension to it.
*
* Boot files are your "main.js"
**/
import Vue from 'vue'
import './import-quasar.js'
import App from 'app/src/App.vue'
import createStore from 'app/src/store/index'
import createRouter from 'app/src/router/index'
export default async function () {
// create store and router instances
const store = typeof createStore === 'function'
? await createStore({Vue})
: createStore
const router = typeof createRouter === 'function'
? await createRouter({Vue, store})
: createRouter
// make router instance available in store
store.$router = router
// Create the app instantiation Object.
// Here we inject the router, store to all child components,
// making them available everywhere as `this.$router` and `this.$store`.
const app = {
router,
store,
render: h => h(App)
}
app.el = '#q-app'
// expose the app, the router and the store.
// note we are not mounting the app here, since bootstrapping will be
// different depending on whether we are in a browser or on the server.
return {
app,
store,
router
}
}
I.e. in principle I should be able to link in without it causing any conflict situations. The question is, how would that look?
I have to link into the rendering afterwards and overwrite it as described in the code example. I would like to stay with the Quasar Cli, because it is very useful and the situation described here is the only exception.
p7
the boot files is the right place to inject and initialize your own dependencies or just configure some startup code for your application.
I have not had the opportunity to use the library you mention, but I detail a little how you could implement
create your boot file
import { plugin } from '#inertiajs/inertia-vue';
export default async({ app, Vue }) => {
Vue.use(plugin);
}
until there you have 50%. On the other hand, you cannot do a mixin to the main instance but you could do it for each page, however I recommend that you make a component part to which you add the data you need and make a mixin of the library you need
<template>
<div />
</template>
<script>
import { App } from '#inertiajs/inertia-vue';
export default {
mixins: [App],
props: ['initialPage', 'resolveComponent'],
}
</script>
In order to do this, modify according to how the library you use works.

Vue/Nuxt: How to define a global method accessible to all components?

I just want to be able to call
{{ globalThing(0) }}
in templates, without needing to define globalThing in each .vue file.
I've tried all manner of plugin configurations (or mixins? not sure if Nuxt uses that terminology.), all to no avail. It seems no matter what I do, globalThing and this.globalThing remain undefined.
In some cases, I can even debug in Chrome and see this this.globalThing is indeed defined... but the code crashes anyway, which I find very hard to explain.
Here is one of my many attempts, this time using a plugin:
nuxt.config.js:
plugins: [
{
src: '~/plugins/global.js',
mode: 'client'
},
],
global.js:
import Vue from 'vue';
Vue.prototype.globalFunction = arg => {
console.log('arg', arg);
return arg;
};
and in the template in the .vue file:
<div>gloabal test {{globalFunction('toto')}}</div>
and... the result:
TypeError
_vm.globalFunction is not a function
Here's a different idea, using Vuex store.
store/index.js:
export const actions = {
globalThing(p) {
return p + ' test';
}
};
.vue file template:
test result: {{test('fafa')}}
.vue file script:
import { mapActions } from 'vuex';
export default {
methods: {
...mapActions({
test: 'globalThing'
}),
}
};
aaaaaaaaand the result is.........
test result: [object Promise]
OK, so at least the method exists this time. I would much prefer not to be forced to do this "import mapActions" dance etc. in each component... but if that's really the only way, whatever.
However, all I get is a Promise, since this call is async. When it completes, the promise does indeed contain the returned value, but that is of no use here, since I need it to be returned from the method.
EDIT
On the client, "this" is undefined, except that..... it isn't! That is to say,
console.log('this', this);
says "undefined", but Chrome's debugger claims that, right after this console log, "this" is exactly what it is supposed to be (the component instance), and so is this.$store!
I'm adding a screenshot here as proof, since I don't even believe my own eyes.
https://nuxtjs.org/guide/plugins/
Nuxt explain this in Inject in $root & context section.
you must inject your global methods to Vue instance and context.
for example we have a hello.js file.
in plugins/hello.js:
export default (context, inject) => {
const hello = (msg) => console.log(`Hello ${msg}!`)
// Inject $hello(msg) in Vue, context and store.
inject('hello', hello)
// For Nuxt <= 2.12, also add 👇
context.$hello = hello
}
and then add this file in nuxt.config.js:
export default {
plugins: ['~/plugins/hello.js']
}
Use Nuxt's inject to get the method available everywhere
export default ({ app }, inject) => {
inject('myInjectedFunction', (string) => console.log('That was easy!', string))
}
Make sure you access that function as $myInjectedFunction (note $)
Make sure you added it in nuxt.config.js plugins section
If all else fails, wrap the function in an object and inject object so you'd have something like $myWrapper.myFunction() in your templates - we use objects injected from plugins all over the place and it works (e.g. in v-if in template, so pretty sure it would work from {{ }} too).
for example, our analytics.js plugin looks more less:
import Vue from 'vue';
const analytics = {
setAnalyticsUsersData(store) {...}
...
}
//this is to help Webstorm with autocomplete
Vue.prototype.$analytics = analytics;
export default ({app}, inject) => {
inject('analytics', analytics);
}
Which is then called as $analytics.setAnalyticsUsersData(...)
P.S. Just noticed something. You have your plugin in client mode. If you're running in universal, you have to make sure that this plugin (and the function) is not used anywhere during SSR. If it's in template, it's likely it actually is used during SSR and thus is undefined. Change your plugin to run in both modes as well.
This would be the approach with Vuex and Nuxt:
// store/index.js
export const state = () => ({
globalThing: ''
})
export const mutations = {
setGlobalThing (state, value) {
state.globalThing = value
}
}
// .vue file script
export default {
created() {
this.$store.commit('setGlobalThing', 'hello')
},
};
// .vue file template
{{ this.$store.state.globalThing }}