Skip to content

Commit 1fe13fe

Browse files
committed
feat: enhance form components with loading state, required and disabled props, and improved event handling
1 parent c9d45a0 commit 1fe13fe

5 files changed

Lines changed: 183 additions & 62 deletions

File tree

packages/components/src/renderers/basic/pagination.tsx

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,31 @@
99
import { ComponentRegistry } from '@object-ui/core';
1010
import type { PaginationSchema } from '@object-ui/types';
1111
import { Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious } from '../../ui/pagination';
12+
import React from 'react';
1213

1314
ComponentRegistry.register('pagination',
1415
({ schema, ...props }: { schema: PaginationSchema; [key: string]: any }) => {
1516
const {
1617
'data-obj-id': dataObjId,
1718
'data-obj-type': dataObjType,
1819
style,
20+
onPageChange,
1921
...paginationProps
2022
} = props;
2123

2224
const currentPage = schema.currentPage || schema.page || 1;
2325
const totalPages = schema.totalPages || 1;
24-
const showEllipsis = totalPages > 7;
2526

27+
const handlePageChange = (page: number, e: React.MouseEvent) => {
28+
e.preventDefault();
29+
if (page === currentPage) return;
30+
if (page < 1 || page > totalPages) return;
31+
32+
if (onPageChange) {
33+
onPageChange(page);
34+
}
35+
};
36+
2637
const getPageNumbers = () => {
2738
if (totalPages <= 7) {
2839
return Array.from({ length: totalPages }, (_, i) => i + 1);
@@ -47,21 +58,36 @@ ComponentRegistry.register('pagination',
4758
>
4859
<PaginationContent>
4960
<PaginationItem>
50-
<PaginationPrevious href="#" />
61+
<PaginationPrevious
62+
href="#"
63+
onClick={(e) => handlePageChange(currentPage - 1, e)}
64+
className={currentPage <= 1 ? "pointer-events-none opacity-50" : "cursor-pointer"}
65+
aria-disabled={currentPage <= 1}
66+
/>
5167
</PaginationItem>
5268
{getPageNumbers().map((page, idx) => (
5369
<PaginationItem key={idx}>
5470
{page === -1 ? (
5571
<PaginationEllipsis />
5672
) : (
57-
<PaginationLink href="#" isActive={page === currentPage}>
73+
<PaginationLink
74+
href="#"
75+
isActive={page === currentPage}
76+
onClick={(e) => handlePageChange(page, e)}
77+
className="cursor-pointer"
78+
>
5879
{page}
5980
</PaginationLink>
6081
)}
6182
</PaginationItem>
6283
))}
6384
<PaginationItem>
64-
<PaginationNext href="#" />
85+
<PaginationNext
86+
href="#"
87+
onClick={(e) => handlePageChange(currentPage + 1, e)}
88+
className={currentPage >= totalPages ? "pointer-events-none opacity-50" : "cursor-pointer"}
89+
aria-disabled={currentPage >= totalPages}
90+
/>
6591
</PaginationItem>
6692
</PaginationContent>
6793
</Pagination>

packages/components/src/renderers/form/button.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,20 @@ import type { ButtonSchema } from '@object-ui/types';
1111
import { Button } from '../../ui';
1212
import { renderChildren } from '../../lib/utils';
1313
import { forwardRef } from 'react';
14+
import { Loader2, icons, type LucideIcon } from 'lucide-react';
15+
16+
// Helper to convert icon names to PascalCase (e.g., "arrow-right" -> "ArrowRight")
17+
function toPascalCase(str: string): string {
18+
return str
19+
.split('-')
20+
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
21+
.join('');
22+
}
23+
24+
// Map of renamed icons in lucide-react
25+
const iconNameMap: Record<string, string> = {
26+
'Home': 'House',
27+
};
1428

1529
const ButtonRenderer = forwardRef<HTMLButtonElement, { schema: ButtonSchema; [key: string]: any }>(
1630
({ schema, ...props }, ref) => {
@@ -22,17 +36,36 @@ const ButtonRenderer = forwardRef<HTMLButtonElement, { schema: ButtonSchema; [ke
2236
...buttonProps
2337
} = props;
2438

39+
// Resolve icon
40+
let Icon: LucideIcon | null = null;
41+
if (schema.icon) {
42+
const iconName = toPascalCase(schema.icon);
43+
const mappedIconName = iconNameMap[iconName] || iconName;
44+
Icon = (icons as any)[mappedIconName] as LucideIcon;
45+
}
46+
47+
// Determine loading state
48+
const isLoading = schema.loading || props.loading;
49+
50+
// Determine disabled state
51+
const isDisabled = schema.disabled || props.disabled || isLoading;
52+
2553
return (
2654
<Button
2755
ref={ref}
56+
type={schema.buttonType || "button"}
2857
variant={schema.variant}
2958
size={schema.size}
3059
className={schema.className}
60+
disabled={isDisabled}
3161
{...buttonProps}
3262
// Apply designer props
3363
{...{ 'data-obj-id': dataObjId, 'data-obj-type': dataObjType, style }}
3464
>
65+
{isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
66+
{!isLoading && Icon && schema.iconPosition !== 'right' && <Icon className="mr-2 h-4 w-4" />}
3567
{schema.label || renderChildren(schema.body || schema.children)}
68+
{!isLoading && Icon && schema.iconPosition === 'right' && <Icon className="ml-2 h-4 w-4" />}
3669
</Button>
3770
);
3871
}

packages/components/src/renderers/form/checkbox.tsx

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,42 +9,63 @@
99
import { ComponentRegistry } from '@object-ui/core';
1010
import type { CheckboxSchema } from '@object-ui/types';
1111
import { Checkbox, Label } from '../../ui';
12+
import { cn } from '../../lib/utils';
13+
import React from 'react';
1214

13-
ComponentRegistry.register('checkbox',
14-
({ schema, className, ...props }: { schema: CheckboxSchema; className?: string; [key: string]: any }) => {
15-
// Extract designer-related props
16-
const {
17-
'data-obj-id': dataObjId,
18-
'data-obj-type': dataObjType,
19-
style,
20-
...checkboxProps
21-
} = props;
15+
const CheckboxRenderer = ({ schema, className, onChange, value, ...props }: { schema: CheckboxSchema; className?: string; onChange?: (val: any) => void; value?: any; [key: string]: any }) => {
16+
// Extract designer-related props
17+
const {
18+
'data-obj-id': dataObjId,
19+
'data-obj-type': dataObjType,
20+
style,
21+
...checkboxProps
22+
} = props;
2223

23-
return (
24+
const handleCheckedChange = (checked: boolean) => {
25+
if (onChange) {
26+
onChange(checked);
27+
}
28+
};
29+
30+
return (
2431
<div
25-
className={`flex items-center space-x-2 ${schema.wrapperClass || ''}`}
32+
className={cn("flex items-center space-x-2", schema.wrapperClass)}
2633
data-obj-id={dataObjId}
2734
data-obj-type={dataObjType}
2835
style={style}
2936
>
30-
<Checkbox id={schema.id} className={className} {...checkboxProps} />
31-
<Label htmlFor={schema.id} className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
37+
<Checkbox
38+
id={schema.id}
39+
className={className}
40+
checked={value ?? schema.checked ?? false}
41+
defaultChecked={value === undefined ? schema.defaultChecked : undefined}
42+
onCheckedChange={handleCheckedChange}
43+
disabled={schema.disabled}
44+
required={schema.required}
45+
name={schema.name}
46+
{...checkboxProps}
47+
/>
48+
<Label htmlFor={schema.id} className={cn("text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70", schema.required && "text-destructive after:content-['*'] after:ml-0.5")}>
3249
{schema.label}
3350
</Label>
3451
</div>
3552
);
36-
},
53+
};
54+
55+
ComponentRegistry.register('checkbox', CheckboxRenderer,
3756
{
3857
namespace: 'ui',
3958
label: 'Checkbox',
4059
inputs: [
4160
{ name: 'label', type: 'string', label: 'Label', required: true },
4261
{ name: 'id', type: 'string', label: 'ID', required: true },
43-
{ name: 'checked', type: 'boolean', label: 'Checked' }
62+
{ name: 'checked', type: 'boolean', label: 'Checked' },
63+
{ name: 'required', type: 'boolean', label: 'Required' },
64+
{ name: 'disabled', type: 'boolean', label: 'Disabled' }
4465
],
4566
defaultProps: {
4667
label: 'Checkbox label',
47-
id: 'checkbox-field' // Will be made unique by designer's ensureNodeIds
68+
id: 'checkbox-field'
4869
}
4970
}
5071
);

packages/components/src/renderers/form/select.tsx

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,45 +16,64 @@ import {
1616
SelectItem,
1717
Label
1818
} from '../../ui';
19+
import { cn } from '../../lib/utils';
20+
import React from 'react';
1921

20-
ComponentRegistry.register('select',
21-
({ schema, className, ...props }: { schema: SelectSchema; className?: string; [key: string]: any }) => {
22-
// Extract designer-related props
23-
const {
24-
'data-obj-id': dataObjId,
25-
'data-obj-type': dataObjType,
26-
style,
27-
...selectProps
28-
} = props;
22+
const SelectRenderer = ({ schema, className, onChange, value, ...props }: { schema: SelectSchema; className?: string; onChange?: (val: any) => void; value?: any; [key: string]: any }) => {
23+
// Extract designer-related props
24+
const {
25+
'data-obj-id': dataObjId,
26+
'data-obj-type': dataObjType,
27+
style,
28+
...selectProps
29+
} = props;
2930

30-
return (
31+
const handleValueChange = (newValue: string) => {
32+
if (onChange) {
33+
onChange(newValue);
34+
}
35+
};
36+
37+
return (
3138
<div
32-
className={`grid w-full max-w-sm items-center gap-1.5 ${schema.wrapperClass || ''}`}
39+
className={cn("grid w-full items-center gap-1.5", schema.wrapperClass)}
3340
data-obj-id={dataObjId}
3441
data-obj-type={dataObjType}
3542
style={style}
3643
>
37-
{schema.label && <Label>{schema.label}</Label>}
38-
<Select defaultValue={schema.defaultValue} {...selectProps}>
44+
{schema.label && <Label className={cn(schema.required && "text-destructive after:content-['*'] after:ml-0.5")}>{schema.label}</Label>}
45+
<Select
46+
defaultValue={value === undefined ? schema.defaultValue : undefined}
47+
value={value ?? schema.value}
48+
onValueChange={handleValueChange}
49+
disabled={schema.disabled}
50+
required={schema.required}
51+
name={schema.name}
52+
{...selectProps}
53+
>
3954
<SelectTrigger className={className}>
4055
<SelectValue placeholder={schema.placeholder} />
4156
</SelectTrigger>
4257
<SelectContent>
4358
{schema.options?.map((opt) => (
44-
<SelectItem key={opt.value} value={opt.value}>{opt.label}</SelectItem>
59+
<SelectItem key={opt.value} value={opt.value} disabled={opt.disabled}>{opt.label}</SelectItem>
4560
))}
4661
</SelectContent>
4762
</Select>
4863
</div>
4964
);
50-
},
65+
};
66+
67+
ComponentRegistry.register('select', SelectRenderer,
5168
{
5269
namespace: 'ui',
5370
label: 'Select',
5471
inputs: [
5572
{ name: 'label', type: 'string', label: 'Label' },
5673
{ name: 'placeholder', type: 'string', label: 'Placeholder' },
5774
{ name: 'defaultValue', type: 'string', label: 'Default Value' },
75+
{ name: 'required', type: 'boolean', label: 'Required' },
76+
{ name: 'disabled', type: 'boolean', label: 'Disabled' },
5877
{
5978
name: 'options',
6079
type: 'array',

packages/components/src/renderers/form/textarea.tsx

Lines changed: 49 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,46 +9,68 @@
99
import { ComponentRegistry } from '@object-ui/core';
1010
import type { TextareaSchema } from '@object-ui/types';
1111
import { Textarea, Label } from '../../ui';
12+
import { cn } from '../../lib/utils';
13+
import React from 'react';
1214

13-
ComponentRegistry.register('textarea',
14-
({ schema, className, ...props }: { schema: TextareaSchema; className?: string; [key: string]: any }) => {
15-
// Extract designer-related props
16-
const {
17-
'data-obj-id': dataObjId,
18-
'data-obj-type': dataObjType,
19-
style,
20-
...inputProps
21-
} = props;
15+
const TextareaRenderer = ({ schema, className, onChange, value, ...props }: { schema: TextareaSchema; className?: string; onChange?: (val: any) => void; value?: any; [key: string]: any }) => {
16+
// Handle change for both raw inputs and form-bound inputs
17+
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
18+
if (onChange) {
19+
onChange(e.target.value);
20+
}
21+
};
22+
23+
// Extract designer-related props
24+
const {
25+
'data-obj-id': dataObjId,
26+
'data-obj-type': dataObjType,
27+
style,
28+
...inputProps
29+
} = props;
30+
31+
return (
32+
<div
33+
className={cn("grid w-full gap-1.5", schema.wrapperClass)}
34+
data-obj-id={dataObjId}
35+
data-obj-type={dataObjType}
36+
style={style}
37+
>
38+
{schema.label && <Label htmlFor={schema.id} className={cn(schema.required && "text-destructive after:content-['*'] after:ml-0.5")}>{schema.label}</Label>}
39+
<Textarea
40+
id={schema.id}
41+
name={schema.name}
42+
placeholder={schema.placeholder}
43+
className={className}
44+
disabled={schema.disabled}
45+
readOnly={schema.readOnly}
46+
required={schema.required}
47+
rows={schema.rows}
48+
value={value ?? schema.value ?? ''}
49+
defaultValue={value === undefined ? schema.defaultValue : undefined}
50+
onChange={handleChange}
51+
{...inputProps}
52+
/>
53+
</div>
54+
);
55+
};
2256

23-
return (
24-
<div
25-
className={`grid w-full gap-1.5 ${schema.wrapperClass || ''}`}
26-
data-obj-id={dataObjId}
27-
data-obj-type={dataObjType}
28-
style={style}
29-
>
30-
{schema.label && <Label htmlFor={schema.id}>{schema.label}</Label>}
31-
<Textarea
32-
id={schema.id}
33-
placeholder={schema.placeholder}
34-
className={className}
35-
{...inputProps}
36-
/>
37-
</div>
38-
);
39-
},
57+
ComponentRegistry.register('textarea', TextareaRenderer,
4058
{
4159
namespace: 'ui',
4260
label: 'Textarea',
4361
inputs: [
4462
{ name: 'label', type: 'string', label: 'Label' },
4563
{ name: 'placeholder', type: 'string', label: 'Placeholder' },
64+
{ name: 'rows', type: 'number', label: 'Rows' },
65+
{ name: 'required', type: 'boolean', label: 'Required' },
66+
{ name: 'disabled', type: 'boolean', label: 'Disabled' },
4667
{ name: 'id', type: 'string', label: 'ID', required: true }
4768
],
4869
defaultProps: {
4970
label: 'Textarea label',
5071
placeholder: 'Enter text here...',
51-
id: 'textarea-field' // Will be made unique by designer's ensureNodeIds
72+
rows: 3,
73+
id: 'textarea-field'
5274
}
5375
}
5476
);

0 commit comments

Comments
 (0)