Vuex-map-fields updating multiple stores through modules - vue.js

I'm working Vuex modules to state my data.
I store the data in multiple modules to keep my code base nice and clean.
When using vuex-map-fields I have a situation where I'm using data from multiple modules.
There seems to be no method to do this or I am doing it wrong.
Below is my current code;
My component
<template>
<div class="">
<input type="text" v-model="no_panels"><br>
<input type="text" v-model="firstName"><br>
<router-link to="/step-2">Go to step 2</router-link>
</div>
</template>
<script>
import { createHelpers } from 'vuex-map-fields';
const { mapFields } = createHelpers({
getterType: [
'getKitchenField',
'getApplicantField',
],
mutationType: 'updateKitchenField',
});
export default {
computed: {
...mapFields(['no_panels', 'firstName', 'lastName'])
},
}
</script>
My store file
import kitchen from './kitchen';
import applicant from "./applicant";
export default {
modules: {
kitchen: kitchen,
applicant: applicant
},
strict: false
}
Applicant.js
import { createHelpers } from 'vuex-map-fields';
const { getApplicantField, updateApplicantField } = createHelpers({
getterType: 'getApplicantField',
mutationType: 'updateApplicantField',
});
export default {
state: {
firstName: '',
lastName: ''
},
getters: {
getApplicantField
},
mutations: {
updateApplicantField
}
}
The code above results in the following error:
Error in render: "TypeError: this.$store.getters[getterType] is not a function"

I'm the creator of vuex-map-fields, the Author asked the same question on GitHub:
Instead of passing multiple getters to createHelpers(), you can destructure and rename the return value of createHelpers() and call it twice.
const { mapFields: mapKitchenFields } = createHelpers({
getterType: 'getKitchenField',
mutationType: 'updateKitchenField',
});
const { mapFields: mapApplicantFields } = createHelpers({
getterType: 'getApplicantField',
mutationType: 'updateApplicantField',
});
export default {
computed: {
...mapKitchenFields(['no_panels']),
...mapApplicantFields(['firstName', 'lastName']),
},
}
If the desctructuring syntax is new to you, you can read more about it from Wes Bos: https://wesbos.com/destructuring-renaming/

Related

How can I change the locale of a VueI18n instance in a Vue file, and have it apply to all other Vue files in the application?

I am currently trying to implement a feature where a user can select a language from a dropdown menu in a Settings page (SettingsDialog.vue), updating all of the text to match the new language. This application has multiple Vue files like a MenuBar.vue, HelpDialog.vue, each pulling from translation.ts for their English translations. However, I noticed that selecting a language from the dropdown menu only changed the elements inside my SettingsDialog.vue file, not all of the other Vue files I have.
I tried using the Vue-I18n documentation implementation of changing locale globally in the file. I was expecting for the locale of the entire application to change after selecting a language in SettingsDialog.vue, applying my English translations in translation.ts to the Menu Bar, Help Page, etc. What happened is that the translations from translation.ts only applied to the SettingsDialog.vue page, no where else.
I guess it would be helpful to add that this is an Electron application, and the Vue files in the project use Quasar. Each file does have the correct import statements.
main.ts:
// ...
window.datalayer = [];
const i18n = createI18n({
legacy: false,
locale: "",
messages,
});
createApp(App)
.use(store, storeKey)
.use(router)
.use(
createGtm({
id: process.env.VUE_APP_GTM_CONTAINER_ID ?? "GTM-DUMMY",
vueRouter: router,
enabled: false,
})
)
.use(Quasar, {
config: {
brand: {
primary: "#a5d4ad",
secondary: "#212121",
},
},
iconSet,
plugins: {
Dialog,
Loading,
},
})
.use(ipcMessageReceiver, { store })
.use(markdownItPlugin)
.use(i18n)
.mount("#app");
SettingsDialog.vue
// ...
<!-- Language Setting Card -->
<q-card flat class="setting-card">
<q-card-actions>
<div id="app" class="text-h5">{{ $t("言語") }}</div>
</q-card-actions>
<q-card-actions class="q-px-md q-py-sm bg-setting-item">
<div id="app">{{ $t("言語を選択する") }}</div>
<q-space />
<q-select
filled
v-model="locale"
dense
emit-value
map-options
options-dense
:options="[
{ value: 'ja', label: '日本語 (Japanese)' },
{ value: 'en', label: '英語 (English)' },
]"
label="Language"
>
<q-tooltip
:delay="500"
anchor="center left"
self="center right"
transition-show="jump-left"
transition-hide="jump-right"
>
Test
</q-tooltip>
</q-select>
</q-card-actions>
</q-card>
// ...
<script lang="ts">
import { useI18n } from "vue-i18n";
// ...
setup(props, { emit }) {
const { t, locale } = useI18n({ useScope: "global" });
// ...
return {
t,
locale,
// ...
};
MenuBar.vue
<template>
<q-bar class="bg-background q-pa-none relative-position">
<div
v-if="$q.platform.is.mac && !isFullscreen"
class="mac-traffic-light-space"
></div>
<img v-else src="icon.png" class="window-logo" alt="application logo" />
<menu-button
v-for="(root, index) of menudata"
:key="index"
:menudata="root"
v-model:selected="subMenuOpenFlags[index]"
:disable="menubarLocked"
#mouseover="reassignSubMenuOpen(index)"
#mouseleave="
root.type === 'button' ? (subMenuOpenFlags[index] = false) :
undefined
"
/>
// ...
<script lang="ts">
import { defineComponent, ref, computed, ComputedRef, watch } from "vue";
import { useStore } from "#/store";
import MenuButton from "#/components/MenuButton.vue";
import TitleBarButtons from "#/components/TitleBarButtons.vue";
import { useQuasar } from "quasar";
import { HotkeyAction, HotkeyReturnType } from "#/type/preload";
import { setHotkeyFunctions } from "#/store/setting";
import {
generateAndConnectAndSaveAudioWithDialog,
generateAndSaveAllAudioWithDialog,
generateAndSaveOneAudioWithDialog,
} from "#/components/Dialog";
import { useI18n } from "vue-i18n";
import messages from "../translation";
type MenuItemBase<T extends string> = {
type: T;
label?: string;
};
export type MenuItemSeparator = MenuItemBase<"separator">;
export type MenuItemRoot = MenuItemBase<"root"> & {
onClick: () => void;
subMenu: MenuItemData[];
};
export type MenuItemButton = MenuItemBase<"button"> & {
onClick: () => void;
};
export type MenuItemCheckbox = MenuItemBase<"checkbox"> & {
checked: ComputedRef<boolean>;
onClick: () => void;
};
export type MenuItemData =
| MenuItemSeparator
| MenuItemRoot
| MenuItemButton
| MenuItemCheckbox;
export type MenuItemType = MenuItemData["type"];
export default defineComponent({
name: "MenuBar",
components: {
MenuButton,
TitleBarButtons,
},
setup() {
const { t } = useI18n({
messages,
});
// ...
};
const menudata = ref<MenuItemData[]>([
{
type: "root",
label: t("ファイル"),
onClick: () => {
closeAllDialog();
},
// ...
]);
translation.ts
const messages = {
en: {
// MenuBar.vue
ファイル: "File",
エンジン: "Engine",
ヘルプ: "Help",
// SettingDialog.vue
言語: 'Language',
言語を選択する: 'Select Language',
オフ: 'OFF',
エンジンモード: 'Engine Mode',
// HelpDialog.vue
ソフトウェアの利用規約: 'test',
}
};
export default messages;
Maybe there are more problems but now I see two:
Your menudata should be computed instead of just ref. Right now you are creating a JS object and setting it label property to result of t() call. When global locale changes this object is NOT created again. It still holds same value the t() function returned the only time it was executed - when setup() was running
// correct
const menudata = computed<MenuItemData[]>(() => [
{
type: "root",
label: t("ファイル"),
onClick: () => {
closeAllDialog();
},
// ...
]);
This way whenever i18n.global.locale changes, your menudata is created again with new translation
As an alternative, set label to key and use t(label) inside the template. However computed is much more effective solution...
You don't need to pass messages to useI18n() in every component. Only to the global instance. By passing config object into a useI18n() in a component you are creating Local scope which makes no sense if you are storing all translations in a single global place anyway

Vue js always run a function to see if user is online or offline

Hi to all i want to find a way to see if user in my webapp is online or offline and run always! If user is offline wait 10 seconds to see if come back online and if not run a function!
Thanks all for the help!
Create a component OfflineComponent.vue like this:
<template>
<div>Offline component after 5 seconds!</div>
</template>
<script>
export default {
data() {
return {
showOnce: false,
}
},
mounted() {
this.startInterval()
},
methods: {
startInterval() {
window.setInterval(() => {
if (this.$nuxt.isOffline && !this.showOnce) {
this.runThisFunc()
this.showOnce = true
}
if (this.showOnce) {
clearInterval(window)
}
}, 5000)
},
runThisFunc() {
console.log('OFFLINE!')
},
},
}
</script>
Now import it inside the page you want. I use the main page:
<template>
<div v-if="$nuxt.isOffline">
<offline-component></offline-component>
</div>
<div v-else>You are Online!</div>
</template>
<script>
import OfflineComponent from '~/components/offlineComponent.vue'
export default {
name: 'IndexPage',
components: {
OfflineComponent,
},
}
</script>

How to re-use component that should use unique vuex store instance

I try to find a way to use vuex with reusable component which store data in a store. The thing is, I need the store to be unique for each component instance.
I thought Reusable module of the doc was the key but finally it doesn't seem to be for this purpose, or i didn't understand how to use it.
The parent component:
(the prop “req-path” is used to pass different URL to make each FileExplorer component commit the action of fetching data from an API, with that url path)
<template>
<div class="container">
<FileExplorer req-path="/folder/subfolder"></FileExplorer>
<FileExplorer req-path="/anotherfolder"></FileExplorer>
</div>
</template>
<script>
import { mapState, mapGetters } from "vuex";
import FileExplorer from "#/components/FileExplorer.vue";
export default {
components: {
FileExplorer
}
};
</script>
The reusable component:
<template>
<div class="container">
<ul v-for="(item, index) in folderIndex" :key="index">
<li>Results: {{ item.name }}</li>
</ul>
</div>
</div>
</template>
<script>
import { mapState, mapGetters } from "vuex";
export default {
props: ["reqPath"],
},
computed: {
...mapState("fileExplorer", ["folderIndex"])
},
created() {
// FETCH DATA FROM API
this.$store
.dispatch("fileExplorer/indexingData", {
reqPath: this.reqPath
})
.catch(error => {
console.log("An error occurred:", error);
this.errors = error.response.data.data;
});
}
};
</script>
store.js where I invoke my store module that I separate in different files, here only fileExplorer module interest us.
EDIT : I simplified the file for clarity purpose but I have some other state and many mutations inside.
import Vue from 'vue'
import Vuex from 'vuex'
// Import modules
import { fileExplorer } from '#/store/modules/fileExplorer'
Vue.use(Vuex)
export default new Vuex.Store({
modules: {
fileExplorer,
…
}
})
#/store/modules/fileExplorer.js
import ApiService from "#/utils/ApiService"
export const fileExplorer = ({
namespaced: true,
state: {
folderIndex: {},
},
mutations: {
// Called from action (indexingData) to fetch folder/fil structure from API
SET_FOLDERS_INDEX(state, data) {
state.folderIndex = data.indexingData
},
actions: {
// Fetch data from API using req-path as url
indexingData({
commit
}, reqPath) {
return ApiService.indexingData(reqPath)
.then((response) => {
commit('SET_FOLDERS_INDEX', response.data);
})
.catch((error) => {
console.log('There was an error:', error.response);
});
}
}
});
I need each component to show different data from those 2 different URL, instead i get the same data in the 2 component instance (not surprising though).
Thanks a lot for any of those who read all that !
Module reuse is about when you are creating multiple modules from the same module config.
First, use a function for declaring module state instead of a plain object.
If we use a plain object to declare the state of the module, then that
state object will be shared by reference and cause cross store/module
state pollution when it's mutated.
const fileExplorer = {
state () {
return {
folderIndex: {}
}
},
// mutations, actions, getters...
}
Then, dynamically register a new module each time a new FileExplorer component is created and unregister that module before the component is destroyed.
<template>
<div class="container">
<ul v-for="(item, index) in folderIndex" :key="index">
<li>Results: {{ item.name }}</li>
</ul>
</div>
</div>
</template>
<script>
import { fileExplorer } from "#/store/modules/fileExplorer";
import store from "#/store/index";
var uid = 1
export default {
props: ["reqPath"],
data() {
return {
namespace: `fileExplorer${uid++}`
}
},
computed: {
folderIndex() {
return this.$store.state[this.namespace].folderIndex
}
},
created() {
// Register the new module dynamically
store.registerModule(this.namespace, fileExplorer);
// FETCH DATA FROM API
this.$store
.dispatch(`${this.namespace}/indexingData`, {
reqPath: this.reqPath
})
.catch(error => {
console.log("An error occurred:", error);
this.errors = error.response.data.data;
});
},
beforeDestroy() {
// Unregister the dynamically created module
store.unregisterModule(this.namespace);
}
};
</script>
You no longer need the static module registration declared at store creation.
export default new Vuex.Store({
modules: {
// fileExplorer, <-- Remove this static module
}
})

vuejs treeselect - delay loading does not work via vuex action

Using Vue TreeSelect Plugin to load a nested list of nodes from firebase backend. It's doc page says,
It's also possible to have root level options to be delayed loaded. If no options have been initially registered (options: null), vue-treeselect will attempt to load root options by calling loadOptions({ action, callback, instanceId }).
loadOptions (in my App.vue) dispatch vuex action_FolderNodesList, fetches (from firebase) formats (as required by vue-treeselect), and mutates the state folder_NodesList, then tries to update options this.options = this.get_FolderNodesList but this does not seems to work.
Here is the loadOptions method (in app.vue)
loadOptions() {
let getFolderListPromise = this.$store.dispatch("action_FolderNodesList");
getFolderListPromise.then(_ => {
this.options = this.get_FolderNodesList;
});
}
Vue errors out with Invalid prop: type check failed for prop "options". Expected Array, got String with value ""
I am not sure what am I doing wrong, why that does not work. A working Codesandbox demo
Source
App.vue
<template>
<div class="section">
<div class="columns">
<div class="column is-7">
<div class="field">
<Treeselect
:multiple="true"
:options="options"
:load-options="loadOptions"
:auto-load-root-options="false"
placeholder="Select your favourite(s)..."
v-model="value" />
<pre>{{ get_FolderNodesList }}</pre>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapGetters } from "vuex";
import Treeselect from "#riophae/vue-treeselect";
import "#riophae/vue-treeselect/dist/vue-treeselect.css";
export default {
data() {
return {
value: null,
options: null,
called: false
};
},
components: {
Treeselect
},
computed: mapGetters(["get_FolderNodesList"]),
methods: {
loadOptions() {
let getFolderListPromise = this.$store.dispatch("action_FolderNodesList");
getFolderListPromise.then(_ => {
this.options = this.get_FolderNodesList;
});
}
}
};
</script>
Store.js
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
export const store = new Vuex.Store({
state: {
folder_NodesList: ""
},
getters: {
get_FolderNodesList(state) {
return state.folder_NodesList;
}
},
mutations: {
mutate_FolderNodesList(state, payload) {
state.folder_NodesList = payload;
}
},
actions: {
action_FolderNodesList({ commit }) {
fmRef.once("value", snap => {
var testObj = snap.val();
var result = Object.keys(testObj).reduce((acc, cur) => {
acc.push({
id: cur,
label: cur,
children: recurseList(testObj[cur])
});
return acc;
}, []);
commit("mutate_FolderNodesList", result);
});
}
}
});
Any help is appreciated.
Thanks
It seems you are calling this.options which would update the entire element while only the current expanding option should be updated.
It seems loadOptions() is called with some arguments that you can use to update only the current childnode. The first argument seems to contain all the required assets so I wrote my loadTreeOptions function like this:
loadTreeOptions(node) {
// On initial load, I set the 'children' to NULL for nodes to contain children
// but inserted an 'action' string with an URL to retrieve the children
axios.get(node.parentNode.action).then(response => {
// Update current node's children
node.parentNode.children = response.data.children;
// notify tree to update structure
node.callback();
}).catch(
errors => this.onFail(errors.response.data)
);
},
Then I set :load-options="loadTreeOptions" on the <vue-treeselect> element on the page. Maybe you were only missing the callback() call which updates the structure. My installation seems simpler than yours but it works properly now.

How can I use this component in a single component in Vue.js?

I am beginner of Vue.js.
I am trying to use a plugin called vue-base64-file-upload in a single component in VueCLI. However, it doesn't work in index.vue...
I have just changed the example (https://github.com/dhhb/vue-base64-file-upload) to this.
<template>
<div class="container">
<h2>Upload file</h2>
<vue-base64-file-upload
class="v1"
accept="image/png,image/jpeg"
image-class="v1-image"
input-class="v1-input"
:max-size="customImageMaxSize"
#size-exceeded="onSizeExceeded"
#file="onFile"
#load="onLoad" />
</div>
</template>
<script>
import VueBase64FileUpload from 'vue-base64-file-upload';
export default {
data () {
return {
customImageMaxSize: 3 // megabytes
};
},
components: {
VueBase64FileUpload
},
methods: {
onFile(file) {
console.log(file); // file object
},
onLoad(dataUri) {
console.log(dataUri); // data-uri string
},
onSizeExceeded(size) {
alert(`Image ${size}Mb size exceeds limits of
${this.customImageMaxSize}Mb!`);
}
},
};
app.$mount('#app');