getting slotProps from nested slot - vuejs3

I have a dynamic table. Table and table columns are separated components. And I am using columns as slot in table components.
main file
<DynamicTable :contents="contents">
// columns finding and displaying data from props dynamically. it works without a problem
<Col th="name" td="name"/>
<Col th="information" td="information"/>
// here I am giving custom slot inside last column, but button not displaying additionally I can't reach the content data via slotProps //
<Col>
<template #content="slotProps">
<button #click="getData(slotProps.content)">click me</button>
</template>
</Col>
</DynamicTable>
dynamicTable component
<slot></slot>
<div v-for="content in contents" :key="content.id">
<div v-for="(td, idx) in body" :key="idx">
// if there is slot given as template render <slot>
<slot v-if="td.children" name="content" :content=content></slot>
// else render dynamic value
<span v-else>{{ content[td.name] }}</span>
</div>
</div>
const slots = useSlots() ? useSlots().default : null;
const body = ref([])
slots().forEach(slot => {
body.value.push({
children: slot.children,
name: slot.props?.td,
})
})
additionally Col template
<script>
export default {
props: {
th: {
type: String,
default: null
},
td: {
type: String,
default: null
},
},
render(){
return null
}
}
</script>
In a situation like above, how can I display button element inside given <Col> component as slot and getting the :content="content" data as slotProps.
And if you need to know, content Array looks like below.
const contents = ref([
{
id: 341,
order_id: 1,
name: "susan",
information: "inf context",
},
{
id: 453,
order_id: 2,
name: "jack",
information: "info context",
}
])

Solved
You're trying to access slotProps in Col component,
But the Col component is not passing any props.
The required data is available in DynamicTable component
And you're already passing it to the slot of DynamicTable
You can access the slotProps as below
//App.vue
<DynamicTable :contents="contents" v-slot="slotProps">
<Col th="name" td="name" />
<Col>
Access slotProps directly, no need to use template
{{slotProps.content}}
</Col>
</DynamicTable>
And you need to remove slot name
//DynamicTable.vue
<slot v-if="td.children" :content="content"></slot>
Also refactor the Col component
//Col.vue
/* remove render function */
...
<template>
<slot />
</template>
Solution
You can find a working demo below.
https://stackblitz.com/edit/vue-cntmmb?file=src/App.vue

Related

VUE3.JS - Modal in a loop

I have a GET request in my home.vue component.
This query allows me to get an array of objects.
To display all the objects, I do a v-for loop and everything works fine.
<div class="commentaires" v-for="(com, index) of coms" :key="index">
My concern is that I want to display an image by clicking on it (coms[index].imageUrl), in a modal (popup).
The modal is displayed fine but not with the correct image, i.e. the modal displays the last image obtained in the loop, which is not correct.
Here is the full code of my home.vue component
<template>
<div class="container">
<div class="commentaires" v-for="(com, index) of coms" :key="index">
<modale :imageUrl="com.imageUrl_$this.index" :revele="revele" :toggleModale="toggleModale"></modale>
<img class="photo" :src=""" alt="image du commentaire" #click="toggleModale">
</div>
</div>
</template>
<script>
//import axios from "axios";
import axios from "axios";
import Modale from "./Modale";
export default {
name: 'HoMe',
data() {
return {
coms: [],
revele: false
}
},
components: {
modale: Modale
},
methods: {
toggleModale: function () {
this.revele = !this.revele;
},
</script>
Here is my modale.vue component
<template>
<div class="bloc-modale" v-if="revele">
<div class="overlay" #click="toggleModale"></div>
<div class="modale card">
<div v-on:click="toggleModale" class="btn-modale btn btn-danger">X</div>
<img :src=""" alt="image du commentaire" id="modal">
</div>
</div>
</template>
<script>
export default {
name: "Modale",
props: ["revele", "toggleModale", "imageUrl"],
};
</script>
I've been working on it for 1 week but I can't, so thank you very much for your help...
in your v-for loop you're binding the same revele and toggleModale to every modal. When there is only one revele then any time it's true, all modals will be displayed. It's therefore likely you're actually opening all modals and simply seeing the last one in the stack. You should modify coms so that each item has it's own revele, e.g.:
coms = [
{
imageUrl: 'asdf',
revele: false
},
{
imageUrl: 'zxcv',
revele: false
},
{
imageUrl: 'ghjk',
revele: false
}
];
then inside your v-for:
<modale
:image-url="com.imageUrl"
:revele="com.revele"
#toggle-modale="com.revele = false"
></modale>
<img class="photo" :src=""" alt="image du commentaire" #click="com.revele = true">
passing the same function as a prop to each modal to control the value of revele is also a bad idea. Anytime a prop value needs to be modified in a child component, the child should emit an event telling the parent to modify the value. Notice in my code snippet above I replaced the prop with an event handler that turns the revele value specific to that modal to false. Inside each modal you should fire that event:
modale.vue
<div class="btn-modale btn btn-danger" #click="$emit('toggle-modale')">
X
</div>
This way you don't need any function at all to control the display of the modals.

Data Binding between parent and child component in vue3 composition api

I have successfully bound data in a two way format in vue 3 like this :
<template>
<p for="">Year of Incorporation</p>
<input type="text" v-model="input" />
<div>
{{ input }}
</div>
</template>
<script>
export default {
setup() {
let input = ref("")
return {input};
},
};
</script>
What I want to do now is put the input inside a child component like this:
Parent Component:
<template>
<Child v-model="input" />
{{input}}
</template>
Child Component:
<template>
<input type="text" v-model="babyInput" />
</template>
<script>
export default {
setup() {
let babyInput = ref("")
return {babyInput};
},
};
</script>
The Vue.js documentation presents a nice way of handling v-model-directives with custom components:
Bind the value to the child component
Emit an event when the Child's input changes
You can see this example from the docs with the composition when you toggle the switch in the right-top corner (at https://vuejs.org/guide/components/events.html#usage-with-v-model).

vue3 Cannot loop over an array of object passed as prop

In vue3 I am passing an array of options from parent component to child component in order to use it as options for a select.
At the moment, I am not able to use it to initialize my select.
Here is the child component SmartNumberInput
<template>
<div>
<div>Selected: {{ selected }} Initial:{{ initial }}</div>
{{ options }}
<div v-for="option in options" :key="option.value">
{{ option.text }}
</div>
<input type="number" v-model="input_value" />
<select v-model="selected">
<option
v-for="option in options"
:value="option.value"
:key="option.value"
>
{{ option.text }}
</option>
</select>
</div>
</template>
<script>
export default {
props: ["initial", "options"],
data() {
return {
selected: "",
input_value: "",
};
},
};
</script>
Here is the parent component
<template>
<div>
<h1>Hi! I am the root component</h1>
<div class="smart-number-input">
<smart-number-input
initial="B"
options="[{text:'Liters',value:'A'},{text:'Gallons',value:'B'},{text:'Pints',value:'C'}]"
/>
</div>
</div>
</template>
<script>
import SmartNumberInput from "./SmartNumberInput.vue";
export default {
data() {
return {
initial: "salut",
};
},
components: { SmartNumberInput },
};
</script>
<style>
.smart-number-input {
width: 100%;
background:#EEE;
}
</style>
In the result I get (see picture) there is no option visible in the select though when the small arrow is clicked it expands with a long empty list.
The {{options}} statement in the child displays what I pass as prop i.e. an array of objects but nothing is displayed in the div where I use a v-for loop.
When I declare the options as data in the child both loops (div and select) work fine.
Could somebody explain what is wrong in the way I pass or use the array of options ?
change options to :options (add colon symbol)
.
if you not put colon, it will treat the value as a String...

Angular Component with Content Projection Not Loading Properly in Storybook until a Control Is Changed

I'm new to Storybook, and am having trouble getting components which project ng-content to correctly receive input arguments on initial load.
Template:
<div id="alert-card">
<div>
<div class="iconDiv" fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="10px">
<mat-icon *ngIf='this.alertType === "Warning"' class="warningIcon" aria-hidden="false" aria-label="Warning">warning</mat-icon>
<mat-icon *ngIf='this.alertType === "Error"' class="errorIcon" aria-hidden="false" aria-label="Error">error</mat-icon>
<mat-icon *ngIf='this.alertType === "Info"' class="infoIcon" aria-hidden="false" aria-label="Info">info</mat-icon>
<mat-icon *ngIf='this.alertType === "Confirm"' class="confirmIcon" aria-hidden="false" aria-label="Confirm">check_circle</mat-icon>
<span><span class="alertType">{{ alertKeyword }}</span>{{ alertMessage }} </span>
</div>
<ng-content select="[cardContent]"></ng-content>
</div>
</div>
Component:
import {
Component,
Input,
ViewEncapsulation,
} from '#angular/core';
#Component({
selector: 'alert-card',
templateUrl: './alert-card.component.html',
styleUrls: ['./alert-card.component.scss'],
encapsulation: ViewEncapsulation.None,
})
// Displays the alert card
export class AlertCardComponent {
// The type of alert
#Input()
alertType: AlertType;
// The keyword at the start of the alert message
#Input()
alertKeyword: string;
// The alert message to display
#Input()
alertMessage: string;
constructor() {}
}
// Indicates a type of alert which has an associated icon and color scheme in the CSS
export enum AlertType {
Warning = "Warning",
Error = "Error",
Info = "Info",
Confirm = "Confirm",
}
Story:
import { componentWrapperDecorator, Meta, Story } from '#storybook/angular';
import { AlertCardComponent, AlertType } from './alert-card.component';
export default {
component: AlertCardComponent,
decorators: [componentWrapperDecorator(AlertCardComponent)],
} as Meta;
const Template: Story = (args) => ({
props: args,
template: `
<div cardContent
style="padding:10px"
fxLayoutAlign="center">
Arbitrary HTML content can go in this area
</div>
`
});
export const Warning = Template.bind({});
Warning.args= {
alertType: AlertType.Warning,
alertKeyword: "Warning: ",
alertMessage: "something has happened!"
};
This is nearly working as expected, but within the story, the component loads without any of the input arguments:
If I change any of the Storybook controls, the input arguments are passed and the component displays as expected:
I feel like I'm missing something obvious. Everything works as expected once I start manipulating the controls within Storybook, but how do I get the input arguments passed on initial load?
I'm also having what I believe are related issues on Storybook's Docs page. The component is displayed as it would without any arguments passed regardless of how the inputs are controlled.

How can I pass class and style attributes on a vue component to a different element like $attrs?

I have a single file component called confirm-document that looks something like this:
Sandbox
<template>
<v-dialog>
<template v-slot:activator="{ on }">
<v-btn v-bind="$attrs" :class="activatorClass" v-on="on">{{
title
}}</v-btn>
</template>
<v-card>
<v-card-title>{{ title }}</v-card-title>
<v-card-text><slot></slot></v-card-text>
</v-card>
</v-dialog>
</template>
<script>
export default {
name: "ConfirmDocument",
props: {
title: String,
activatorClass: {},
},
};
</script>
So when I then use this component like:
<ConfirmDocument
class="useless-class"
activator-class="mt-4 ml-n4"
title="Consent"
> Document Content </ConfirmDocument>
Sandbox
The classes get applied to the v-dialog, which ends up as an invisible div with nothing inside and both the activator and modal attached as sibling nodes.
Since this is mainly a wrapper component to provide a consistent UI, I actually only need for the activator to be positionable. So I want to pass the class and style props to the v-activator.
The activator-class prop that I have declared actualy works fine. But I am very curious if there a way to change the element to which the component's class and style attributes are bound, so that I can use class instead?
This is fixed in Vue.js v3 ref
For Vue.js v2, you can try this
Check v-bind and attrs computed property below
<template>
<v-dialog>
<template v-slot:activator="{ on }">
<v-btn v-bind="attrs" v-on="on">{{
title
}}</v-btn>
</template>
<v-card>
<v-card-title>{{ title }}</v-card-title>
<v-card-text><slot></slot></v-card-text>
</v-card>
</v-dialog>
</template>
<script>
export default {
name: "ConfirmDocument",
inheritAttrs: false, // <- added just for safety
props: {
title: String,
},
computed: {
attrs() {
const attrs = { ...self.$attrs }
// adding class and style for `v-bind`
attrs.class = self.$vnode.data.staticClass
attrs.style = self.$vnode.data.staticStyle
return attrs
}
}
};
</script>
Explanation - In Vue.js version 2.x.x, $attrs does not include class and style (ref) but there are many scenarios in which we wanted to pass on all the props along with class and style into another component so, $attrs should have class and style in it which they did add in version 3 of vue.js.
There is a detailed discussion on this topic on github do check it out for more details.
For version 2, what we wanted to achieve is that class and style can be passed to another component. We can pull it off by getting the class [as string] and style [as object] from the current component node i.e. vnode and pass it on to the other component using v-bind.
Now, you can pass props along with class and style into another component inside ConfirmDocument directly from the parent (or caller) component
What's about simple using props to handle this?
<template>
<v-dialog>
<template v-slot:activator="{ on }">
<v-btn :class="btnClass" v-on="on">Read {{ title }}</v-btn>
</template>
<v-card>
<slot></slot>
</v-card>
</v-dialog>
</template>
<script>
export default {
props: {
btnClass: { type: String },
title: { type: String }
}
}
</script>
and using the component:
<confirm-document
btn-class="mt-0 mb-0"
title="Privacy Policy"
>
Lorem ipsum dolor sit amet
</confirm-document>
I think you can use the inheritAttrs: false property. What it does it to make sure that attributes are not applied automatically to the root element and it lets you choose where to apply them instead.
<template>
<v-dialog>
<template v-slot:activator="{ on }">
<v-btn v-bind="buttonAttrs" >Read {{ title }}</v-btn>
</template>
<v-card>
<slot></slot>
</v-card>
</v-dialog>
</template>
<script>
export default {
inheritAttrs: false,
props: {
title: {type: String},
},
computed: {
buttonAttrs () {
// select which attrs to apply
const { title, ...rest } = this.$attrs;
return rest;
}
}
}
</script>
A working (and a bit cluttered) example can be found here.

Resources