Created
June 14, 2024 11:45
-
-
Save benceszenassy/7c06de0043e2dfe937008477489d24eb to your computer and use it in GitHub Desktop.
custom-element-vue-wrapper for IBM carbon thinkering
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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