Skip to content

Instantly share code, notes, and snippets.

@benceszenassy
Created June 14, 2024 11:45
Show Gist options
  • Save benceszenassy/7c06de0043e2dfe937008477489d24eb to your computer and use it in GitHub Desktop.
Save benceszenassy/7c06de0043e2dfe937008477489d24eb to your computer and use it in GitHub Desktop.
custom-element-vue-wrapper for IBM carbon thinkering
export type CvActionableNotificationProps = HTMLElement & {
/** Inline notification type. */
inline?: boolean;
/** Specify if pressing the escape key should close notifications */
closeOnEscape?: boolean;
/** Specify if focus should be moved to the component when the notification contains actions */
hasFocus?: boolean;
/** `true` to hide the close button. */
hideCloseButton?: boolean;
/** Low contrast mode */
lowContrast?: boolean;
/** `true` if the notification should be open. */
open?: boolean;
/** Pass in the action button label that will be rendered within the ActionableNotification. */
actionButtonLabel?: string;
/** The caption. */
caption?: string;
/** Provide a description for "close" icon button that can be read by screen readers */
ariaLabel?: string;
/** Provide a description for "status" icon that can be read by screen readers */
statusIconDescription?: string;
/** Notification kind. */
kind?: string;
/** Specify an optional duration the notification should be closed in */
timeout?: number | null;
/** The subtitle. */
subtitle?: string;
/** The title. */
title?: string;
};
<template>
<cds-actionable-notification
class="cv-actionable-notification"
v-bind="props"
>
<template v-slot:subtitle>
<slot name="subtitle" />
</template>
<template v-slot:title>
<slot name="title" />
</template>
<slot></slot>
</cds-actionable-notification>
</template>
<script setup lang="ts">
import "@carbon/web-components/es/components/notification/actionable-notification";
import type { CvActionableNotificationProps } from "./CvActionableNotification.ts";
const props = defineProps<CvActionableNotificationProps>();
const emits = defineEmits<{
// The custom event fired before this notification is being closed upon a user gesture. Cancellation of this event stops the user-initiated action of closing this notification.
(e: "cds-notification-beingclosed", value: undefined): void;
// The custom event fired after this notification is closed upon a user gesture.
(e: "cds-notification-closed", value: undefined): void;
// undefined
(e: "undefined", value: CustomEvent): void;
}>();
const slots = defineSlots<{
// The subtitle.
subtitle: (scope: any) => any;
// The title.
title: (scope: any) => any;
}>();
</script>
import {
EventName,
MappedAttribute,
ComponentAttributes,
Options,
} from "./types.js";
import {
RESERVED_WORDS,
createEventName,
} from "./utils.js";
import {
createOutDir,
logBlue,
logYellow,
saveFile,
} from "../../../tools/integrations/index.js";
import {
CEM,
Component,
getComponents,
} from "../../../tools/cem-utils/index.js";
import type { Attribute, ClassField } from "custom-elements-manifest";
import { toCamelCase } from "../../../tools/utilities/index.js";
let config: Options = {};
export function generateVueWrappers(
customElementsManifest: CEM,
options: Options
) {
if (options.skip) {
logYellow("[vue-wrappers] - Skipped", options.hideLogs);
return;
}
logBlue("[vue-wrappers] - Generating wrappers...", options.hideLogs);
updateConfig(options);
const components = getComponents(customElementsManifest, config.exclude);
createOutDir(config.outdir!);
components.forEach((component) => {
const events = getEventNames(component);
const { booleanAttributes, attributes } = getAttributes(component);
const properties = getProperties(component, attributes, booleanAttributes);
generateVueWrapper(component, events);
generateTypeDefinition(
component,
booleanAttributes,
attributes,
properties
);
});
generateManifests(components, config.outdir!);
logBlue(
`[vue-wrappers] - Generated wrappers in "${config.outdir}".`,
config.hideLogs
);
}
function updateConfig(options: Options) {
config = {
outdir: "./vue",
exclude: [],
typesSrc: "types",
attributeMapping: {},
...options,
};
}
function generateVueWrapper(component: Component, events: EventName[]) {
const result = getVueComponentTemplate(component, events);
createOutDir(
`${config.outdir!}/${(component.name || "noComponentNameProvided").replace(
"CDS",
"Cv"
)}`
);
saveFile(
`${config.outdir!}/${(component.name || "noComponentNameProvided").replace(
"CDS",
"Cv"
)}`,
`${(component.name || "noComponentNameProvided").replace("CDS", "Cv")}.vue`,
result,
"vue"
);
}
function generateTypeDefinition(
component: Component,
booleanAttributes: Attribute[],
attributes: Attribute[],
properties?: ClassField[]
) {
const result = getTypeDefinitionTemplate(
component,
booleanAttributes,
attributes,
properties
);
saveFile(
`${config.outdir!}/${(component.name || "noComponentNameProvided").replace(
"CDS",
"Cv"
)}`,
`${(component.name || "noComponentNameProvided").replace(
"CDS",
"Cv"
)}.ts`,
result,
"typescript"
);
}
function generateManifests(components: Component[], outdir: string) {
saveFile(
outdir,
"index.js",
getManifestContentTemplate(components),
"typescript"
);
saveFile(
outdir,
"index.ts",
getManifestContentTemplate(components),
"typescript"
);
}
function getProperties(
component: Component,
attributes: MappedAttribute[],
booleanAttributes: MappedAttribute[]
) {
const attributeFieldNames = attributes.map((attr) => attr.fieldName);
return component?.members?.filter(
(member) =>
member.kind === "field" &&
!member.static &&
member.privacy !== "private" &&
member.privacy !== "protected" &&
!attributeFieldNames.includes(member.name) &&
(member.description || member.deprecated) &&
!booleanAttributes.find((x) => x.propName === member.name) &&
!attributes.find((x) => x.propName === member.name)
) as ClassField[];
}
function getEventNames(component: Component): EventName[] {
return (
component?.events?.map((event) => {
return {
name: event.name,
vueName: createEventName(event || "NNNNMissingNameNNNN"),
description: event.description,
type: event.type?.text,
};
}) || []
);
}
function getAttributes(component: Component): ComponentAttributes {
const result: {
attributes: MappedAttribute[];
booleanAttributes: MappedAttribute[];
} = {
attributes: [],
booleanAttributes: [],
};
component?.attributes?.forEach((attr) => {
if (!attr?.name) {
return;
}
/** Handle reserved keyword attributes */
if (RESERVED_WORDS.includes(attr?.name)) {
/** If we have a user-specified mapping, rename */
if (attr.name in config.attributeMapping!) {
const attribute = getMappedAttribute(attr);
addAttribute(attribute, result);
return;
}
throwKeywordException(attr, component);
}
addAttribute(attr as MappedAttribute, result);
});
return result;
}
function throwKeywordException(attr: Attribute, component: Component) {
throw new Error(
`Attribute \`${attr.name}\` in custom element \`${component.name}\` is a reserved keyword and cannot be used. Please provide an \`attributeMapping\` in the plugin options to rename the JavaScript variable that gets passed to the attribute.`
);
}
function addAttribute(
attribute: MappedAttribute,
componentAttributes: ComponentAttributes
) {
const existingAttr = componentAttributes.attributes.find(
(x) => x.name === attribute.name
);
const existingBool = componentAttributes.booleanAttributes.find(
(x) => x.name === attribute.name
);
if (existingAttr || existingBool) {
return;
}
attribute.propName = toCamelCase(attribute.name);
if (attribute?.type?.text.includes("boolean")) {
componentAttributes.booleanAttributes.push(attribute);
} else {
componentAttributes.attributes.push(attribute);
}
}
function getMappedAttribute(attr: Attribute): MappedAttribute {
return {
...attr,
originalName: attr.name,
name: config.attributeMapping![attr.name],
};
}
function getVueComponentTemplate(component: Component, events: EventName[]) {
const componentName = (component.name || "noComponentNameProvided").replace(
"CDS",
"Cv"
);
const componentPropsName = `${componentName}Props`;
const componentTagName = component.tagName || "no-tagname-provided";
const componentSlots = component.slots ?? [];
return `
<template>
<${componentTagName} class="${componentTagName.replace('cds', 'cv')}" v-bind="props">
${componentSlots
.map(
(slot) => `<template v-slot:${slot.name}>
<slot name="${slot.name}" />
</template>\n`
)
.join("")}
<slot></slot>
</${componentTagName}>
</template>
<script setup lang="ts">
import '@carbon/web-components/es/${
// @ts-expect-error, it thinks path not exists but it does
component.path.replace("src/", "").replace(".ts", "")
}';
import type { ${componentPropsName} } from './${componentName}.ts';
const props = defineProps<${componentPropsName}>()
${
events.length
? `const emits = defineEmits<{
${events
.map(
(event) =>
`// ${event.description}\n(e: '${event.name}', value: ${event.type}): void`
)
.join(",\n")}
}>()`
: ""
}
${
componentSlots.length
? `const slots = defineSlots<{
${componentSlots
.map(
(slot) =>
`// ${slot.description}\n'${slot.name}': (scope: any) => any`
)
.join(",\n")}
}>()`
: ""
}
</script>
`;
}
function getTypeDefinitionTemplate(
component: Component,
booleanAttributes: Attribute[],
attributes: Attribute[],
properties?: ClassField[]
) {
const props = getPropsInterface(
booleanAttributes,
attributes,
properties
);
return `
export type ${(component.name || "noComponentNameProvided").replace(
"CDS",
"Cv"
)}Props = HTMLElement${props.length ? ` & {\n${props}\n}` : ''}
`;
}
function getPropsInterface(
booleanAttributes: MappedAttribute[],
attributes: MappedAttribute[],
properties?: ClassField[]
) {
return [
...getBooleanPropsTemplate(booleanAttributes),
...getAttributePropsTemplate(attributes),
...getPropertyPropsTemplate(properties)
]?.join("");
}
function getBooleanPropsTemplate(booleanAttributes: MappedAttribute[]) {
return (
booleanAttributes?.map(
(attr) => `
/** ${attr.description} */
${attr?.propName}?: ${attr?.type?.text || "boolean"};
`
) || []
);
}
function getAttributePropsTemplate(
attributes: MappedAttribute[]
) {
return (
(attributes || []).map(
(attr) => `
/** ${attr.description} */
${attr.propName}?: ${attr.type?.text || "string"};
`
)
);
}
function getPropertyPropsTemplate(
properties: ClassField[] | undefined
) {
return (
[...(properties || []), ...(config.globalProps || [])]?.map(
(prop) => {
if (prop.type?.text && !['boolean', 'number', 'undefined'].includes(prop.type.text)) {
console.log(prop.type?.text)
}
return `
/** ${prop.description} */
${prop.name}?: ${prop.type?.text || "string"};
`}
)
);
}
function getManifestContentTemplate(components: Component[]) {
const exports = components
.map(
(component) => {
const componentName = (component.name || "noComponentNameProvided").replace("CDS", "Cv");
return `export * from './${componentName}/${componentName}.ts';`
}
)
.join("");
return exports;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment