Using Vue multiselect to change Algolia index - vue.js

I currently have a page where I'm able to switch Algolia indices with this:
<template>
<button #click="selectedIndex = a">List A</button>
<button #click="selectedIndex = b">List B</button>
<A v-if="selectedIndex === a" />
<B v-if="selectedIndex === b" />
</template>
<script>
import A from '#/A.vue';
import B from '#/B.vue';
export default {
components: {
A,
B
},
data() {
return {
selectedIndex: `a_${this.$root.index}`,
query: ''
};
},
computed: {
a() {
return `a_${this.$root.index}`;
},
b() {
return `b_${this.$root.index}`;
}
}
};
</script>
This is in a file called Index.vue. The different indices are in files A.vue and B.vue.
But now I need to be able to do the same content switching using a vue-multiselect in A.vue and B.vue.
Currently in A.vue, I have
<template>
<ais-instant-search
:search-client="searchClient"
:index-name="a"
:routing="routing"
>
<multiselect
v-model="selectedIndex"
:options="switcherOptions"
:searchable="false"
:close-on-select="true"
:show-labels="false"
placeholder="Choose"
>
<template slot="singleLabel" slot-scope="{ option }">
{{ option.text }}
</template>
<template slot="option" slot-scope="{ option }">
{{ option.text }}
</template>
</multiselect>
</ais-instant-search>
</template>
export default {
components: {
Multiselect
},
data() {
searchClient: algoliasearch(window.algolia.id, window.algolia.key),
selectedIndex: { value: 'a', text: 'List A' },
switcherOptions: [
{ value: 'a', text: 'List A' },
{ value: 'b', text: 'List B' }
]
};
},
};
What I don't know how to do now is send the value from the multi-select from A.vue back up to Index.vue where the different indices are defined.

First observation that I have, in your A.vue you are binding variable named a to your index, but do not have a defined in your data, same thing for routing.
What is the point of computed properties a and b, they are just returning strings, with do not do any computing, this could probably be defined in data:
data() {
return {
selectedIndex: 'a_index',
query: '',
a: 'a_index',
b: 'b_index',
};
},
Are A.vue and B.vue identical components? If only bindings are different, you can probably combine them into one component, and just pass different props to them. Hard to know for sure without seeing complete code.
So one way of doing this is emitting events from child to parent components. Documentation reference: https://vuejs.org/guide/essentials/event-handling.html
On your multiselect component add #select="$emit("indexSelected", selectedIndex)"
Like so:
<multiselect
v-model="selectedIndex"
:options="switcherOptions"
:searchable="false"
:close-on-select="true"
:show-labels="false"
placeholder="Choose"
#select="$emit("indexSelected", selectedIndex)"
>
This will emit event with name of indexSelected and included selectedIndex value in its payload.
Then in Index.vue you need to add these event listeners to both A and B Components:
<A v-if="selectedIndex === a" #indexSelected="selectedIndex = $event.value === 'a' ? 'a_index' : 'b_index'" />
<B v-if="selectedIndex === b" #indexSelected="selectedIndex = $event.value === 'a' ? 'a_index' : 'b_index'" />
Or if you would like cleaner template you can create a method:
methods: {
updateSelectedIndex(event){
this.selectedIndex = event.value === 'a' ? 'a_index' : 'b_index';
}
}
And then update template to:
<A v-if="selectedIndex === a" #indexSelected="updateSelectedIndex" />
<B v-if="selectedIndex === b" #indexSelected="updateSelectedIndex" />
EDIT:
I think this is kind of what you are looking for(reading between the lines lol), this rolls Index.vue, A.vue, B.vue into one component, because what you are trying to achieve is a lot simpler this way IMO. Obviously your actual app is more complex, so apply this as needed.
IndexAB.vue would look like this, values replaces with algolia demo, so substitute as needed:
<template>
<div>
<p>
<strong>Changing Index with buttons: </strong>
<button
v-for="option in searchIndexOptions"
:key="option.value"
#click="searchIndexName = option"
>
{{ option.text }}
</button>
</p>
<ais-instant-search
:search-client="searchClient"
:index-name="searchIndexName.value"
>
<p>
<strong>Changing Index with vue-multiselect: </strong>
<VueMultiselect
v-model="searchIndexName"
:options="searchIndexOptions"
:searchable="false"
:close-on-select="true"
track-by="value"
label="text"
placeholder="Change Search Index Here"
>
<template v-slot:singleLabel="{ option }">
<strong>{{ option.text }}</strong>
</template>
<template v-slot:option="{ option }">
<strong>{{ option.text }}</strong>
</template>
</VueMultiselect>
</p>
<ais-search-box />
<ais-hits>
<template v-slot:item="{ item }">
<h2>{{ item.name }}</h2>
</template>
</ais-hits>
</ais-instant-search>
</div>
</template>
<script>
import VueMultiselect from "vue-multiselect";
import algoliasearch from "algoliasearch/lite";
import "instantsearch.css/themes/satellite-min.css";
import "vue-multiselect/dist/vue-multiselect.css";
export default {
name: "IndexAB",
components: { VueMultiselect },
data: () => ({
searchClient: algoliasearch("latency", "6be0576ff61c053d5f9a3225e2a90f76"),
searchIndexName: { value: "instant_search", text: "List A" }, // Defaults to instant_search/List A
searchIndexOptions: [
{ value: "instant_search", text: "List A" },
{ value: "airbnb", text: "List B" },
{ value: "airports", text: "List C" },
],
}),
};
</script>
And sandbox: https://codesandbox.io/s/compassionate-ptolemy-9ljmhh?file=/src/components/IndexAB.vue
I had started with Vue 3 sandbox, so few things will be sligtly different(like import of vue-multiselect, and v-slot syntax)

Related

vue component detect own index among siblings

Suppose that I have some Vue components with this relationship. CompA uses a slot.
<CompA>
<CompB />
<CompB />
<CompC />
</CompA>
If I wanted to know which numbered sibling each of these are I could hard code it like:
<CompA>
<CompB i="0" />
<CompB i="1" />
<CompC i="2" />
</CompA>
...while defining an i prop on the definitions of both CompB and CompC.
Is there any method for these components to automatically detect what its own index is in the list of siblings which belong to its parent so that I do not need to hard code these indexes as props? Something that perhaps uses the computed option?
Answers specific to Vue 3 are okay.
Edit
The use case here is for building reusable widgets which conform to ARIA specifications where ARIA attributes need to reference unique IDs.
Example:
<TabContainer tabsWidgetUnique="Tabswidget-1">
<template v-slot:buttons="{ slotProps }">
<TabButton :index="0" :slotProps="slotProps">Button A</TabButton>
<TabButton :index="1" :slotProps="slotProps">Button B </TabButton>
<TabButton :index="2" :slotProps="slotProps">Button C </TabButton>
</template>
<template v-slot:panels="{ slotProps }">
<TabPanel :index="0" :slotProps="slotProps"> Panel A </TabPanel>
<TabPanel :index="1" :slotProps="slotProps"> Panel B </TabPanel>
<TabPanel :index="2" :slotProps="slotProps"> Panel C </TabPanel>
</template>
</TabContainer>
In this case I am manually setting the index of each so that in the components I can say:
<template>
<div
class="tab-panel"
v-show="index === activeTab"
tabindex="0"
:id="tabsWidgetUnique + '-panel-' + index"
:aria-labelledby="tabsWidgetUnique + '-button-' + index"
>
<slot />
</div>
</template>
I don't have any direct answer to your question, but I can offer you a different, and more practical approach.
<template>
<TabContaier :data="tabData" :widget="1" />
</template>
<script>
data() {
return {
tabData: {
buttons: [
{ id: 1, label: Button A},
{ id: 2, label: Button B},
],
panels: [
{ id: 1, content: "<h1>Title 1</h1><p>your content...</p>"},
{ id: 2, content: "<h1>Title 2</h1><p>your content...</p>"},
]
}
}
}
</script>
Tab.vue
<template>
<div class="tab-container">
<div class="buttons">
<TabButton
v-for="button in tabData.buttons"
:key="button.id"
#click="activeTab = button.id"
>{{ button.label }}</TabButton>
</div>
<div class="pabels">
<TabPanel
v-for="panel in tabData.panels"
:key="panel.id"
:class="{'is-visible': activeTab == pabel.id}"
:content="panel.content"
>
</div>
</div>
</template>
<script>
data() {
props: {
tabData: {
type: Object,
default: {}
},
widget: {
type: Number,
default: null
}
},
return {
avtiveTab: 1
}
}
</script>
TabPabel.vue
<div class="panel" v-html="content"></div>
Let me know, if you have any question.
If you don't declare a passed attribute as prop in child component it will be included in the $attrs property like :
this.$attrs.i
in option API or in composition one like:
setup(props,{attrs}){
//then here you could use attrs.i
}

Including text when an image/icon is shown

At the moment I am trying to include different text when an image and/or icon shows on the page. Here is the code for the vue file:
<template>
<div class="profile">
<div
:class="{
'flags--relevant': hasFlagType('medication'),
'flags--active': flag == 'medication'
}"
class="flags"
#click="setFlag('medication')"
>
<medication-icon
:class="[
hasFlagType('medication')
? 'medication-icon--focus'
: 'medication-icon--blur',
]"
/>
</div>
<div
:class="{
'flags--relevant': hasFlagType('condition'),
'flags--active': flag == 'condition'
}"
class="flags"
#click="setFlag('condition')"
>
<treatment-icon
:class="[
hasFlagType('condition')
? 'treatment-icon--focus'
: 'treatment-icon--blur',
]"
/>
</div>
<div
:class="{
'flags--relevant': hasFlagType('translator'),
'flags--active': flag == 'translator',
}"
class="flags"
#click="setFlag('translator')"
>
<foreign-dialect-icon
:class="[
hasFlagType('translator')
? 'foreign-dialect-icon--focus'
: 'foreign-dialect-icon--blur',
]"
/>
</div>
</div>
</template>
<script>
export default {
components: {
ForeignDialectIcon,
MedicationIcon,
TreatmentIcon,
},
props: {
userFlags: {
type: Array,
default() {
return {};
},
},
},
data() {
return {
flags: this.userFlags,
flag: null,
title: "Requires daily medication",
title2: "Specialist health condition",
title3: "Requires a translator",
};
},
methods: {
hasFlagType(flagType) {
return this.flags[flagType] !== undefined;
},
setFlag(flagType) {
if (this.hasFlagType(flagType)) {
this.flag = flagType;
}
},
resetFlag() {
this.flag = null;
},
},
};
</script>
I have tried outputting the titles in the data section for each icon and they still show even if the icon doesn't show. I need it to output the title when the image is shown and the many attempts I've tried haven't worked so was wondering how I am able to solve this?
Assuming the title should only appear when the corresponding icon is focused, you could use the same condition (hasFlagType(...)) with v-if to render the title:
<div>
<medication-icon .../>
<span v-if="hasFlagType('medication')">{{ title }}</span>
</div>
<div>
<treatment-icon .../>
<span v-if="hasFlagType('condition')">{{ title2 }}</span>
</div>
<div>
<foreign-dialect-icon .../>
<span v-if="hasFlagType('translator')">{{ title3 }}</span>
</div>

Vue: how to navigate to specific user's profile

i have code below and i want to navigate the user's profile when click on the name after asked by, i tried code below but it's not working and my json file structured as below
**
questions:Array[10] 0:Object category:"General Knowledge" user:"Saffron" difficulty:"medium" incorrect_answers:Array[3] 0:"Cinnamon" 1:"Cardamom" 2:"Vanilla" question:"What is the world's most expensive spice by weight?" type:"multiple"
**
<template>
<div class="container" width=800px>
<b-row>
<b-col cols="8">
<h1> Recently Asked </h1>
<ul class="container-question" v-for="(question1,index) in questions" :key="index"
>
<li v-if="answered" >
{{question1.question.selectedAnswer[index]}}
<li v-else >
{{question1.question}}
<b-row id="asked-info">
<p>Asked by: </p>
<div
id="user"
v-for="(answer, index) in answers(question1)"
:key="index"
>
<router-link to='/profile'> {{ answer }} </router-link>
</div>
</b-row>
<b-row>
<div class="category" v-for="(category,index) in category(question1)" :key="index" #click="selectedAnswer(index)">
<mark> {{ category }} </mark>
</div>
<b-button class="outline-primary" style="margin:auto;">Answer</b-button>
</b-row>
</li></ul>
</b-col>
<b-col>
<div class="ask-button">
<b-button href="#" class="primary">Ask Question</b-button>
</div>
<div>
<b-card
title="Card Title"
style="max-width: 20rem;"
class="mb-2"
>
<b-card-text>
Some quick example text to build on the card title and make up the bulk of the card's content.
</b-card-text>
</b-card>
</div>
<div>
<b-card
title="Card Title"
style="max-width: 20rem;"
class="mb-2"
>
<b-card-text>
Some quick example text to build on the card title and make up the bulk of the card's content.
</b-card-text>
</b-card>
</div>
</b-col>
</b-row>
<router-view />
</div>
</template>
<script>
export default {
data(){
return{
questions: [],
answered: null,
index: 0,
selectedIndex: null,
}
},
watch: {
question1: {
handler() {
this.selectedIndex = null;
this.answered = false;
},
},
},
methods: {
answers(question1) {
let answers = [question1.correct_answer];
return answers;
},
category(question1){
let category = [...question1.incorrect_answers];
return category
},
selectedAnswer(index) {
this.selectedIndex = index;
this.answered = true;
},
// filteredCategory(question1){
// return (question1.filter((question) => question.incorrect_answers == "Kfc"));
// },
},
mounted: function(){
fetch('https://opentdb.com/api.php?amount=10&category=9&difficulty=medium&type=multiple',{
method: 'get'
})
.then((response) => {
return response.json()
})
.then((jsonData) => {
this.questions = jsonData.results
})
}
}
</script>
routes.js
import question from './views/question.vue';
import App from './App.vue';
import Register from '#/components/Register'
import Login from '#/components/Login'
import Logout from '#/components/Logout'
import profile from '#/components/profile'
import contactus from '#/views/contactus'
export const routes = [
{ path: "/question", component: question },
{ path: "/", component: App },
{
path: '/login',
name: 'Login',
component: Login
},
{
path: '/register',
name: 'Register',
component: Register
},
{
path: '/logout',
name: 'Logout',
component: Logout
},
{
path: '/profile',
name: 'profile',
component: profile
},
{
path: '/contactus',
name: 'contactus',
component: contactus
},
]
There are a few things I want to point out here, you don't have to implement all of these, but understand what you are doing wrong:
You are missing the <router-view> tag, which is where you want your component to render because you use <router-link>
params are ignored if a path is provided. Instead, you need to provide the name of the route or manually specify the whole path with any parameter. Here for your reference: https://router.vuejs.org/guide/essentials/navigation.html
if you don't v-bind your property to, it will treat the value as a string. So you want to go like this <router-link :to="yourLink" />
In your case, you want to use <router-link :to="" /> with a dynamic string. There are 2 ways to achieve this: Declarative (<router-link>) and Programmatic (router.push)
Declarative:
<router-link> is like an anchor tag, so you should return a string.
<router-link :to="yourLink"> {{ userr }} </router-link>
computed: {
yourLink() {
return `/profile/${user(question1)}`
}
}
Programmatic
When you do a router.push, you are not doing a “link” anymore. It’s just a click event at that point. So change your router-link to a button with a click event
<button #click="yourLink"> {{ userr }} </button>
methods: {
yourLink() {
this.$router.push({name:'yourComponentName'} ,params:{user(question1)}})
}
}
EDIT
In your router.js:
{
path:'/profile/:user',
name: 'profile,
component: profile,
props: true
}
In your router-link:
<router-link :to="`/profile/${user(question1)}`"> {{ userr }} </router-link>
Then in your profile.vue, you can query the params by this.$route.params.user, for example, if you want to output it in a <p> tag:
<p>{{ this.$route.params.user }}</p>

How can I set custom template for item in list and than use it inside `v-for` loop?

What I want to achieve is something like:
<li v-for="(item, index) in items" :key="index>
<div v-if="item.Component">
<item.Component :value="item.value" />
</div>
<div v-else>{{ item.value }}</div>
</li>
But anyway I don't like at all this solution. The idea of defining Component key for an item in items list is hard to maintain since at least it is hard to write it in template-style way (usually we are talking about too long HTML inside). Also I don't like to wrap item.Component inside div.
data() {
return {
list: [{
value: 'abc',
Component: {
props: ['value'],
template: `123 {{ value }} 312`
}
}]
};
}
Does anyone know the best-practice solution for this and where Vue describes such case in their docs?
You can use Vue's <component/> tag to dynamically set your component in your list.
<li v-for="(item, index) in items" :key="index>
<component v-if="item.Component" :is="item.Component" :value="item.value"></component>
<div v-else>{{ item.value }}</div>
</li>
<script>
...,
data: () => ({
list: [{
value: 'abc',
Component: {
props: ['value'],
template: `<div>123 {{ value }} 312</div>` // must be enclosed in a element.
}
}]
})
</script>
You can also import a component too so you can create a new file and put your templates and scripts there.
Parent.vue
<script>
import SomeComponent from "#/components/SomeComponent.vue"; //import your component here.
export default {
data() {
return {
list: [
{
value: "abc",
Component: SomeComponent // define your imported component here.
},
]
};
}
};
</script>
SomeComponent.vue
<template>
<div>123 {{ value }} 312</div>
</template>
<script>
export default {
name: "SomeComponent",
props: ["value"]
};
</script>
Here's a demo.

Vue: data from grandparent to grandchild (search component)

In my Vue app I have a view ('projects.vue') that gets some .json and which has a child component ('subheader.vue') which imports a search/filter mixin. I had this working but I wanted to split out the search elements from the subheader component to its own component, so subheader would hold only the headings and then import the search component. Despite adapting the props and bindings from the working version, my new three-component setup is throwing an error. Here's the setup:
the searchMixin.js:
export default {
computed: {
filteredProjects: function() {
const searchTerm = this.search.toLowerCase();
if (!searchTerm) {
return false;
}
return this.projects.filter((project) => {
return (project.client.toLowerCase().match(searchTerm)) ||
(project.contacts.filter((el) => {
return el.name.toLowerCase().match(searchTerm);
}).length > 0) ||
(project.projectReference.toLowerCase().match(searchTerm));
});
}
}
}
Projects.vue:
<template>
<div class="container" id="projects">
<!-- app-subheader is global component, imported & registered in main.js -->
<app-subheader v-bind:title="title" v-bind:subtitle="subtitle" />
[ snip irrelevant stuff ]
</div>
</template>
<script>
export default {
data () {
return {
title: "Projects",
subtitle: "",
projects: []
}
},
created: function() {
this.$http.get('https://xyz.firebaseio.com/projects.json')
.then(function(data){
return data.json();
})
.then(function(data){
var projectsArray = [];
for (let key in data) {
data[key].id = key;
this.projectID = key;
projectsArray.push(data[key]);
}
this.projects = projectsArray;
})
},
} // export default
</script>
subheader.vue:
<template>
<div class="subheader">
<div class="headings">
<h1 v-html="title"></h1>
<h2 v-html="subtitle"></h2>
<!-- I want to conditionally include <app-search /> according to an ID on an element in the grandparent (e.g., projects.vue) -->
<app-search v-bind:projects="projects" />
</div>
</div>
</template>
<script>
import Search from './search.vue';
export default {
components: {
'app-search': Search
},
props: [ "title", "subtitle", "projects" ],
data() {
return {
search: "",
projects: []
}
},
created: function() {
console.log("created; log projects", this.projects);
},
methods: {},
computed: {}
}
</script>
search.vue:
<template>
<div class="search-wrapper">
<div class="search">
<input type="text" v-model="search" placeholder="search by client, contact name, description, project, source" />
</div>
<div class="search-results-wrapper">
<h3>search-results:</h3>
<span class="results-count" v-if="filteredProjects.length == 0">
no results matching search "{{ search }}":
</span>
<span class="results-count" v-if="filteredProjects.length > 0">
{{ filteredProjects.length }} result<span v-if="filteredProjects.length > 1">s</span> matching search "{{ search }}":
</span>
<ul class="search-results" v-bind:class="{ open: filteredProjects.length > 0 }">
<li v-for="(project, ix) in filteredProjects" :key="ix">
{{ ix + 1 }}:
<router-link v-bind:to="'/project-detail/' + project.id">
{{ project.client }} ({{ project.projectReference }})
</router-link>
</li>
</ul>
</div>
</div><!-- END .search-wrapper -->
</template>
<script>
import searchMixin from '../mixins/searchMixin.js';
export default {
props: [ "projects" ],
data() {
return {
search: "",
}
},
created: function() {
},
mixins: [ searchMixin ],
}
When the search function is invoked, this error is thrown:
[Vue warn]: Error in render: "TypeError: Cannot read property 'filter' of undefined"
found in --->
<AppSearch> at src/components/search.vue
<AppSubheader> at src/components/subheader.vue
<Projects> at src/components/projects.vue
<App> at src/App.vue
... which seems to suggest the search mixin is not getting 'projects'.
Also, in subheader.vue, I get various errors whether I have 'projects' as a prop or 'projects: []' as a data key, and in once case or another I either get no results from the search function or an error, "Property or method "projects" is not defined on the instance but referenced during render".
Obviously I'm lacking clarity on the docs re; grandparent-parent-child data flow. Any help is greatly appreciated.
Whiskey T.