vue component detect own index among siblings - vue.js

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
}

Related

Using Vue multiselect to change Algolia index

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)

How to pass class attribute to child component element

how can I pass class attribute from parent to child component element?
Look here:
https://codesandbox.io/s/pedantic-shamir-1wuuv
I'm adding class to the Component "Input Field"
And my goal is that the 'custom-class' will be implemented on the 'input' element in the child component
But just still using the class attribute, and not setting a new prop like "customClass" and accept it in the props of the component
Thanks!
This depends on the template structure of your ChildComponent
Your Parent Component can look like this:
<div id="app">
<InputField class="anyClass" />
</div>
If your Child looks like this:
<template>
<input ... />
</template
Because if you have only 1 root Element in your template this will inherit the classes given automatically.
if your Template e.g. looks like this: (only available in vue3)
<template>
<input v-bind="$attrs" />
<span> hi </span>
</template
You will need the v-bind="$attrs" so Vue knows where to put the attributes to. Another Solution would be giving classes as props and assigning it to the element with :class="classes"
The pattern for the customs form component in Vue 2 where the props go to a child element looks something like this.
<template>
<div class="input-field">
<label v-if="label">{{ label }}</label>
<input
:value="value"
:class="inputClass"
#input="updateValue"
v-bind="$attrs"
v-on="listeners"
/>
</div>
</template>
<script>
export default {
inheritAttrs: false,
props: {
label: {
type: String,
default: "",
},
value: [String, Number],
inputClass: {
type: String,
default: "",
},
},
computed: {
listeners() {
return {
...this.$listeners,
input: this.updateValue,
};
},
},
methods: {
updateValue(event) {
this.$emit("input", event.target.value);
},
},
};
</script>
The usage of those components could look something like this.
```html
<template>
<div id="app">
<InputField
v-model="name"
label="What is your name?"
type="text"
class="custom"
inputClass="input-custom"
/>
<p>{{ name }}</p>
</div>
</template>
<script>
import InputField from "./components/InputField";
export default {
name: "App",
components: {
InputField,
},
data() {
return {
name: "",
};
},
};
</script>
A demo is available here
https://codesandbox.io/s/vue-2-custom-input-field-4vldv?file=/src/components/InputField.vue
You need to use vue props . Like this:
child component:
<template>
<div>
<input :class="className" type="text" value="Test" v-bind="$attrs" />
</div>
</template>
<script>
export default {
props:{
className:{
type:String,
default:'',
}
}
};
</script>
Parent component:
<div id="app">
<InputField :class-name="'custom-class'" />
</div>
Since you're using vue2 the v-bind=$attrs is being hooked to the root element of your component the <div> tag. Check the docs. You can put this wrapper on the parent element if you need it or just get rid of it.
Here is a working example
Another approach
There is also the idea of taking the classes from the parent and passing it to the child component with a ref after the component is mounted
Parent Element:
<template>
<div id="app">
<InputField class="custom-class" ref="inputField" />
</div>
</template>
<script>
import InputField from "./components/InputField";
export default {
name: "App",
components: {
InputField,
},
mounted() {
const inputField = this.$refs.inputField;
const classes = inputField.$el.getAttribute("class");
inputField.setClasses(classes);
},
};
</script>
Child Element:
<template>
<div>
<input type="text" value="Test" :class="classes" />
</div>
</template>
<script>
export default {
data() {
return {
classes: "",
};
},
methods: {
setClasses: function (classes) {
this.classes = classes;
},
},
};
</script>
Here a working example
In Vue2, the child's root element receives the classes.
If you need to pass them to a specific element of your child component, you can read those classes from the root and set them to a specific element.
Example of a vue child component:
<template>
<div ref="root">
<img ref="img" v-bind="$attrs" v-on="$listeners" :class="classes"/>
</div>
</template>
export default {
data() {
return {
classes: null,
}
},
mounted() {
this.classes = this.$refs.root.getAttribute("class")
this.$refs.root.removeAttribute("class")
},
}
Pretty much it.

Vue2 How to use nested <template> tags?

I'm using Vue2's latest version.
I have 2 components; 1st component:
<stack>
<template #first>
<template #cell(details)="row">
{{ row.details ? "Hide" : "Show" }}
</template>
</template>
</stack>
2nd component:
<template>
<div>
<slot name="first"></slot>
</div>
</template>
<script>
export default {
data() {
return {
row: {
name: 'Test',
details: true
}
}
},
}
</script>
I want to simply insert the contents of the <template #first> from the 1st component into the slot in the 2nd component while maintaining everything inside the <template #first> tag.
So the desired outcome would look like this; the slot would be replaced with the contents of the <template #first> tag:
<template>
<div>
<template #cell(details)="row">
{{ row.details ? "Hide" : "Show" }}
</template>
</div>
</template>
<script>
export default {
data() {
return {
row: {
name: 'Test',
details: true
}
}
},
}
</script>
However, the 1st component does not work like this. It says that I cannot have a template inside a template. Is this actually possible what I'm trying to achieve here? If yes, how?

VueJS component ref component is not accessible at all

I don't understand the refs when using in Vue component. It is not working properly.. I have two files
show.vue
<template>
<div>
<b-container fluid class="bg-white" v-if="$refs.chart">
<b-row class="topTab types">
<b-col
:class="{ active: currentTab === index }"
v-for="(tabDisplay, index) in $refs.chart.tabDisplays"
:key="index"
>
<router-link
:to="{ query: { period: $route.query.period, tab: index } }"
>
{{ tabDisplay }}
</router-link>
</b-col>
</b-row>
</b-container>
<component v-bind:is="currentGame" ref="chart" />
</div>
</template>
<script>
export default {
computed: {
currentGame() {
return () =>
import(
`#/components/Trend/example/Charts/${this.group}/${this.id}/Base.vue`
);
},
}
};
</script>
Base.vue
<template>
<div>
dadsas
</div>
</template>
<script>
export default {
data: function() {
return {
tabDisplays: {
1: "example1",
2: "example2",
3: "example3",
4: "example4"
}
};
}
};
</script>
Take note that the second file renders properly showing the dasdas but the $refs.chart.tabDisplays is not. It will only show when I change something inside the <script> tag like adding 5: "example5" in the tabDisplays data then if I refresh it will be gone again. Basically, I just want to access the computed property of my child component. I am very aware I can use vuex but I want to try accessing a component's computed property via ref. What is wrong with my $.refs.chart?
As I noted in my comment, refs are only populated after rendering, so you won't have access to them during rendering. This is mentioned in the docs, see https://v2.vuejs.org/v2/api/#ref. The child component doesn't exist at the point you're trying to access it. The rendering process is responsible for creating the child components, it all gets a bit circular if you try to access them during that rendering process.
It looks like you've already made several key design decisions here about how to structure your application, such as component boundaries and data ownership, and those decisions are making it difficult to get where you want to be. It's not easy to make concrete suggestions about how to fix that based purely on the code provided.
So instead I will attempt to suggest a minimal change that should fix the immediate problem you're having.
To access the property of the child you're going to need the parent component to render twice. The first time it will create the chart and the second time it will have the relevant property available. One way to do this would be to copy the relevant property to the parent after rendering.
<template>
<div>
<b-container fluid class="bg-white" v-if="tabDisplays">
<b-row class="topTab types">
<b-col
:class="{ active: currentTab === index }"
v-for="(tabDisplay, index) in tabDisplays"
:key="index"
>
<router-link
:to="{ query: { period: $route.query.period, tab: index } }"
>
{{ tabDisplay }}
</router-link>
</b-col>
</b-row>
</b-container>
<component v-bind:is="currentGame" ref="chart" />
</div>
</template>
<script>
export default {
data () {
return { tabDisplays: null };
},
computed: {
currentGame() {
return () =>
import(
`#/components/Trend/example/Charts/${this.group}/${this.id}/Base.vue`
);
},
},
mounted () {
this.tabDisplays = this.$refs.chart.tabDisplays;
},
updated () {
this.tabDisplays = this.$refs.chart.tabDisplays;
}
};
</script>
In the code above I've introduced a tabDisplays property and that is then being synced with the child in mounted and updated. Within the template there's no reference to $refs at all.
While this should work I would repeat my earlier point that the 'correct' solution probably involves more significant changes. Syncing data up to a parent like this is not a normal Vue pattern and strongly suggests an architectural failure of some kind.
This is based on #skirtle's answer. I just made a few tweaks in his answer to produce what I really want
show.vue
<template>
<div>
<b-container fluid class="bg-white" v-if="chart">
<b-row class="topTab types">
<b-col
:class="{ active: currentTab === index }"
v-for="(tabDisplay, index) in chart.tabDisplays"
:key="index"
>
<router-link
:to="{ query: { period: $route.query.period, tab: index } }"
>
{{ tabDisplay }}
</router-link>
</b-col>
</b-row>
</b-container>
<component v-bind:is="currentGame" ref="chart" />
</div>
</template>
<script>
export default {
data: function() {
return {
chart: undefined
};
},
computed: {
currentGame() {
return () =>
import(
`#/components/Trend/高频彩/Charts/${this.group}/${this.id}/Base.vue`
);
},
},
updated() {
this.chart = this.$refs.chart;
}
};
</script>

When using conditional rendering, how do I prevent repeating the child components on each condition?

Scenario
I have a custom button component in Vue:
<custom-button type="link">Save</custom-button>
This is its template:
// custom-button.vue
<template>
<a v-if="type === 'link'" :href="href">
<span class="btn-label"><slot></slot></span>
</a>
<button v-else :type="type">
<span class="btn-label"><slot></slot></span>
</button>
</template>
You can see from the template that it has a type prop. If the type is link, instead of the <button> element, I am using <a>.
Question
You'll notice from the template that I repeated the child component, i.e. <span class="btn-label"><slot></slot></span> on both root components. How do I make it so that I won't have to repeat the child components?
In JSX, it's pretty straightforward. I just have to assign the child component to a variable:
const label = <span class="btn-label">{text}</span>
return (type === 'link')
? <a href={href}>{label}</a>
: <button type={type}>{label}</button>
In this situation, I would probably opt to write the render function directly since the template is small (with or without JSX), but if you want to use a template then you can use the <component> component to dynamically choose what you want to render as that element, like this:
Vue.component('custom-button', {
template: '#custom-button',
props: [
'type',
'href',
],
computed: {
props() {
return this.type === 'link'
? { is: 'a', href: this.href }
: { is: 'button', type: this.type };
},
},
});
new Vue({
el: '#app',
});
<script src="https://rawgit.com/vuejs/vue/dev/dist/vue.js"></script>
<div id="app">
<custom-button type="button">Button</custom-button>
<custom-button type="submit">Submit</custom-button>
<custom-button type="link" href="http://www.google.com">Link</custom-button>
</div>
<template id="custom-button">
<component v-bind="props">
<span class="btn-label"><slot></slot></span>
</component>
</template>
Well you could always create a locally registered component...
// in custom-button.vue
components : {
'label' : {template : '<span class="btn-label"><slot></slot></span>'}
}