@startupjs-ui/input 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/README.helpers.js +70 -0
- package/README.mdx +94 -0
- package/globalCustomInputs.ts +5 -0
- package/helpers/extraSchemaTypes.ts +5 -0
- package/helpers/getInputTestId.ts +26 -0
- package/helpers/guessInput.ts +23 -0
- package/index.d.ts +69 -0
- package/index.tsx +108 -0
- package/inputs.ts +442 -0
- package/package.json +43 -0
- package/useCustomInputs.ts +9 -0
- package/useLayout.ts +19 -0
- package/wrapInput.tsx +297 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Change Log
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
|
+
|
|
6
|
+
## [0.1.3](https://github.com/startupjs/startupjs-ui/compare/v0.1.2...v0.1.3) (2025-12-29)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @startupjs-ui/input
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
## [0.1.2](https://github.com/startupjs/startupjs-ui/compare/v0.1.1...v0.1.2) (2025-12-29)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Bug Fixes
|
|
18
|
+
|
|
19
|
+
* **input:** return the default inputs the first time ([7151fce](https://github.com/startupjs/startupjs-ui/commit/7151fce8ff3d2714e672523d0a5f37e4af6eb719))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
### Features
|
|
23
|
+
|
|
24
|
+
* add mdx and docs packages. Refactor docs to get rid of any @startupjs/ui usage and use startupjs-ui instead ([703c926](https://github.com/startupjs/startupjs-ui/commit/703c92636efb0421ffd11783f692fc892b74018f))
|
|
25
|
+
* **array-input:** refactor ArrayInput component. Fix circular dependency issue in Input/inputs (wrapInput). ([14e7204](https://github.com/startupjs/startupjs-ui/commit/14e720423874cbbae8220160e3f5f0713a44b67a))
|
|
26
|
+
* **input:** refactor Input component. Temporary mock usage of ObjectInput and ArrayInput until they are refactored too ([47c4b46](https://github.com/startupjs/startupjs-ui/commit/47c4b467d2d279a474d46d898b417c7856716843))
|
|
27
|
+
* **object-input:** refactor ObjectInput component ([f21693c](https://github.com/startupjs/startupjs-ui/commit/f21693c7f2a31198f445ec3656fb780feb2269bd))
|
|
28
|
+
* **startupjs-ui:** move UiProvider and the main ui plugin into startupjs-ui package ([deeefaa](https://github.com/startupjs/startupjs-ui/commit/deeefaa8ca104efc835d1ff207d5450a83a5f484))
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { $ } from 'startupjs'
|
|
2
|
+
|
|
3
|
+
const DEFAULT_INPUT_TYPE = 'text'
|
|
4
|
+
|
|
5
|
+
export function getPropsForType () {
|
|
6
|
+
const $input = $.session.Sandbox.Input
|
|
7
|
+
const type = $input.type.get() || DEFAULT_INPUT_TYPE
|
|
8
|
+
|
|
9
|
+
const onChangeValue = (value) => $input.value.set(value)
|
|
10
|
+
const commonProps = {
|
|
11
|
+
type,
|
|
12
|
+
value: $input.value.get(),
|
|
13
|
+
$value: $input.value
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
switch (type) {
|
|
17
|
+
case 'text':
|
|
18
|
+
case 'string':
|
|
19
|
+
case 'password':
|
|
20
|
+
return { ...commonProps, onChangeText: onChangeValue }
|
|
21
|
+
|
|
22
|
+
case 'checkbox':
|
|
23
|
+
case 'boolean':
|
|
24
|
+
return { ...commonProps, onChange: onChangeValue }
|
|
25
|
+
|
|
26
|
+
case 'select':
|
|
27
|
+
case 'radio':
|
|
28
|
+
case 'multiselect':
|
|
29
|
+
return { ...commonProps, options: ['New York', 'Los Angeles', 'Tokyo'] }
|
|
30
|
+
|
|
31
|
+
case 'date':
|
|
32
|
+
case 'time':
|
|
33
|
+
case 'datetime':
|
|
34
|
+
return { ...commonProps, onChangeDate: onChangeValue }
|
|
35
|
+
|
|
36
|
+
case 'number':
|
|
37
|
+
case 'integer':
|
|
38
|
+
return { ...commonProps, onChangeNumber: onChangeValue }
|
|
39
|
+
|
|
40
|
+
case 'array':
|
|
41
|
+
return { ...commonProps, items: { type: 'text' } }
|
|
42
|
+
|
|
43
|
+
case 'object':
|
|
44
|
+
return {
|
|
45
|
+
...commonProps,
|
|
46
|
+
properties: {
|
|
47
|
+
email: { input: 'text', label: 'Email' },
|
|
48
|
+
password: {
|
|
49
|
+
input: 'text',
|
|
50
|
+
label: 'Password',
|
|
51
|
+
description: "Make sure it's at least 15 characters OR at least 8 characters including a number and a lowercase letter"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
case 'color':
|
|
57
|
+
return commonProps
|
|
58
|
+
|
|
59
|
+
case 'range':
|
|
60
|
+
return { ...commonProps, onChange: onChangeValue }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function getDefaultValueForType () {
|
|
65
|
+
const $input = $.session.Sandbox.Input
|
|
66
|
+
const type = $input.type.get() || DEFAULT_INPUT_TYPE
|
|
67
|
+
|
|
68
|
+
if (type === 'array') return ['Green', 'Blue']
|
|
69
|
+
return undefined
|
|
70
|
+
}
|
package/README.mdx
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { useEffect } from 'react'
|
|
2
|
+
import Input, { _PropsJsonSchema as InputPropsJsonSchema } from './index'
|
|
3
|
+
import { $ } from 'startupjs'
|
|
4
|
+
import { getPropsForType, getDefaultValueForType } from './README.helpers'
|
|
5
|
+
import { Sandbox } from '@startupjs-ui/docs'
|
|
6
|
+
|
|
7
|
+
# Input
|
|
8
|
+
|
|
9
|
+
Input provides a wrapper api around input components by adding two-way data bindings, different customizable layouts that allow you to display inputs with label and description in different ways and provides the ability to display an error.
|
|
10
|
+
|
|
11
|
+
```jsx
|
|
12
|
+
import { Input } from 'startupjs-ui'
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
**Input components**
|
|
16
|
+
|
|
17
|
+
Possible types are: [array](/docs/forms/Array), [checkbox](/docs/forms/Checkbox), [date](/docs/forms/DateTimePicker), [datetime](/docs/forms/DateTimePicker), [multiselect](/docs/forms/Multiselect), [number](/docs/forms/NumberInput), [object](/docs/forms/ObjectInput), [password](/docs/forms/PasswordInput), [radio](/docs/forms/Radio), [range](/docs/forms/RangeInput), [select](/docs/forms/Select), [time](/docs/forms/DateTimePicker), [text](/docs/forms/TextInput).
|
|
18
|
+
|
|
19
|
+
You can use any of the above components by specifying the `type` property.
|
|
20
|
+
|
|
21
|
+
**Layouts**
|
|
22
|
+
|
|
23
|
+
Possible types are:
|
|
24
|
+
- `pure` displays input without label and description
|
|
25
|
+
- `rows` displays input in one row with provided label and description
|
|
26
|
+
- `columns` displays input in two columns with provided label and description
|
|
27
|
+
|
|
28
|
+
There are several rules that determine which layout to use:
|
|
29
|
+
- for tablet screen resolutions and less the `rows` layout is always used
|
|
30
|
+
- for screen resolutions larger than a tablet you can use any of the above layouts by specifying the `layout` property of the component, but if the layout is not passed to the component it is determined automatically by logic: `rows` if `label` or `description` property is specified, otherwise `pure`
|
|
31
|
+
|
|
32
|
+
## Simple example
|
|
33
|
+
|
|
34
|
+
```jsx example
|
|
35
|
+
const $value = $()
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<Input
|
|
39
|
+
type='text'
|
|
40
|
+
$value={$value}
|
|
41
|
+
/>
|
|
42
|
+
)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Layout
|
|
46
|
+
|
|
47
|
+
```jsx example
|
|
48
|
+
const $value = $()
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<Input
|
|
52
|
+
type='text'
|
|
53
|
+
label='Password'
|
|
54
|
+
description="Make sure it's at least 15 characters OR at least 8 characters including a number and a lowercase letter"
|
|
55
|
+
layout='columns'
|
|
56
|
+
$value={$value}
|
|
57
|
+
/>
|
|
58
|
+
)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Displaying errors
|
|
62
|
+
|
|
63
|
+
To display an error, pass the error text to the `error` property
|
|
64
|
+
|
|
65
|
+
```jsx example
|
|
66
|
+
const $value = $()
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<Input
|
|
70
|
+
$value={$value}
|
|
71
|
+
error={$value.get() ? '' : 'Need to fill the field'}
|
|
72
|
+
/>
|
|
73
|
+
)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Sandbox
|
|
77
|
+
|
|
78
|
+
export function SandboxWrapper () {
|
|
79
|
+
const $input = $.session.Sandbox.Input
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
$input.set(getPropsForType())
|
|
82
|
+
$input.value.set(getDefaultValueForType())
|
|
83
|
+
}, [$input.type.get()])
|
|
84
|
+
return (
|
|
85
|
+
<Sandbox
|
|
86
|
+
Component={Input}
|
|
87
|
+
propsJsonSchema={InputPropsJsonSchema}
|
|
88
|
+
$props={$input}
|
|
89
|
+
block
|
|
90
|
+
/>
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
<SandboxWrapper />
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export default function getInputTestId (props: {
|
|
2
|
+
testId?: string
|
|
3
|
+
label?: unknown
|
|
4
|
+
description?: unknown
|
|
5
|
+
placeholder?: unknown
|
|
6
|
+
} & Record<string, any>): string | undefined {
|
|
7
|
+
if (props.testId) return props.testId
|
|
8
|
+
|
|
9
|
+
const inputName =
|
|
10
|
+
(typeof props.label === 'string' && props.label !== '' ? props.label : null) ??
|
|
11
|
+
(typeof props.description === 'string' && props.description !== '' ? props.description : null) ??
|
|
12
|
+
(typeof props.placeholder === 'string' && props.placeholder !== '' ? props.placeholder : null)
|
|
13
|
+
|
|
14
|
+
if (!inputName || typeof inputName !== 'string') return undefined
|
|
15
|
+
|
|
16
|
+
const nameHash = simpleNumericHash(inputName)
|
|
17
|
+
const allowedCharacters = inputName.match(/\w+/g)
|
|
18
|
+
|
|
19
|
+
return (allowedCharacters ?? []).join('_').slice(0, 20) + '-' + nameHash
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function simpleNumericHash (s: string): number {
|
|
23
|
+
let h = 0
|
|
24
|
+
for (let i = 0; i < s.length; i++) h = Math.imul(31, h) + s.charCodeAt(i) | 0
|
|
25
|
+
return h
|
|
26
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// guess input type based on schema type and props
|
|
2
|
+
export default function guessInput (
|
|
3
|
+
input?: string,
|
|
4
|
+
type?: string,
|
|
5
|
+
props: Record<string, any> = {}
|
|
6
|
+
): string {
|
|
7
|
+
if (input) return input
|
|
8
|
+
if (type) {
|
|
9
|
+
if (props.enum) return 'select'
|
|
10
|
+
if (SCHEMA_TYPES_TO_INPUT[type]) return SCHEMA_TYPES_TO_INPUT[type]
|
|
11
|
+
return type
|
|
12
|
+
}
|
|
13
|
+
return 'text'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const SCHEMA_TYPES_TO_INPUT: Record<string, string> = {
|
|
17
|
+
string: 'text',
|
|
18
|
+
boolean: 'checkbox',
|
|
19
|
+
integer: 'number',
|
|
20
|
+
number: 'number',
|
|
21
|
+
array: 'array',
|
|
22
|
+
object: 'object'
|
|
23
|
+
}
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
// DO NOT MODIFY THIS FILE - IT IS AUTOMATICALLY GENERATED ON COMMITS.
|
|
3
|
+
|
|
4
|
+
import { type RefObject } from 'react';
|
|
5
|
+
import type { InputWrapperConfiguration } from './wrapInput';
|
|
6
|
+
export declare const _PropsJsonSchema: {};
|
|
7
|
+
export interface InputProps {
|
|
8
|
+
/** Explicit input type override (ignores schema guessing) */
|
|
9
|
+
input?: 'array' | 'checkbox' | 'color' | 'date' | 'datetime' | 'time' | 'multiselect' | 'number' | 'object' | 'password' | 'file' | 'rank' | 'radio' | 'range' | 'select' | 'text';
|
|
10
|
+
/** Schema or input type @default 'text' */
|
|
11
|
+
type?: 'array' | 'checkbox' | 'color' | 'date' | 'datetime' | 'time' | 'multiselect' | 'number' | 'object' | 'password' | 'file' | 'rank' | 'radio' | 'range' | 'select' | 'text' | 'string' | 'boolean' | 'integer';
|
|
12
|
+
/** Input value */
|
|
13
|
+
value?: any;
|
|
14
|
+
/** Two-way binding for value */
|
|
15
|
+
$value?: any;
|
|
16
|
+
/** Label text */
|
|
17
|
+
label?: string;
|
|
18
|
+
/** Description text */
|
|
19
|
+
description?: string;
|
|
20
|
+
/** Layout for label and description @default 'rows' when label/description is present */
|
|
21
|
+
layout?: 'pure' | 'rows' | 'columns';
|
|
22
|
+
/** Configuration overrides for the wrapper */
|
|
23
|
+
configuration?: InputWrapperConfiguration;
|
|
24
|
+
/** Error message or list of messages */
|
|
25
|
+
error?: string | string[];
|
|
26
|
+
/** Required flag or json-schema required object */
|
|
27
|
+
required?: boolean | object;
|
|
28
|
+
/** Disable interactions */
|
|
29
|
+
disabled?: boolean;
|
|
30
|
+
/** Render as read-only */
|
|
31
|
+
readonly?: boolean;
|
|
32
|
+
/** Test id for generated testID */
|
|
33
|
+
testId?: string;
|
|
34
|
+
/** Placeholder text */
|
|
35
|
+
placeholder?: string;
|
|
36
|
+
/** Options for select-like inputs */
|
|
37
|
+
options?: any;
|
|
38
|
+
/** Schema enum for select-like inputs */
|
|
39
|
+
enum?: any[];
|
|
40
|
+
/** Schema items for array inputs */
|
|
41
|
+
items?: any;
|
|
42
|
+
/** Schema properties for object inputs */
|
|
43
|
+
properties?: Record<string, any>;
|
|
44
|
+
/** Value change handler (checkbox/select/range/etc.) */
|
|
45
|
+
onChange?: (...args: any[]) => void;
|
|
46
|
+
/** Text change handler */
|
|
47
|
+
onChangeText?: (...args: any[]) => void;
|
|
48
|
+
/** Number change handler */
|
|
49
|
+
onChangeNumber?: (...args: any[]) => void;
|
|
50
|
+
/** Date/time change handler */
|
|
51
|
+
onChangeDate?: (...args: any[]) => void;
|
|
52
|
+
/** Color change handler */
|
|
53
|
+
onChangeColor?: (...args: any[]) => void;
|
|
54
|
+
/** Focus handler */
|
|
55
|
+
onFocus?: (...args: any[]) => void;
|
|
56
|
+
/** Blur handler */
|
|
57
|
+
onBlur?: (...args: any[]) => void;
|
|
58
|
+
/** Imperative ref to the rendered input */
|
|
59
|
+
ref?: RefObject<any>;
|
|
60
|
+
/** Additional props passed to the underlying input */
|
|
61
|
+
[key: string]: any;
|
|
62
|
+
}
|
|
63
|
+
declare const _default: import("react").ComponentType<InputProps>;
|
|
64
|
+
export default _default;
|
|
65
|
+
export { default as wrapInput, isWrapped, IS_WRAPPED } from './wrapInput';
|
|
66
|
+
export { default as guessInput } from './helpers/guessInput';
|
|
67
|
+
export { setCustomInputs, customInputs } from './globalCustomInputs';
|
|
68
|
+
export { useInputMeta } from './inputs';
|
|
69
|
+
export { default as useCustomInputs, CustomInputsContext } from './useCustomInputs';
|
package/index.tsx
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { useRef, useImperativeHandle, type ReactNode, type RefObject } from 'react'
|
|
2
|
+
import { pug, observer } from 'startupjs'
|
|
3
|
+
import guessInput from './helpers/guessInput'
|
|
4
|
+
import getInputTestId from './helpers/getInputTestId'
|
|
5
|
+
import { useInputMeta } from './inputs'
|
|
6
|
+
import type { InputWrapperConfiguration } from './wrapInput'
|
|
7
|
+
|
|
8
|
+
export const _PropsJsonSchema = {/* InputProps */}
|
|
9
|
+
|
|
10
|
+
export interface InputProps {
|
|
11
|
+
/** Explicit input type override (ignores schema guessing) */
|
|
12
|
+
input?: 'array' | 'checkbox' | 'color' | 'date' | 'datetime' | 'time' | 'multiselect' | 'number' | 'object' | 'password' | 'file' | 'rank' | 'radio' | 'range' | 'select' | 'text'
|
|
13
|
+
/** Schema or input type @default 'text' */
|
|
14
|
+
type?: 'array' | 'checkbox' | 'color' | 'date' | 'datetime' | 'time' | 'multiselect' | 'number' | 'object' | 'password' | 'file' | 'rank' | 'radio' | 'range' | 'select' | 'text' | 'string' | 'boolean' | 'integer'
|
|
15
|
+
/** Input value */
|
|
16
|
+
value?: any
|
|
17
|
+
/** Two-way binding for value */
|
|
18
|
+
$value?: any
|
|
19
|
+
/** Label text */
|
|
20
|
+
label?: string
|
|
21
|
+
/** Description text */
|
|
22
|
+
description?: string
|
|
23
|
+
/** Layout for label and description @default 'rows' when label/description is present */
|
|
24
|
+
layout?: 'pure' | 'rows' | 'columns'
|
|
25
|
+
/** Configuration overrides for the wrapper */
|
|
26
|
+
configuration?: InputWrapperConfiguration
|
|
27
|
+
/** Error message or list of messages */
|
|
28
|
+
error?: string | string[]
|
|
29
|
+
/** Required flag or json-schema required object */
|
|
30
|
+
required?: boolean | object
|
|
31
|
+
/** Disable interactions */
|
|
32
|
+
disabled?: boolean
|
|
33
|
+
/** Render as read-only */
|
|
34
|
+
readonly?: boolean
|
|
35
|
+
/** Test id for generated testID */
|
|
36
|
+
testId?: string
|
|
37
|
+
/** Placeholder text */
|
|
38
|
+
placeholder?: string
|
|
39
|
+
/** Options for select-like inputs */
|
|
40
|
+
options?: any
|
|
41
|
+
/** Schema enum for select-like inputs */
|
|
42
|
+
enum?: any[]
|
|
43
|
+
/** Schema items for array inputs */
|
|
44
|
+
items?: any
|
|
45
|
+
/** Schema properties for object inputs */
|
|
46
|
+
properties?: Record<string, any>
|
|
47
|
+
/** Value change handler (checkbox/select/range/etc.) */
|
|
48
|
+
onChange?: (...args: any[]) => void
|
|
49
|
+
/** Text change handler */
|
|
50
|
+
onChangeText?: (...args: any[]) => void
|
|
51
|
+
/** Number change handler */
|
|
52
|
+
onChangeNumber?: (...args: any[]) => void
|
|
53
|
+
/** Date/time change handler */
|
|
54
|
+
onChangeDate?: (...args: any[]) => void
|
|
55
|
+
/** Color change handler */
|
|
56
|
+
onChangeColor?: (...args: any[]) => void
|
|
57
|
+
/** Focus handler */
|
|
58
|
+
onFocus?: (...args: any[]) => void
|
|
59
|
+
/** Blur handler */
|
|
60
|
+
onBlur?: (...args: any[]) => void
|
|
61
|
+
/** Imperative ref to the rendered input */
|
|
62
|
+
ref?: RefObject<any>
|
|
63
|
+
/** Additional props passed to the underlying input */
|
|
64
|
+
[key: string]: any
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function Input ({
|
|
68
|
+
input,
|
|
69
|
+
type = 'text',
|
|
70
|
+
ref,
|
|
71
|
+
...props
|
|
72
|
+
}: InputProps): ReactNode {
|
|
73
|
+
const inputType = guessInput(input, type, props)
|
|
74
|
+
|
|
75
|
+
const testID = getInputTestId(props)
|
|
76
|
+
const { Component, useProps } = useInputMeta(inputType)
|
|
77
|
+
|
|
78
|
+
if (!Component) {
|
|
79
|
+
throw Error(`
|
|
80
|
+
Input component for '${inputType}' not found.
|
|
81
|
+
Make sure you have passed it to 'customInputs' in your Form
|
|
82
|
+
or connected it as a plugin in the 'customFormInputs' hook.
|
|
83
|
+
`)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ref: https://stackoverflow.com/a/68163315 (why innerRef is needed here)
|
|
87
|
+
const innerRef = useRef<any>(null)
|
|
88
|
+
|
|
89
|
+
const componentProps = useProps({ ...props, testID }, innerRef)
|
|
90
|
+
|
|
91
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
92
|
+
useImperativeHandle(ref, () => innerRef.current, [Component])
|
|
93
|
+
|
|
94
|
+
return pug`
|
|
95
|
+
Component(
|
|
96
|
+
ref=innerRef
|
|
97
|
+
...componentProps
|
|
98
|
+
)
|
|
99
|
+
`
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export default observer(Input)
|
|
103
|
+
|
|
104
|
+
export { default as wrapInput, isWrapped, IS_WRAPPED } from './wrapInput'
|
|
105
|
+
export { default as guessInput } from './helpers/guessInput'
|
|
106
|
+
export { setCustomInputs, customInputs } from './globalCustomInputs'
|
|
107
|
+
export { useInputMeta } from './inputs'
|
|
108
|
+
export { default as useCustomInputs, CustomInputsContext } from './useCustomInputs'
|
package/inputs.ts
ADDED
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
import { type ReactNode, type RefObject } from 'react'
|
|
2
|
+
import { type StyleProp, type ViewStyle } from 'react-native'
|
|
3
|
+
import { pug, useBind } from 'startupjs'
|
|
4
|
+
import ArrayInput from '@startupjs-ui/array-input'
|
|
5
|
+
import Card from '@startupjs-ui/card'
|
|
6
|
+
import Checkbox from '@startupjs-ui/checkbox'
|
|
7
|
+
import ColorPicker from '@startupjs-ui/color-picker'
|
|
8
|
+
import FileInput from '@startupjs-ui/file-input'
|
|
9
|
+
import DateTimePicker from '@startupjs-ui/date-time-picker'
|
|
10
|
+
import Multiselect from '@startupjs-ui/multi-select'
|
|
11
|
+
import NumberInput from '@startupjs-ui/number-input'
|
|
12
|
+
import ObjectInput from '@startupjs-ui/object-input'
|
|
13
|
+
import PasswordInput from '@startupjs-ui/password-input'
|
|
14
|
+
import Rank from '@startupjs-ui/rank'
|
|
15
|
+
import Radio from '@startupjs-ui/radio'
|
|
16
|
+
import RangeInput from '@startupjs-ui/range-input'
|
|
17
|
+
import Select from '@startupjs-ui/select'
|
|
18
|
+
import TextInput from '@startupjs-ui/text-input'
|
|
19
|
+
import wrapInput, { isWrapped } from './wrapInput'
|
|
20
|
+
import useCustomInputs from './useCustomInputs'
|
|
21
|
+
import { customInputs } from './globalCustomInputs'
|
|
22
|
+
|
|
23
|
+
export type InputUseProps = (
|
|
24
|
+
props: Record<string, any>,
|
|
25
|
+
ref?: RefObject<any>
|
|
26
|
+
) => Record<string, any>
|
|
27
|
+
|
|
28
|
+
export interface InputComponentMeta {
|
|
29
|
+
Component: any
|
|
30
|
+
useProps?: InputUseProps
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function useBoundProps<T extends Record<string, any>> (props: T): T {
|
|
34
|
+
return useBind(props) as T
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const useArrayProps = (props: Record<string, any>): Record<string, any> => {
|
|
38
|
+
return props
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const useCheckboxProps = ({ value, $value, onChange, ...props }: Record<string, any>): Record<string, any> => {
|
|
42
|
+
;({ value, onChange } = useBoundProps({ value, $value, onChange }))
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
value,
|
|
46
|
+
configuration: { isLabelClickable: !props.disabled && !props.readonly },
|
|
47
|
+
onChange,
|
|
48
|
+
_onLabelPress: () => { onChange(!value) },
|
|
49
|
+
...props
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const useColorProps = ({
|
|
54
|
+
value,
|
|
55
|
+
$value,
|
|
56
|
+
onChangeColor,
|
|
57
|
+
...props
|
|
58
|
+
}: Record<string, any>, ref?: RefObject<any>): Record<string, any> => {
|
|
59
|
+
;({ value, onChangeColor } = useBoundProps({ value, $value, onChangeColor }))
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
value,
|
|
63
|
+
configuration: { isLabelClickable: !props.disabled },
|
|
64
|
+
onChangeColor,
|
|
65
|
+
_onLabelPress: () => { ref?.current?.show() },
|
|
66
|
+
...props
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const useDateProps = ({
|
|
71
|
+
value,
|
|
72
|
+
$value,
|
|
73
|
+
onChangeDate,
|
|
74
|
+
...props
|
|
75
|
+
}: Record<string, any>, ref?: RefObject<any>): Record<string, any> => {
|
|
76
|
+
;({ value, onChangeDate } = useBoundProps({ value, $value, onChangeDate }))
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
mode: 'date',
|
|
80
|
+
date: value,
|
|
81
|
+
configuration: { isLabelClickable: !props.disabled && !props.readonly },
|
|
82
|
+
onChangeDate,
|
|
83
|
+
_onLabelPress: () => ref?.current?.focus(),
|
|
84
|
+
...props
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const useDateTimeProps = ({
|
|
89
|
+
value,
|
|
90
|
+
$value,
|
|
91
|
+
onChangeDate,
|
|
92
|
+
...props
|
|
93
|
+
}: Record<string, any>, ref?: RefObject<any>): Record<string, any> => {
|
|
94
|
+
;({ value, onChangeDate } = useBoundProps({ value, $value, onChangeDate }))
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
mode: 'datetime',
|
|
98
|
+
date: value,
|
|
99
|
+
configuration: { isLabelClickable: !props.disabled && !props.readonly },
|
|
100
|
+
onChangeDate,
|
|
101
|
+
_onLabelPress: () => ref?.current?.focus(),
|
|
102
|
+
...props
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const useTimeProps = ({
|
|
107
|
+
value,
|
|
108
|
+
$value,
|
|
109
|
+
onChangeDate,
|
|
110
|
+
...props
|
|
111
|
+
}: Record<string, any>, ref?: RefObject<any>): Record<string, any> => {
|
|
112
|
+
;({ value, onChangeDate } = useBoundProps({ value, $value, onChangeDate }))
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
mode: 'time',
|
|
116
|
+
date: value,
|
|
117
|
+
configuration: { isLabelClickable: !props.disabled && !props.readonly },
|
|
118
|
+
onChangeDate,
|
|
119
|
+
_onLabelPress: () => ref?.current?.focus(),
|
|
120
|
+
...props
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const useMultiselectProps = ({
|
|
125
|
+
value,
|
|
126
|
+
$value,
|
|
127
|
+
onChange,
|
|
128
|
+
...props
|
|
129
|
+
}: Record<string, any>, ref?: RefObject<any>): Record<string, any> => {
|
|
130
|
+
;({ value, onChange } = useBoundProps({ value, $value, onChange }))
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
value,
|
|
134
|
+
configuration: { isLabelClickable: !props.disabled && !props.readonly },
|
|
135
|
+
onChange,
|
|
136
|
+
_onLabelPress: () => ref?.current?.focus(),
|
|
137
|
+
...props
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const useNumberProps = ({
|
|
142
|
+
value,
|
|
143
|
+
$value,
|
|
144
|
+
onChangeNumber,
|
|
145
|
+
...props
|
|
146
|
+
}: Record<string, any>, ref?: RefObject<any>): Record<string, any> => {
|
|
147
|
+
;({ value, onChangeNumber } = useBoundProps({ value, $value, onChangeNumber }))
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
value,
|
|
151
|
+
configuration: { isLabelClickable: !props.disabled && !props.readonly },
|
|
152
|
+
onChangeNumber,
|
|
153
|
+
_onLabelPress: () => ref?.current?.focus(),
|
|
154
|
+
...props
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const useObjectProps = (props: Record<string, any>): Record<string, any> => {
|
|
159
|
+
return props
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const usePasswordProps = ({
|
|
163
|
+
value,
|
|
164
|
+
$value,
|
|
165
|
+
onChangeText,
|
|
166
|
+
...props
|
|
167
|
+
}: Record<string, any>, ref?: RefObject<any>): Record<string, any> => {
|
|
168
|
+
;({ value, onChangeText } = useBoundProps({ value, $value, onChangeText }))
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
value,
|
|
172
|
+
configuration: { isLabelClickable: !props.disabled && !props.readonly },
|
|
173
|
+
onChangeText,
|
|
174
|
+
_onLabelPress: () => ref?.current?.focus(),
|
|
175
|
+
...props
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const useFileProps = ({ value, $value, onChange, ...props }: Record<string, any>): Record<string, any> => {
|
|
180
|
+
;({ value, onChange } = useBoundProps({ value, $value, onChange }))
|
|
181
|
+
return {
|
|
182
|
+
value,
|
|
183
|
+
onChange,
|
|
184
|
+
...props
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const useRankProps = ({ value, $value, onChange, ...props }: Record<string, any>): Record<string, any> => {
|
|
189
|
+
;({ value, onChange } = useBoundProps({ value, $value, onChange }))
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
value,
|
|
193
|
+
onChange,
|
|
194
|
+
...props
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const useRadioProps = ({ value, $value, onChange, ...props }: Record<string, any>): Record<string, any> => {
|
|
199
|
+
;({ value, onChange } = useBoundProps({ value, $value, onChange }))
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
value,
|
|
203
|
+
onChange,
|
|
204
|
+
...props
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const useRangeProps = ({ value, $value, onChange, ...props }: Record<string, any>): Record<string, any> => {
|
|
209
|
+
;({ value, onChange } = useBoundProps({ value, $value, onChange }))
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
value,
|
|
213
|
+
onChange,
|
|
214
|
+
...props
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const useSelectProps = ({ value, $value, enum: _enum, options, onChange, ...props }: Record<string, any>): Record<string, any> => {
|
|
219
|
+
;({ value, onChange } = useBoundProps({ value, $value, onChange }))
|
|
220
|
+
// if json-schema `enum` is passed, use it as options
|
|
221
|
+
if (!options && _enum) options = _enum
|
|
222
|
+
return {
|
|
223
|
+
value,
|
|
224
|
+
onChange,
|
|
225
|
+
options,
|
|
226
|
+
...props
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const useTextProps = ({
|
|
231
|
+
value,
|
|
232
|
+
$value,
|
|
233
|
+
onChangeText,
|
|
234
|
+
...props
|
|
235
|
+
}: Record<string, any>, ref?: RefObject<any>): Record<string, any> => {
|
|
236
|
+
;({ value, onChangeText } = useBoundProps({ value, $value, onChangeText }))
|
|
237
|
+
return {
|
|
238
|
+
value,
|
|
239
|
+
configuration: { isLabelClickable: !props.disabled && !props.readonly },
|
|
240
|
+
onChangeText,
|
|
241
|
+
_onLabelPress: () => ref?.current?.focus(),
|
|
242
|
+
...props
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function cardWrapper (style: StyleProp<ViewStyle> | undefined, children: ReactNode): ReactNode {
|
|
247
|
+
return pug`
|
|
248
|
+
Card(
|
|
249
|
+
style=style
|
|
250
|
+
variant='outlined'
|
|
251
|
+
)
|
|
252
|
+
= children
|
|
253
|
+
`
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// NOTE: lazy initialization is needed to prevent circular dependencies
|
|
257
|
+
// with 'wrapInput': ArrayInput and ObjectInput depend on Input (this file)
|
|
258
|
+
let _defaultInputs: Record<string, InputComponentMeta>
|
|
259
|
+
function getDefaultInputs () {
|
|
260
|
+
if (_defaultInputs) return _defaultInputs
|
|
261
|
+
const WrappedArrayInput = wrapInput(
|
|
262
|
+
ArrayInput,
|
|
263
|
+
{
|
|
264
|
+
rows: { _renderWrapper: cardWrapper },
|
|
265
|
+
columns: { _renderWrapper: cardWrapper }
|
|
266
|
+
}
|
|
267
|
+
)
|
|
268
|
+
const WrappedCheckbox = wrapInput(
|
|
269
|
+
Checkbox,
|
|
270
|
+
{
|
|
271
|
+
rows: {
|
|
272
|
+
labelPosition: 'right',
|
|
273
|
+
descriptionPosition: 'bottom'
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
)
|
|
277
|
+
const WrappedColorPicker = wrapInput(
|
|
278
|
+
ColorPicker,
|
|
279
|
+
{ rows: { descriptionPosition: 'bottom' } }
|
|
280
|
+
)
|
|
281
|
+
const WrappedDateTimePicker = wrapInput(
|
|
282
|
+
DateTimePicker,
|
|
283
|
+
{
|
|
284
|
+
rows: { descriptionPosition: 'bottom' },
|
|
285
|
+
isLabelColoredWhenFocusing: true
|
|
286
|
+
}
|
|
287
|
+
)
|
|
288
|
+
const WrappedMultiselect = wrapInput(
|
|
289
|
+
Multiselect,
|
|
290
|
+
{
|
|
291
|
+
isLabelColoredWhenFocusing: true
|
|
292
|
+
}
|
|
293
|
+
)
|
|
294
|
+
const WrappedNumberInput = wrapInput(
|
|
295
|
+
NumberInput,
|
|
296
|
+
{
|
|
297
|
+
rows: {
|
|
298
|
+
descriptionPosition: 'bottom'
|
|
299
|
+
},
|
|
300
|
+
isLabelColoredWhenFocusing: true
|
|
301
|
+
}
|
|
302
|
+
)
|
|
303
|
+
const WrappedObjectInput = wrapInput(
|
|
304
|
+
ObjectInput,
|
|
305
|
+
{
|
|
306
|
+
rows: { _renderWrapper: cardWrapper },
|
|
307
|
+
columns: { _renderWrapper: cardWrapper }
|
|
308
|
+
}
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
const WrappedPasswordInput = wrapInput(
|
|
312
|
+
PasswordInput,
|
|
313
|
+
{
|
|
314
|
+
rows: {
|
|
315
|
+
descriptionPosition: 'bottom'
|
|
316
|
+
},
|
|
317
|
+
isLabelColoredWhenFocusing: true
|
|
318
|
+
}
|
|
319
|
+
)
|
|
320
|
+
const WrappedFileInput = wrapInput(FileInput)
|
|
321
|
+
const WrappedRank = wrapInput(Rank)
|
|
322
|
+
const WrappedRadio = wrapInput(Radio)
|
|
323
|
+
const WrappedSelect = wrapInput(
|
|
324
|
+
Select,
|
|
325
|
+
{
|
|
326
|
+
rows: {
|
|
327
|
+
descriptionPosition: 'bottom'
|
|
328
|
+
},
|
|
329
|
+
isLabelColoredWhenFocusing: true
|
|
330
|
+
}
|
|
331
|
+
)
|
|
332
|
+
const WrappedTextInput = wrapInput(
|
|
333
|
+
TextInput,
|
|
334
|
+
{
|
|
335
|
+
rows: {
|
|
336
|
+
descriptionPosition: 'bottom'
|
|
337
|
+
},
|
|
338
|
+
isLabelColoredWhenFocusing: true
|
|
339
|
+
}
|
|
340
|
+
)
|
|
341
|
+
const WrappedRange = wrapInput(RangeInput)
|
|
342
|
+
|
|
343
|
+
_defaultInputs = {
|
|
344
|
+
array: {
|
|
345
|
+
Component: WrappedArrayInput,
|
|
346
|
+
useProps: useArrayProps
|
|
347
|
+
},
|
|
348
|
+
checkbox: {
|
|
349
|
+
Component: WrappedCheckbox,
|
|
350
|
+
useProps: useCheckboxProps
|
|
351
|
+
},
|
|
352
|
+
color: {
|
|
353
|
+
Component: WrappedColorPicker,
|
|
354
|
+
useProps: useColorProps
|
|
355
|
+
},
|
|
356
|
+
date: {
|
|
357
|
+
Component: WrappedDateTimePicker,
|
|
358
|
+
useProps: useDateProps
|
|
359
|
+
},
|
|
360
|
+
datetime: {
|
|
361
|
+
Component: WrappedDateTimePicker,
|
|
362
|
+
useProps: useDateTimeProps
|
|
363
|
+
},
|
|
364
|
+
time: {
|
|
365
|
+
Component: WrappedDateTimePicker,
|
|
366
|
+
useProps: useTimeProps
|
|
367
|
+
},
|
|
368
|
+
multiselect: {
|
|
369
|
+
Component: WrappedMultiselect,
|
|
370
|
+
useProps: useMultiselectProps
|
|
371
|
+
},
|
|
372
|
+
number: {
|
|
373
|
+
Component: WrappedNumberInput,
|
|
374
|
+
useProps: useNumberProps
|
|
375
|
+
},
|
|
376
|
+
object: {
|
|
377
|
+
Component: WrappedObjectInput,
|
|
378
|
+
useProps: useObjectProps
|
|
379
|
+
},
|
|
380
|
+
password: {
|
|
381
|
+
Component: WrappedPasswordInput,
|
|
382
|
+
useProps: usePasswordProps
|
|
383
|
+
},
|
|
384
|
+
file: {
|
|
385
|
+
Component: WrappedFileInput,
|
|
386
|
+
useProps: useFileProps
|
|
387
|
+
},
|
|
388
|
+
rank: {
|
|
389
|
+
Component: WrappedRank,
|
|
390
|
+
useProps: useRankProps
|
|
391
|
+
},
|
|
392
|
+
radio: {
|
|
393
|
+
Component: WrappedRadio,
|
|
394
|
+
useProps: useRadioProps
|
|
395
|
+
},
|
|
396
|
+
range: {
|
|
397
|
+
Component: WrappedRange,
|
|
398
|
+
useProps: useRangeProps
|
|
399
|
+
},
|
|
400
|
+
select: {
|
|
401
|
+
Component: WrappedSelect,
|
|
402
|
+
useProps: useSelectProps
|
|
403
|
+
},
|
|
404
|
+
text: {
|
|
405
|
+
Component: WrappedTextInput,
|
|
406
|
+
useProps: useTextProps
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return _defaultInputs
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
export function useInputMeta (input: string): { Component: any, useProps: InputUseProps } {
|
|
413
|
+
const customInputsFromContext = useCustomInputs()
|
|
414
|
+
const componentMeta = customInputsFromContext[input] || customInputs[input] || getDefaultInputs()?.[input]
|
|
415
|
+
if (!componentMeta) throw Error(ERRORS.inputNotFound(input))
|
|
416
|
+
let Component
|
|
417
|
+
let useProps: InputUseProps | undefined
|
|
418
|
+
if (componentMeta.Component) {
|
|
419
|
+
;({ Component, useProps } = componentMeta)
|
|
420
|
+
} else {
|
|
421
|
+
Component = componentMeta
|
|
422
|
+
}
|
|
423
|
+
if (!isWrapped(Component)) {
|
|
424
|
+
if (!autoWrappedInputs.has(Component)) {
|
|
425
|
+
autoWrappedInputs.set(Component, wrapInput(Component))
|
|
426
|
+
}
|
|
427
|
+
Component = autoWrappedInputs.get(Component)
|
|
428
|
+
}
|
|
429
|
+
useProps ??= (props: Record<string, any>) => props
|
|
430
|
+
return { Component, useProps }
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const autoWrappedInputs = new WeakMap<any, any>()
|
|
434
|
+
|
|
435
|
+
const ERRORS = {
|
|
436
|
+
inputAlreadyDefined: (input: string) => `
|
|
437
|
+
Custom input type "${input}" is already defined by another plugin. It will be overridden!
|
|
438
|
+
`,
|
|
439
|
+
inputNotFound: (input: string) => `
|
|
440
|
+
Implementation for a custom input type "${input}" was not found!
|
|
441
|
+
`
|
|
442
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@startupjs-ui/input",
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"main": "index.tsx",
|
|
8
|
+
"types": "index.d.ts",
|
|
9
|
+
"type": "module",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./index.tsx",
|
|
12
|
+
"./globalCustomInputs": "./globalCustomInputs.ts"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@fortawesome/free-solid-svg-icons": "^5.12.0",
|
|
16
|
+
"@startupjs-ui/array-input": "^0.1.3",
|
|
17
|
+
"@startupjs-ui/card": "^0.1.3",
|
|
18
|
+
"@startupjs-ui/checkbox": "^0.1.3",
|
|
19
|
+
"@startupjs-ui/color-picker": "^0.1.3",
|
|
20
|
+
"@startupjs-ui/core": "^0.1.3",
|
|
21
|
+
"@startupjs-ui/date-time-picker": "^0.1.3",
|
|
22
|
+
"@startupjs-ui/div": "^0.1.3",
|
|
23
|
+
"@startupjs-ui/file-input": "^0.1.3",
|
|
24
|
+
"@startupjs-ui/icon": "^0.1.3",
|
|
25
|
+
"@startupjs-ui/multi-select": "^0.1.3",
|
|
26
|
+
"@startupjs-ui/number-input": "^0.1.3",
|
|
27
|
+
"@startupjs-ui/object-input": "^0.1.3",
|
|
28
|
+
"@startupjs-ui/password-input": "^0.1.3",
|
|
29
|
+
"@startupjs-ui/radio": "^0.1.3",
|
|
30
|
+
"@startupjs-ui/range-input": "^0.1.3",
|
|
31
|
+
"@startupjs-ui/rank": "^0.1.3",
|
|
32
|
+
"@startupjs-ui/select": "^0.1.3",
|
|
33
|
+
"@startupjs-ui/span": "^0.1.3",
|
|
34
|
+
"@startupjs-ui/text-input": "^0.1.3",
|
|
35
|
+
"lodash": "^4.17.20"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"react": "*",
|
|
39
|
+
"react-native": "*",
|
|
40
|
+
"startupjs": "*"
|
|
41
|
+
},
|
|
42
|
+
"gitHead": "fd964ebc3892d3dd0a6c85438c0af619cc50c3f0"
|
|
43
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createContext, useContext, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
export const CustomInputsContext = createContext<Record<string, any> | undefined>(undefined)
|
|
4
|
+
|
|
5
|
+
export default function useCustomInputs (): Record<string, any> {
|
|
6
|
+
// useState is used to avoid re-creating the object on every render
|
|
7
|
+
const [empty] = useState<Record<string, any>>({})
|
|
8
|
+
return useContext(CustomInputsContext) ?? empty
|
|
9
|
+
}
|
package/useLayout.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { useMedia } from '@startupjs-ui/core'
|
|
2
|
+
|
|
3
|
+
export default function useLayout ({
|
|
4
|
+
layout,
|
|
5
|
+
label,
|
|
6
|
+
description
|
|
7
|
+
}: {
|
|
8
|
+
layout?: 'pure' | 'rows' | 'columns'
|
|
9
|
+
label?: string
|
|
10
|
+
description?: string
|
|
11
|
+
} = {}): 'pure' | 'rows' | 'columns' {
|
|
12
|
+
const { tablet } = useMedia()
|
|
13
|
+
|
|
14
|
+
const hasLabel = Boolean(label)
|
|
15
|
+
const hasDescription = Boolean(description)
|
|
16
|
+
layout = layout ?? (hasLabel || hasDescription ? 'rows' : 'pure')
|
|
17
|
+
if (layout !== 'pure' && !tablet) layout = 'rows'
|
|
18
|
+
return layout
|
|
19
|
+
}
|
package/wrapInput.tsx
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { useEffect, useState, type ReactNode, type RefObject } from 'react'
|
|
2
|
+
import { Text } from 'react-native'
|
|
3
|
+
import { pug, styl, observer } from 'startupjs'
|
|
4
|
+
import { themed } from '@startupjs-ui/core'
|
|
5
|
+
import Div from '@startupjs-ui/div'
|
|
6
|
+
import Icon from '@startupjs-ui/icon'
|
|
7
|
+
import Span from '@startupjs-ui/span'
|
|
8
|
+
import { faExclamationCircle } from '@fortawesome/free-solid-svg-icons/faExclamationCircle'
|
|
9
|
+
import merge from 'lodash/merge'
|
|
10
|
+
import useLayout from './useLayout'
|
|
11
|
+
|
|
12
|
+
export const IS_WRAPPED = Symbol('wrapped into wrapInput()')
|
|
13
|
+
|
|
14
|
+
export type InputLayout = 'pure' | 'rows' | 'columns'
|
|
15
|
+
|
|
16
|
+
export interface InputWrapperLayoutConfiguration {
|
|
17
|
+
labelPosition?: 'top' | 'right'
|
|
18
|
+
descriptionPosition?: 'top' | 'bottom'
|
|
19
|
+
_renderWrapper?: any
|
|
20
|
+
[key: string]: any
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface InputWrapperConfiguration extends InputWrapperLayoutConfiguration {
|
|
24
|
+
rows?: InputWrapperLayoutConfiguration
|
|
25
|
+
columns?: InputWrapperLayoutConfiguration
|
|
26
|
+
isLabelColoredWhenFocusing?: boolean
|
|
27
|
+
isLabelClickable?: boolean
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface InputWrapperProps {
|
|
31
|
+
label?: string
|
|
32
|
+
description?: string
|
|
33
|
+
layout?: InputLayout
|
|
34
|
+
configuration?: InputWrapperConfiguration
|
|
35
|
+
error?: string | string[]
|
|
36
|
+
required?: boolean | object
|
|
37
|
+
disabled?: boolean
|
|
38
|
+
readonly?: boolean
|
|
39
|
+
onFocus?: (...args: any[]) => void
|
|
40
|
+
onBlur?: (...args: any[]) => void
|
|
41
|
+
_onLabelPress?: () => void
|
|
42
|
+
ref?: RefObject<any>
|
|
43
|
+
style?: any
|
|
44
|
+
[key: string]: any
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function isWrapped (Component: any): boolean {
|
|
48
|
+
return Component[IS_WRAPPED]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export default function wrapInput (Component: any, configuration: InputWrapperConfiguration = {}): any {
|
|
52
|
+
configuration = merge(
|
|
53
|
+
{
|
|
54
|
+
rows: {
|
|
55
|
+
labelPosition: 'top',
|
|
56
|
+
descriptionPosition: 'top'
|
|
57
|
+
},
|
|
58
|
+
isLabelColoredWhenFocusing: false,
|
|
59
|
+
isLabelClickable: false
|
|
60
|
+
},
|
|
61
|
+
configuration
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
function InputWrapper ({
|
|
65
|
+
label,
|
|
66
|
+
description,
|
|
67
|
+
layout,
|
|
68
|
+
configuration: componentConfiguration,
|
|
69
|
+
error,
|
|
70
|
+
onFocus,
|
|
71
|
+
required,
|
|
72
|
+
onBlur,
|
|
73
|
+
_onLabelPress,
|
|
74
|
+
ref,
|
|
75
|
+
...props
|
|
76
|
+
}: InputWrapperProps): ReactNode {
|
|
77
|
+
const currentLayout = useLayout({
|
|
78
|
+
layout,
|
|
79
|
+
label,
|
|
80
|
+
description
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
configuration = merge(configuration, componentConfiguration)
|
|
84
|
+
configuration = merge(configuration, configuration[currentLayout])
|
|
85
|
+
|
|
86
|
+
const {
|
|
87
|
+
labelPosition,
|
|
88
|
+
descriptionPosition,
|
|
89
|
+
isLabelColoredWhenFocusing,
|
|
90
|
+
isLabelClickable
|
|
91
|
+
} = configuration
|
|
92
|
+
|
|
93
|
+
const [focused, setFocused] = useState(false)
|
|
94
|
+
const isReadOnlyOrDisabled = [props.readonly, props.disabled].some(Boolean)
|
|
95
|
+
|
|
96
|
+
function handleFocus (...args: any[]) {
|
|
97
|
+
setFocused(true)
|
|
98
|
+
onFocus && onFocus(...args)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function handleBlur (...args: any[]) {
|
|
102
|
+
setFocused(false)
|
|
103
|
+
onBlur && onBlur(...args)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// NOTE
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
if (!isLabelColoredWhenFocusing) return
|
|
109
|
+
if (focused && isReadOnlyOrDisabled) setFocused(false)
|
|
110
|
+
}, [focused, isLabelColoredWhenFocusing, isReadOnlyOrDisabled])
|
|
111
|
+
|
|
112
|
+
const hasError = Array.isArray(error) ? error.length > 0 : !!error
|
|
113
|
+
|
|
114
|
+
const _label = pug`
|
|
115
|
+
if label
|
|
116
|
+
Span.label(
|
|
117
|
+
key='label'
|
|
118
|
+
part='label'
|
|
119
|
+
styleName=[
|
|
120
|
+
currentLayout,
|
|
121
|
+
currentLayout + '-' + labelPosition,
|
|
122
|
+
{
|
|
123
|
+
focused: isLabelColoredWhenFocusing ? focused : false,
|
|
124
|
+
error: hasError
|
|
125
|
+
}
|
|
126
|
+
]
|
|
127
|
+
onPress=isLabelClickable
|
|
128
|
+
? _onLabelPress
|
|
129
|
+
: undefined
|
|
130
|
+
)
|
|
131
|
+
= label
|
|
132
|
+
if required === true
|
|
133
|
+
Text.required= ' *'
|
|
134
|
+
`
|
|
135
|
+
const _description = pug`
|
|
136
|
+
if description
|
|
137
|
+
Span.description(
|
|
138
|
+
key='description'
|
|
139
|
+
part='description'
|
|
140
|
+
styleName=[
|
|
141
|
+
currentLayout,
|
|
142
|
+
descriptionPosition,
|
|
143
|
+
currentLayout + '-' + descriptionPosition
|
|
144
|
+
]
|
|
145
|
+
description
|
|
146
|
+
)= description
|
|
147
|
+
`
|
|
148
|
+
|
|
149
|
+
const passRef = ref ? { ref } : {}
|
|
150
|
+
|
|
151
|
+
const input = pug`
|
|
152
|
+
Component(
|
|
153
|
+
key='input'
|
|
154
|
+
part='wrapper'
|
|
155
|
+
layout=currentLayout
|
|
156
|
+
_hasError=hasError
|
|
157
|
+
onFocus=handleFocus
|
|
158
|
+
onBlur=handleBlur
|
|
159
|
+
...passRef
|
|
160
|
+
...props
|
|
161
|
+
)
|
|
162
|
+
`
|
|
163
|
+
const err = pug`
|
|
164
|
+
if hasError
|
|
165
|
+
each _error, index in (Array.isArray(error) ? error : [error])
|
|
166
|
+
Div.errorContainer(
|
|
167
|
+
key='error-' + index
|
|
168
|
+
styleName=[
|
|
169
|
+
currentLayout,
|
|
170
|
+
currentLayout + '-' + descriptionPosition,
|
|
171
|
+
]
|
|
172
|
+
vAlign='center'
|
|
173
|
+
row
|
|
174
|
+
)
|
|
175
|
+
Icon.errorContainer-icon(icon=faExclamationCircle)
|
|
176
|
+
Span.errorContainer-text= _error
|
|
177
|
+
`
|
|
178
|
+
|
|
179
|
+
return pug`
|
|
180
|
+
Div.root(
|
|
181
|
+
part='root'
|
|
182
|
+
styleName=[currentLayout]
|
|
183
|
+
)
|
|
184
|
+
if currentLayout === 'rows'
|
|
185
|
+
if labelPosition === 'top'
|
|
186
|
+
= _label
|
|
187
|
+
if descriptionPosition === 'top'
|
|
188
|
+
= _description
|
|
189
|
+
= err
|
|
190
|
+
if labelPosition === 'right'
|
|
191
|
+
Div(vAlign='center' row)
|
|
192
|
+
= input
|
|
193
|
+
= _label
|
|
194
|
+
else
|
|
195
|
+
= input
|
|
196
|
+
if descriptionPosition === 'bottom'
|
|
197
|
+
= err
|
|
198
|
+
= _description
|
|
199
|
+
else if currentLayout === 'columns'
|
|
200
|
+
Div.leftBlock
|
|
201
|
+
= _label
|
|
202
|
+
= _description
|
|
203
|
+
Div.rightBlock
|
|
204
|
+
= input
|
|
205
|
+
= err
|
|
206
|
+
else if currentLayout === 'pure'
|
|
207
|
+
= input
|
|
208
|
+
= err
|
|
209
|
+
`
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const componentDisplayName = Component.displayName ?? Component.name
|
|
213
|
+
|
|
214
|
+
InputWrapper.displayName = componentDisplayName + 'InputWrapper'
|
|
215
|
+
|
|
216
|
+
const ObservedInputWrapper = observer(
|
|
217
|
+
themed('InputWrapper', InputWrapper)
|
|
218
|
+
) as any
|
|
219
|
+
|
|
220
|
+
ObservedInputWrapper[IS_WRAPPED] = true
|
|
221
|
+
|
|
222
|
+
return ObservedInputWrapper
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
styl`
|
|
226
|
+
$errorColor = var(--color-text-error)
|
|
227
|
+
$focusedColor = var(--color-text-primary)
|
|
228
|
+
|
|
229
|
+
// common
|
|
230
|
+
.label
|
|
231
|
+
color var(--InputWrapper-label-color)
|
|
232
|
+
align-self flex-start
|
|
233
|
+
font(body2)
|
|
234
|
+
|
|
235
|
+
&.focused
|
|
236
|
+
color $focusedColor
|
|
237
|
+
|
|
238
|
+
&.error
|
|
239
|
+
color $errorColor
|
|
240
|
+
|
|
241
|
+
.description
|
|
242
|
+
font(caption)
|
|
243
|
+
|
|
244
|
+
.required
|
|
245
|
+
color $errorColor
|
|
246
|
+
font-weight bold
|
|
247
|
+
|
|
248
|
+
.errorContainer
|
|
249
|
+
margin-top 1u
|
|
250
|
+
margin-bottom 0.5u
|
|
251
|
+
|
|
252
|
+
&-icon
|
|
253
|
+
color $errorColor
|
|
254
|
+
|
|
255
|
+
&-text
|
|
256
|
+
font(caption)
|
|
257
|
+
margin-left 0.5u
|
|
258
|
+
color $errorColor
|
|
259
|
+
|
|
260
|
+
// rows
|
|
261
|
+
.rows
|
|
262
|
+
&-top
|
|
263
|
+
.label&
|
|
264
|
+
margin-bottom 0.5u
|
|
265
|
+
|
|
266
|
+
.description&
|
|
267
|
+
margin-bottom 1u
|
|
268
|
+
|
|
269
|
+
.errorContainer&
|
|
270
|
+
margin-top 0
|
|
271
|
+
margin-bottom 1u
|
|
272
|
+
|
|
273
|
+
&-right
|
|
274
|
+
.label&
|
|
275
|
+
margin-left 1u
|
|
276
|
+
|
|
277
|
+
&-bottom
|
|
278
|
+
.description&
|
|
279
|
+
margin-top 0.5u
|
|
280
|
+
|
|
281
|
+
// columns
|
|
282
|
+
.leftBlock
|
|
283
|
+
.rightBlock
|
|
284
|
+
flex 1
|
|
285
|
+
|
|
286
|
+
.leftBlock
|
|
287
|
+
margin-right 1.5u
|
|
288
|
+
|
|
289
|
+
.rightBlock
|
|
290
|
+
margin-left 1.5u
|
|
291
|
+
|
|
292
|
+
.columns
|
|
293
|
+
.root&
|
|
294
|
+
flex-direction row
|
|
295
|
+
align-items center
|
|
296
|
+
|
|
297
|
+
`
|