I want to create two custom Vue InstantSearch widgets in the same component that display data from two different category groups.
At the moment, I have:
import { createWidgetMixin } from 'vue-instantsearch';
import { connectHierarchicalMenu } from 'instantsearch.js/es/connectors';
export default {
mixins: [createWidgetMixin({ connector: connectHierarchicalMenu })],
computed: {
widgetParams() {
return {
attributes: ['category_group_1.lvl0', 'category_group_1.lvl1']
};
},
},
};
and then I can access the data in the template with v-for="item in items".
But elsewhere I want to also be able to loop through items that come from a different category group, e.g. category_group_2.lvl0. How do I do that?
Related
I tried asking this question in the Vue Forums with no response, so I am going to try repeating it here:
I have an app where clients login and can manage multiple accounts (websites). In the header of the app, there’s a dropdown where the user can select the active account, and this will affect all of the components in the app that display any account-specific information.
Because this account info is needed in components throughout the app, I tried to follow the store example shown here (Vuex seemed like overkill in my situation):
https://v2.vuejs.org/v2/guide/state-management.html
In my src/main.js, I define this:
Vue.prototype.$accountStore = {
accounts: [],
selectedAccountId: null,
selectedAccountDomain: null
}
And this is my component to load/change the accounts:
<template>
<div v-if="hasMoreThanOneAccount">
<select v-model="accountStore.selectedAccountId" v-on:change="updateSelectedAccountDomain">
<option v-for="account in accountStore.accounts" v-bind:value="account.id" :key="account.id">
{{ account.domain }}
</option>
</select>
</div>
</template>
<script>
export default {
name: 'AccountSelector',
data: function () {
return {
accountStore: this.$accountStore,
apiInstance: new this.$api.AccountsApi()
}
},
methods: {
updateSelectedAccountDomain: function () {
this.accountStore.selectedAccountDomain = this.findSelectedAccountDomain()
},
findSelectedAccountDomain: function () {
for (var i = 0; i < this.accountStore.accounts.length; i++) {
var account = this.accountStore.accounts[i]
if (account.id === this.accountStore.selectedAccountId) {
return account.domain
}
}
return 'invalid account id'
},
loadAccounts: function () {
this.apiInstance.getAccounts(this.callbackWrapper(this.accountsLoaded))
},
accountsLoaded: function (error, data, response) {
if (error) {
console.error(error)
} else {
this.accountStore.accounts = data
this.accountStore.selectedAccountId = this.accountStore.accounts[0].id
this.updateSelectedAccountDomain()
}
}
},
computed: {
hasMoreThanOneAccount: function () {
return this.accountStore.accounts.length > 1
}
},
mounted: function () {
this.loadAccounts()
}
}
</script>
<style scoped>
</style>
To me this doesn’t seem like the best way to do it, but I’m really not sure what the better way is. One problem is that after the callback, I set the accounts, then the selectedAccountId, then the selectedAccountDomain manually. I feel like selectedAccountId and selectedDomainId should be computed properties, but I’m not sure how to do this when the store is not a Vue component.
The other issue I have is that until the selectedAccountId is loaded for the first time, I can’t make any API calls in any other components because the API calls need to know the account ID. However, I’m not sure what the best way is to listen for this change and then make API calls, both the first time and when it is updated later.
At the moment, you seem to use store to simply hold values. But the real power of the Flux/Store pattern is actually realized when you centralize logic within the store as well. If you sprinkle store-related logic across components throughout the app, eventually it will become harder and harder to maintain because such logic cannot be reused and you have to traverse the component tree to reach the logic when fixing bugs.
If I were you, I will create a store by
Defining 'primary data', then
Defining 'derived data' that can be derived from primary data, and lastly,
Defining 'methods' you can use to interact with such data.
IMO, the 'primary data' are user, accounts, and selectedAccount. And the 'derived data' are isLoggedIn, isSelectedAccountAvailable, and hasMoreThanOneAccount. As a Vue component, you can define it like this:
import Vue from "vue";
export default new Vue({
data() {
return {
user: null,
accounts: [],
selectedAccount: null
};
},
computed: {
isLoggedIn() {
return this.user !== null;
},
isSelectedAccountAvailable() {
return this.selectedAccount !== null;
},
hasMoreThanOneAccount() {
return this.accounts.length > 0;
}
},
methods: {
login(username, password) {
console.log("performing login");
if (username === "johnsmith" && password === "password") {
console.log("committing user object to store and load associated accounts");
this.user = {
name: "John Smith",
username: "johnsmith",
email: "john.smith#somewhere.com"
};
this.loadAccounts(username);
}
},
loadAccounts(username) {
console.log("load associated accounts from backend");
if (username === "johnsmith") {
// in real code, you can perform check the size of array here
// if it's the size of one, you can set selectedAccount here
// this.selectedAccount = array[0];
console.log("committing accounts to store");
this.accounts = [
{
id: "001234",
domain: "domain001234"
},
{
id: "001235",
domain: "domain001235"
}
];
}
},
setSelectedAccount(account) {
this.selectedAccount = account;
}
}
});
Then, you can easily import this store in any Vue component, and start referencing values, or call methods, from this store.
For example, suppose you are creating a Login.vue component, and that component should redirect when user object becomes available within a store, you can achieve this by doing the following:
<template>
<div>
<input type="text" v-model="username"><br/>
<input type="password" v-model="password"><br/>
<button #click="submit">Log-in</button>
</div>
</template>
<script>
import store from '../basic-store';
export default {
data() {
return {
username: 'johnsmith',
password: 'password'
};
},
computed: {
isLoggedIn() {
return store.isLoggedIn;
},
},
watch: {
isLoggedIn(newVal) {
if (newVal) { // if computed value from store evaluates to 'true'
console.log("moving on to Home after successful login.");
this.$router.push({ name: "home" });
}
}
},
methods: {
submit() {
store.login(this.username, this.password);
}
}
};
</script>
In addition, with isSelectedAccountAvailable we compute, we can easily disable/enable button on the screen, to prevent user from making API calls until an account is selected:
<button :disabled="!isSelectedAccountAvailable" #click="performAction()">make api call</button>
If you want to see the whole project, you can access it from this runnable codesandbox. Pay attention at how basic-store.js is defined and used in Login.vue and Home.vue. And, if you'd like, you can also see how store is defined in vuex by taking a peek at store.js.
Good luck!
Updated:
About how you should organize dependent/related API calls, the answer is actually right in front of you. If you take a closer look at the store, you'll notice that my login() method subsequently calls this.loadAccounts(username) once the login succeeds. So, basically, you have all the flexibility to chain/nested API calls in store's methods to accommodate your business rules. The watch() is there simply because the UI needs to perform navigation based on change(s) made in the store. For most simple data changes, computed properties will suffice.
Further, from how I designed it, the reason watch() is used in <Login> component is twofold:
Separation of concerns: for me who has been working on server-side code for years, I'd like my view-related code to be cleanly separated from model. By restricting navigation logic inside a component, my model in a store doesn't need to know/care about navigation at all.
However, even if I don't separate concerns, it will still be pretty hard to import vue-router into my store. This is because my router.js already imports basic-store.js to perform navigation guard preventing unauthenticated users from accessing <Home> component:
router.beforeEach((to, from, next) => {
if (!store.isLoggedIn && to.name !== "login") {
console.log(`redirect to 'login' instead of '${to.name}'.`);
next({ name: "login" });
} else {
console.log(`proceed to '${to.name}' normally.`);
next();
}
});
And, because javascript doesn't support cyclic dependency yet (e.g., router imports store, and store imports router), to keep my code acyclic, my store can't perform route navigations.
I have a navigation flow consisting in
SearchPage -> ...Others or SearchPage -> ...Others or SearchPage ->
and wanna persist what was the search string when navigate back.
<template id="ListCustomersPage">
<q-layout>
<q-layout-header>
<toolbar :title="title" :action="doCreate" label="New"></toolbar>
<q-search inverted placeholder="Type Name, Code, Nit, Phone Number or Email" float-label="Search" v-model="search" />
<q-btn icon="search" color="secondary" #click="doSearch" />
</q-layout-header>
</q-layout>
</template>
Now, the problem is how correlate the stack of the queries and the one of the routers, when the fact the user can navigate elsewhere.
P.D All is in a single page. If possible to persist the screen without refresh them (but only for the search pages until popped back) will be better.
Option 1: Navigation Guards
You can use a so called Navigation Guard that allows you to add global actions before, after and on route updates. You can also add it directly to your component by using the In-component Guards, which will allow you to persist the content of the search data.
const VueFoo = {
// I guess your search attribute is in your data
data() {
return {
search: ''
}
},
mounted() {
// retrieve your information from your persistance layer
},
beforeRouteLeave (to, from, next) {
// called when the route that renders this component is about to
// be navigated away from.
// has access to `this` component instance.
// persist this.search in localstorage or wherever you like it to be stored
}
}
Option 2: Using a (Vuex) Store
If you're able to add a Vuex Store or any Store alike, I would highly recommend to do so. Since you tagged quasar I want to link to the Vuex Store Documentation provided there. You can basically outsource your search property and let the Store persist it for you across your application.
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
search_term: ''
},
mutations: {
SET_SEARCH_TERM (state, payload) {
state.search_term = payload.search_term
}
},
actions: {
SEARCH ({ commit, state }, payload) {
commit('SET_SEARCH_TERM', payload.search_term)
// your api call to search which can also be stored in the state
}
}
})
export default store
In your component where you want to persist your search query using a mutation (not bound to an action):
store.commit('SET_SEARCH_TERM', {
search_term: this.search // your local search query
})
In your code where you trigger the search ACTION if you want to persist during every search
store.dispatch('SEARCH', {
search_term: this.search
})
Accessing the property search_term or however you want to call it can be done using a computed property. You can also bind the state and mutations directly without the need for Navigation guards at all:
// your Vue component
computed: {
search: {
get () {
return this.$store.state.search_term
},
set (val) {
this.$store.commit('SET_SEARCH_TERM', { search_term: val })
}
}
}
Make sure to learn about the basic concept before using: https://vuex.vuejs.org/
I'm searching for a way to resolve Vue (sfc) component names 'on the fly'. Basically I'm getting kind of component names from a backend and need to translate them into real component names on the frontend. Something like this:
<text/> => /components/TextElement.vue
or
<component v-bind:is="text"></component> => => /components/TextElement.vue
The idea is providing a global function that maps the backend names to the frontend names. I don't want to do this in every place where those components are used, so maybe there is a kind of hook to change the names dynamically?
Thanks a lot,
Boris
You could create a wrapper <component> that maps the component names and globally registers the specified component on the fly.
Create a component with a prop (e.g., named "xIs") for the <component>'s is property:
export default {
props: {
xIs: {
type: String,
required: true
}
},
};
Add a template that wraps <component>, and a data property (e.g., named "translatedName") that contains the mapped component name (from backend to front-end name):
export default {
data() {
return {
translatedName: ''
}
}
};
Add a watcher on the prop that will register a component by the name indicated in the prop value:
import componentNames from './component-names.json';
export default {
watch: {
xIs: {
immediate: true, // run handler immediately on current `xIs`
async handler(value) {
try {
const name = componentNames[value]; // 1. lookup real component name
if (name) {
Vue.component(name, () => import(`#/components/${name}`)); // 2. register component
this.translatedName = name; // 3. set name for
}
} catch (e) {
console.warn(`cannot resolve component name: ${value}`, e);
}
}
}
}
}
Been reading the docs and googling around for best practice to handle api calls in bigger projects without luck (or ateast not what Im searching for).
I want to create a service / facade for the backend that I can load in every component that needs it. For exampel.
I want to fetch historical data for weather in a service so in every component I need this I can just load the weather-serivce and use a getter to fetch the wanted data. I would like to end up with something like below. But I dosent get it to work. So I wonder, what is best practice for this in vue.js?
import WeatherFacade from './data/WeatherFacade.vue'
export default {
name: 'Chart',
created () {
console.log(WeatherFacade.getWeather())
},
components: {
WeatherFacade
}
}
ps. using vue 2.1.10
It could be easily done by creating some external object that will hold those data and module bundling.What I usually do in my projects is that I create services directory and group them in order I want.
Let's break it down - services/WeatherFascade.js (using VueResource)
import Vue from 'vue'
export default {
getWeather() {
return Vue.http.get('api/weather')
}
}
If you have to pass some dynamic data such as ID, pass it as just parameter
import Vue from 'vue'
export default {
getWeather(id) {
return Vue.http.get(`api/weather/${id}`)
}
}
Then in your component you can import this service, pass parameters (if you have them) and got data back.
import WeatherFascade from '../services/WeatherFascade'
export default {
data() {
return {
weatherItems: []
}
},
created() {
this.getWeatherData()
},
methods: {
getWeatherData() {
WeatherFascade.getWather(// you can pass params here)
.then(response => this.weatherItems = response.data)
.catch(error => console.log(error))
}
}
}
You can use any library for that you like, for instance axios is cool.
Vuex allows you to inject the store into your root instance, making it accessible via this.$store in all child components.
Without Vuex, is it possible to inject a custom store implementation into child components?
e.g.
// main.js
let app = new Vue({router, store, ...App}).$mount("#flightdeck-app")
export { app, store, router }
// SomeComponent.vue
export default {
name: "Overview",
components: { "credentials": Credentials },
computed: {
count() {
// injected store; is currently undefined.
return this.$store.state.items.length
}
},
Attempting to access this.$store results in undefined in child components, as Vuex seemingly has additional hooks to make this happen.
You may create a custom plugin for vue that register an initialize your custom store (or anything, I'll create a logger object just for demonstration).
For example you could have
//myLogger.js
export default {
install(Vue, options) {
function log(type, title, text) {
console.log(`[${type}] ${title} - ${text}`);
}
Vue.prototype.$log = {
error(title, text) { log('danger', title, text) },
success(title, text) { log('success', title, text) },
log
}
}
}
Before your main Vue instance tell to register your plugin
//main.js
import Logger from './path/to/myLogger';
Vue.use(Logger);
var vm = new Vue({
el: '#app',
template: '<App/>',
components: { App }
})
Now you can call this.$log on any child component
//myComponent.vue
export default {
data() {
return {};
},
methods: {
Save() {
this.$log.success('Transaction saved!');
}
}
}
Hope it helps, for more detail please see Vue plugins documentation
Just use in your components files:
import store from './vuex/store.js'
Place your store in separate file to get it clear.
Import store to every component where you need store.