The following is a quick intro to Malty components: their basic structure, and functionality.
The file structure:
<component_name>
├──<component_name>.stories.tsx /* Storybook Stories */
├──<component_name>.styled.ts /* Styled-Components */
├──<component_name>.test.tsx /* Jest testing */
├──<component_name>.tsx /* Component code */
├──<component_name>.types.ts /* TS Type definitions */
├──<component_name>.helper.ts /* You can isolate some functions */
├──package.json /* Package Config file */
├──tsconfig.json /* TypeScript Config file */
└──index.ts /* Entry point, exports */
<component_name>.test.tsx.snap
The file, as well as its folder, are auto-generated by Jest, further details on it here. For more details on the specifics and purpose of this snapshot comparison file, here.
<component_name>.stories.tsx
Storybook Stories are our main focus for documenting components, and therefore should be the focus for development docs. If you're not familiar with Storybook, please familiarize yourself here. To learn more about Storybook Stories, and the huge range of options available, please read more here.
<component_name>.styled.ts
This is where Styled-Components styling happens. If you don't feel you know enough about Styled-Components, they have excellent, succinct docs here.
Again, much like Storybook Stories, there's a huge range of options in how to write Styled-Components, and maintaining a degree of consistency within Malty is critical. Please try to stick to the following logic:
import styled, { css } from 'styled-components';
export const StyledCheckboxLabelText = styled.span`
color: ${({ theme }) => theme.color.default.value};
margin-left: ${({ theme }) => theme.variables.checkbox.label.leftPadding.value}px;
font-size: ${({ theme }) => theme.typography.text.medium['font-size'].value}px;
`;
In the above example, please pay close attention to the theme object. This is how the theme parameters will be passed to the styles, and how to consume them. For more details on the ThemeProvider, please read here.
On how to access component properties, please consider the following example:
export const StyledCheckboxDisplayInput = styled.div<{ checked?: boolean }>`
width: ${({ theme }) => theme.variables.checkbox.size.value}px;
${({ checked }) =>
checked &&
css`
background-color: ${({ theme }) => theme.color.default.value};
&:before {
border-color: ${({ theme }) => theme.color.white.value};
}
`}
`;
<component_name>.test.tsx
Unit testing takes place here. Integrated into the build pipeline, and results available in the Bit.dev platform, testing doesn't always have to be exhaustive, but they should cover all the basic events and states. An example is the following:
import { fireEvent, jsonRenderer, render, screen } from '@carlsberggroup/malty.utils.test';
import React from 'react';
import { Checkbox } from './Checkbox';
describe('checkbox', () => {
it('matches snapshot', () => {
const view = jsonRenderer(<Checkbox labelText="Label" onValueChange={fn()} />);
expect(view).toMatchSnapshot();
});
it('renders elements', () => {
const mockFn = jest.fn();
render(<Checkbox error="Error" value="Value" onValueChange={mockFn} />);
expect(screen.getByText('Error')).toBeInTheDocument();
expect(screen.getByDisplayValue('Value')).toBeInTheDocument();
});
it('calls function on click', () => {
const mockFn = jest.fn();
render(<Checkbox error="Error" value="Value" onValueChange={mockFn} />);
const checkbox = screen.getByDisplayValue('Test value');
fireEvent.click(checkbox);
expect(mockFn).toHaveBeenCalledTimes(1);
});
});
As you can see above (@carlsberggroup/malty.utils.test), we've wrapped some of the testing tools in a component, in Malty. This was done to allow us to wrap components in the MaltyThemeProvider, through a custom rendering function. Feel free to look at it here — as it's not documented in Malty.
For more information on Jest, here. For more information on React Testing Library, here.
<component_name>.tsx
This is where the component JS and markup lives. As you can see on the example below, thanks to a very versatile Styled-Components CSS styling, we can move a lot of the logic there:
import { globalTheme as defaultTheme, TypographyProvider } from '@carlsberggroup/malty.theme.malty-theme-provider';
import React, { useContext } from 'react';
import { ThemeContext } from 'styled-components';
import { StyledParagraph } from './Text.styled';
import { TextProps } from './Text.types';
export const Text = ({
align,
color,
children,
underline }: TextProps) => {
const theme = useContext(ThemeContext) || defaultTheme;
return (
<TypographyProvider>
<StyledParagraph
align={align}
underline={underline}
color={color}
theme={theme}
>
{children}
</StyledParagraph>
</TypographyProvider>
);
};
In practical terms what that means is that a chunk of the logic that would otherwise end up in markup logic, can go directly into our styling, as shown above (<component_name>.styled.ts).
For details on the TypographyProvider element, above, please look here.
<component_name>.types.ts
This is where all types, enums, and definitions go. The idea is that all options, states, and properties should be typed.
Todo: Eventually the goal for Malty is to abstract many of the types being used, up to a component that provides all common types for all components.
index.ts
This file is the entry point for exporting all the necessary elements for the component to be utilized externally. Not all elements must be exported, but consider any components, Styled-Components, types, and helper functions.
A critical note on relative paths
Components must be self-contained, and any paths outside the component folder (/malty/atoms/<component>), must be absolute paths (@carlsberg/malty.atoms.icons) — we can have relative paths (../<component>.types.ts) within components, for types, styles, etc.
A good example of this structure is @carlsberg/malty.atoms.checkbox