Reutilizar componente base o una de sus variantes en un template
Se explica el flujo para crear un componente para el contexto de un proyecto o producto particular, reutilizando como punto de partida un componente base ya existente.
Prerequisitos
Acciones previas antes de implementar un desarrollo nuevo en la librería
Clonar el repositorio de Inlaze-ui-react perteneciente a la organización de github sport-enlace-sas
Instalar dependencias con npm
Ejecutar el comando npm run libs:storybook para ir visualizando y documentando el componente a agregar.
Que ya exista un componente base creado y sus variantes en caso de necesitarlas.
Casos de uso
Agregar a un template ya existente un componente base
Crear un nuevo template que reutilice los componentes base
Paso a paso para el ciclo de vida completa en la adición de un componente nuevo
Paso 1: Agregar un template nuevo o un componente a un template ya existente
Determinar si existe el template, de no existir crear una carpeta nueva en libs/src/lib/templates con el nombre del proyecto o producto nuevo en UpperCamelCase
Determinar el {componentType} si el componente es UIkit o CompoundComponents, y validar si su correspondiente componente base existe en common/UiKit o en common/CompundComponents según sea el caso. De no existir es necesario primero desarrollar el componente base como se indica en agregar un componente base.
Ejecutar comando Si el componente del template se origina de un componente base (y no una de sus variantes) ejecutar un el comando del CLI de NX: npx nx g @nx/react:component {fileLocation}, el fileLocation pude variar en 2 casos:
Si el componente va a tener variantes, entonces fileLocation =
libs/src/lib/templates/{ProductName}/{ComponentType}/{PluralComponentName}/{SingularComponentName}
Si el componente no va a tener variantes, entonces fileLocation =
libs/src/lib/templates/{ProductName}/{ComponentType}/{SingularComponentName}
Una vez creado el componente se crea en la historia para que se pueda visualizar en el storybook ejecutando el comando:
npm run libs:stories
Step 2: Desarrollar el componente del template
Todo componente del template no puede crearse desde cero, sino tener como punto de partida su correspondiente componente base, luego todo componente base.
Para cuando un children es cerrado se debe poder igualmente permitir personalizar los estilos de la etiqueta contenedora por medio la prop customCssModule que es común a todo compoente base, y esa prop sobreescribe el cssMdoule del componete base por el enviado por el cliente.
export type ButtonBaseProps = BaseComponentProps<
ButtonProps,
CssModuleButtonBase,
ButtonHTMLAttributes<HTMLButtonElement>
>;
export function ButtonBase({
children,
className,
customCssModule,
...props
}: Readonly<ButtonBaseProps>): JSX.Element {
const MIXED_CSS_MODULE = mixCssModules<CssModuleButtonBase>(cssModuleButtonBase, customCssModule);
const DYNAMIC_CLASSNAME = dynamicClassName([
[Boolean(props.disabled), MIXED_CSS_MODULE["button--disabled"]],
MIXED_CSS_MODULE[`button--${props.variant}`],
MIXED_CSS_MODULE.button,
className,
]);
return (
<button className={DYNAMIC_CLASSNAME} {...props}>
{children}
</button>
);
}
export default ButtonBase;
export type InlazeButtonProps = ButtonBaseProps;
import InlazeStyles from "./InlazeButton.module.css"
export function InlazeButtonBase(props: Readonly<Omit<InlazeButtonProps, "customCssModule">>): JSX.Element {
return <CommonButtonBase {...props} customCssModule={InlazeStyles} />;
}
export default InlazeButtonBase;
Para cuando un children es abierto lo que se espera del componente template comparta las props de su componente base menos el children, ya que este debe tener una estructura más definida de acuerdo a las características del producto o proyecto propio del template.
En el siguiente ejemplo, el carrusel de children abierto permite que desde la definición del children se tenga acceso a todas las variables que este necesite para su funcionamiento.
export type CarouselBaseProps = BaseComponentProps<CarouselPropsBase, undefined>;
export function CarouselBase({
implementation,
pages,
onPageChange,
animationTime,
}: CarouselBaseProps): React.ReactNode {
const carousel = useCarousel(pages, animationTime, onPageChange);
return implementation(carousel);
}
export default CarouselBase;
Y al reutilizarlo en el template ese children abierto correspondiente a la prop implementation se define en el template y el cliente ya no puede definir un chidren, sino que ya hay uno con unos estilos determinados.
export function InlazeCarousel(carouselProps: InlazeCarouselProps): JSX.Element {
const { children, onPageChange, animationTime } = carouselProps;
const pages = Children.count(children);
const carouselRef = useRef<HTMLUListElement>(null);
return (
<CarouselBase
pages={pages}
onPageChange={onPageChange}
animationTime={animationTime}
implementation={(carousel) => {
const { carouselClass, wrapperClass, pageClass, buttonsClass, buttonClass } =
createCarouselClass(carousel.carouselState, carouselProps);
const handleButtonClick = (value: number): void => {
const state = { ...carousel.carouselState, currentPage: value };
carousel.handleSetState(state);
};
const translateValue = (): number => {
const carouselElement = carouselRef.current;
if (carouselElement) {
return carouselElement.clientWidth * carousel.carouselState.currentPage;
}
return 0;
};
return (
<section
role="slider"
aria-valuenow={carousel.carouselState.currentPage}
className={carouselClass}
onMouseEnter={() => carousel.handleHover(true)}
onMouseLeave={() => carousel.handleHover(false)}
onPointerDown={(e) => carousel.handlePointer(e, POINTER_START)}
onPointerUp={(e) => carousel.handlePointer(e, POINTER_END)}
onKeyDown={carousel.handleKeyboard}
draggable={true}
tabIndex={0}
>
<ul
ref={carouselRef}
aria-live="polite"
aria-atomic="true"
className={wrapperClass}
style={{ translate: `-${translateValue()}px` }}
>
{Children.toArray(children).map((child, index) => (
<li
key={`Page_${index + 1}`}
aria-label={`Page_${index + 1}`}
data-active-page={index === carousel.carouselState.currentPage}
className={pageClass}
>
{child}
</li>
))}
</ul>
<div className={buttonsClass}>
{Array.from({ length: pages }, (_, index) => (
<InlazeButtonNormal
key={`button_${index + 1}`}
className={buttonClass}
data-active={index === carousel.carouselState.currentPage}
aria-label={`Navigate to page ${index + 1}`}
onClick={() => handleButtonClick(index)}
/>
))}
</div>
</section>
);
}}
/>
);
}
export default InlazeCarousel;
Sin embargo el CarruselBase e InlazeCarousel tiene props comunes menos children
export interface CarouselPropsBase {
implementation: (carousel: CarouselHook, cssModule?: Record<string, string>) => ReactNode;
onPageChange?: (page: number) => void;
pages: number;
animationTime?: number;
}
export type CarouselBaseProps = BaseComponentProps<CarouselPropsBase, undefined>;
export interface InlazeCarouselPropsBase {
buttonsConfig?: ButtonsConfig;
slideDirection?: Orientations;
animationTime?: number;
onPageChange?: (page: number) => void;
}
export type InlazeCarouselProps = BaseComponentProps<InlazeCarouselPropsBase, CarouselStyle>;
Paso 3: Desarrollo de tests unitarios
Dado que esta librería tiene como propósito ser usado en los múltiples proyectos de la organización, especialmente los componentes base deben ser testeados dado que al momento de hacerles mantenimiento se debe garantizar que las nuevas features no afecten el desarrollo existente o de hacerlo sea intencionalmente y se reporte en el breaking change en la version a publicar.
Entonces los test unitarios y no las pruebas manuales del desarrollador deben ser las que identifiquen el impacto quepuede llegar a tener una actualización en todos los clientes que lo consumen.
Como factor a tener en cuenta en la librería es importante que los tests prueben que:
Si el children es cerrado se debe poder probar que el cliente tiene acceso a los atributos nativos HTML de la etiqueta contenedora.
Se debe poder probar el desacoplamiento entre el html y las funcionalidades del componente, es decir que el cliente puede definir cualquier HTML como children y ese children puede acceder a las funcionaldiades que el componente necesita para cumplir su responsabildiad.
Probar el comportamiento de las funciones específicas propias del componente como hooks y utilidades.
Paso 4: Validar que el storybook halla documentado claramente como un cliente puede usar el componente.
El storybook debe tener un control por cada una de las props y el ejemplo de como el componente se ve afectado al modificar interactivamente esos controles.
Entorno a como configurar los controles del storybook véase: Storybook argtypes
Conclusión
Una vez completada la fase de desarrollo, testing y documentación de storybook, se puede ver en la siguiente página como probar en local la librería con la actualización y como publicar la nueva versión una vez probado el feature nuevo o mantenido.
Last updated