Skip to content

Commit 4d94642

Browse files
committed
feat: Upgraded package versions for NodeJS types, react, ink and ink ui libraries. Modified state to support duplicates and added Add Item + Remove Item buttons
1 parent af553b2 commit 4d94642

7 files changed

Lines changed: 1080 additions & 1439 deletions

File tree

package.json

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,22 +23,22 @@
2323
"license": "MIT",
2424
"bugs": "https://github.com/lukasbach/ink-form/issues",
2525
"devDependencies": {
26-
"@types/node": "^14.14.45",
26+
"@types/node": "20.14.8",
2727
"@types/react": "^18.2.41",
28-
"ink": "^4.4.1",
28+
"ink": "^5.1.0",
2929
"prettier": "^2.2.1",
3030
"publish-fast": "^0.0.20",
3131
"react": "^18.2.0",
32-
"ts-node": "^10.9.1",
32+
"tsx": "^4.19.2",
3333
"typedoc": "^0.25.4",
3434
"typescript": "^5.3.2"
3535
},
3636
"scripts": {
3737
"start": "ts-node-esm src/demo/overview.tsx",
38-
"demo:overview": "ts-node-esm src/demo/overview.tsx",
39-
"demo:packagejson": "ts-node-esm src/demo/packagejson.tsx",
40-
"demo:custommanager": "ts-node-esm src/demo/custommanager.tsx",
41-
"demo:imperative": "ts-node-esm src/demo/imperative.ts",
38+
"demo:overview": "tsx src/demo/overview.tsx",
39+
"demo:packagejson": "tsx src/demo/packagejson.tsx",
40+
"demo:custommanager": "tsx src/demo/custommanager.tsx",
41+
"demo:imperative": "tsx src/demo/imperative.ts",
4242
"build": "tsc",
4343
"build:docs": "typedoc --out docs src/index.ts",
4444
"lint": "prettier --check .",
@@ -47,11 +47,11 @@
4747
"release": "publish-fast"
4848
},
4949
"peerDependencies": {
50-
"ink": ">=4",
50+
"ink": ">=5",
5151
"react": ">=18"
5252
},
5353
"dependencies": {
54-
"ink-select-input": "^5.0.0",
54+
"ink-select-input": "^6.0.0",
5555
"ink-text-input": "^6.0.0"
5656
},
5757
"publishConfig": {

src/Button.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { Box, Text, useFocus, useInput } from 'ink';
2+
import React from 'react';
3+
4+
export function Button(props: {
5+
label: string;
6+
onClicked?: () => void;
7+
}) {
8+
const { isFocused } = useFocus({});
9+
useInput((input, key) => {
10+
if (key.return && isFocused) {
11+
props.onClicked?.();
12+
}
13+
});
14+
15+
return <Box marginX={2} paddingX={1} borderStyle="round" borderColor={isFocused ? 'blue' : 'magenta'}>
16+
<Box flexGrow={1}>
17+
<Text underline={isFocused} color={isFocused ? 'blue' : undefined}>
18+
{props.label}
19+
</Text>
20+
</Box>
21+
</Box>
22+
}

src/Form.tsx

Lines changed: 66 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,14 @@ import { FormFieldRenderer } from './FormFieldRenderer.js';
77
import { DescriptionRenderer } from './DescriptionRenderer.js';
88
import { canSubmit } from './canSubmit.js';
99
import { SubmitButton } from './SubmitButton.js';
10+
import { Button } from './Button.js';
1011

1112
export const Form: React.FC<FormProps> = props => {
1213
const isControlled = props.value !== undefined;
1314
const [currentTab, setCurrentTab] = useState(0);
14-
const [value, setValue] = useState<object>(props.value ?? {});
15+
const [isSubmitted, setIsSubmitted] = useState(false);
16+
const [sections, setSections] = useState(props.form.sections);
17+
const [value, setValue] = useState<Array<Record<string, unknown>>>(props.value ?? Array.from({ length: sections.length }, () => ({})));
1518
const [editingField, setEditingField] = useState<string>();
1619
const canSubmitForm = useMemo(() => canSubmit(props.form, value), [value, props.form]);
1720
const focusManager = useFocusManager();
@@ -29,21 +32,25 @@ export const Form: React.FC<FormProps> = props => {
2932
useEffect(() => {
3033
// Set initial values
3134
if (!isControlled) {
32-
setValueAndPropagate({
33-
...value,
34-
...props.form.sections
35-
.map(section =>
36-
section.fields
37-
.map(field => (field.initialValue !== undefined ? { [field.name]: field.initialValue } : {}))
38-
.reduce((obj1, obj2) => ({ ...obj1, ...obj2 }), {})
39-
)
40-
.reduce((obj1, obj2) => ({ ...obj1, ...obj2 }), {}),
41-
});
35+
setValue(Array.from({ length: sections.length }, () => ({})));
36+
37+
38+
// setValueAndPropagate({
39+
// ...value,
40+
// ...props.form.sections
41+
// .map(section =>
42+
// section.fields
43+
// .map(field => (field.initialValue !== undefined ? { [field.name]: field.initialValue } : {}))
44+
// .reduce((obj1, obj2) => ({ ...obj1, ...obj2 }), {})
45+
// )
46+
// .reduce((obj1, obj2) => ({ ...obj1, ...obj2 }), {}),
47+
// });
4248
}
4349
}, []);
4450

45-
const setValueAndPropagate = (value: object) => {
46-
setValue(value);
51+
const setValueAndPropagate = (index: number, newValue: Record<string, unknown>) => {
52+
value[index] = newValue
53+
setValue(structuredClone(value));
4754
props.onChange?.(value);
4855
};
4956

@@ -58,33 +65,62 @@ export const Form: React.FC<FormProps> = props => {
5865
{ isActive: !editingField }
5966
);
6067

68+
const duplicateCurrentItem = () => {
69+
const newTabs = [...sections]
70+
newTabs.splice(currentTab + 1, 0, sections[currentTab])
71+
72+
const newValue = { ...value[currentTab]};
73+
value.splice(currentTab + 1, 0, newValue);
74+
75+
setSections(newTabs);
76+
setValue([...value]);
77+
}
78+
79+
const removeCurrentItem = () => {
80+
const newTabs = [...sections]
81+
newTabs.splice(currentTab, 1);
82+
value.splice(currentTab, 1)
83+
84+
setSections(newTabs);
85+
setValue([...value]);
86+
}
87+
6188
return (
62-
<Box width="100%" height="100%" flexDirection="column">
63-
<FormHeader {...props} currentTab={currentTab} onChangeTab={setCurrentTab} editingField={editingField} />
64-
{!editingField && props.form.sections[currentTab].description && (
89+
!isSubmitted && <Box width="100%" height="90%" flexDirection="column" overflowY="hidden">
90+
<FormHeader {...props} form={{ ...props.form, sections }} currentTab={currentTab} onChangeTab={setCurrentTab} editingField={editingField} />
91+
{!editingField && sections[currentTab].description && (
6592
<Box marginX={4}>
66-
<DescriptionRenderer description={props.form.sections[currentTab].description} />
93+
<DescriptionRenderer description={props.form.sections[currentTab]?.description} />
6794
</Box>
6895
)}
6996
<Box flexDirection="column">
7097
{currentTab > props.form.sections.length - 1
7198
? null
72-
: props.form.sections[currentTab].fields.map(field => (
73-
<FormFieldRenderer
74-
field={field}
75-
key={field.name}
76-
form={props.form}
77-
value={value[field.name]}
78-
onChange={v => setValueAndPropagate({ ...value, [field.name]: v })}
79-
onSetEditingField={setEditingField}
80-
editingField={editingField}
81-
customManagers={props.customManagers}
82-
/>
83-
))}
99+
: sections[currentTab].fields.map((field, index) => (
100+
<FormFieldRenderer
101+
field={field}
102+
key={field.name + currentTab}
103+
form={props.form}
104+
value={value[currentTab][field.name]}
105+
onChange={v => setValueAndPropagate(currentTab, { ...value[currentTab], [field.name]: v })}
106+
onSetEditingField={setEditingField}
107+
editingField={editingField}
108+
customManagers={props.customManagers}
109+
/>
110+
))}
111+
<Box flexDirection="row-reverse">
112+
<Button label="Add Item (duplicate)" onClicked={() => duplicateCurrentItem()}/>
113+
</Box>
114+
<Box flexDirection="row-reverse">
115+
<Button label="Remove" onClicked={() => removeCurrentItem()}/>
116+
</Box>
84117
</Box>
85118
{!editingField && (
86119
<Box flexDirection="row-reverse">
87-
<SubmitButton canSubmit={canSubmitForm} onSubmit={() => props.onSubmit?.(value)} />
120+
<SubmitButton canSubmit={canSubmitForm} onSubmit={() => {
121+
props.onSubmit?.(value)
122+
setIsSubmitted(true);
123+
}}/>
88124
</Box>
89125
)}
90126
</Box>

src/FormHeader.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export const FormHeader: React.FC<
4444
<Box>
4545
{!props.editingField ? (
4646
sections.map((section, id) => (
47-
<Box key={section.title}>
47+
<Box key={section.title + id}>
4848
<Text color="gray">[{id + 1}] </Text>
4949
<Text color={props.currentTab === id ? 'blue' : undefined} underline={props.currentTab === id}>
5050
{section.title}

src/ScrollArea.tsx

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { Box, measureElement, useInput } from 'ink';
2+
import React from 'react';
3+
4+
const reducer = (state, action) => {
5+
switch (action.type) {
6+
case 'SET_INNER_HEIGHT':
7+
return {
8+
...state,
9+
innerHeight: action.innerHeight
10+
};
11+
12+
case 'SCROLL_DOWN':
13+
return {
14+
...state,
15+
scrollTop: Math.min(
16+
state.innerHeight - state.height,
17+
state.scrollTop + 1
18+
)
19+
};
20+
21+
case 'SCROLL_UP':
22+
return {
23+
...state,
24+
scrollTop: Math.max(0, state.scrollTop - 1)
25+
};
26+
27+
default:
28+
return state;
29+
}
30+
};
31+
32+
export function ScrollArea({height, children}) {
33+
const [state, dispatch] = React.useReducer(reducer, {
34+
height,
35+
scrollTop: 0
36+
});
37+
38+
const innerRef = React.useRef();
39+
40+
React.useEffect(() => {
41+
const dimensions = measureElement(innerRef.current);
42+
43+
dispatch({
44+
type: 'SET_INNER_HEIGHT',
45+
innerHeight: dimensions.height
46+
});
47+
}, []);
48+
49+
useInput((_input, key) => {
50+
if (key.downArrow) {
51+
dispatch({
52+
type: 'SCROLL_DOWN'
53+
});
54+
}
55+
56+
if (key.upArrow) {
57+
dispatch({
58+
type: 'SCROLL_UP'
59+
});
60+
}
61+
});
62+
63+
return (
64+
<Box height={height} flexDirection="column" overflow="hidden">
65+
<Box
66+
ref={innerRef}
67+
flexShrink={0}
68+
flexDirection="column"
69+
marginTop={-state.scrollTop}
70+
>
71+
{children}
72+
</Box>
73+
</Box>
74+
);
75+
}

src/types.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,10 @@ export type Description = string | string[] | JSX.Element | undefined | false;
2020
export interface FormProps {
2121
/** Structure of the form, i.e. which fields are contained in which sections. */
2222
form: FormStructure;
23-
initialValue?: object;
23+
initialValue?: Array<Record<string, any>>;
2424

2525
/** Current value of the form. Omit to leave the component in uncontrolled mode. */
26-
value?: object;
26+
value?: Array<Record<string, any>>;
2727
onChange?: (value: object) => void;
2828

2929
/**
@@ -32,7 +32,7 @@ export interface FormProps {
3232
*
3333
* @param value the final value of the form.
3434
* */
35-
onSubmit?: (value: object) => void;
35+
onSubmit?: (value: Array<Record<string, any>>) => void;
3636

3737
/**
3838
* You can use custom field implementations, by specifying their ``type`` attribute to a custom

0 commit comments

Comments
 (0)