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 (
+
+ )}
+
+ );
+};
+
+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 (
+
+ )}
+
+ );
+};
+
+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 ;
+};
+```
+
+### Exapmle spec
+
+```js
+{
+ type: 'object',
+ properties: {
+ spec: {
+ type: 'string',
+ viewSpec: {
+ type: 'base',
+ layout: 'row',
+ layoutTitle: 'spec',
+ },
+ },
+ value: {
+ type: 'string',
+ viewSpec: {
+ type: 'base',
+ layout: 'row',
+ layoutTitle: 'value',
+ },
+ },
+ error: {
+ type: 'string',
+ viewSpec: {
+ type: 'base',
+ layout: 'row',
+ layoutTitle: 'error',
+ },
+ },
+ all: {
+ type: 'string',
+ viewSpec: {
+ type: 'base',
+ layout: 'row',
+ layoutTitle: 'all',
+ },
+ },
+ mutate: {
+ type: 'string',
+ enum: ['spec', 'value', 'error', 'all'],
+ viewSpec: {
+ type: 'mutations-select',
+ layout: 'row',
+ layoutTitle: 'mutate',
+ },
+ },
+ },
+ viewSpec: {
+ type: 'base',
+ layout: 'accordeon',
+ layoutTitle: 'Candidate',
+ layoutOpen: true,
+ },
+};
+```
+
+The MutationsSelect component is a custom React component designed to interact with the `@gravity-ui/dynamic-forms` library. It extends the functionality of a standard Select input by applying dynamic mutations to the form's state based on the user's selection. These mutations can modify the form's spec (`spec`), field values (`values`), and validation errors (`errors`).
+
+Remember to:
+
+- Use the useMutateDFState hook to access the mutateDFState function.
+- Define mutations for spec, values, and/or errors as needed.
+- Apply mutations in your input's event handlers (e.g., onChange).
+
+# External Mutators
+
+External mutators allow you to modify the form's state from outside the input components. This is particularly useful when you need to trigger state changes based on events that occur outside of your form inputs, such as data fetching, external user actions, or any side effects.
+
+## Overview
+
+- Purpose: To apply mutations to the form's state from outside the form components, enabling dynamic updates based on external events.
+- Usage: Pass a mutators object to the `DynamicField` component to apply mutations to the form's state.
+
+## How External Mutators Work
+
+When an external event occurs (e.g., a data fetch completes, a timeout, a global state change), you can use the mutators prop to pass mutations to the `DynamicField` component. The form will apply these mutations to update its state accordingly.
+
+```tsx
+import React, {useEffect, useState} from 'react';
+import {DynamicField, DynamicFormMutators} from '@gravity-ui/dynamic-forms';
+
+const ExternalMutatorsExample = () => {
+ const [mutators, setMutators] = useState({});
+ const spec = {
+ // Your form spec
+ };
+
+ useEffect(() => {
+ // Simulate an external event, e.g., data fetching
+ fetchData().then((data) => {
+ // Define the mutations you want to apply
+ const newMutators: DynamicFormMutators = {
+ values: {
+ fieldName: data.value,
+ },
+ spec: {
+ fieldName: {
+ disabled: data.disableField,
+ },
+ },
+ errors: data.hasError ? {fieldName: 'Error message'} : {},
+ };
+ // Update the mutators state to trigger mutations
+ setMutators(newMutators);
+ });
+ }, []);
+
+ return ;
+};
+
+export default ExternalMutatorsExample;
+```
+
+- Defining Mutations: We create a DynamicFormMutators object that specifies the mutations we want to apply. This object can include values, spec, and errors.
+
+```tsx
+const newMutators: DynamicFormMutators = {
+ values: {
+ fieldName: data.value,
+ },
+ spec: {
+ fieldName: {
+ disabled: data.disableField,
+ },
+ },
+ errors: data.hasError ? {fieldName: 'Error message'} : {},
+};
+```
+
+- Updating Mutators State: By calling setMutators, we update the mutators state, which triggers the form to apply the mutations.
+
+```tsx
+setMutators(newMutators);
+```
+
+- Passing Mutators to DynamicField: We pass the mutators object as a prop to the DynamicField component.
+
+```tsx
+
+```
+
+## Conclusion
+
+External mutators provide a powerful way to dynamically update your form's state based on external events or conditions. By passing a mutators object to the `DynamicField`, you can modify field `values`, `spec`, and validation `errors` without directly interacting with the input components.
+
+This approach enhances the flexibility and responsiveness of your forms, allowing for a more dynamic user experience.
+
+---
+
+Example Use Cases:
+
+- Conditional Form Fields: Show or hide fields based on external data.
+- Prefilling Forms: Populate form fields with data fetched from an API.
+- Form Validation: Apply custom validation messages based on server-side logic.
+- Disabling Inputs: Disable or enable inputs based on user roles or permissions.
+
+---
diff --git a/src/stories/DocsSpec.mdx b/src/stories/DocsSpec.mdx
new file mode 100644
index 00000000..103db294
--- /dev/null
+++ b/src/stories/DocsSpec.mdx
@@ -0,0 +1,21 @@
+import {Meta, Markdown} from '@storybook/addon-docs';
+
+import Spec from '../../docs/spec.md?raw';
+
+
+
+export const replacements = [
+ ['./lib.md', '?path=/docs/docs-lib--docs'],
+ ['./config.md', '?path=/docs/docs-config--docs'],
+ ['./input-props-map.md', '?path=/docs/docs-input-props-map--docs'],
+];
+
+export const applyReplacements = (text, replacements) => {
+ return replacements.reduce((result, [searchValue, replaceValue]) => {
+ return result.split(searchValue).join(replaceValue);
+ }, text);
+};
+
+export const content = applyReplacements(Spec, replacements);
+
+{content}
diff --git a/src/stories/DocsValidators.mdx b/src/stories/DocsValidators.mdx
new file mode 100644
index 00000000..6521c5e6
--- /dev/null
+++ b/src/stories/DocsValidators.mdx
@@ -0,0 +1,577 @@
+import {Meta, Markdown} from '@storybook/addon-docs';
+
+
+
+# Validators in Dynamic Forms
+
+The `@gravity-ui/dynamic-forms` library provides a basic set of validators and methods for validating inputs. In this guide, we'll explore how to work with validators for various data types, how to customize them to suit your needs, and how to create your own custom validators.
+
+## Contents
+
+1. [Validation of Arrays](#1-validation-of-arrays)
+2. [Validation of Boolean Values](#2-validation-of-boolean-values)
+3. [Validation of Numbers](#3-validation-of-numbers)
+4. [Validation of Objects](#4-validation-of-objects)
+5. [Validation of Strings](#5-validation-of-strings)
+6. [Adding Custom Validation](#6-adding-custom-validation)
+7. [Conclusion](#conclusion)
+
+---
+
+## 1. Validation of Arrays
+
+In dynamic forms, you can set restrictions on the number of elements in an array using the `maxLength` and `minLength` fields. You can also specify that an array is `required`, meaning it must contain at least one element.
+
+Additionally, you can use the base validator by spec `"validator": "base"` in the field.
+
+### Example Usage in Specification:
+
+```json
+{
+ "type": "array",
+ "items": {
+ "type": "string",
+ "viewSpec": {
+ "type": "base",
+ "layout": "row",
+ "layoutTitle": "Element"
+ }
+ },
+ "viewSpec": {
+ "type": "base",
+ "layout": "accordeon",
+ "layoutTitle": "Elements",
+ "layoutOpen": true,
+ "itemLabel": "Add element"
+ },
+ "required": true,
+ "maxLength": 5,
+ "minLength": 2,
+ "validator": "base"
+}
+```
+
+### Implementation of the Array Validator
+
+```ts
+interface CommonValidatorParams {
+ ignoreRequiredCheck?: boolean;
+ customErrorMessages?: Partial;
+}
+
+export interface GetArrayValidatorParams extends CommonValidatorParams {
+ ignoreMaxLengthCheck?: boolean;
+ ignoreMinLengthCheck?: boolean;
+}
+
+export const getArrayValidator = (params: GetArrayValidatorParams = {}) => {
+ const {ignoreRequiredCheck, ignoreMaxLengthCheck, ignoreMinLengthCheck, customErrorMessages} =
+ params;
+
+ return (spec: ArraySpec, value?: ArrayValue) => {
+ const errorMessages = {...ErrorMessages, ...customErrorMessages};
+
+ const valueLength = value?.length || 0;
+
+ if (!ignoreRequiredCheck && spec.required && !Array.isArray(value)) {
+ return errorMessages.REQUIRED;
+ }
+
+ if (
+ !ignoreMaxLengthCheck &&
+ typeof spec.maxLength === 'number' &&
+ valueLength > spec.maxLength
+ ) {
+ return errorMessages.maxLengthArr(spec.maxLength);
+ }
+
+ if (
+ !ignoreMinLengthCheck &&
+ typeof spec.minLength === 'number' &&
+ valueLength < spec.minLength
+ ) {
+ return errorMessages.minLengthArr(spec.minLength);
+ }
+
+ return false;
+ };
+};
+```
+
+**Explanation:**
+
+ - **ignoreRequiredCheck**, **ignoreMaxLengthCheck**, **ignoreMinLengthCheck**: Parameters that allow you to ignore corresponding checks during validation.
+ - **customErrorMessages**: Allows you to override default error messages.
+ - **valueLength**: Length of the array value.
+ - The validator returns an error string if validation fails, or false if everything is correct.
+
+---
+
+## 2. Validation of Boolean Values
+
+For boolean values, you can also specify the `required` field, indicating that the value must be set (not undefined).
+
+### Example Usage in Specification
+
+```json
+{
+ "type": "boolean",
+ "viewSpec": {
+ "type": "base",
+ "layout": "row",
+ "layoutTitle": "Flag"
+ },
+ "required": true,
+ "validator": "base"
+}
+```
+
+### Implementation of the Boolean Validator
+
+```ts
+export interface GetBooleanValidatorParams extends CommonValidatorParams {}
+
+export const getBooleanValidator = (params: GetBooleanValidatorParams = {}) => {
+ const {ignoreRequiredCheck, customErrorMessages} = params;
+
+ return (spec: BooleanSpec, value?: boolean) => {
+ const errorMessages = {...ErrorMessages, ...customErrorMessages};
+
+ if (!ignoreRequiredCheck && spec.required && value === undefined) {
+ return errorMessages.REQUIRED;
+ }
+
+ return false;
+ };
+};
+```
+
+**Explanation:**
+
+- **`ignoreRequiredCheck`**: Parameter to ignore the required check.
+- The validator checks that if the `required`
+
+field is specified, the value should not be `undefined`.
+
+- Returns an error message or false.
+
+---
+
+## 3. Validation of Numbers
+
+For numbers, several validator options are available to check:
+
+ - Required input (`required`).
+ - Maximum and minimum values (`maximum`, `minimum`).
+
+### Example Usage in Specification:
+
+```json
+{
+ "type": "number",
+ "viewSpec": {
+ "type": "base",
+ "layout": "row",
+ "layoutTitle": "Age",
+ "placeholder": "placeholder text"
+ },
+ "required": true,
+ "maximum": 12,
+ "minimum": 2,
+ "validator": "base"
+}
+```
+
+### Implementation of the Number Validator
+
+```ts
+export interface GetNumberValidatorParams extends CommonValidatorParams {
+ ignoreSpaceStartCheck?: boolean;
+ ignoreSpaceEndCheck?: boolean;
+ ignoreNumberCheck?: boolean;
+ ignoreMaximumCheck?: boolean;
+ ignoreMinimumCheck?: boolean;
+ ignoreIntCheck?: boolean;
+ ignoreDotEnd?: boolean;
+ ignoreZeroStart?: boolean;
+}
+
+export const getNumberValidator = (params: GetNumberValidatorParams = {}) => {
+ const {
+ ignoreRequiredCheck,
+ ignoreSpaceStartCheck,
+ ignoreSpaceEndCheck,
+ ignoreNumberCheck,
+ ignoreMaximumCheck,
+ ignoreMinimumCheck,
+ ignoreIntCheck,
+ ignoreDotEnd,
+ ignoreZeroStart,
+ customErrorMessages,
+ } = params;
+
+ return (spec: NumberSpec, value: string | number = '') => {
+ const errorMessages = {...ErrorMessages, ...customErrorMessages};
+
+ const stringValue = String(value).trim();
+
+ if (!ignoreRequiredCheck && spec.required && !stringValue.length) {
+ return errorMessages.REQUIRED;
+ }
+
+ if (!ignoreSpaceStartCheck && /^\s/.test(stringValue)) {
+ return errorMessages.SPACE_START;
+ }
+
+ if (!ignoreSpaceEndCheck && /\s$/.test(stringValue)) {
+ return errorMessages.SPACE_END;
+ }
+
+ const numberValue = Number(stringValue);
+
+ if (!ignoreNumberCheck && isNaN(numberValue)) {
+ return errorMessages.MUST_BE_NUMBER;
+ }
+
+ if (!ignoreMaximumCheck && typeof spec.maximum === 'number' && numberValue > spec.maximum) {
+ return errorMessages.MAXIMUM_NUMBER(spec.maximum);
+ }
+
+ if (!ignoreMinimumCheck && typeof spec.minimum === 'number' && numberValue < spec.minimum) {
+ return errorMessages.MINIMUM_NUMBER(spec.minimum);
+ }
+
+ if (!ignoreIntCheck && spec.wholeNumber && !Number.isInteger(numberValue)) {
+ return errorMessages.MUST_BE_INTEGER;
+ }
+
+ if (!ignoreDotEnd && /\.$/.test(stringValue)) {
+ return errorMessages.NO_DOT_AT_END;
+ }
+
+ if (!ignoreZeroStart && /^0\d+/.test(stringValue)) {
+ return errorMessages.NO_LEADING_ZERO;
+ }
+
+ return false;
+ };
+};
+```
+
+**Explanation:**
+
+ - **Check Ignoring Parameters**: Allow you to disable specific validator checks.
+ - **stringValue**: String representation of the value, trimmed of extra spaces.
+ - **Checks Performed**:
+ - Required field.
+ - No spaces at the start and end.
+ - Numeric value correctness.
+ - Compliance with maximum and minimum values.
+ - Integer number.
+ - No dot at the end.
+ - No leading zeros.
+
+## 4. Validation of Objects
+
+For objects, you can specify that they are required using the `required` field. This means the object must have at least one property with a value.
+
+You can also use the base validator by setting `"validator": "base"`.
+
+### Example Usage in Specification:
+
+```json
+{
+ "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": "accordeon",
+ "layoutTitle": "Candidate",
+ "layoutOpen": true
+ },
+ "required": true,
+ "validator": "base"
+}
+```
+
+### Implementation of the Object Validator
+
+```ts
+export interface GetObjectValidatorParams extends CommonValidatorParams {}
+
+export const getObjectValidator = (params: GetObjectValidatorParams = {}) => {
+ const {ignoreRequiredCheck, customErrorMessages} = params;
+
+ return (spec: ObjectSpec, value?: ObjectValue) => {
+ const errorMessages = {...ErrorMessages, ...customErrorMessages};
+
+ if (!ignoreRequiredCheck && spec.required && !value) {
+ return errorMessages.REQUIRED;
+ }
+
+ return false;
+ };
+};
+```
+
+**Explanation:**
+
+ - **`ignoreRequiredCheck`**: Allows skipping the required object check.
+ - The validator checks that if the object is required, the value should not be `undefined` or `null`.
+ - Returns an error message or `false`.
+
+---
+
+## 5. Validation of Strings
+
+For strings, multiple validation options are available:
+
+ - Required field (`required`).
+ - Maximum and minimum length (`maxLength`, `minLength`).
+ - Pattern matching using a regular expression (`pattern`).
+
+### Example Usage in Specification:
+
+```json
+{
+ "type": "string",
+ "viewSpec": {
+ "type": "base",
+ "layout": "row",
+ "layoutTitle": "Name",
+ "placeholder": "placeholder text"
+ },
+ "maxLength": 20,
+ "minLength": 2,
+ "pattern": "^[a-zA-Z0-9]+$",
+ "patternError": "Only Latin letters and numbers are allowed",
+ "validator": "base"
+}
+```
+
+### Implementation of the String Validator
+
+```ts
+export interface GetStringValidatorParams extends CommonValidatorParams {
+ ignoreSpaceStartCheck?: boolean;
+ ignoreSpaceEndCheck?: boolean;
+ ignoreMaxLengthCheck?: boolean;
+ ignoreMinLengthCheck?: boolean;
+ ignoreRegExpCheck?: boolean;
+}
+
+export const getStringValidator = (params: GetStringValidatorParams = {}) => {
+ const {
+ ignoreRequiredCheck,
+ ignoreSpaceStartCheck,
+ ignoreSpaceEndCheck,
+ ignoreMaxLengthCheck,
+ ignoreMinLengthCheck,
+ ignoreRegExpCheck,
+ customErrorMessages,
+ } = params;
+
+ return (spec: StringSpec, value = '') => {
+ const errorMessages = {...ErrorMessages, ...customErrorMessages};
+
+ const valueLength = value.length;
+
+ if (!ignoreRequiredCheck && spec.required && !valueLength) {
+ return errorMessages.REQUIRED;
+ }
+
+ if (valueLength) {
+ if (!ignoreSpaceStartCheck && /^\s/.test(value)) {
+ return errorMessages.SPACE_START;
+ }
+
+ if (!ignoreSpaceEndCheck && /\s$/.test(value)) {
+ return errorMessages.SPACE_END;
+ }
+ }
+
+ if (
+ !ignoreMaxLengthCheck &&
+ typeof spec.maxLength === 'number' &&
+ valueLength > spec.maxLength
+ ) {
+ return errorMessages.maxLength(spec.maxLength);
+ }
+
+ if (
+ !ignoreMinLengthCheck &&
+ typeof spec.minLength === 'number' &&
+ valueLength < spec.minLength
+ ) {
+ return errorMessages.minLength(spec.minLength);
+ }
+
+ if (spec.pattern && !ignoreRegExpCheck) {
+ const regex = new RegExp(spec.pattern);
+
+ if (!regex.test(value)) {
+ return spec.patternError || errorMessages.INVALID;
+ }
+ }
+
+ return false;
+ };
+};
+```
+
+**Explanation:**
+
+ - **Check Ignoring Parameters**: Allow you to disable specific validator checks.
+ - **`valueLength`**: Length of the string.
+ - **Checks Performed**:
+ - Required field.
+ - No spaces at the start and end.
+ - Maximum and minimum length.
+ - Pattern matching using regular expressions.
+ - **`spec.pattern`**: Regular expression for string validation.
+ - **`spec.patternError`**: Error message displayed if the string does not match the pattern.
+
+---
+
+## 6. Adding Custom Validation
+
+Sometimes, the standard validators may not be sufficient for your specific requirements. In such cases, you can add your own custom validation. There are two ways to do this:
+
+### Method 1: Configuring the Base Validator
+
+You can use an existing base validator and pass the necessary parameters to configure it.
+
+#### Steps:
+
+ 1. Import the base validator, for example, getStringValidator.
+
+ 2. Customize the validator's parameters to suit your needs.
+
+ 3. Register your custom validator in the dynamic form configuration.
+
+#### Example:
+
+```ts
+import _ from 'lodash';
+
+import type {DynamicFormConfig} from '@gravity-ui/dynamic-forms';
+import {dynamicConfig as libConfig, getStringValidator} from '@gravity-ui/dynamic-forms';
+
+const getDynamicConfig = (): DynamicFormConfig => {
+ const dynamicConfig = _.cloneDeep(libConfig);
+
+ // Register our custom validator with a specific name
+ dynamicConfig.string.validators['custom_validator'] = getStringValidator({
+ ignoreSpaceStartCheck: true,
+ ignoreSpaceEndCheck: true,
+ });
+
+ return dynamicConfig;
+};
+
+export const dynamicConfig = getDynamicConfig();
+```
+
+**Explanation:**
+
+ - We disabled checks for spaces at the start and end of the string by passing the corresponding parameters to `getStringValidator`.
+ - Assigned our validator the name `'custom_validator'`, under which it can be used in the form spec.
+
+#### Using in Specification:
+
+```json
+{
+ "type": "string",
+ "viewSpec": {
+ "type": "base",
+ "layout": "row",
+ "layoutTitle": "Name",
+ "placeholder": "placeholder text"
+ },
+ "maxLength": 20,
+ "minLength": 2,
+ "pattern": "^[a-zA-Z0-9]+$",
+ "patternError": "Only Latin letters and numbers are allowed",
+ "validator": "custom_validator"
+}
+```
+
+### Method 2: Creating Your Own Validator
+
+You can create your own validation function that meets your unique requirements.
+
+#### Steps:
+
+1. Create your validator function that corresponds to the required data type.
+
+```ts
+const myCustomValidator = (spec: StringSpec, value: string) => {
+ if (!value.includes('example')) {
+ return 'The string must contain the word "example"';
+ }
+ return false;
+};
+```
+
+2. Register your validator in the dynamic form configuration.
+
+```ts
+import _ from 'lodash';
+import type {DynamicFormConfig} from '@gravity-ui/dynamic-forms';
+import {dynamicConfig as libConfig} from '@gravity-ui/dynamic-forms';
+
+const getDynamicConfig = (): DynamicFormConfig => {
+ const dynamicConfig = _.cloneDeep(libConfig);
+
+ // Register our custom validator with a specific name
+ dynamicConfig.string.validators['custom_validator'] = myCustomValidator;
+
+ return dynamicConfig;
+};
+
+export const dynamicConfig = getDynamicConfig();
+```
+
+**Explanation:**
+
+ - We created the `myCustomValidator` function, which checks if the string contains the word `"example"`.
+ - Registered this validator under the name 'custom_validator'.
+
+**Note:** Ensure that your validator function matches the signature expected by the library and returns either a string with an error message or `false` if validation is successful.
+
+---
+
+## Conclusion
+
+Validating data in dynamic forms is an essential part of ensuring users enter information correctly. The `@gravity-ui/dynamic-forms` library provides flexible options for configuring validators for various data types and allows you to create your own custom validators that cater to the specific needs of your application.
+
+**Benefits of Using Validators:**
+
+ - Ensuring data integrity and correctness.
+ - Improving user experience by providing timely feedback on input errors.
+ - Ability to finely tune checks to meet specific requirements.
diff --git a/src/stories/Readme.mdx b/src/stories/Readme.mdx
new file mode 100644
index 00000000..7e49889a
--- /dev/null
+++ b/src/stories/Readme.mdx
@@ -0,0 +1,59 @@
+import {Meta} from '@storybook/addon-docs';
+
+
+
+# Readme
+
+The JSON Schema-based library for rendering forms and form values.
+
+## Install
+
+```shell
+npm install --save-dev @gravity-ui/dynamic-forms
+```
+
+## Usage
+
+```tsx
+import {DynamicField, Spec, dynamicConfig} from '@gravity-ui/dynamic-forms';
+
+// To embed in a final-form
+;
+
+import {DynamicView, dynamicViewConfig} from '@gravity-ui/dynamic-forms';
+
+// To get an overview of the values
+;
+```
+
+### I18N
+
+Certain components include text tokens (words and phrases) that are available in two languages: `en` (the default) and `ru`. To set the language, use the `configure` function:
+
+```js
+// index.js
+
+import {configure, Lang} from '@gravity-ui/dynamic-forms';
+
+configure({lang: Lang.Ru});
+```
+
+## DOCS
+
+ - [Config](?path=/docs/docs-config--docs)
+ - [Spec](?path=/docs/docs-spec--docs)
+ - [Input props map](?path=/docs/docs-input-props-map--docs)
+ - [Lib](?path=/docs/docs-lib--docs)
+ - [How use dynamic forms](?path=/docs/docs-how-use-dynamic-forms--docs)
+ - [Custom input](?path=/docs/docs-custom-input--docs)
+ - [Validators](?path=/docs/docs-validators--docs)
+ - [Mutators](?path=/docs/docs-mutators--docs)
+
+## Development
+
+To start the development server with storybook execute the following command:
+
+```shell
+npm ci
+npm run dev
+```