How do you abstract / extend a 3rd party component? #10166
Replies: 3 comments 2 replies
-
I have this exact same issue (with the same library). I think the main problem is that there really isn't a straightforward way to do this with the Composition API. The Options API provides the Furthermore, if you're using TypeScript, it gets even more complicated. I've encountered, especially with PrimeVue, that the Vue really has very very poor tools for creating abstracted or traditional "higher order" components for this reason. The Composition API is, IMO, not yet feature complete and lacks a lot of the versatility of the Options API, even though for 99% of use cases, it's better DX. |
Beta Was this translation helpful? Give feedback.
-
@martinszeltins I think this will really help you! I figured out a pattern that works.
<script lang="ts">
import Badge from 'primevue/badge'
import SpinnerIcon from 'primevue/icons/spinner'
import Ripple from 'primevue/ripple'
import BaseButton from 'primevue/button/BaseButton.vue'
import Link from './Link.vue'
/** Copied mostly from existing PrimeVue Button implementation */
export default {
/**
* Vue has a bug where <component :is> will render this component
* if it has the same name as the component it's being used in,
* regardless of casing.
*
* Not sure if this is tracked?
*/
name: 'CustomButton',
extends: BaseButton,
props: {
variant: {
type: String,
default: 'primary'
},
href: {
type: String,
default: undefined
},
to: {
type: String,
default: undefined
}
},
methods: {
getPTOptions(key: string) {
return this.ptm(key, {
context: {
disabled: this.disabled
}
})
}
},
computed: {
tag() {
return this.href || this.to ? Link : 'button'
},
disabled() {
return this.$attrs.disabled || this.$attrs.disabled === '' || this.loading
},
defaultAriaLabel() {
return this.label ? this.label + (this.badge ? ' ' + this.badge : '') : this.$attrs.ariaLabel
},
hasIcon() {
return this.icon || this.$slots.icon
}
},
components: {
SpinnerIcon,
Badge
},
directives: {
ripple: Ripple
}
}
</script>
<template>
<component
:is="tag"
v-ripple
:class="[cx('root'), variant]"
:type="tag === 'button' ? 'button' : undefined"
:aria-label="defaultAriaLabel"
:disabled="disabled"
v-bind="getPTOptions('root')"
:data-pc-severity="severity"
>
<slot v-if="loading" name="loadingicon" :class="[cx('loadingIcon'), cx('icon')]">
<span v-if="loadingIcon" :class="[cx('loadingIcon'), cx('icon'), loadingIcon]" v-bind="ptm('loadingIcon')" />
<SpinnerIcon
v-else
:class="[cx('loadingIcon'), cx('icon')]"
spin
v-bind="ptm('loadingIcon')"
/>
</slot>
<slot v-else name="icon" :class="[cx('icon')]">
<span v-if="icon" :class="[cx('icon'), icon, iconClass]" v-bind="ptm('icon')"></span>
</slot>
<slot v-if="!!$slots.default"></slot>
<span v-else :class="cx('label')" v-bind="ptm('label')">{{ label || ' ' }}</span>
<Badge
v-if="badge"
:value="badge"
:class="badgeClass"
:severity="badgeSeverity"
:unstyled="unstyled"
v-bind="ptm('badge')"
/>
</component>
</template>
<style scoped lang="less">
.p-button {
gap: 0.5rem;
border-radius: 4px;
outline-color: transparent;
padding: 0.5rem 0.75rem;
text-decoration: none;
appearance: none;
-webkit-appearance: none;
border: 1px solid transparent;
font-size: 1rem;
line-height: 1.5;
background: transparent;
&.p-button-sm {
padding: 0.25rem 0.5rem;
font-size: 0.9rem;
}
&.primary {
background: var(--primary-color);
color: var(--primary-color-text);
}
&.p-button-outlined {
background-color: transparent;
color: var(--text-color-secondary);
border-color: var(--surface-400);
&:hover {
background-color: var(--translucent-surface-0);
}
}
&.p-button-rounded {
padding: 0.5rem;
border-radius: 50%;
}
&.p-button-plain {
background: none;
border: none;
color: var(--text-color-secondary);
}
&.link {
background: none;
border: none;
color: var(--primary-color);
text-decoration: underline;
display: inline;
}
}
</style>
import { type ClassComponent } from 'primevue/ts-helpers'
import { type ButtonProps, type ButtonSlots, type ButtonEmits } from 'primevue/button'
declare module './Button.vue' {
interface Props extends ButtonProps {
variant?: 'primary' | 'secondary' | 'link'
href?: string
to?: string
}
declare class Button extends ClassComponent<Props, ButtonSlots, ButtonEmits> {}
export = Button
} Now, I effectively have mostly a PrimeVue button with just a few changes. (It's possible you could have also used Note again there is no way to do this with the Composition API. I wasted a lot of hours trying to find a way, and there just isn't one. |
Beta Was this translation helpful? Give feedback.
-
I have the same question extending Dialog component. I made MyDialog with custom header e footer styles, and i wish fallthrough v-model:show from MyDialog to Dialog, but MyDialog don't exposes v-model:show as it signature to however its using MyDialog. To be honest i don't know if its a good idea.
i haven't try this, but sad knowing even that doesn't work |
Beta Was this translation helpful? Give feedback.
-
I am using PrimeVue library and I wanted to abstract the PrimeVue
Button
component so I can add my own features on top of it. I would like to create anAppButton
component that wraps the PrimeVue Button component but I ran into a problem. This means I need to copy/paste all the props, emits and slots into my ownAppButton
component for this to work properly, correct? And if props have complex validation rules, I need to copy those too. Also, it seems like it can have a default slot and work without it, so I need to handle that too?Is there an easier way to do this? What is the recommendation / best practice for this? I wish I didn't have to re-implement the whole thing in my own component. I just wanted to change a few things.
AppButton.vue
Beta Was this translation helpful? Give feedback.
All reactions