I am using Webpack 2 and importing my components via special require syntax.
There are over 100 components, but only 5-10 used at a time. Most of them (but not all) partially have same functionality like props and lifecycle hooks.
Here is code:
// app.js
...
Vue.component("foo", resolve => {
require(['./components/foo.vue'], resolve);
});
...
I want to apply mixin to async component, but how to do that? Global mixin apply to all components, but that's not what I need.
I found that feature request, but it closed.
I found some creepy(?) solution, but it works:
// mixins.js
export default class Mixins {
static fooMixin() {
return {
created: function () {
console.log('mixin hook called');
}
}
}
}
// app.js
Vue.component("foo", resolve => {
require(['./components/foo.vue'], resolve);
});
// foo.vue
<script>
import Mixins from "mixins";
export default {
...
mixins: [Mixins.fooMixin()]
}
</script>
But I hope that there is a more elegant solution.
Related
I am working on my first Vue project. I'm used to React and vanilla js, but just getting my head around a few concepts in Vue here.
In particular, importing state and action props from a Pinia store, and seemingly having to import those multiple times in a single Vue component (something I don't need to do in React).
In this example, I am importing a simple count value, and an increment function, and trying to use these in a few different places:
<script setup>
// I import everything initially in setup, which works fine,
// and these props (currentCount and incrementCount)
// can be used in my template:
import { storeToRefs } from 'pinia';
import { useStore } from '#/stores/store';
const { currentCount } = storeToRefs(useStore());
const { incrementCount } = useStore();
</script>
<template>
<main>
Current count: {{ currentCount }}
<button #click="incrementCount">Increment</button>
</main>
</template>
<script>
// I can't use store values from setup here.
// This doesn't work:
// console.log(currentCount);
// I also can't import store values here.
// I get the following error:
// "getActivePinia was called with no active Pinia"
// const { currentCount } = storeToRefs(useStore());
export default {
mounted() {
// I have to import store values here for them to work:
const { currentCount } = storeToRefs(useStore());
console.log(currentCount);
},
watch: {
// weirdly, this reference to watching "currentCount" works:
currentCount() {
// I also have to import store values here for them to work:
const { currentCount } = storeToRefs(useStore());
console.log(currentCount);
},
},
};
</script>
As you can see, if I want to use store values in my template, on mount, and in a watcher (whereby I'd use React's useEffect hook) I am having to import the store props 3 times in total.
Is this correct / normal? Is there a simpler way to achieve what I'm doing, where I only import props once? I want to be sure I haven't missed something and am not doing something in an unusual way.
Thanks for any help and advice!
Pinia was designed with Composition API in mind.
So its intended usage is inside setup() function, where you'd only import it once.
To use it outside of a setup() function, you have two main routes:
inside components, you can just return it from setup() and it becomes available in any hook/method/getter. Either as this.store or spread:
import { useStore } from '#/store'
import { toRefs } from 'vue'
// or from '#vue/composition-api' in Vue2
export default {
setup: () => ({ ...toRefs(useStore()) })
}
/* this makes every state prop, getter or action directly available
on current component instance. In your case, `this.currentCount`.
Obviously, you can also make the entire store available as `this.someStore`:
setup: () => ({ someStore: useSomeStore() })
// now you can use `this.someStore` anywhere
*/
a more general approach is to export the pinia instance (returned by createPinia()), from main.(js|ts), import it where you need the store and then call useStore() passing the pinia instance as an argument.
This can be done anywhere, even outside of components.
Generic example:
import { pinia } from 'main.js'
import { useSomeStore } from '#/store'
const someStore = useSomeStore(pinia);
I should probably also mention the mapState helper provided by pinia. It allows you to select only a few of the keys exposed to current instance. Example:
import { mapState } from 'pinia'
// ...
computed: {
...mapState(useSomeStore, [ 'currentCount'])
}
// Now `this.currentCount` is available
Note: mapState is weirdly named, as it allows you to access more than just state props (also getters and actions). It was named mapState to match the similar helper from vuex.
An even more general approach is to add your store as global, using the plugin registration API in Vue2:
import { useSomeStore } from '#/store';
import { createPinia } from 'pinia';
const pinia = createPinia();
const someStorePlugin = {
install(Vue, options) {
Vue.prototype.someStore = useSomeStore(options.pinia)
}
};
Vue.use(someStorePlugin, { pinia });
new Vue({ pinia });
After this, every single component of your Vue instance will have this.someStore available on it, without you needing to import it.
Note: I haven't tested adding a store in globals (and I definitely advise against it - you should avoid globals), but i expect it to work.
If you want to combine pinia stores with the options API, one way to do it is to use the setup() function inside the options to call useStore:
<script>
import { useStore } from '#/stores/store';
export default {
setup() {
const store = useStore();
return {store}
},
watch: {
store.currentBrightness(newVal, oldVal){
// your code
}
},
methods: {
// inside methods use this.store
},
mounted() {
console.log(this.store.currentCount);
}
}
</script>
Some might consider this as a unwanted mix of composition and options API, but in my view it is a quite good solution for pinia stores.
Nechoj, has the most straightforward answer. Also if you have multiple stores you can always import the stores as necessary into a parent component then use inject just add some parts. For example I have a route data that is called via an api, I don't need it everywhere all the time so i call it in a parent then use inject to use those routes in a drop down that might be a great grandchild component. I don't need that whole utils store just the routes.
index page:
import { useUtilsStore } from "src/stores/utilsStore";
const passengerRoutes = computed(() => utilsStore.getPassengerRoutes);
provide("passengerRoutes", passengerRoutes);
grandchild component:
const compRoutes = inject("passengerRoutes");
I have multiple components which all need to do the same thing. (A simple function which maps over their child components and does something to each one). At the moment I am defining this method in each of the components. But I only want to define it once.
I could define it in the top level component and then pass it down as a prop. But that doesn't feel quite right. It is more a library function than a prop. (It seems to me).
What is the correct way of doing this?
Utils.js with latest Javascript ES6 syntax
Create the Utils.js file like this with multiple functions, etc
const someCommonValues = ['common', 'values'];
export const doSomethingWithInput = (theInput) => {
//Do something with the input
return theInput;
};
export const justAnAlert = () => {
alert('hello');
};
Then in your components that you want to use the util functions, import the specific functions that are needed. You don't have to import everything
import {doSomethingWithInput, justAnAlert} from './path/to/Utils.js'
And then use these functions within the component like this:
justAnAlert();
<p>{doSomethingWithInput('hello')}</p>
If you use something like browserify then you can have an external file i.e util.js that exports some utility functions.
var doSomething = function(num) {
return num + 1;
}
exports.doSomething = doSomething;
Then require it as needed
var doSomething = require('./util.js').doSomething;
If you want to manipulate state in helper functions follow this:
Create a Helpers.js file:
export function myFunc(){ return this.state.name; //define it according to your needs }
Import helper function in your component file:
import {myFunc} from 'path-to/Helpers.js'
In your constructor add that helper function to the class
constructor(){ super() this.myFunc = myFunc.bind(this) }
In your render function use it:
`render(){
{this.myFunc()}
}`
Here are some examples on how you can reuse a function (FetchUtil.handleError) in a React component (App).
Solution 1: Using CommonJS module syntax
module.exports = {
handleError: function(response) {
if (!response.ok) throw new Error(response.statusText);
return response;
},
};
Solution 2: Using "createClass" (React v16)
util/FetchUtil.js
const createReactClass = require('create-react-class');
const FetchUtil = createReactClass({
statics: {
handleError: function(response) {
if (!response.ok) throw new Error(response.statusText);
return response;
},
},
render() {
},
});
export default FetchUtil;
Note: If you are using React v15.4 (or below) you need to import createClass as follows:
import React from 'react';
const FetchUtil = React.createClass({});
Source: https://reactjs.org/blog/2017/04/07/react-v15.5.0.html#migrating-from-reactcreateclass
Component (which reuses FetchUtil)
components/App.jsx
import Categories from './Categories.jsx';
import FetchUtil from '../utils/FetchUtil';
import Grid from 'material-ui/Grid';
import React from 'react';
class App extends React.Component {
constructor(props) {
super(props);
this.state = {categories: []};
}
componentWillMount() {
window
.fetch('/rest/service/v1/categories')
.then(FetchUtil.handleError)
.then(response => response.json())
.then(categories => this.setState({...this.state, categories}));
}
render() {
return (
<Grid container={true} spacing={16}>
<Grid item={true} xs={12}>
<Categories categories={this.state.categories} />
</Grid>
</Grid>
);
}
}
export default App;
I'll show two styles below, and you'll want to choose depending on how much the components' logic relate to each other.
Style 1 - Relatively related components can be created with callback references, like this, in ./components/App.js...
<SomeItem
ref={(instance) => {this.childA = instance}}
/>
<SomeOtherItem
ref={(instance) => {this.childB = instance}}
/>
And then you can use shared functions between them like this...
this.childA.investigateComponent(this.childB); // call childA function with childB as arg
this.childB.makeNotesOnComponent(this.childA); // call childB function with childA as arg
Style 2 - Util-type components can be created like this, in ./utils/time.js...
export const getTimeDifference = function (start, end) {
// return difference between start and end
}
And then they can be used like this, in ./components/App.js...
import React from 'react';
import {getTimeDifference} from './utils/time.js';
export default class App extends React.Component {
someFunction() {
console.log(getTimeDifference("19:00:00", "20:00:00"));
}
}
Which to use?
If the logic is relatively-related (they only get used together in the same app), then you should share states between components. But if your logic is distantly-related (i.e., math util, text-formatting util), then you should make and import util class functions.
Another solid option other than creating a util file would be to use a higher order component to create a withComponentMapper() wrapper. This component would take in a component as a parameter and return it back with the componentMapper() function passed down as a prop.
This is considered a good practice in React. You can find out how to do so in detail here.
Sounds like a utility function, in that case why not put it in a separate static utility module?
Otherwise if using a transpiler like Babel you can make use of es7's static methods:
class MyComponent extends React.Component {
static someMethod() { ...
Or else if you are using React.createClass you can use the statics object:
var MyComponent = React.createClass({
statics: {
customMethod: function(foo) {
return foo === 'bar';
}
}
However I don't advise those options, it doesn't make sense to include a component for a utility method.
Also you shouldn't be passing a method down through all your components as a prop it will tightly couple them and make refactoring more painful. I advise a plain old utility module.
The other option is to use a mixin to extend the class, but I don't recommend that as you can't do it in es6+ (and I don't see the benefit in this case).
Shouldn't you use a Mixin for this ? See https://facebook.github.io/react/docs/reusable-components.html
Although they are falling out of favour see https://medium.com/#dan_abramov/mixins-are-dead-long-live-higher-order-components-94a0d2f9e750
Might be useful
I'm learning TDD whilst building my Vue app, and trying to abide by the strict laws of only writing enough production code to satisfy a failing unit test. I am really enjoying this approach, but I have run into a roadblock in regards to adding methods to a Vue instance, and testing that they have been called when the event fires from the element in the template.
I cannot find any suggestions as to how I can mock a Vue method given that if I mock the proxied method, it ends up not being called (I'm using Jest and Vue Test Utils).
I am also using Cypress, so I can fill in this test in e2e, but I would like to be able to cover as much as possible with unit tests.
I own the book "Testing Vue.js Applications" by Edd Yerburgh, but in the section regarding testing component methods, he simply states the following:
Often, components use methods internally. For example, to log to the console when a button is clicked [...] You can think of these as private methods—they aren’t intended to be used outside of the component. Private methods are implementation details, so you don’t write tests for them directly.
This approach obviously does not allow the stricter laws of TDD to be followed, so how do the TDD purists handle this?
ButtonComponent.vue
<template>
<button #click="method">Click me</button>
</template>
<script>
export default: {
methods: {
method () {
// Have I been called?
}
}
}
</script>
ButtonComponent.spec.js
it('will call the method when clicked', () => {
const wrapper = shallowMount(ButtonComponent)
const mockMethod = jest.fn()
wrapper.vm.method = mockMethod
const button = wrapper.find('button')
button.vm.$emit('click')
expect(mockMethod).toHaveBeenCalled()
// Expected mock function to have been called, but it was not called
})
Solution 1: jest.spyOn(Component.methods, 'METHOD_NAME')
You could use jest.spyOn to mock the component method before mounting:
import MyComponent from '#/components/MyComponent.vue'
describe('MyComponent', () => {
it('click does something', async () => {
const mockMethod = jest.spyOn(MyComponent.methods, 'doSomething')
await shallowMount(MyComponent).find('button').trigger('click')
expect(mockMethod).toHaveBeenCalled()
})
})
Solution 2: Move methods into separate file that could be mocked
The official recommendation is to "abstract the hard parts away", and use Jest's various mocking mechanisms to mock the abstracted module invoked by the component under test.
For example, to verify a click-handler is invoked:
Move the click-handler's body into a shared JavaScript file.
Import the shared module into the component under test and in your tests (make sure to use the same import path in both cases).
Call jest.mock() to mock the exported functions of the shared module.
Reset the mock in your test suite's beforeEach(). This might only be necessary when there are multiple tests in the suite.
// #/components/MyComponent/utils.js
export function doSomething() { /*...*/ } //1️⃣
// #/components/MyComponent/MyComponent.vue (<script>)
import { doSomething } from '#/components/MyComponent/utils' //2️⃣
export default {
methods: {
onClick() {
doSomething() //1️⃣
}
}
}
// #/test/MyComponent.spec.js
import { doSomething } from '#/components/MyComponent/utils' //2️⃣
jest.mock('#/components/MyComponent/utils') //3️⃣
describe('MyComponent', () => {
beforeEach(() => doSomething.mockClear()) //4️⃣
it('click does something', async () => {
await shallowMount(MyComponent).find('button').trigger('click')
expect(doSomething).toHaveBeenCalled()
})
})
Solution 3: setMethods() (pre v1.0)
Use setMethods() (deprecated as of v1.0) to overwrite a component method:
describe('MyComponent', () => {
it('click does something', async () => {
// Option A:
const mockMethod = jest.fn()
const wrapper = shallowMount(MyComponent)
wrapper.setMethods({ doSomething: mockMethod })
await wrapper.find('button').trigger('click')
expect(mockMethod).toHaveBeenCalled()
// Option B:
const mockMethod = jest.fn()
const wrapper = shallowMount(MyComponent, {
methods: {
doSomething: mockMethod
}
})
await wrapper.find('button').trigger('click')
expect(mockMethod).toHaveBeenCalled()
})
})
demo
If jest.spyOn() doesn't work and you need to stub a method coming from a third party library, you could try creating a mixin that you can inject when mounting the component:
const mock = jest.fn()
const wrapper = shallowMount(MyComponent, {
localVue,
store,
router,
mixins: [{
methods: {
$thirdPartyMethod: mock
}
}]
})
// later on in your test...
expect(mock).toHaveBeenCalled()
As tony19 mentioned, using the spyOn method will work for you. I have also found that I need to add parentheses () to the method in the template in order for it to be picked up. I got the tests to pass with the following files:
ButtonComponent.vue
<template>
<button #click="method()">Click me</button>
</template>
<script>
export default {
methods: {
method() {
// Have I been called?
}
}
}
</script>
ButtonComponent.spec.js
import ButtonComponent from '#/components/ButtonComponent'
import { shallowMount } from '#vue/test-utils'
it('will call the method when clicked', () => {
const wrapper = shallowMount(ButtonComponent)
const mockMethod = jest.spyOn(wrapper.vm, 'method')
const button = wrapper.find('button')
button.trigger('click')
expect(mockMethod).toHaveBeenCalled()
// passes
})
Let's say that I have a script file, foo.js:
function doStuff() {
// how to access store and other plugins here?
}
export default { doStuff }
Without passing the calling component as an argument, how can I access things like app or installed plugins like store, i18n in a script file like the one above?
There are multiple ways to call custom function with this being a reference to the component it was invoked in.
1) Using mixins
Custom function can be declared as a method and used within component via this.customMethod.
customHelpers.js
const customHelpers = {
methods: {
doStuff () {
// this will be referenced to component it is executed in
}
}
}
component.vue
// component.vue
import customHelpers from '~/mixins/customHelpers'
export default {
mixins: [customHelpers],
mounted () {
this.doStuff()
}
}
2. Using context injection
Declare custom plugin:
plugins/customHelpers.js
import Vue from 'vue'
Vue.prototype.$doStuff = () => { /* stuff happens here */ }
And use plugin in nuxt.config.js
export default {
..., // other nuxt options
plugins: ['~/plugins/customHelpers.js']
}
This makes method available inside every component:
export default {
mounted () {
this.$doStuff()
}
}
3) Using combined injection
Same as method 2, but injection will be also accessible inside fetch, asyncData and inside store modules. Bindings to this may vary, since context is not available everywhere.
plugins/customHelpers.js
export default ({ app }, inject) => {
inject('doStuff', () => { /* stuff happens here */ })
}
And use plugin in nuxt.config.js
export default {
..., // other nuxt options
plugins: ['~/plugins/customHelpers.js']
}
Usage example:
export default {
asyncData ({ app }) {
app.$doStuff()
}
}
Please, refer to documentation for more examples.
I have a situation to convert several jQuery components to VueJs.
In general, I know what to do, but in some cases, I need to replace some functions calls.
For instance:
Component
const Component = (function () {
const initialize = () => {
return 'Tony Stark'
}
return {
initialize: initialize
}
})
export default Component
Random file, using exported function
$( document ).ready(function() {
Component.initialize()
});
What is the best solution to Component.initialize() still working?
Because I have this request in several files.
I got a solution:
import Component from './component'
// Call method
Component.methods.method()
You may import the component to every Vue component and use it like this:
import someComponent from './someComponent'
export default {
created () {
Component.initialize()
}
}
Or you could use instance properties, see https://v2.vuejs.org/v2/cookbook/adding-instance-properties.html