@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 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,5 @@
1
+ export const customInputs: Record<string, any> = {}
2
+
3
+ export function setCustomInputs (newCustomInputs: Record<string, any> = {}): void {
4
+ Object.assign(customInputs, newCustomInputs)
5
+ }
@@ -0,0 +1,5 @@
1
+ const EXTRA_SCHEMA_TYPES = ['string', 'boolean', 'integer'] as const
2
+
3
+ export type ExtraSchemaType = typeof EXTRA_SCHEMA_TYPES[number]
4
+
5
+ export default EXTRA_SCHEMA_TYPES
@@ -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
+ `