Skip to content

Commit

Permalink
Merge pull request #22 from receter/default-markup-form-field
Browse files Browse the repository at this point in the history
Improved types and new coding style + rendering complex components
  • Loading branch information
receter authored Dec 5, 2024
2 parents 1cd2014 + 01e463a commit 6c514bb
Show file tree
Hide file tree
Showing 68 changed files with 1,443 additions and 890 deletions.
92 changes: 31 additions & 61 deletions .vscode/global.code-snippets
Original file line number Diff line number Diff line change
Expand Up @@ -15,86 +15,56 @@
// ],
// "description": "Log output to console"
// }
"sys42: create new unstyled component": {
"scope": "typescriptreact,typescript",
"prefix": "sys42:unstyledComponent",
"body": [
"import { concatClassNames as cn } from '@sys42/utils'",
"import { Sys42Component, Sys42UnstyledComponent } from '../../types';",
"",
"export type ${1:${TM_DIRECTORY/^.+[\\/\\\\]+(.*)$/${1:/pascalcase}/}}Props = Sys42Component;",
"",
"type Unstyled$1Props = Sys42UnstyledComponent<$1Props, {",
" ${2:${TM_DIRECTORY/^.+[\\/\\\\]+(.*)$/${1:/camelcase}/}}: string,",
"}>;",
"",
"export function $1(props: Unstyled$1Props) {",
" const {",
" className,",
" styles,",
" ...restProps",
" } = props;",
"",
" return (",
" <${3:div} {...restProps} className={cn(styles.$2, className)} />",
" );",
"}",
],
"description": "Create new component",
},
"sys42: create new useBase hook": {
"sys42: create new useBaseComponent hook": {
"scope": "typescriptreact,typescript",
"prefix": "sys42:useBaseHook",
"body": [
"import React, { HTMLAttributes, useRef } from 'react';",
"import { mergeRefs } from 'react-merge-refs';",
"",
"// Define specific props for the component",
"interface ${1:${TM_DIRECTORY/^.+[\\/\\\\]+(.*)$/${1:/pascalcase}/}}Props {}",
"// If no props are needed, a interface with an empty object can be used",
"export interface Base${1:${TM_DIRECTORY/^.+[\\/\\\\]+(.*)$/${1:/pascalcase}/}}Props {}",
"",
"export type Base${1}Props<ElemProps> = Sys42Props<${1}Props, ElemProps>;",
"export function useBase${1}<TTagName extends HTMLElementTagName>(",
" { props, forwardedRef }: UseComponentOptions<Base${1}Props, TTagName>,",
" interceptor?: UseComponentInterceptor<TTagName>,",
") {",
" const { ...restProps } = props;",
"",
"export type UseBase${1}Options<Props, Elem extends HTMLElement> = {",
" props: Props;",
" elementType: keyof JSX.IntrinsicElements;",
" forwardedRef: React.ForwardedRef<Elem>;",
"};",
" const draft = {",
" elementProps:",
" restProps satisfies EmptyObject as React.ComponentPropsWithoutRef<TTagName>,",
" };",
"",
"export function useBase${1}<",
" Props extends Base${1}Props<HTMLAttributes<HTMLElement>>,",
" Elem extends HTMLElement",
">({ props, elementType, forwardedRef }: UseBase${1}Options<Props, Elem>) {",
" const ref = useRef<Elem>(null);",
" interceptor?.(draft);",
"",
" return {",
" ${2:${TM_DIRECTORY/^.+[\\/\\\\]+(.*)$/${1:/camelcase}/}}Props: {",
" ...props,",
" },",
" ${2}Ref: mergeRefs([forwardedRef, ref]),",
" elementProps: draft.elementProps,",
" elementRef: forwardedRef,",
" };",
"}",
],
"description": "Create new useBase… hook",
"description": "Create new useBaseComponent hook",
},
"sys42: create new styled component": {
"sys42: create new useComponent hook": {
"scope": "typescriptreact,typescript",
"prefix": "sys42:styledComponent",
"prefix": "sys42:useComponentHook",
"body": [
"import { ${1:${TM_DIRECTORY/^.+[\\/\\\\]+(.*)$/${1:/pascalcase}/}} as Unstyled$1, $1Props } from '../../unstyled/$1'",
"import { cn } from \"@sys42/utils\";",
"",
"import styles from './styles.module.css'",
"import { Base${1:${TM_DIRECTORY/^.+[\\/\\\\]+(.*)$/${1:/pascalcase}/}}Props, useBase${1} } from \"./useBase${1}\";",
"",
"const ${2:${TM_DIRECTORY/^.+[\\/\\\\]+(.*)$/${1:/camelcase}/}}Styles = {",
" $2: styles.$2,",
"}",
"import styles from \"./styles.module.css\";",
"",
"export type ${1}Props = Base${1}Props;",
"",
"export function $1(props: $1Props) {",
" return <Unstyled$1",
" {...props}",
" styles={$2Styles}",
" />;",
"export function use${1}<TTagName extends HTMLElementTagName>(options: UseComponentOptions<${1}Props, TTagName>) {",
" return useBase${1}(options, (draft) => {",
" draft.elementProps.className = cn(",
" draft.elementProps.className,",
" styles.${2:${TM_DIRECTORY/^.+[\\/\\\\]+(.*)$/${1:/camelcase}/}},",
" );",
" });",
"}",
],
"description": "Create new component",
"description": "Create new useComponent hook",
},
}
1 change: 1 addition & 0 deletions packages/example-consumer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
},
"dependencies": {
"@sys42/ui": "workspace:^",
"@sys42/utils": "workspace:^",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
Expand Down
180 changes: 100 additions & 80 deletions packages/ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,101 +51,123 @@ function App() {

For all components, there are hooks available that can be used to change how the component is rendered. The hooks are named the same as the components but with a `use` prefix. The main use case for the hooks is to change the element type of the component.

For example you can use the `useButton` hook and spread the returned `buttonProps` on a `Link` component.
#### Simple Components

For simple components that render a single element, like a button, simply spread the returned `elementProps` and attach the `elementRef` to the element.

```jsx
import { Link } from "react-router-dom";
import { useButton } from "@sys42/ui";

export const ButtonLink = forwardRef<
HTMLAnchorElement,
ButtonProps<React.ComponentProps<Link>>
>((props, forwardedRef) => {
const { buttonProps, buttonRef } = useButton({
props,
elementType: "a",
forwardedRef,
});

return <Link {...buttonProps} ref={buttonRef} />;
import { createComponent, useButton } from "@sys42/ui";

export const ButtonLink = createComponent<ButtonProps, "a">("a", (hookOptions) => {
const { elementProps, elementRef } = useButton(hookOptions);
return <Link {...elementProps} ref={elementRef} />;
});
```

### Using the Base Hooks
#### Complex Components

In addition to the React components and [Component Hooks](#using-the-component-hooks), there are Base Hooks that can be used in case you want to opt out of the default styling.
For more complex components that render multiple elements, you'll need to invoke an additional render function to handle the children. The hook will return a `renderArgs` object, which must be passed to the render function. You can either use the default render function provided for each component or write your own.

Base Hooks do not import any CSS and have no style-related props, such as the `variant` prop for styled buttons.

The hooks are named the same as the components but with a `useBase…` prefix. The hooks return everything you need to render the component. You can refer to implementation of the Component Hooks like `useButton` to for examples on how to use the Base Hooks.
```jsx
import {
FormFieldProps,
createComponent,
useFormField,
renderFormField,
} from "@sys42/ui";

export const MyFormField = createComponent<FormFieldProps, "div">(
"div",
(hookOptions) => {
const { elementProps, elementRef, renderArgs } = useFormField(hookOptions);

return (
<div {...elementProps} ref={elementRef}>
{renderFormField(renderArgs)}
</div>
);
}
);
```

#### Simple Usage
### Using the Base Hooks

This is the most basic way to use a Base Hook. It will render the default internal markup and only change the element type and/or modify the attributes of the wrapper element.
"In addition to React components and [Component Hooks](#using-the-component-hooks), the library provides Base Hooks for cases where you want to opt out of default styling.

```jsx
import { useBaseFoobar } from "@sys42/ui";

export const MyFoobar = forwardRef<
HTMLDivElement,
ButtonProps<React.ComponentProps<"div">>
>((props, forwardedRef) => {
const { foobarProps, foobarRef } = useBaseFoobar({
props,
elementType: "div",
forwardedRef,
});

// If you want to attach a CSS class to the component
// you can do this by simply mutating the className
foobarProps.className = "btn btn-blue";

return <div {...foobarProps} ref={foobarRef} />;
};
```
Base Hooks don’t include any CSS and omit style-related props, such as the `variant` prop in styled buttons. These hooks are prefixed with `useBase…` and return everything needed to render the component.

#### Advanced Usage
For guidance on using Base Hooks, you can refer to the implementation of the corresponding Component Hook like `useButton`.

Some components that render more complex markup might give you advanced control over the rendered markup by returning additional props that allow you to customize the rendered markup. In this cases you can choose to either use the returned props or ignore them in favor of the default children.
Here’s a simple example of using a Base Hook to create a custom styled component:

```jsx
import { useBaseFoobar } from "@sys42/ui";

export const MyComplexThing = forwardRef<
HTMLDivElement,
ButtonProps<React.ComponentProps<"div">>
>((props, forwardedRef) => {
const {
complexThingProps,
complexThingRef,
internalThingProps,
internalThingRef,
readTheDocs
} = useBaseComplexThing({
props,
elementType: "div",
forwardedRef,
});

// To render the default markup you can simply spread the props
// which will contain children with the default markup

// return <div {...complexThingProps} ref={complexThingRef} />;

// If you want to render custom markup you can do this by
// recreating the component with the help of the returned variables.
// It might be a good idea to read the documentation of the component
// to understand the requirements of the custom markup.
import {
BaseFormFieldProps,
createComponent,
useBaseFormField,
renderFormField,
ExactProps,
} from "@sys42/ui";

import { cn } from "@sys42/utils";

type MyFormFieldProps = BaseFormFieldProps & {
myProp: boolean;
};

return (
<div {...complexThingProps} ref={complexThingRef}>
<nav {...internalThingProps} ref={internalThingRef} />
{readTheDocs}
</div>
function useMyFormField<TTagName extends HTMLElementTagName>(
options: UseComponentOptions<MyFormFieldProps, TTagName>
) {
// The custom prop "myProp" is extracted from the props
const { myProp, ...baseProps } = options.props;

return useBaseFormField(
{
...options,
props: baseProps satisfies ExactProps<
BaseFormFieldProps,
MyFormFieldProps
>,
},
(draft) => {
// You can modify the formField here
draft.elementProps.className = cn(
draft.elementProps.className,
"my-form-field"
);

if (myProp) {
draft.elementProps.className += " my-form-field--my-prop";
}

draft.labelProps.className = cn(
draft.labelProps.className,
"my-form-field-label"
);
}
);
};
}

export const MyFormField = createComponent<MyFormFieldProps, "div">(
"div",
(hookOptions) => {
const { elementProps, elementRef, renderArgs } =
useMyFormField(hookOptions);

return (
<div {...elementProps} ref={elementRef}>
{renderFormField(renderArgs)}
</div>
);
}
);
```

## Types

Read more about the types in the [Type Strategy](./TypeStrategy.md) document.

## Custom Properties

You can find all available custom properties here: [default-custom-properties.css](./lib/default-custom-properties.css)
Expand All @@ -162,12 +184,10 @@ If you want to override styles for a specific occurence of a component, you can

## Styling opinions

System 42 is designed to be a flexible design system that can be customized to fit your needs.
There are some opinionated decisions that are made in the design system:
System 42 is built as a flexible design system, allowing you to customize it to suit your needs. However, it includes a few opinionated design choices:

**Margin Top**

Whenever `margin` is used to create space between elements, `margin-top` is preferred. The the CSS reset (which is base on `normalize.css`) is extended and removes `margin-top` for some elements.
Whenever `margin` is used to create space between elements, `margin-top` is preferred. The the CSS reset (which is base on `normalize.css`) is extended and removes `margin` for some elements.

For more information see this [article](https://dev.to/receter/why-i-fell-in-love-with-margin-top-3flg).
Loading

0 comments on commit 6c514bb

Please sign in to comment.