diff --git a/.storybook/main.ts b/.storybook/main.ts index f89b655e..a8fb1705 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -3,8 +3,8 @@ import type {StorybookConfig} from '@storybook/react-webpack5'; const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin'); const config: StorybookConfig = { - stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'], - addons: ['@storybook/addon-essentials', '@storybook/preset-scss'], + stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'], + addons: ['@storybook/addon-essentials', '@storybook/preset-scss', '@storybook/addon-docs'], framework: { name: '@storybook/react-webpack5', options: {fastRefresh: true}, diff --git a/src/stories/DocsConfig.mdx b/src/stories/DocsConfig.mdx new file mode 100644 index 00000000..d2dca686 --- /dev/null +++ b/src/stories/DocsConfig.mdx @@ -0,0 +1,21 @@ +import {Meta, Markdown} from '@storybook/addon-docs'; + +import Config from '../../docs/config.md?raw'; + + + +export const replacements = [ + ['../src', 'https://github.com/gravity-ui/dynamic-forms/blob/main/src/'], + ['./lib.md', '?path=/docs/docs-lib--docs'], + ['./spec.md', '?path=/docs/docs-spec--docs'], +]; + +export const applyReplacements = (text, replacements) => { + return replacements.reduce((result, [searchValue, replaceValue]) => { + return result.split(searchValue).join(replaceValue); + }, text); +}; + +export const content = applyReplacements(Config, replacements); + +{content} diff --git a/src/stories/DocsCustomInput.mdx b/src/stories/DocsCustomInput.mdx new file mode 100644 index 00000000..17d89e7f --- /dev/null +++ b/src/stories/DocsCustomInput.mdx @@ -0,0 +1,157 @@ +import {Meta} from '@storybook/addon-docs'; + + + +# Custom Input in Dynamic Forms + +The `@gravity-ui/dynamic-forms` library provides a standard set of inputs, but sometimes you may need to add your own custom input. In this guide, we'll walk through how to create and integrate a custom text input into a dynamic forms. + +## Steps to Add a Custom Input + +1. [Create Your Own Input](#1-create-your-own-input) +2. [Create a View for the Input](#2-create-a-view-for-the-input) +3. [Integrate the Input into the Config](#3-integrate-the-input-into-the-config) +4. [Use the Custom Input](#4-use-the-custom-input) + +--- + +### 1. Create Your Own Input + +First, we need to create our own input component. Inputs in `@gravity-ui/dynamic-forms` come in the following types: + + - `array` + - `string` + - `object` + - `number` + - `boolean` + +We'll be creating a string type input, intended for text data entry. + + ```tsx + import React from 'react'; + import { isNil } from 'lodash'; + + import type { FieldRenderProps, StringInput } from '@gravity-ui/dynamic-forms'; + import type { TextInputProps as TextInputBaseProps } from '@gravity-ui/uikit'; + import { TextInput } from '@gravity-ui/uikit'; + + export interface TextProps + extends Omit< + TextInputBaseProps, + 'value' | 'onBlur' | 'onFocus' | 'onUpdate' | 'disabled' | 'placeholder' | 'qa' + > {} + + export const CustomTextInput: StringInput = ({ + name, + input: { value, onBlur, onChange, onFocus }, + spec, + inputProps, + }) => { + const props = { + hasClear: true, + ...inputProps, + value: isNil(value) ? '' : ${value}, // Set default value if value is null or undefined + onBlur, + onFocus, + onUpdate: onChange as FieldRenderProps['input']['onChange'], + disabled: spec.viewSpec.disabled, + placeholder: spec.viewSpec.placeholder, + qa: name, + }; + + return ; + }; + ``` + +### 2. Create a View for the Input + +To display the input's value in view mode, we need to create a view component. + + ```tsx + import React from 'react'; + + import type {StringView} from '@gravity-ui/dynamic-forms'; + import {Text} from '@gravity-ui/uikit'; + + export const CustomTextInputView: StringView = ({value}) => { + return {value}; + }; + ``` + +### 3. Integrate the Input into the Config + +Next, we need to integrate our custom input and its view into the dynamic form configurations. + + ```tsx + import _ from 'lodash'; + + import type { DynamicFormConfig, DynamicViewConfig } from '@gravity-ui/dynamic-forms'; + import { + dynamicConfig as libConfig, + dynamicViewConfig as libViewConfig, + } from '@gravity-ui/dynamic-forms'; + + import { CustomTextInput } from './CustomTextInput'; + import { CustomTextInputView } from './CustomTextInputView'; + + const getDynamicConfig = (): DynamicFormConfig => { + const dynamicConfig = _.cloneDeep(libConfig); + + // Register our custom input with a specific name + dynamicConfig.string.inputs['custom_text_input'] = { Component: CustomTextInput }; + + return dynamicConfig; + }; + + export const dynamicConfig = getDynamicConfig(); + + const getDynamicViewConfig = (): DynamicViewConfig => { + const dynamicViewConfig = _.cloneDeep(libViewConfig); + + // Register the view for our custom input + dynamicViewConfig.string.views['custom_text_input'] = { Component: CustomTextInputView }; + + return dynamicViewConfig; + }; + + export const dynamicViewConfig = getDynamicViewConfig(); + ``` + +**Explanations:** + + - We clone the base library configuration using `\_.cloneDeep` to avoid modifying the original settings and prevent potential conflicts. + - In the square brackets, we specify the name of our custom input `'custom_text_input'`. + - If you use a name that matches an existing one in the library, your input will override the standard one. + +### 4. Use the Custom Input + +Now, you can use your custom input in the form specification by setting its type to `'custom_text_input'`. + +#### Example Field Spec: + + ``` json + { + "type": "string", + "viewSpec": { + "type": "custom_text_input", // Specify the input name in 'type' + "layout": "row", + "layoutTitle": "Name", + "placeholder": "Enter your name" + } + } + ``` + +**Explanations:** + + - The field will use our custom input `'custom_text_input'`, which we registered in the configuration. + - layout, layoutTitle, and placeholder are used to configure the field's appearance and behavior. + +## Conclusion + +In this guide, we've explored how to create and integrate a custom text input into a dynamic form using the `@gravity-ui/dynamic-forms` library. Now, you can create custom inputs tailored to your specific requirements. + +**Benefits of Creating Custom Inputs:** + + - Flexibility in displaying and processing data. + - Ability to implement unique logic and styles. + - Enhancing user experience through customization. diff --git a/src/stories/DocsHowUse.mdx b/src/stories/DocsHowUse.mdx new file mode 100644 index 00000000..7e089258 --- /dev/null +++ b/src/stories/DocsHowUse.mdx @@ -0,0 +1,248 @@ +import {Meta, Markdown} from '@storybook/addon-docs'; + +import Lib from '../../docs/lib.md?raw'; + + + +# How to Use Dynamic Forms + +To use the DynamicField component from the `@gravity-ui/dynamic-forms` library, you need to integrate it with a form management library `react-final-form`. + +## Basic Usage + +Here's a basic example of how to use `DynamicField` within a form. + +```tsx +import React from 'react'; +import {Form} from 'react-final-form'; +import {DynamicField, DynamicFormConfig} from '@gravity-ui/dynamic-forms'; + +const MyForm = () => { + const onSubmit = (formValues) => { + console.log(formValues); + }; + + const spec = { + type: 'object', + properties: { + name: { + type: 'string', + viewSpec: { + type: 'base', + layout: 'row', + layoutTitle: 'Name', + }, + }, + age: { + type: 'number', + viewSpec: { + type: 'base', + layout: 'row', + layoutTitle: 'Age', + }, + }, + license: { + type: 'boolean', + viewSpec: { + type: 'base', + layout: 'row', + layoutTitle: 'License', + }, + }, + }, + viewSpec: { + type: 'base', + layout: 'accordion', + layoutTitle: 'Candidate', + layoutOpen: true, + }, + }; + + return ( +
+ {({handleSubmit}) => ( + + + + + )} + + ); +}; + +export default MyForm; +``` + +**Explanation:** + +- We first import the necessary components from `react-final-form` and `@gravity-ui/dynamic-forms`. +- The `spec` object defines the structure of our dynamic form fields. + - It describes an object with properties name, age, and license. + - Each property has a type and a viewSpec that defines how the field should be displayed. +- The `DynamicField` component uses this `spec` to render the form fields dynamically. +- The name prop of DynamicField (`"dynamicfield"`) is the key under which the form values will be stored in the `react-final-form` state. +- When the form is submitted, the onSubmit function will log the form values. + +**Form State:** + +In the `react-final-form` state, the dynamicfield object will be registered. As you fill out the form, the state will look like this: + +```json +{ + "dynamicfield": { + "name": "John Doe", + "age": 30, + "license": true + } +} +``` + +--- + +## Providing Initial Values to DynamicField + +To pass initial values into the `DynamicField`, you can set the `initialValues` prop on the Form component from `react-final-form`. The `DynamicField` will pick up the initial values from the form's state. + +### Example: + +```tsx +import React from 'react'; +import {Form} from 'react-final-form'; +import {DynamicField, DynamicFormConfig} from '@gravity-ui/dynamic-forms'; + +const MyForm = () => { + const onSubmit = (formValues) => { + console.log(formValues); + }; + + const initialValues = { + dynamicfield: { + name: 'John Doe', + age: 30, + license: true, + }, + }; + + const spec = { + // ... (same as before) + }; + + return ( +
+ {({handleSubmit}) => ( + + + + + )} + + ); +}; + +export default MyForm; +``` + +**Explanation:** + +- We define `initialValues` with the initial data we want to populate the form with. +- The key `'dynamicfield'` corresponds to the name prop of `DynamicField`. +- The initialValues object is passed to the Form component. +- When the form renders, the fields will be pre-filled with the initial values provided. + +--- + +## Displaying Filled Values Using DynamicView + +To display the values that have been filled in the form, you should use the `DynamicView` component provided by the `@gravity-ui/dynamic-forms` library. The `DynamicView` component renders the form values based on the same `spec` used to render the form. + +### Example: + +```tsx +import React from 'react'; +import {DynamicView, DynamicViewConfig} from '@gravity-ui/dynamic-forms'; + +const MyView = () => { + const spec = { + type: 'object', + properties: { + name: { + type: 'string', + viewSpec: { + type: 'base', + layout: 'row', + layoutTitle: 'Name', + }, + }, + age: { + type: 'number', + viewSpec: { + type: 'base', + layout: 'row', + layoutTitle: 'Age', + }, + }, + license: { + type: 'boolean', + viewSpec: { + type: 'base', + layout: 'row', + layoutTitle: 'License', + }, + }, + }, + viewSpec: { + type: 'base', + layout: 'accordion', + layoutTitle: 'Candidate', + layoutOpen: true, + }, + }; + + const values = { + name: 'John Doe', + age: 30, + license: true, + }; + + return ; +}; + +export default MyView; +``` + +**Explanation:** + +- We import `DynamicView` and `DynamicViewConfig` from `@gravity-ui/dynamic-forms`. +- The spec object is the same as the one used to render the form fields with `DynamicField`. +- The values object contains the data we want to display. +- The `DynamicView` component takes the values and the spec to render the data appropriately. +- This allows you to display the filled values in a structured and consistent way, matching the layout and styling of your forms. + +## Notes on Using DynamicField with react-final-form + +- The `name` prop of `DynamicField` determines where the form data will be stored within the form state. +- All fields rendered by `DynamicField` will be nested under the key specified by name. +- It's essential to match the shape of `initialValues` with the expected structure of the form data. +- The `spec` object defines the schema of your dynamic form and is crucial for rendering the correct fields. + +--- + +## Notes on Using DynamicView + +- `DynamicView` uses the same spec as `DynamicField`, ensuring consistency between the form and the display of its values. +- You can customize the `viewSpec` within the spec to adjust how the data is displayed. +- The config prop allows you to provide custom configurations for the `DynamicView` component, similar to how you can configure `DynamicField`. + +--- + +## Conclusion + +In this guide, we've discussed: + +- Integrating `DynamicField` with `react-final-form`: Rendering dynamic forms by specifying a schema and managing form state. +- Providing Initial Values: How to set default values for your dynamic form fields using `initialValues`. +- Displaying Form Values with DynamicView: Using DynamicView to display the values filled in the form in a structured and styled manner. + +By following these steps, you can create flexible forms and display their data consistently, enhancing the user experience. + +--- diff --git a/src/stories/DocsInputPropsMap.mdx b/src/stories/DocsInputPropsMap.mdx new file mode 100644 index 00000000..9716ecb6 --- /dev/null +++ b/src/stories/DocsInputPropsMap.mdx @@ -0,0 +1,19 @@ +import {Meta, Markdown} from '@storybook/addon-docs'; + +import InputPropsMap from '../../docs/input-props-map.md?raw'; + + + +export const replacements = [ + ['../src', 'https://github.com/gravity-ui/dynamic-forms/blob/main/src/'], +]; + +export const applyReplacements = (text, replacements) => { + return replacements.reduce((result, [searchValue, replaceValue]) => { + return result.split(searchValue).join(replaceValue); + }, text); +}; + +export const content = applyReplacements(InputPropsMap, replacements); + +{content} diff --git a/src/stories/DocsLib.mdx b/src/stories/DocsLib.mdx new file mode 100644 index 00000000..4d7766ad --- /dev/null +++ b/src/stories/DocsLib.mdx @@ -0,0 +1,25 @@ +import {Meta, Markdown} from '@storybook/addon-docs'; + +import Lib from '../../docs/lib.md?raw'; + + + +export const replacements = [ + [ + '../src/lib/kit/constants/config.tsx', + 'https://github.com/gravity-ui/dynamic-forms/blob/main/src/lib/kit/constants/config.tsx', + ], + ['./spec.md', '?path=/docs/docs-spec--docs'], + ['./config.md', '?path=/docs/docs-config--docs'], + +]; + +export const applyReplacements = (text, replacements) => { + return replacements.reduce((result, [searchValue, replaceValue]) => { + return result.split(searchValue).join(replaceValue); + }, text); +}; + +export const content = applyReplacements(Lib, replacements); + +{content} diff --git a/src/stories/DocsMutators.mdx b/src/stories/DocsMutators.mdx new file mode 100644 index 00000000..f575c647 --- /dev/null +++ b/src/stories/DocsMutators.mdx @@ -0,0 +1,284 @@ +import {Meta} from '@storybook/addon-docs'; + + + +# Mutators + +Let's explore how to implement a mutator inside your custom input component. `Mutators` are used to dynamically change the form's `spec`, field `values`, and `errors`. This can be particularly useful when you need to update the form's state based on user interactions within a [custom input](?path=/docs/docs-custom-input--docs). + +## Mutators Interface + +The DynamicFormMutators interface defines the shape of the mutators object: + +```ts +export interface DynamicFormMutators { + errors?: Record; + values?: Record; + spec?: Record; +} +``` + +- **values**: An object mapping field paths to new values. +- **spec**: An object mapping field paths to spec mutations (SpecMutator), allowing you to change field properties like disabled, hidden, etc. +- **errors**: An object mapping field paths to validation errors. + +_Note_: The keys in these objects are strings representing the paths to the fields in the form specification. + +## Purpose of Mutators + +- Purpose: To provide a select input that triggers specific mutations on the form's state when a new option is selected. +- Functionality: Depending on the selected option, it can: + - Disable or enable other fields. + - Update the values of other fields. + - Set validation errors on other fields. + - Combine any of the above. + +Here's how you can create a mutator within your custom input: + +```tsx +import React from 'react'; + +import type { + BaseValidateError, + FormValue, + SelectProps, + SpecMutator, + StringInputProps, +} from '@gravity-ui/dynamic-forms'; +import {Select, useMutateDFState} from '@gravity-ui/dynamic-forms'; + +type OnChangeValue = Parameters[0]; + +type MutationVariants = { + spec?: SpecMutator; + values?: FormValue; + errors?: BaseValidateError; +}; + +const MUTATION_VARIANTS: Record = { + spec: { + spec: { + viewSpec: { + disabled: true, + }, + }, + }, + value: { + values: 'mutation value', + }, + error: { + errors: 'mutation error', + }, + all: { + spec: { + viewSpec: { + disabled: true, + }, + }, + values: 'mutation value', + errors: 'mutation error', + }, +}; + +function removeAfterLastDot(str: string) { + const lastDotIndex = str.lastIndexOf('.'); + if (lastDotIndex === -1) { + return str; + } + return str.substring(0, lastDotIndex); +} + +export const MutationsSelect = (props: StringInputProps) => { + const mutate = useMutateDFState(); + + const rowFieldName = React.useMemo(() => removeAfterLastDot(props.name), [props.name]); + + const handleChange = React.useCallback( + (newType: OnChangeValue) => { + props.input.onChange(newType); + + if (typeof newType !== 'string') { + return; + } + + const nameMutationField = `${rowFieldName}.${newType}`; + + mutate({ + spec: {[nameMutationField]: {...MUTATION_VARIANTS[newType].spec}}, + values: {[nameMutationField]: MUTATION_VARIANTS[newType].values}, + errors: {[nameMutationField]: MUTATION_VARIANTS[newType].errors}, + }); + }, + [props.input, rowFieldName, mutate], + ); + + const newProps = { + ...props, + input: { + ...props.input, + onChange: (value: OnChangeValue) => handleChange(value), + }, + }; + + return