I want to create a Card
component using vue that has a header and content section using the vuejs slots api.
I am having trouble understanding how I can create a structure in my Card component. The card should contain all the wireframe styles (padding & margin), but be open for extension from the child template that uses the component.
I would like to know if there are any standard ways of styling wrapper elements with slots inside them from the child component.
The basic Card
element has 2 slots wrapped with structural elements that I would like to allow the child template to modify where necessary.
<div class="Card">
<header class="CardHeader">
<slot name="header"></slot>
</header>
<div class="CardContent">
<slot></slot>
</div>
</div>
Ideally I would like to add a :class
attribute to the #slot
element and have that be used on the element in the slot, however, it seems that the old syntax has been depreciated in the newest version of the framework eg.
<Card>
<template #header class="extendHeader">
stuff
</template>
</Card>
I can think of 3 ways to do it that will work, each with their drawbacks.
- Use props to add a class via the root element
- Use a class on the root element and have style hooks on the components elements
- Wrap the slot content in another element
A must for my project is using css-modules to reduce as much global css as possible because it has caused me so much grief in the past.
1. Use props to add a class via the root element
This method would allow me to use css-modules and would make the Card
elements open for extension
The component template would need to compose extra styles from the child template
<header :class="$style.Header, ...headerClasses]">
<slot name="header"></slot>
</header>
The child template can then use :header-classes
to extend the base Card classes, or overwrite unwanted styles.
<Card :header-classes="[$style.header]">
<template #header>
stuff
</template>
</Card>
<style module>
.header {
background: var(--v-blue);
}
</style>
2. Use a class on the root element and have style hooks on the components elements
The Card
template would need to assign extra classes that could be used to extend the base structural components of the component. The css-module classes should not be used as the names are hashed to avoid collisions.
<div :class="[$style.Card, 'CardStyleHook']">
<header :class="[$style.Header, 'CardHeaderStyleHook']">
<slot name="header"></slot>
</header>
<div :class="[$style.Content, 'CardContentStyleHook']">
<slot></slot>
</div>
</div>
Then a single class could be added to the component and styles to the extra classes could be used
<Card :class="$style.extendCard">
<template #header>
stuff
</template>
</Card>
<style module>
.extendCard .CardHeaderStyleHook {
background: var(--v-blue);
}
</style>
3. Wrap the slot content in another element
I think this kind of defeats the purpose of slots, and it will lead to issues where I need to remove the padding from the wrapping structural element in the Card
, it's not really open for extension and would require more workarounds to allow this style to work.
<Card>
<template #header>
<div :class="$style.customHeader">
stuff
</div>
</template>
</Card>
<style module>
.customHeader{
background: var(--v-blue);
}
</style>
Is there any standard way to handle these cases since the old slots have been depreciated? have I missed something in the documentation, I have only been playing with vue for a couple of days so it's definitely possible.
There is full examples in the snippet below with working code if you need to provide any examples.
Thanks for any help!
const use$styleMocking = {
beforeMount() {
// mock css-modules output
this.$style = {
Card: 'Card__x1337',
Header: 'CardHeader__x1337',
Content: 'CardContent__x1337',
}
}
}
Vue.component('Card', {
template: '#card-template',
mixins: [ use$styleMocking ],
props: {
cardClasses: { default: () => [] },
headerClasses: { default: () => [] },
contentClasses: { default: () => [] },
}
})
Vue.component('style-hooked-card', {
template: '#style-hooked-card-template',
mixins: [ use$styleMocking ]
})
const app = new Vue({
el: '#app',
template: '#app-template'
})
:root {
--white: #fff;
--v-teal: #00c58e;
--v-jade: #108775;
--v-blue: #2f495e;
--card-padding: 0.5rem 1rem;
--grid-gap: 1.25rem;
}
html, body {
font-family: sans-serif;
line-height: 1.44;
}
.main {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-column-gap: var(--grid-gap);
grid-row-gap: var(--grid-gap);
}
/* --------- Card Styles --------- */
/*
styles have __x1337 to mock css-modules, the client should never know the hash
and should not use it to style elements
*/
.Card__x1337 {
box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
transition: 400ms box-shadow;
margin: 0;
flex: 1;
}
.CardHeader__x1337 {
padding: var(--card-padding);
background: var(--v-teal);
}
.CardHeader__x1337 h3 {
color: var(--v-blue);
margin: 0;
padding: 0;
font-size: 1rem;
}
.CardContent__x1337 {
padding: var(--card-padding);
}
/* --------- End Card Styles --------- */
.extendHeader {
background: var(--v-blue);
}
.extendHeader h3 {
color: var(--white);
}
.extendCard .CardHeaderStyleHook {
background: var(--v-jade);
}
.extendCard .CardHeaderStyleHook h3 {
color: var(--white);
}
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<template type="text/x-template" id="app-template">
<main class="main">
<Card>
<template #header class="extendHeader">
<h3>Basic Card Styles</h3>
</template>
<template>
<p>It would be ideal to add a class to the template and have it available to add to the classes of the wrapper element around slots</p>
</template>
</Card>
<Card :header-classes="['extendHeader']">
<template #header>
<h3>1. Extend Header Styles</h3>
</template>
<template>
<p>This card uses props on the Card to extend the wrapper class, this example is using a global class, but this would use css-modules locally scoped classes correctly</p>
</template>
</Card>
<Style-Hooked-Card class="extendCard">
<template #header>
<h3>2. Extend Card Styles</h3>
</template>
<template>
<p>This card adds a hook class to every component so that it can be extended from the outside</p>
</template>
</Style-Hooked-Card>
</main>
</template>
<template type="text/x-template" id="card-template">
<div :class="[$style.Card, ...cardClasses]">
<header :class="[$style.Header, ...headerClasses]">
<slot name="header"></slot>
</header>
<div :class="[$style.Content, ...contentClasses]">
<slot></slot>
</div>
</div>
</template>
<template type="text/x-template" id="style-hooked-card-template">
<div :class="[$style.Card, 'CardStyleHook']">
<header :class="[$style.Header, 'CardHeaderStyleHook']">
<slot name="header"></slot>
</header>
<div :class="[$style.Content, 'CardContentStyleHook']">
<slot></slot>
</div>
</div>
</template>
<div id="app"></div>