Write a part with its own Vue logic and connect it to a primitive.
The parts from Rendering Primitives are enough until one needs its own logic. You can replace just that part and keep the same rating.ts definition from Compose Behaviors.
Write a Root Part
Here's a replacement for the generated RatingRoot. usePrimitive creates its instance, and usePart connects the root slot:
import { usePart, usePrimitive, type PartSlots } from "@ui-sdk/vue/primitive"
import { defineComponent, type HTMLAttributes } from "vue"
import { rating } from "./rating"
const RatingRoot = defineComponent(
(
props: { value?: number; defaultValue?: number } & HTMLAttributes,
{ emit }
) => {
const handle = usePrimitive(rating, () => ({
value: props.value,
defaultValue: props.defaultValue,
onValueChange(value, details) {
emit("value-change", value, details)
if (!details.canceled) {
emit("update:value", value)
}
},
}))
return usePart(handle, "root", undefined, undefined, "div")
},
{
inheritAttrs: false,
props: ["value", "defaultValue"],
emits: ["value-change", "update:value"],
slots: Object as PartSlots<typeof rating, "root">,
}
)
export { RatingRoot }Call both helpers in setup. The options getter keeps values current; usePrimitive also shares the instance with child parts. usePart returns a render function that forwards attrs and slots. inheritAttrs: false avoids applying attrs twice.
HTMLAttributes makes native attrs available to TypeScript callers; only value and defaultValue are declared component props, so the other attrs still reach usePart.
This root supports v-model:value by emitting update:value after the change callback has had a chance to cancel. PartSlots types the default and #render slots. The adapter manages the instance's lifetime.
Connect a Child Part
Pass the primitive itself to usePart to find the instance shared by the root above it. This RatingStar works inside the manual root above or the generated RatingRoot:
import { usePart, type PartSlots } from "@ui-sdk/vue/primitive"
import { defineComponent, type ButtonHTMLAttributes } from "vue"
import { rating } from "./rating"
const RatingStar = defineComponent(
(props: { value: string } & Omit<ButtonHTMLAttributes, "value">) => {
return usePart(
rating,
"star",
() => ({ value: props.value }),
undefined,
"button"
)
},
{
inheritAttrs: false,
props: ["value"],
slots: Object as PartSlots<typeof rating, "star">,
}
)
export { RatingStar }Use it in place of the generated star in Render the Rating. ButtonHTMLAttributes types its native attrs. The value getter identifies the star; the remaining props go to its button. Keep it inside RatingRoot and give it an aria-label.
Merge Props
usePart merges the component's attrs with the slot's. Slot attributes win, classes and styles combine, and your listeners run first. An id, or a name or typing hint you pass, like aria-label or autocomplete, replaces the slot's. Call preventDefault() in a listener to skip the built-in handler.
The last argument can be a tag, or an object with tag, children, and extend. Use extend for extra props after the slot's props, such as attributes supplied by a Field. Keep the slot's roles, IDs, and ARIA state intact.
Compose a Popup
A custom component can also use an existing Popover or Dialog. Compose Ours shows that connection with an emoji picker.