@object-ui/react 0.3.0 → 0.5.0

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.
Files changed (48) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/CHANGELOG.md +8 -0
  3. package/dist/LazyPluginLoader.d.ts +38 -0
  4. package/dist/LazyPluginLoader.js +35 -0
  5. package/dist/SchemaRenderer.d.ts +7 -0
  6. package/dist/SchemaRenderer.js +51 -12
  7. package/dist/__tests__/LazyPluginLoader.test.d.ts +8 -0
  8. package/dist/__tests__/LazyPluginLoader.test.js +49 -0
  9. package/dist/components/form/FieldFactory.d.ts +43 -0
  10. package/dist/components/form/FieldFactory.js +157 -0
  11. package/dist/components/form/FormRenderer.d.ts +40 -0
  12. package/dist/components/form/FormRenderer.js +87 -0
  13. package/dist/components/form/FormRenderer.test.d.ts +8 -0
  14. package/dist/components/form/FormRenderer.test.js +534 -0
  15. package/dist/components/form/index.d.ts +11 -0
  16. package/dist/components/form/index.js +9 -0
  17. package/dist/context/SchemaRendererContext.d.ts +14 -0
  18. package/dist/context/SchemaRendererContext.js +23 -0
  19. package/dist/context/index.d.ts +8 -1
  20. package/dist/context/index.js +8 -1
  21. package/dist/hooks/index.d.ts +9 -1
  22. package/dist/hooks/index.js +9 -1
  23. package/dist/hooks/useActionRunner.d.ts +32 -0
  24. package/dist/hooks/useActionRunner.js +65 -0
  25. package/dist/hooks/useExpression.d.ts +27 -0
  26. package/dist/hooks/useExpression.js +41 -0
  27. package/dist/index.d.ts +9 -0
  28. package/dist/index.js +9 -0
  29. package/dist/index.test.d.ts +7 -0
  30. package/dist/index.test.js +7 -0
  31. package/package.json +26 -6
  32. package/src/LazyPluginLoader.tsx +52 -0
  33. package/src/SchemaRenderer.tsx +72 -12
  34. package/src/__tests__/LazyPluginLoader.test.tsx +80 -0
  35. package/src/components/form/FieldFactory.tsx +504 -0
  36. package/src/components/form/FormRenderer.test.tsx +599 -0
  37. package/src/components/form/FormRenderer.tsx +209 -0
  38. package/src/components/form/README.md +352 -0
  39. package/src/components/form/index.ts +12 -0
  40. package/src/context/SchemaRendererContext.tsx +43 -0
  41. package/src/context/index.ts +9 -1
  42. package/src/hooks/index.ts +11 -1
  43. package/src/hooks/useActionRunner.ts +81 -0
  44. package/src/hooks/useExpression.ts +56 -0
  45. package/src/index.test.ts +8 -0
  46. package/src/index.ts +10 -0
  47. package/tsconfig.tsbuildinfo +1 -1
  48. package/vitest.config.ts +2 -0
@@ -0,0 +1,65 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ import { useState, useCallback, useMemo } from 'react';
9
+ import { ActionRunner } from '@object-ui/core';
10
+ /**
11
+ * Hook for executing actions with loading state
12
+ *
13
+ * @example
14
+ * ```tsx
15
+ * const { execute, loading, error, result } = useActionRunner({ data: formData });
16
+ *
17
+ * const handleClick = () => {
18
+ * execute({
19
+ * type: 'action',
20
+ * api: '/api/save',
21
+ * successMessage: 'Saved successfully!'
22
+ * });
23
+ * };
24
+ * ```
25
+ */
26
+ export function useActionRunner(context = {}) {
27
+ const [loading, setLoading] = useState(false);
28
+ const [error, setError] = useState(null);
29
+ const [result, setResult] = useState(null);
30
+ const runner = useMemo(() => new ActionRunner(context), [JSON.stringify(context)]);
31
+ const execute = useCallback(async (action) => {
32
+ setLoading(true);
33
+ setError(null);
34
+ setResult(null);
35
+ try {
36
+ const actionResult = await runner.execute(action);
37
+ setResult(actionResult);
38
+ if (!actionResult.success) {
39
+ setError(actionResult.error || 'Action failed');
40
+ }
41
+ return actionResult;
42
+ }
43
+ catch (err) {
44
+ const errorMessage = err.message;
45
+ setError(errorMessage);
46
+ const failureResult = { success: false, error: errorMessage };
47
+ setResult(failureResult);
48
+ return failureResult;
49
+ }
50
+ finally {
51
+ setLoading(false);
52
+ }
53
+ }, [runner]);
54
+ const updateContext = useCallback((newContext) => {
55
+ runner.updateContext(newContext);
56
+ }, [runner]);
57
+ return {
58
+ execute,
59
+ loading,
60
+ error,
61
+ result,
62
+ updateContext,
63
+ runner
64
+ };
65
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ /**
9
+ * Hook for evaluating expressions with dynamic context
10
+ *
11
+ * @example
12
+ * ```tsx
13
+ * const isVisible = useExpression('${data.age >= 18}', { data });
14
+ * const label = useExpression('Hello ${user.name}!', { user });
15
+ * ```
16
+ */
17
+ export declare function useExpression(expression: string | boolean | number | null | undefined, context?: Record<string, any>): any;
18
+ /**
19
+ * Hook for evaluating conditional expressions
20
+ * Returns a boolean value
21
+ *
22
+ * @example
23
+ * ```tsx
24
+ * const isVisible = useCondition('${data.status === "active"}', { data });
25
+ * ```
26
+ */
27
+ export declare function useCondition(condition: string | boolean | undefined, context?: Record<string, any>): boolean;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ import { useMemo } from 'react';
9
+ import { ExpressionEvaluator } from '@object-ui/core';
10
+ /**
11
+ * Hook for evaluating expressions with dynamic context
12
+ *
13
+ * @example
14
+ * ```tsx
15
+ * const isVisible = useExpression('${data.age >= 18}', { data });
16
+ * const label = useExpression('Hello ${user.name}!', { user });
17
+ * ```
18
+ */
19
+ export function useExpression(expression, context = {}) {
20
+ // We evaluate directly without caching the evaluator to avoid issues with context changes
21
+ return useMemo(() => {
22
+ const evaluator = new ExpressionEvaluator(context);
23
+ return evaluator.evaluate(expression);
24
+ }, [expression, context]);
25
+ }
26
+ /**
27
+ * Hook for evaluating conditional expressions
28
+ * Returns a boolean value
29
+ *
30
+ * @example
31
+ * ```tsx
32
+ * const isVisible = useCondition('${data.status === "active"}', { data });
33
+ * ```
34
+ */
35
+ export function useCondition(condition, context = {}) {
36
+ // We evaluate directly without caching the evaluator to avoid issues with context changes
37
+ return useMemo(() => {
38
+ const evaluator = new ExpressionEvaluator(context);
39
+ return evaluator.evaluateCondition(condition);
40
+ }, [condition, context]);
41
+ }
package/dist/index.d.ts CHANGED
@@ -1,3 +1,12 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
1
8
  export * from './SchemaRenderer';
2
9
  export * from './hooks';
3
10
  export * from './context';
11
+ export * from './components/form';
12
+ export * from './LazyPluginLoader';
package/dist/index.js CHANGED
@@ -1,3 +1,12 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
1
8
  export * from './SchemaRenderer';
2
9
  export * from './hooks'; // will be empty for now
3
10
  export * from './context'; // will be empty for now
11
+ export * from './components/form';
12
+ export * from './LazyPluginLoader';
@@ -1 +1,8 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
1
8
  export {};
@@ -1,3 +1,10 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
1
8
  import { describe, it, expect } from 'vitest';
2
9
  describe('react', () => {
3
10
  it('should work', () => {
package/package.json CHANGED
@@ -1,19 +1,39 @@
1
1
  {
2
2
  "name": "@object-ui/react",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
+ "type": "module",
4
5
  "license": "MIT",
5
- "main": "dist/index.js",
6
- "types": "dist/index.d.ts",
6
+ "description": "React bindings and SchemaRenderer component for Object UI",
7
+ "homepage": "https://www.objectui.org",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/objectstack-ai/objectui.git",
11
+ "directory": "packages/react"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/objectstack-ai/objectui/issues"
15
+ },
16
+ "main": "./dist/index.js",
17
+ "module": "./dist/index.js",
18
+ "types": "./dist/index.d.ts",
19
+ "exports": {
20
+ ".": {
21
+ "types": "./dist/index.d.ts",
22
+ "import": "./dist/index.js"
23
+ }
24
+ },
7
25
  "dependencies": {
8
- "@object-ui/core": "0.3.0"
26
+ "@objectstack/spec": "^0.9.1",
27
+ "react-hook-form": "^7.71.1",
28
+ "@object-ui/core": "0.5.0"
9
29
  },
10
30
  "peerDependencies": {
11
31
  "react": "^18.0.0 || ^19.0.0",
12
32
  "react-dom": "^18.0.0 || ^19.0.0"
13
33
  },
14
34
  "devDependencies": {
15
- "@types/react": "^18.2.0",
16
- "@types/react-dom": "^18.2.0",
35
+ "@types/react": "^19.2.10",
36
+ "@types/react-dom": "^19.2.3",
17
37
  "typescript": "^5.9.3"
18
38
  },
19
39
  "scripts": {
@@ -0,0 +1,52 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+
9
+ import React, { lazy, Suspense } from 'react';
10
+
11
+ export interface LazyPluginOptions {
12
+ /**
13
+ * Fallback component to show while loading
14
+ */
15
+ fallback?: React.ReactNode;
16
+ }
17
+
18
+ /**
19
+ * Create a lazy-loaded plugin component with Suspense wrapper
20
+ *
21
+ * @param importFn - Dynamic import function that returns a module with default export
22
+ * @param options - Configuration options for the lazy plugin
23
+ * @returns A component that lazy loads the plugin
24
+ *
25
+ * @example
26
+ * ```tsx
27
+ * // Basic usage
28
+ * const ObjectGrid = createLazyPlugin(
29
+ * () => import('@object-ui/plugin-grid')
30
+ * );
31
+ *
32
+ * // With custom fallback
33
+ * const ObjectGrid = createLazyPlugin(
34
+ * () => import('@object-ui/plugin-grid'),
35
+ * { fallback: <div>Loading grid...</div> }
36
+ * );
37
+ * ```
38
+ */
39
+ export function createLazyPlugin<P extends object = any>(
40
+ importFn: () => Promise<{ default: React.ComponentType<P> }>,
41
+ options?: LazyPluginOptions
42
+ ): React.ComponentType<P> {
43
+ const LazyComponent = lazy(importFn);
44
+
45
+ const PluginWrapper: React.FC<P> = (props) => (
46
+ <Suspense fallback={options?.fallback || null}>
47
+ <LazyComponent {...props} />
48
+ </Suspense>
49
+ );
50
+
51
+ return PluginWrapper;
52
+ }
@@ -1,30 +1,90 @@
1
- import React, { forwardRef } from 'react';
2
- import { SchemaNode, ComponentRegistry } from '@object-ui/core';
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+
9
+ import React, { forwardRef, useContext, useMemo } from 'react';
10
+ import { SchemaNode, ComponentRegistry, ExpressionEvaluator } from '@object-ui/core';
11
+ import { SchemaRendererContext } from './context/SchemaRendererContext';
3
12
 
4
13
  export const SchemaRenderer = forwardRef<any, { schema: SchemaNode } & Record<string, any>>(({ schema, ...props }, _ref) => {
5
- if (!schema) return null;
14
+ const context = useContext(SchemaRendererContext);
15
+ const dataSource = context?.dataSource || {};
16
+
17
+ // Evaluate schema expressions against the data source
18
+ const evaluatedSchema = useMemo(() => {
19
+ if (!schema || typeof schema === 'string') return schema;
20
+
21
+ const evaluator = new ExpressionEvaluator({ data: dataSource });
22
+ // Shallow copy
23
+ const newSchema = { ...schema };
24
+
25
+ // COMPAT: Hoist 'properties' up to schema level
26
+ // This allows support for strict configs that wrap all props in 'properties'
27
+ if (newSchema.properties) {
28
+ Object.assign(newSchema, newSchema.properties);
29
+ }
30
+
31
+ // Evaluate 'content' (common in Text, Button)
32
+ if (typeof newSchema.content === 'string') {
33
+ newSchema.content = evaluator.evaluate(newSchema.content);
34
+ }
35
+
36
+ // Evaluate 'props'
37
+ if (newSchema.props) {
38
+ const newProps = { ...newSchema.props };
39
+ for (const [key, val] of Object.entries(newProps)) {
40
+ newProps[key] = evaluator.evaluate(val as any);
41
+ }
42
+ newSchema.props = newProps;
43
+ }
44
+
45
+ return newSchema;
46
+ }, [schema, dataSource]);
47
+
48
+ if (!evaluatedSchema) return null;
6
49
  // If schema is just a string, render it as text
7
- if (typeof schema === 'string') return <>{schema}</>;
50
+ if (typeof evaluatedSchema === 'string') return <>{evaluatedSchema}</>;
8
51
 
9
- const Component = ComponentRegistry.get(schema.type);
52
+ const Component = ComponentRegistry.get(evaluatedSchema.type);
10
53
 
11
54
  if (!Component) {
12
55
  return (
13
56
  <div className="p-4 border border-red-500 rounded text-red-500 bg-red-50 my-2">
14
- Unknown component type: <strong>{schema.type}</strong>
15
- <pre className="text-xs mt-2 overflow-auto">{JSON.stringify(schema, null, 2)}</pre>
57
+ Unknown component type: <strong>{evaluatedSchema.type}</strong>
58
+ <pre className="text-xs mt-2 overflow-auto">{JSON.stringify(evaluatedSchema, null, 2)}</pre>
16
59
  </div>
17
60
  );
18
61
  }
19
62
 
20
63
  // Note: We don't forward the ref to the Component because components in the registry
21
64
  // may not support refs. The SchemaRenderer itself can still receive refs for its own use.
65
+
66
+ // Extract schema metadata properties that should NOT be passed as React props
67
+ const {
68
+ type: _type,
69
+ children: _children,
70
+ body: _body,
71
+ schema: _schema,
72
+ visible: _visible,
73
+ visibleOn: _visibleOn,
74
+ hidden: _hidden,
75
+ hiddenOn: _hiddenOn,
76
+ disabled: _disabled,
77
+ disabledOn: _disabledOn,
78
+ ...componentProps
79
+ } = evaluatedSchema;
80
+
22
81
  return React.createElement(Component, {
23
- schema,
24
- ...(schema.props || {}),
25
- className: schema.className,
26
- 'data-obj-id': schema.id,
27
- 'data-obj-type': schema.type,
82
+ schema: evaluatedSchema,
83
+ ...componentProps, // Spread non-metadata schema properties as props
84
+ ...(evaluatedSchema.props || {}), // Override with explicit props if provided
85
+ className: evaluatedSchema.className,
86
+ 'data-obj-id': evaluatedSchema.id,
87
+ 'data-obj-type': evaluatedSchema.type,
28
88
  ...props
29
89
  });
30
90
  });
@@ -0,0 +1,80 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+
9
+ import { describe, it, expect, vi } from 'vitest';
10
+ import { render, screen, waitFor } from '@testing-library/react';
11
+ import React from 'react';
12
+ import { createLazyPlugin } from '../LazyPluginLoader';
13
+
14
+ describe('createLazyPlugin', () => {
15
+ it('should create a lazy-loaded component', async () => {
16
+ const TestComponent = () => <div>Test Component</div>;
17
+
18
+ const LazyComponent = createLazyPlugin(
19
+ () => Promise.resolve({ default: TestComponent })
20
+ );
21
+
22
+ render(<LazyComponent />);
23
+
24
+ await waitFor(() => {
25
+ expect(screen.getByText('Test Component')).toBeInTheDocument();
26
+ });
27
+ });
28
+
29
+ it('should show custom fallback while loading', async () => {
30
+ const TestComponent = () => <div>Test Component</div>;
31
+
32
+ const LazyComponent = createLazyPlugin(
33
+ () => new Promise<{ default: React.ComponentType }>((resolve) => {
34
+ setTimeout(() => resolve({ default: TestComponent }), 100);
35
+ }),
36
+ { fallback: <div>Loading...</div> }
37
+ );
38
+
39
+ render(<LazyComponent />);
40
+
41
+ expect(screen.getByText('Loading...')).toBeInTheDocument();
42
+
43
+ await waitFor(() => {
44
+ expect(screen.getByText('Test Component')).toBeInTheDocument();
45
+ });
46
+ });
47
+
48
+ it('should pass props to the lazy component', async () => {
49
+ interface TestProps {
50
+ message: string;
51
+ }
52
+
53
+ const TestComponent = ({ message }: TestProps) => <div>{message}</div>;
54
+
55
+ const LazyComponent = createLazyPlugin<TestProps>(
56
+ () => Promise.resolve({ default: TestComponent })
57
+ );
58
+
59
+ render(<LazyComponent message="Hello World" />);
60
+
61
+ await waitFor(() => {
62
+ expect(screen.getByText('Hello World')).toBeInTheDocument();
63
+ });
64
+ });
65
+
66
+ it('should handle loading errors gracefully', async () => {
67
+ // Mock console.error to avoid noise in test output
68
+ const originalError = console.error;
69
+ console.error = vi.fn();
70
+
71
+ const LazyComponent = createLazyPlugin<any>(
72
+ () => Promise.reject(new Error('Load error'))
73
+ );
74
+
75
+ expect(() => render(<LazyComponent />)).not.toThrow();
76
+
77
+ // Restore console.error
78
+ console.error = originalError;
79
+ });
80
+ });