Files
mykopkb-fe/src/docs/pages/slot.vue
T
ISMAIL MASSERAN df0d0e743b first init
2026-06-08 11:37:14 +08:00

197 lines
6.5 KiB
Vue

<script lang="ts" setup>
import {
Preview,
SectionTitle,
SectionContent,
PreviewCode,
} from "@/components/docs";
import { Box } from "@/components/ui/box";
import { Button } from "@/components/ui/button";
import { ChevronLeft, ChevronRight } from "@lucide/vue";
</script>
<template>
<div id="preview" class="-mt-20">
<Preview>
<template #preview>
<div class="justify-center items-center flex gap-2">
<Box as-child>
<Button variant="ghost" class="me-2 px-2">
<ChevronLeft class="size-5" />
</Button>
</Box>
<Box as-child>
<Button variant="ghost" class="px-2">
<ChevronRight class="size-5" />
</Button>
</Box>
</div>
</template>
<template #code>
<PreviewCode>
{{ `
<div class="justify-center items-center flex gap-2">
<Box as-child>
<Button variant="ghost" class="me-2 px-2">
<ChevronLeft class="size-5" />
</Button>
</Box>
<Box as-child>
<Button variant="ghost" class="px-2">
<ChevronRight class="size-5" />
</Button>
</Box>
</div>
` }}
</PreviewCode>
</template>
</Preview>
</div>
<div id="installation">
<SectionTitle>Installation</SectionTitle>
<SectionContent>
By using <code>Slot</code> at the root, you ensure that props are correctly merged whether
you're using it as a direct wrapper or an <code>asChild</code> bridge.
</SectionContent>
<SectionContent>
Copy and paste the following code into your project.
</SectionContent>
<PreviewCode title="components/ui/slot/index.ts">
{{ `import {
defineComponent,
h,
cloneVNode,
Fragment,
isVNode,
type VNode,
type PropType,
} from "vue";
import { calculateSlot, flattenItems, type AnyProps } from "./slot";
export const Slot = defineComponent({
name: "Slot",
inheritAttrs: false,
props: {
children: {
type: [Object, Array] as PropType<any>,
},
},
setup(props, { attrs, slots }) {
return () => {
const raw = props.children ?? slots.default?.();
const isValidVNode = (item: any): item is VNode => isVNode(item) && typeof item.type !== "symbol";
const items = flattenItems<VNode>(
raw as any,
(item) => isVNode(item) && item.type === Fragment,
(item) => (isVNode(item) && Array.isArray(item.children) ? (item.children as VNode[]) : [])
).filter(isValidVNode);
const result = calculateSlot<VNode>({
props: attrs as AnyProps,
items,
isValid: isValidVNode,
getProps: (item) => (item.props as AnyProps) || {},
getChildren: (item) => item.children,
});
if (result.type === "wrapper") {
return h("div", result.props, result.children as any);
}
const target = result.target;
return cloneVNode(target, result.props, false);
};
},
});
export { Slot as Root };` }}
</PreviewCode>
<PreviewCode title="components/ui/slot/slot.ts">
{{ `export type AnyProps = Record<string, any>;
export interface SlotParams<T> {
props: AnyProps;
items: T[];
isValid: (item: T) => boolean;
getProps: (item: T) => AnyProps;
getChildren: (item: T) => any;
}
export type SlotResult<T> =
| { type: "slotted"; target: T; props: AnyProps; children: any }
| { type: "wrapper"; target: "div"; props: AnyProps; children: T[] };
export function mergeProps(slotProps: AnyProps, childProps: AnyProps): AnyProps {
const result: AnyProps = { ...childProps };
for (const key in slotProps) {
const slotValue = slotProps[key];
const isHandler = /^on[A-Z]/.test(key);
if (isHandler) {
const childValue = childProps[key];
if (typeof slotValue === "function" && typeof childValue === "function") {
result[key] = (...args: any[]) => {
childValue(...args);
slotValue(...args);
};
} else if (slotValue) {
result[key] = slotValue;
}
continue;
}
if (key === "class" || key === "className") {
const slotClasses = (slotValue || "").split(/\\s+/);
const childClasses = (childProps.class || childProps.className || "").split(/\\s+/);
const combined = Array.from(new Set([...slotClasses, ...childClasses])).filter(Boolean).join(" ");
result[key] = combined;
continue;
}
if (key === "style") {
result[key] = { ...slotValue, ...childProps.style };
continue;
}
if (childProps[key] === undefined) {
result[key] = slotValue;
}
}
return result;
}
export function flattenItems<T>(items: T | T[], isFragment: (item: T) => boolean, getChildren: (item: T) =>
T
| T[]): T[] {
const result: T[] = [];
const list = Array.isArray(items) ? items : [items];
list.forEach((item) => {
if (item === null || item === undefined) return;
if (isFragment(item)) {
const children = getChildren(item);
result.push(...flattenItems(children, isFragment, getChildren));
} else {
result.push(item);
}
});
return result;
}
export function calculateSlot<T>(params: SlotParams<T>): SlotResult<T> {
const { props, items, isValid, getProps, getChildren } = params;
if (items.length === 1) {
const primary = items[0];
if (isValid(primary)) {
return {
type: "slotted",
target: primary,
props: mergeProps(props, getProps(primary)),
children: getChildren(primary),
};
}
}
return { type: "wrapper", target: "div", props: props, children: items };
}` }}
</PreviewCode>
</div>
</template>