diff --git a/packages/grafana-ui/src/components/Forms/Form.mdx b/packages/grafana-ui/src/components/Forms/Form.mdx
new file mode 100644
index 00000000000..b49fe4fd0e8
--- /dev/null
+++ b/packages/grafana-ui/src/components/Forms/Form.mdx
@@ -0,0 +1,155 @@
+import { Meta, Story, Preview, Props } from '@storybook/addon-docs/blocks';
+import { Form } from './Form';
+
+
+
+# Form
+
+Form component provides a way to build simple forms at Grafana. It is built on top of [react-hook-form](https://react-hook-form.com/) library and incorporates the same concepts while adjusting the API slightly.
+
+## Usage
+
+```jsx
+import { Forms } from '@grafana/ui';
+
+interface UserDTO {
+ name: string;
+ email: string;
+ //...
+}
+
+const defaultUser: Partial
= {
+ name: 'Roger Waters',
+ // ...
+}
+
+ await createrUser(user)}
+>{({register, errors}) => {
+ return (
+
+
+
+
+
+ )
+}}
+```
+
+### Form API
+
+`Form` component exposes API via render prop. Three properties are exposed: `register`, `errors` and `control`
+
+#### `register`
+
+`register` allows to register form elements(inputs, selects, radios, etc) in the form. In order to do that you need to pass `register` as a `ref` property to the form input. For example:
+
+```jsx
+
+```
+
+Register accepts an object which describes validation rules for a given input:
+
+```jsx
+ { // custom validation rule }
+ })}
+/>
+```
+
+#### `errors`
+
+`errors` in an object that contains validation errors of the form. To show error message and invalid input indication in your form, wrap input element with `` component and pass `invalid` and `error` props to it:
+
+```jsx
+
+
+
+```
+
+#### `control`
+
+By default `Form` component assumes form elements are uncontrolled (https://reactjs.org/docs/glossary.html#controlled-vs-uncontrolled-components). There are some components like `RadioButton` or `Select` that are controlled-only and require some extra work. To make them work with the form, you need to render those using `Forms.InputControl` component:
+
+```jsx
+import { Forms } from '@grafana/ui';
+
+// render function
+{({register, errors, control}) => (
+ <>
+
+
+
+
+
+
+
+ >
+)}
+
+```
+
+### Default values
+
+Default values of the form can be passed either via `defaultValues` property on the `Form` element, or directly on form's input via `defaultValue` prop:
+
+```jsx
+// Passing default values to the Form
+
+interface FormDTO {
+ name: string;
+ isAdmin: boolean;
+}
+
+const defaultValues: FormDto {
+ name: 'Roger Waters',
+ isAdmin: false,
+}
+
+{...}
+```
+
+```jsx
+// Passing default value directly to form inputs
+
+interface FormDTO {
+ name: string;
+ isAdmin: boolean;
+}
+
+const defaultValues: FormDto {
+ name: 'Roger Waters',
+ isAdmin: false,
+}
+
+{
+ ({register}) => (
+ <>
+
+ >
+ )}
+
+```
+
+### Props
+
+
diff --git a/packages/grafana-ui/src/components/Forms/Form.story.tsx b/packages/grafana-ui/src/components/Forms/Form.story.tsx
index 8396fd60aa3..338289f6da0 100644
--- a/packages/grafana-ui/src/components/Forms/Form.story.tsx
+++ b/packages/grafana-ui/src/components/Forms/Form.story.tsx
@@ -1,4 +1,4 @@
-import React, { useState } from 'react';
+import React from 'react';
import { Legend } from './Legend';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
@@ -8,96 +8,131 @@ import { Input } from './Input/Input';
import { Button } from './Button';
import { Form } from './Form';
import { Switch } from './Switch';
-import { Icon } from '../Icon/Icon';
import { Checkbox } from './Checkbox';
-import { TextArea } from './TextArea/TextArea';
+
+import { RadioButtonGroup } from './RadioButtonGroup/RadioButtonGroup';
+import { Select } from './Select/Select';
+import Forms from './index';
+import mdx from './Form.mdx';
export default {
- title: 'UI/Forms/Test forms/Server admin',
+ title: 'UI/Forms/Test forms',
decorators: [withStoryContainer, withCenteredStory],
+ parameters: {
+ docs: {
+ page: mdx,
+ },
+ },
};
-export const users = () => {
- const [name, setName] = useState();
- const [email, setEmail] = useState();
- const [username, setUsername] = useState();
- const [password, setPassword] = useState();
- const [disabledUser, setDisabledUser] = useState(false);
- const [checked, setChecked] = useState(false);
+const selectOptions = [
+ {
+ label: 'Option 1',
+ value: 'option1',
+ },
+ {
+ label: 'Option 2',
+ value: 'option2',
+ },
+ {
+ label: 'Option 3',
+ value: 'option3',
+ },
+];
+interface FormDTO {
+ name: string;
+ email: string;
+ username: string;
+ checkbox: boolean;
+ switch: boolean;
+ radio: string;
+ select: string;
+ nested: {
+ path: string;
+ };
+}
+
+const renderForm = (defaultValues?: Partial) => (
+
+);
+
+export const basic = () => {
+ return <>{renderForm()}>;
+};
+
+export const defaultValues = () => {
return (
<>
-
-
-
-
+ {renderForm({
+ name: 'Roger Waters',
+ nested: {
+ path: 'Nested path default value',
+ },
+ radio: 'option2',
+ select: 'option1',
+ switch: true,
+ })}
>
);
};
diff --git a/packages/grafana-ui/src/components/Forms/Form.tsx b/packages/grafana-ui/src/components/Forms/Form.tsx
index 0fcb59b20ac..9e934b6015f 100644
--- a/packages/grafana-ui/src/components/Forms/Form.tsx
+++ b/packages/grafana-ui/src/components/Forms/Form.tsx
@@ -3,9 +3,10 @@
*/
import React from 'react';
-import { stylesFactory, useTheme } from '../../themes';
+import { useForm, Mode, OnSubmit, DeepPartial, FormContextValues } from 'react-hook-form';
import { GrafanaTheme } from '@grafana/data';
import { css } from 'emotion';
+import { stylesFactory, useTheme } from '../../themes';
const getFormStyles = stylesFactory((theme: GrafanaTheme) => {
return {
@@ -15,8 +16,26 @@ const getFormStyles = stylesFactory((theme: GrafanaTheme) => {
};
});
-export const Form: React.FC = ({ children }) => {
+type FormAPI = Pick, 'register' | 'errors' | 'control'>;
+
+interface FormProps {
+ validateOn?: Mode;
+ defaultValues?: DeepPartial;
+ onSubmit: OnSubmit;
+ children: (api: FormAPI) => React.ReactNode;
+}
+
+export function Form({ validateOn, defaultValues, onSubmit, children }: FormProps) {
const theme = useTheme();
+ const { handleSubmit, register, errors, control } = useForm({
+ mode: validateOn || 'onSubmit',
+ defaultValues,
+ });
const styles = getFormStyles(theme);
- return {children}
;
-};
+
+ return (
+
+ );
+}
diff --git a/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButton.tsx b/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButton.tsx
index 80edf8134cf..f8f23488b07 100644
--- a/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButton.tsx
+++ b/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButton.tsx
@@ -114,7 +114,7 @@ const getRadioButtonStyles = stylesFactory((theme: GrafanaTheme, size: RadioButt
buttonActive: css`
background: ${bgActive};
border: ${borderActive};
- border-left: none;
+ border-left: 0;
color: ${textColorActive};
text-shadow: ${fakeBold};
diff --git a/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButtonGroup.tsx b/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButtonGroup.tsx
index aa32e97bc9b..5120d215d93 100644
--- a/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButtonGroup.tsx
+++ b/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButtonGroup.tsx
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { useCallback } from 'react';
import { css } from 'emotion';
import { SelectableValue } from '@grafana/data';
import { RadioButtonSize, RadioButton } from './RadioButton';
@@ -14,11 +14,11 @@ const getRadioButtonGroupStyles = () => {
};
};
interface RadioButtonGroupProps {
- value: T;
+ value?: T;
disabled?: boolean;
disabledOptions?: T[];
options: Array>;
- onChange: (value?: T) => void;
+ onChange?: (value?: T) => void;
size?: RadioButtonSize;
}
@@ -30,6 +30,16 @@ export function RadioButtonGroup({
disabledOptions,
size = 'md',
}: RadioButtonGroupProps) {
+ const handleOnClick = useCallback(
+ (option: SelectableValue) => {
+ return () => {
+ if (onChange) {
+ onChange(option.value);
+ }
+ };
+ },
+ [onChange]
+ );
const styles = getRadioButtonGroupStyles();
return (
@@ -42,9 +52,7 @@ export function RadioButtonGroup({
disabled={isItemDisabled || disabled}
active={value === o.value}
key={o.label}
- onClick={() => {
- onChange(o.value);
- }}
+ onClick={handleOnClick(o)}
>
{o.label}
diff --git a/packages/grafana-ui/src/components/Forms/Switch.story.tsx b/packages/grafana-ui/src/components/Forms/Switch.story.tsx
index c9cab1879b4..0e1d5b7f8e7 100644
--- a/packages/grafana-ui/src/components/Forms/Switch.story.tsx
+++ b/packages/grafana-ui/src/components/Forms/Switch.story.tsx
@@ -1,4 +1,4 @@
-import React, { useState } from 'react';
+import React, { useState, useCallback } from 'react';
import { boolean } from '@storybook/addon-knobs';
import { withCenteredStory, withHorizontallyCenteredStory } from '../../utils/storybook/withCenteredStory';
import { Switch } from './Switch';
@@ -15,17 +15,16 @@ export default {
},
};
-export const simple = () => {
+export const controlled = () => {
const [checked, setChecked] = useState(false);
+ const onChange = useCallback(e => setChecked(e.currentTarget.checked), [setChecked]);
const BEHAVIOUR_GROUP = 'Behaviour props';
const disabled = boolean('Disabled', false, BEHAVIOUR_GROUP);
- return (
- {
- setChecked(checked);
- }}
- />
- );
+ return ;
+};
+
+export const uncontrolled = () => {
+ const BEHAVIOUR_GROUP = 'Behaviour props';
+ const disabled = boolean('Disabled', false, BEHAVIOUR_GROUP);
+ return ;
};
diff --git a/packages/grafana-ui/src/components/Forms/Switch.tsx b/packages/grafana-ui/src/components/Forms/Switch.tsx
index e809fffce05..143a1795479 100644
--- a/packages/grafana-ui/src/components/Forms/Switch.tsx
+++ b/packages/grafana-ui/src/components/Forms/Switch.tsx
@@ -1,25 +1,47 @@
-import React from 'react';
+import React, { HTMLProps } from 'react';
import { stylesFactory, useTheme } from '../../themes';
import { GrafanaTheme } from '@grafana/data';
import { css, cx } from 'emotion';
-import { getFocusStyle } from './commonStyles';
+import { getFocusCss } from './commonStyles';
-export interface SwitchProps {
- checked?: boolean;
- disabled?: boolean;
- onChange?: (e: React.SyntheticEvent, checked: boolean) => void;
+export interface SwitchProps extends Omit, 'value'> {
+ value?: boolean;
}
export const getSwitchStyles = stylesFactory((theme: GrafanaTheme) => {
return {
- slider: cx(
- css`
- width: 32px;
- height: 16px;
- background: ${theme.colors.formSwitchBg};
- transition: all 0.30s ease;
- border-radius: 50px;
+ switch: css`
+ width: 32px;
+ height: 16px;
+ position: relative;
+
+ input {
+ opacity: 0;
+ width: 100% !important;
+ height: 100%;
position: relative;
+ z-index: 1;
+ cursor: pointer;
+
+ &:focus ~ div {
+ ${getFocusCss(theme)};
+ }
+ &[disabled] {
+ background: ${theme.colors.formSwitchBgDisabled};
+ }
+ }
+
+ input ~ div {
+ width: 100%;
+ height: 100%;
+ background: red;
+ z-index: 0;
+ position: absolute;
+ top: 0;
+ left: 0;
+ background: ${theme.colors.formSwitchBg};
+ transition: all 0.3s ease;
+ border-radius: 50px;
border: none;
display: block;
padding: 0;
@@ -30,6 +52,7 @@ export const getSwitchStyles = stylesFactory((theme: GrafanaTheme) => {
content: '';
transition: transform 0.2s cubic-bezier(0.19, 1, 0.22, 1);
position: absolute;
+ z-index: 0;
top: 50%;
display: block;
width: 12px;
@@ -38,42 +61,41 @@ export const getSwitchStyles = stylesFactory((theme: GrafanaTheme) => {
border-radius: 6px;
transform: translate3d(2px, -50%, 0);
}
- &:focus {
- /* border: 1px solid ${theme.colors.formSwitchDot}; */
- }
- &[disabled] {
- background: ${theme.colors.formSwitchBgDisabled};
- }
- `,
- getFocusStyle(theme)
- ),
- sliderActive: css`
- background: ${theme.colors.formSwitchBgActive};
- &:hover {
- background: ${theme.colors.formSwitchBgActiveHover};
}
- &:after {
- transform: translate3d(16px, -50%, 0);
+ input:checked ~ div {
+ background: ${theme.colors.formSwitchBgActive};
+ &:hover {
+ background: ${theme.colors.formSwitchBgActiveHover};
+ }
+
+ &:after {
+ transform: translate3d(16px, -50%, 0);
+ }
}
`,
};
});
-export const Switch: React.FC = ({ checked = false, disabled = false, onChange }) => {
- const theme = useTheme();
- const styles = getSwitchStyles(theme);
+export const Switch = React.forwardRef(
+ ({ value, checked, disabled = false, onChange, ...inputProps }, ref) => {
+ const theme = useTheme();
+ const styles = getSwitchStyles(theme);
- return (
-