Skip to content

Commit c7be663

Browse files
authored
feat: enhance vanilla form factory with new components and improved f… (#24)
* feat: enhance vanilla form factory with new components and improved functionality - Added new default components for the vanilla form factory, including FormField, FormSectionContainer, and various input components (InputText, InputSelect, InputCheckbox, etc.) to streamline form creation. - Implemented support for nested value handling in the VanillaFormAdapter, allowing for more complex data structures. - Enhanced the form rendering process with improved error handling and validation feedback. - Updated the pnpm-lock.yaml to include new dependencies for the added components. - Refactored existing components for better integration and consistency across the vanilla factory. * refactor: update vanilla form factory to use new renderer creation method - Replaced the registration of default renderers with a new function that creates renderers using the FormAdapter instance. - Updated related imports and adjusted the merging of renderers in the form factory. - Modified tests to use a direct URL for navigation instead of a base URL variable for consistency across React and Vue tests.
1 parent 6633033 commit c7be663

35 files changed

Lines changed: 1601 additions & 119 deletions

.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,16 @@ docs/CODESANDBOX_TEMPLATES.md
2222
.env.local
2323
.env.*.local
2424

25+
# Playwright test artifacts
2526
test-results/
27+
test-screenshots/
2628
tests/test-results/
2729
tests/playwright-report/
30+
playwright-report/
31+
32+
# Screenshots (ignore all images in root)
33+
/*.png
34+
/*.jpg
35+
/*.jpeg
36+
/*.gif
37+
/*.webp

packages/adapters/vanilla/src/form-adapter.ts

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,43 @@ export class VanillaFormAdapter implements FormAdapter {
2929
}
3030

3131
getValue(field: string): any {
32+
// Support nested paths (e.g., "user.name")
33+
if (field.includes('.')) {
34+
const parts = field.split('.');
35+
let value: any = this.values;
36+
for (const part of parts) {
37+
if (value === undefined || value === null) return undefined;
38+
value = value[part];
39+
}
40+
return value;
41+
}
3242
return this.values[field];
3343
}
3444

3545
setValue(field: string, value: any): void {
36-
const oldValue = this.values[field];
37-
this.values[field] = value;
46+
const oldValue = this.getValue(field);
47+
48+
// Support nested paths (e.g., "user.name")
49+
if (field.includes('.')) {
50+
const parts = field.split('.');
51+
let current: any = this.values;
52+
53+
// Create nested structure if needed
54+
for (let i = 0; i < parts.length - 1; i++) {
55+
const part = parts[i];
56+
if (!current[part] || typeof current[part] !== 'object') {
57+
current[part] = {};
58+
}
59+
current = current[part];
60+
}
61+
62+
// Set the final value
63+
const lastPart = parts[parts.length - 1];
64+
current[lastPart] = value;
65+
} else {
66+
this.values[field] = value;
67+
}
68+
3869
this.validateField(field);
3970
this.emitter.emit('change', { field, value, oldValue });
4071
this.emitter.emit(`change:${field}`, { value, oldValue });

packages/adapters/vanilla/src/provider.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { defaultDebugConfig } from '@schepta/core';
1515
*/
1616
export interface ScheptaProviderProps {
1717
components?: Record<string, ComponentSpec>;
18+
customComponents?: Record<string, ComponentSpec>;
1819
renderers?: Partial<Record<ComponentType, RendererFn>>;
1920
middlewares?: MiddlewareFn[];
2021
debug?: DebugConfig;
@@ -27,6 +28,7 @@ export interface ScheptaProviderProps {
2728
*/
2829
export interface ScheptaContextType {
2930
components: Record<string, ComponentSpec>;
31+
customComponents: Record<string, ComponentSpec>;
3032
renderers: Record<ComponentType, RendererFn>;
3133
middlewares: MiddlewareFn[];
3234
debug: DebugConfig;
@@ -62,6 +64,7 @@ export function createScheptaProvider(
6264

6365
return {
6466
components: { ...parentContext.components, ...(props.components || {}) },
67+
customComponents: { ...parentContext.customComponents, ...(props.customComponents || {}) },
6568
renderers: mergedRenderers,
6669
middlewares: [...parentContext.middlewares, ...(props.middlewares || [])],
6770
debug: { ...parentContext.debug, ...props.debug },
@@ -75,6 +78,7 @@ export function createScheptaProvider(
7578

7679
return {
7780
components: props.components || {},
81+
customComponents: props.customComponents || {},
7882
renderers: props.renderers,
7983
middlewares: props.middlewares || [],
8084
debug: mergedDebug,

packages/adapters/vanilla/src/runtime-adapter.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,40 @@
44
* Implements RuntimeAdapter using DOM APIs
55
*/
66

7-
import type { RuntimeAdapter, ComponentSpec, RenderResult } from '@schepta/core';
7+
import type { RuntimeAdapter, ComponentSpec, RendererSpec, RenderResult } from '@schepta/core';
88

99
import type { DOMElement } from './types';
1010

1111
/**
1212
* Vanilla JS runtime adapter implementation
1313
*/
1414
export class VanillaRuntimeAdapter implements RuntimeAdapter {
15-
create(spec: ComponentSpec, props: Record<string, any>): RenderResult {
15+
create(spec: ComponentSpec | RendererSpec, props: Record<string, any>): RenderResult {
16+
if (!spec) {
17+
console.error('[VanillaRuntime] Invalid spec:', spec);
18+
throw new Error(`Invalid spec: spec is null/undefined`);
19+
}
20+
21+
// Handle RendererSpec (for field renderers, etc)
22+
if ('renderer' in spec && spec.renderer && typeof spec.renderer === 'function') {
23+
const result = spec.renderer(props);
24+
if (result instanceof HTMLElement) {
25+
return this.wrapElement(result, props);
26+
}
27+
if (result && 'element' in result) {
28+
return result as DOMElement;
29+
}
30+
throw new Error(`Renderer ${spec.id} did not return a valid element`);
31+
}
32+
33+
// Handle ComponentSpec
34+
if (!spec.component) {
35+
console.error('[VanillaRuntime] Invalid spec:', spec);
36+
throw new Error(`Invalid component spec: spec.id=${spec.id}, has component=${!!spec.component}`);
37+
}
38+
39+
// Call component function - it returns the element or factory
40+
// Pass runtime adapter as second arg for compatibility with spec interface
1641
const component = spec.component(props, this);
1742

1843
// If component returns a DOM element directly

packages/factories/vanilla/src/components/DefaultFormContainer.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { createDefaultSubmitButton, type SubmitButtonFactory } from './DefaultSu
1313
*/
1414
export interface FormContainerProps {
1515
/** Child elements to render inside the form */
16-
children?: HTMLElement[];
16+
children?: any[];
1717
/** Submit handler - when provided, renders a submit button */
1818
onSubmit?: () => void;
1919
/** External context passed from FormFactory */
@@ -50,12 +50,23 @@ export function createDefaultFormContainer(props: FormContainerProps): HTMLEleme
5050
props.onSubmit?.();
5151
});
5252

53-
// Append children
54-
props.children?.forEach(child => form.appendChild(child));
53+
// Append children - handle both HTMLElement and DOMElement wrapper
54+
if (props.children) {
55+
props.children.forEach(child => {
56+
if (child && typeof child === 'object') {
57+
if ('element' in child && child.element instanceof HTMLElement) {
58+
form.appendChild(child.element);
59+
} else if (child instanceof HTMLElement) {
60+
form.appendChild(child);
61+
}
62+
}
63+
});
64+
}
5565

5666
// Add submit button if onSubmit is provided
5767
if (props.onSubmit) {
58-
const submitButton = createDefaultSubmitButton({ onSubmit: props.onSubmit });
68+
const submitButtonFactory = props.createSubmitButton || createDefaultSubmitButton;
69+
const submitButton = submitButtonFactory({ onSubmit: props.onSubmit });
5970
form.appendChild(submitButton);
6071
}
6172

packages/factories/vanilla/src/components/DefaultSubmitButton.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export function createDefaultSubmitButton(props: SubmitButtonProps): HTMLElement
2828
container.style.textAlign = 'right';
2929

3030
const button = document.createElement('button');
31-
button.type = 'button';
31+
button.type = 'submit';
3232
button.textContent = 'Submit';
3333
button.dataset.testId = 'submit-button';
3434
button.style.padding = '12px 24px';
@@ -39,7 +39,6 @@ export function createDefaultSubmitButton(props: SubmitButtonProps): HTMLElement
3939
button.style.cursor = 'pointer';
4040
button.style.fontSize = '16px';
4141
button.style.fontWeight = '500';
42-
button.addEventListener('click', props.onSubmit);
4342

4443
container.appendChild(button);
4544
return container;
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* Vanilla FormField Component
3+
*/
4+
5+
export interface FormFieldProps {
6+
children?: any[];
7+
}
8+
9+
export function createFormField(props: FormFieldProps): HTMLElement {
10+
const wrapper = document.createElement('div');
11+
wrapper.style.marginBottom = '16px';
12+
13+
// Render children
14+
if (props.children) {
15+
props.children.forEach(child => {
16+
if (child && typeof child === 'object') {
17+
if ('element' in child && child.element instanceof HTMLElement) {
18+
wrapper.appendChild(child.element);
19+
} else if (child instanceof HTMLElement) {
20+
wrapper.appendChild(child);
21+
}
22+
}
23+
});
24+
}
25+
26+
return wrapper;
27+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/**
2+
* Vanilla FormSectionContainer Component
3+
*/
4+
5+
export interface FormSectionContainerProps {
6+
children?: any[];
7+
}
8+
9+
export function createFormSectionContainer(props: FormSectionContainerProps): HTMLElement {
10+
const wrapper = document.createElement('div');
11+
wrapper.style.marginBottom = '24px';
12+
wrapper.style.padding = '20px';
13+
wrapper.style.border = '1px solid #e5e7eb';
14+
wrapper.style.borderRadius = '8px';
15+
wrapper.style.background = '#fff';
16+
17+
// Render children
18+
if (props.children) {
19+
props.children.forEach(child => {
20+
if (child && typeof child === 'object') {
21+
if ('element' in child && child.element instanceof HTMLElement) {
22+
wrapper.appendChild(child.element);
23+
} else if (child instanceof HTMLElement) {
24+
wrapper.appendChild(child);
25+
}
26+
}
27+
});
28+
}
29+
30+
return wrapper;
31+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/**
2+
* Vanilla FormSectionGroup Component
3+
*/
4+
5+
export interface FormSectionGroupProps {
6+
children?: any[];
7+
}
8+
9+
export function createFormSectionGroup(props: FormSectionGroupProps): HTMLElement {
10+
const wrapper = document.createElement('div');
11+
wrapper.style.display = 'flex';
12+
wrapper.style.flexDirection = 'column';
13+
wrapper.style.gap = '8px';
14+
15+
// Render children
16+
if (props.children) {
17+
props.children.forEach(child => {
18+
if (child && typeof child === 'object') {
19+
if ('element' in child && child.element instanceof HTMLElement) {
20+
wrapper.appendChild(child.element);
21+
} else if (child instanceof HTMLElement) {
22+
wrapper.appendChild(child);
23+
}
24+
}
25+
});
26+
}
27+
28+
return wrapper;
29+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/**
2+
* Vanilla FormSectionGroupContainer Component
3+
*/
4+
5+
export interface FormSectionGroupContainerProps {
6+
children?: any[];
7+
}
8+
9+
export function createFormSectionGroupContainer(props: FormSectionGroupContainerProps): HTMLElement {
10+
const wrapper = document.createElement('div');
11+
wrapper.style.display = 'grid';
12+
wrapper.style.gridTemplateColumns = 'repeat(2, 1fr)';
13+
wrapper.style.gap = '16px';
14+
wrapper.style.marginBottom = '16px';
15+
16+
// Render children
17+
if (props.children) {
18+
props.children.forEach(child => {
19+
if (child && typeof child === 'object') {
20+
if ('element' in child && child.element instanceof HTMLElement) {
21+
wrapper.appendChild(child.element);
22+
} else if (child instanceof HTMLElement) {
23+
wrapper.appendChild(child);
24+
}
25+
}
26+
});
27+
}
28+
29+
return wrapper;
30+
}

0 commit comments

Comments
 (0)