I cannot make my store work like I want.
I know how to do a store using vue 3 but with Nuxt 3 my value does not update etc
My Function To add something to the store:
<script setup>
function addWeight() {
setters.setWeights(state(), {weight : weightInput.value, date : new Date().getTime()})
}
<script/>
My Store :
export const state = () => ({
weights: []
})
export const getters = {
getWeights(state){
return state.weights;
}
}
export const setters = {
setWeights(state, {weight , date}) {
state.weights.push({
weight, date
});
}
}
export const actions = {
async fetchWeights(state) {
const res = { data : [64, 67, 79, 70, 100, 123, 23]};
state.weights = res.data;
return res.data;
}
}
state.weight or getters.getWeights(state) always return []
bash npm install pinia #pinia/nuxt --legacy-peer-deps
store/index.ts file:
import {defineStore} from "pinia";
interface weightInterface {
weight: number;
date: string;
}
export const useStore = defineStore({
id: 'store',
state : () => ({
weights : Array<weightInterface>()
}),
getters : {
getWeights(): weightInterface[]{
return this.weights;
},
getDateWeights(): weightInterface[] {
//sort by date
this.weights.sort((weight1, weight2) => +weight1.date - +weight2.date);
//from 1238173458 to 09/23/2022
for (let i = 0; i < this.weights.length; i++){
this.weights[i].date = new Date(this.weights[i].date).toLocaleDateString();
}
return this.weights;
}
},
actions : {
addWeight(weight: number, date: string): void {
const newWeight: weightInterface = { weight : weight,date: date};
this.weights.push(newWeight);
}
}
});
Works like a charm
Related
I am using Quasar v2, using the Vue Composition API and vue-i18n, and I would like the site title to change display when the active language changes (via a drop down), but whatever I am trying does not result in the title language being changed. Any ideas?
Below is what I have right now (just the essentials):
import { defineComponent, ref, computed } from 'vue';
import { useMeta } from 'quasar';
export default defineComponent({
setup () {
const { t: translate } = useI18n() as any;
const siteTitle = computed(() => translate('title.app') as string);
const pageMetadata = {
title: 'untitled',
titleTemplate: (title: string) => `${title} - ${siteTitle.value}`
};
useMeta(pageMetadata);
}
});
The code I am using to switch languages:
async onChangeLanguage () {
try {
let locale = this.language;
if (this.language === 'en') {
locale = 'en-GB';
}
this.$i18n.locale = locale;
const quasarLang = await import(`quasar/lang/${locale}`);
if (quasarLang) {
Quasar.lang.set(quasarLang.default);
}
} catch (error) {
this.$log.error(error);
}
}
According to the documentation, useMeta will not be reactive if you pass a simple object to it. Rather, you should pass a function that returns the desired value:
export default defineComponent({
setup () {
const { t: translate } = useI18n() as any;
const siteTitle = computed(() => translate('title.app') as string);
useMeta(() => {
const title = 'untitled';
const titleTemplate = `${title} - ${siteTitle.value}`
return { title, titleTemplate }
});
});
So I am trying to add product impressions to my site by following this article:
https://developers.google.com/tag-manager/enhanced-ecommerce#product-impressions
I have created a bit of logic to fire off the required data like this:
import { getCurrentInstance } from "#vue/composition-api";
import { useGtm } from "#gtm-support/vue2-gtm";
export function useTrackProductImpressions(items: any[]) {
console.log("trying to track products", items);
if (!items?.length) return;
const gtm = useGtm();
if (!gtm.enabled()) return;
const dataLayer = window.dataLayer;
if (!dataLayer) return;
console.log(items);
const products = items.map((product, i) => {
const retailers = product.retailers ?? [];
return {
name: product.title, // Name or ID is required.
id: product.id,
price: retailers[0].price,
brand: product.brand,
category: product.categorySlug,
variant: product.variant,
position: i,
};
});
const instance = getCurrentInstance();
const route = instance.proxy.$route;
const routeName = route.meta?.title ?? route.name;
dataLayer.push({ ecommerce: null }); // Clear the previous ecommerce object.
dataLayer.push({
event: "productClick",
ecommerce: {
click: {
actionField: { list: routeName }, // Optional list property.
products,
},
},
// eventCallback: function () {
// document.location = productObj.url;
// },
});
}
This seems pretty normal and I have a click version of this that works fine.
The problem is, the click event can be fired when a link is clicked, this one needs to fire when the view loads, I assume in setup.
So, I have my apollo logic:
import { useQuery, useResult } from "#vue/apollo-composable";
import * as listProducts from "#graphql/api/query.products.gql";
export const defaultParameters: {
identifier?: string;
searchTerm: string;
itemsToShow: number;
page: number;
filters: any;
facets: string[];
} = {
searchTerm: "*",
itemsToShow: 12,
page: 1,
filters: [],
facets: ["Criteria/Attribute,count:100"],
};
export function useSearchProducts(params) {
const { result, loading, error, fetchMore } = useQuery(listProducts, params);
const response = useResult(result, null, (data) => data.searchProducts);
return { response, loading, error, fetchMore };
}
And from my setup I invoke like this:
const { category } = toRefs(props);
const page = ref(1);
const skip = ref(0);
const orderBy = ref([
{
key: "InVenue",
value: "desc",
},
]);
const params = computed(() => {
const filters = createFilters("CategorySlug", [category.value.slug]);
const request = createRequest(
defaultParameters,
page.value,
filters,
orderBy.value
);
return { search: request };
});
const { response, loading, error} = useSearchProducts(params);
Which I can then return to the template like this:
return { response, loading, error };
Now I have done this, I want to add some tracking, so initially I did this:
watch(response, (result) => useTrackProductImpressions(result?.value?.items));
But it was always undefined.
I added console log on result within the watch method and it is always undefined.
So I changed to this:
const track = computed(() => {
useTrackProductImpressions(response.value.items);
});
But this never gets invoked (I assume because it has no return value and I don't use it in the template).
My question is, which is the best way to do what I am attempting? Am I missing something or am I on the write track?
I think I was close, I just used the computed property to return my products like this:
const products = computed(() => {
if (!response.value) return [];
useTrackProductImpressions(response.value.items);
return response.value.items;
});
const total = computed(() => {
if (!response.value) return 0;
return response.value.total;
});
const hasMoreResults = computed(() => {
if (!response.value) return false;
return response.value.hasMoreResults;
});
return {
products,
loading,
error,
total,
hasMoreResults,
skip,
more,
search,
};
I am new to Vue and followed the recommendation to use vue testing library. The only issue is I can't seem to find a way to inject my code into globalProperties in render function.
Does anyone know of an example where I can inject or mock it out?
main.js
app.config.globalProperties.$globals = globalMethods
...
const app = createApp(App)
app.config.globalProperties.$globals = globalMethods
app.config.globalProperties.$globalVars = globalVars
app.component("font-awesome-icon", fontawesome)
app.use(applicationStore);
app.use (Hotjar, hotjarConfig)
app.use(i18n)
app.use(router)
app.mount('#app')
From my vue component in create I am able to call
Component.vue
let formatedObj = this.$globals.maskValues(this.inputValue, this.inputType, this);
...
,
created() {
let formatedObj = this.$globals.maskValues(this.inputValue, this.inputType, this);
this.myInputValue = formatedObj.formatedString;
this.formatedCharacterCount = formatedObj.formatedCharacterCount;
this.prevValue = this.myInputValue;
},
...
tesst.spec.js
import { render } from '#testing-library/vue'
import FormatedNumericInput from '#/components/Component.vue'
import {globalMethods} from'#/config/global-methods'
const label = 'Price'
const initSettings = {
props: {
inputId: 'testInputId1',
labelTxt: label
}
};
beforeEach(() => {
});
test('a simple string that defines your test', () => {
const { getByLabelText } = render(FormatedNumericInput, initSettings)
const input = getByLabelText(label)
// testing logic
expect(input != null).toBe(true)
expect(FormatedNumericInput != null).toBe(true)
})
** ERROR **
TypeError: Cannot read property 'maskValues' of undefined
85 | },
86 | created() {
> 87 | let formatedObj = this.$globals.maskValues(this.inputValue, this.inputType, this);
| ^
88 | this.myInputValue = formatedObj.formatedString;
89 | this.formatedCharacterCount = formatedObj.formatedCharacterCount;
90 | this.prevValue = this.myInputValue;
at Proxy.created (src/components/FormatedNumericInput.vue:87:37)
The second argument of render() is passed to #vue/test-utils mount(), so you could include the global.mocks mounting option to mock $globals.maskValues:
const { getByLabelText } = render(FormatedNumericInput, {
...initSettings,
global: {
mocks: {
$globals: {
maskValues: (inputValue, inputType) => {
const formatedString = globalFormatValue(inputValue) // declared elsewhere
return {
formatedString,
formatedCharacterCount: formatedString.length,
}
}
}
}
}
})
This is my solution in actual Vue3/Vite/Vitest environment, I set some mocks globally, so I don't need to in every test suite.
// vitest.config.ts
import { mergeConfig } from 'vite';
import { defineConfig } from 'vitest/config';
import viteConfig from './vite.config';
export default defineConfig(
mergeConfig(viteConfig, { // extending app vite config
test: {
setupFiles: ['tests/unit.setup.ts'],
environment: 'jsdom',
}
})
);
// tests/unit.setup.ts
import { config } from "#vue/test-utils"
config.global.mocks = {
$t: tKey => tKey; // just return translation key
};
so for you it will be something like
config.global.mocks = {
$globals: {
maskValues: (inputValue, inputType) => {
// ...implementation
return {
formatedString,
formatedCharacterCount,
}
}
}
}
I'm trying to call vuex action in vue component with multiple parameters. But in action method cannot access these passed arguments.
I have already tried passing value in payload as object which is mostly suggested here. but still it is not working.
Please look for
this.getMessageFromServer(payload);
MessageBox.vue
import Vue from 'vue';
import { mapGetters, mapActions } from 'vuex';
import MessageView from './MessageView.vue';
export default Vue.component('message-box',{
components:{
MessageView
},
data() {
return {
messageList :[],
}
},
created() {
this.fetchTimeMessage();
console.log("reaching inside ");
},
computed:{
...mapGetters(['getMessage','getActiveMessageData']),
...mapActions(['getMessageFromServer']),
},
methods: {
fetchTimeMessage:function(){
console.log("fetchTimeMessage : ");
var messageUser = this.getMessage.findIndex((e) => e.muid == this.getActiveMessageData.id);
console.log("fetchTimeMessage : " , {messageUser});
if (messageUser == -1) {
let user_id = this.getActiveMessageData.id;
let user_type = this.getActiveMessageData.type;
console.log("inside fetch Message : " + user_id);
console.log("inside fetch Message : " + user_type);
const payload = {
'uType': user_type,
'uid' : user_id,
'limit': 50
};
this.getMessageFromServer(payload);
}
},
},
});
Vuex modules message.js
const state = {
messages:[],
activeMessage : {}
};
const getters = {
getActiveUserId: (state) => {
let activeUserId = "";
if (!utils.isEmpty(state.activeMessage)) {
activeUserId = state.activeMessage.id;
}
return activeUserId;
},
getActiveMessage:(state) => { return !utils.isEmpty(state.activeMessage);},
getActiveMessageData : (state) => {return state.activeMessage } ,
getMessage: (state) => {return state.messages},
};
const actions = {
getMessageFromServer({ commit, state },{utype,uid,limit}){
console.log("mesage callback asdas : " + uid);
let messageRequest = CCManager.messageRequestBuilder(utype, uid, limit);
messageRequest.fetchPrevious().then(messages => {
//console.log("mesage callback : " + JSON.stringify(messages));
// handle list of messages received
let payload = {
'messsages':messages,
'id': uid
};
console.log("inside action_view : " + JSON.stringify(payload));
//commit('updateMessageList',payload);
})
},
setActiveMessages:function({commit},data){
commit('updateActiveMessage',data);
},
};
const mutations = {
updateMessageList(state,{messages,id}){
console.log("action details" + id);
//uid is not present
var tempObj = {
'muid' : id,
'message' : messages
}
state.messages.push(tempObj);
}
},
updateActiveMessage(state,action){
state.activeMessage = {
type: action.type,
id: action.uid
};
}
};
export default {
state,
getters,
actions,
mutations
};
Change the way you call the action in your component:
this.$store.dispatch('getMessageFromServer', payload);
And pass the payload as a single object in your action function:
getMessageFromServer({ commit, state }, payload)
And you can then access the payload properties in the action like this:
getMessageFromServer({ commit, state }, payload) {
var uid = payload.uid;
var uType = payload.uType;
var limit = payload.limit;
}
There are plenty of tutorials how to test effects in ngrx 3.
However, I've found only 1 or 2 for ngrx4 (where they removed the classical approach via EffectsTestingModule ), e.g. the official tutorial
However, in my case their approach doesn't work.
effects.spec.ts (under src/modules/list/store/list in the link below)
describe('addItem$', () => {
it('should return LoadItemsSuccess action for each item', async() => {
const item = makeItem(Faker.random.word);
actions = hot('--a-', { a: new AddItem({ item })});
const expected = cold('--b', { b: new AddUpdateItemSuccess({ item }) });
// comparing marbles
expect(effects.addItem$).toBeObservable(expected);
});
})
effects.ts (under src/modules/list/store/list in the link below)
...
#Effect() addItem$ = this._actions$
.ofType(ADD_ITEM)
.map<AddItem, {item: Item}>(action => {
return action.payload
})
.mergeMap<{item: Item}, Observable<Item>>(payload => {
return Observable.fromPromise(this._listService.add(payload.item))
})
.map<any, AddUpdateItemSuccess>(item => {
return new AddUpdateItemSuccess({
item,
})
});
...
Error
should return LoadItemsSuccess action for each item
Expected $.length = 0 to equal 1.
Expected $[0] = undefined to equal Object({ frame: 20, notification: Notification({ kind: 'N', value: AddUpdateItemSuccess({ payload: Object({ item: Object({ title: Function }) }), type: 'ADD_UPDATE_ITEM_SUCCESS' }), error: undefined, hasValue: true }) }).
at compare (webpack:///node_modules/jasmine-marbles/index.js:82:0 <- karma-test-shim.js:159059:33)
at Object.<anonymous> (webpack:///src/modules/list/store/list/effects.spec.ts:58:31 <- karma-test-shim.js:131230:42)
at step (karma-test-shim.js:131170:23)
NOTE: the effects use a service which involves writing to PouchDB. However, the issue doesn't seem related to that
and also the effects work in the running app.
The full code is a Ionic 3 app and be found here (just clone, npm i and npm run test)
UPDATE:
With ReplaySubject it works, but not with hot/cold marbles
const item = makeItem(Faker.random.word);
actions = new ReplaySubject(1) // = Observable + Observer, 1 = buffer size
actions.next(new AddItem({ item }));
effects.addItem$.subscribe(result => {
expect(result).toEqual(new AddUpdateItemSuccess({ item }));
});
My question was answered by #phillipzada at the Github issue I posted.
For anyone checking this out later, I report here the answer:
Looks like this is a RxJS issue when using promises using marbles. https://stackoverflow.com/a/46313743/4148561
I did manage to do a bit of a hack which should work, however, you will need to put a separate test the service is being called unless you can update the service to return an observable instead of a promise.
Essentially what I did was extract the Observable.fromPromise call into its own "internal function" which we can mock to simulate a call to the service, then it looks from there.
This way you can test the internal function _addItem without using marbles.
Effect
import 'rxjs/add/observable/fromPromise';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/mergeMap';
import { Injectable } from '#angular/core';
import { Actions, Effect } from '#ngrx/effects';
import { Action } from '#ngrx/store';
import { Observable } from 'rxjs/Observable';
export const ADD_ITEM = 'Add Item';
export const ADD_UPDATE_ITEM_SUCCESS = 'Add Item Success';
export class AddItem implements Action {
type: string = ADD_ITEM;
constructor(public payload: { item: any }) { }
}
export class AddUpdateItemSuccess implements Action {
type: string = ADD_UPDATE_ITEM_SUCCESS;
constructor(public payload: { item: any }) { }
}
export class Item {
}
export class ListingService {
add(item: Item) {
return new Promise((resolve, reject) => { resolve(item); });
}
}
#Injectable()
export class SutEffect {
_addItem(payload: { item: Item }) {
return Observable.fromPromise(this._listService.add(payload.item));
}
#Effect() addItem$ = this._actions$
.ofType<AddItem>(ADD_ITEM)
.map(action => action.payload)
.mergeMap<{ item: Item }, Observable<Item>>(payload => {
return this._addItem(payload).map(item => new AddUpdateItemSuccess({
item,
}));
});
constructor(
private _actions$: Actions,
private _listService: ListingService) {
}
}
Spec
import { cold, hot, getTestScheduler } from 'jasmine-marbles';
import { async, TestBed } from '#angular/core/testing';
import { Actions } from '#ngrx/effects';
import { Store, StoreModule } from '#ngrx/store';
import { getTestActions, TestActions } from 'app/tests/sut.helpers';
import { AddItem, AddUpdateItemSuccess, ListingService, SutEffect } from './sut.effect';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
describe('Effect Tests', () => {
let store: Store<any>;
let storeSpy: jasmine.Spy;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
StoreModule.forRoot({})
],
providers: [
SutEffect,
{
provide: ListingService,
useValue: jasmine.createSpyObj('ListingService', ['add'])
},
{
provide: Actions,
useFactory: getTestActions
}
]
});
store = TestBed.get(Store);
storeSpy = spyOn(store, 'dispatch').and.callThrough();
storeSpy = spyOn(store, 'select').and.callThrough();
}));
function setup() {
return {
effects: TestBed.get(SutEffect) as SutEffect,
listingService: TestBed.get(ListingService) as jasmine.SpyObj<ListingService>,
actions$: TestBed.get(Actions) as TestActions
};
}
fdescribe('addItem$', () => {
it('should return LoadItemsSuccess action for each item', async () => {
const { effects, listingService, actions$ } = setup();
const action = new AddItem({ item: 'test' });
const completion = new AddUpdateItemSuccess({ item: 'test' });
// mock this function which we can test later on, due to the promise issue
spyOn(effects, '_addItem').and.returnValue(Observable.of('test'));
actions$.stream = hot('-a|', { a: action });
const expected = cold('-b|', { b: completion });
expect(effects.addItem$).toBeObservable(expected);
expect(effects._addItem).toHaveBeenCalled();
});
})
})
Helpers
import { Actions } from '#ngrx/effects';
import { Observable } from 'rxjs/Observable';
import { empty } from 'rxjs/observable/empty';
export class TestActions extends Actions {
constructor() {
super(empty());
}
set stream(source: Observable<any>) {
this.source = source;
}
}
export function getTestActions() {
return new TestActions();
}