Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@ docs/CODESANDBOX_TEMPLATES.md
.env.local
.env.*.local

# Playwright test artifacts
test-results/
test-screenshots/
tests/test-results/
tests/playwright-report/
playwright-report/

# Screenshots (ignore all images in root)
/*.png
/*.jpg
/*.jpeg
/*.gif
/*.webp
35 changes: 33 additions & 2 deletions packages/adapters/vanilla/src/form-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,43 @@ export class VanillaFormAdapter implements FormAdapter {
}

getValue(field: string): any {
// Support nested paths (e.g., "user.name")
if (field.includes('.')) {
const parts = field.split('.');
let value: any = this.values;
for (const part of parts) {
if (value === undefined || value === null) return undefined;
value = value[part];
}
return value;
}
return this.values[field];
}

setValue(field: string, value: any): void {
const oldValue = this.values[field];
this.values[field] = value;
const oldValue = this.getValue(field);

// Support nested paths (e.g., "user.name")
if (field.includes('.')) {
const parts = field.split('.');
let current: any = this.values;

// Create nested structure if needed
for (let i = 0; i < parts.length - 1; i++) {
const part = parts[i];
if (!current[part] || typeof current[part] !== 'object') {
current[part] = {};
}
current = current[part];
}

// Set the final value
const lastPart = parts[parts.length - 1];
current[lastPart] = value;
} else {
this.values[field] = value;
}

this.validateField(field);
this.emitter.emit('change', { field, value, oldValue });
this.emitter.emit(`change:${field}`, { value, oldValue });
Expand Down
4 changes: 4 additions & 0 deletions packages/adapters/vanilla/src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { defaultDebugConfig } from '@schepta/core';
*/
export interface ScheptaProviderProps {
components?: Record<string, ComponentSpec>;
customComponents?: Record<string, ComponentSpec>;
renderers?: Partial<Record<ComponentType, RendererFn>>;
middlewares?: MiddlewareFn[];
debug?: DebugConfig;
Expand All @@ -27,6 +28,7 @@ export interface ScheptaProviderProps {
*/
export interface ScheptaContextType {
components: Record<string, ComponentSpec>;
customComponents: Record<string, ComponentSpec>;
renderers: Record<ComponentType, RendererFn>;
middlewares: MiddlewareFn[];
debug: DebugConfig;
Expand Down Expand Up @@ -62,6 +64,7 @@ export function createScheptaProvider(

return {
components: { ...parentContext.components, ...(props.components || {}) },
customComponents: { ...parentContext.customComponents, ...(props.customComponents || {}) },
renderers: mergedRenderers,
middlewares: [...parentContext.middlewares, ...(props.middlewares || [])],
debug: { ...parentContext.debug, ...props.debug },
Expand All @@ -75,6 +78,7 @@ export function createScheptaProvider(

return {
components: props.components || {},
customComponents: props.customComponents || {},
renderers: props.renderers,
middlewares: props.middlewares || [],
debug: mergedDebug,
Expand Down
29 changes: 27 additions & 2 deletions packages/adapters/vanilla/src/runtime-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,40 @@
* Implements RuntimeAdapter using DOM APIs
*/

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

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

/**
* Vanilla JS runtime adapter implementation
*/
export class VanillaRuntimeAdapter implements RuntimeAdapter {
create(spec: ComponentSpec, props: Record<string, any>): RenderResult {
create(spec: ComponentSpec | RendererSpec, props: Record<string, any>): RenderResult {
if (!spec) {
console.error('[VanillaRuntime] Invalid spec:', spec);
throw new Error(`Invalid spec: spec is null/undefined`);
}

// Handle RendererSpec (for field renderers, etc)
if ('renderer' in spec && spec.renderer && typeof spec.renderer === 'function') {
const result = spec.renderer(props);
if (result instanceof HTMLElement) {
return this.wrapElement(result, props);
}
if (result && 'element' in result) {
return result as DOMElement;
}
throw new Error(`Renderer ${spec.id} did not return a valid element`);
}

// Handle ComponentSpec
if (!spec.component) {
console.error('[VanillaRuntime] Invalid spec:', spec);
throw new Error(`Invalid component spec: spec.id=${spec.id}, has component=${!!spec.component}`);
}

// Call component function - it returns the element or factory
// Pass runtime adapter as second arg for compatibility with spec interface
const component = spec.component(props, this);

// If component returns a DOM element directly
Expand Down
19 changes: 15 additions & 4 deletions packages/factories/vanilla/src/components/DefaultFormContainer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { createDefaultSubmitButton, type SubmitButtonFactory } from './DefaultSu
*/
export interface FormContainerProps {
/** Child elements to render inside the form */
children?: HTMLElement[];
children?: any[];
/** Submit handler - when provided, renders a submit button */
onSubmit?: () => void;
/** External context passed from FormFactory */
Expand Down Expand Up @@ -50,12 +50,23 @@ export function createDefaultFormContainer(props: FormContainerProps): HTMLEleme
props.onSubmit?.();
});

// Append children
props.children?.forEach(child => form.appendChild(child));
// Append children - handle both HTMLElement and DOMElement wrapper
if (props.children) {
props.children.forEach(child => {
if (child && typeof child === 'object') {
if ('element' in child && child.element instanceof HTMLElement) {
form.appendChild(child.element);
} else if (child instanceof HTMLElement) {
form.appendChild(child);
}
}
});
}

// Add submit button if onSubmit is provided
if (props.onSubmit) {
const submitButton = createDefaultSubmitButton({ onSubmit: props.onSubmit });
const submitButtonFactory = props.createSubmitButton || createDefaultSubmitButton;
const submitButton = submitButtonFactory({ onSubmit: props.onSubmit });
form.appendChild(submitButton);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export function createDefaultSubmitButton(props: SubmitButtonProps): HTMLElement
container.style.textAlign = 'right';

const button = document.createElement('button');
button.type = 'button';
button.type = 'submit';
button.textContent = 'Submit';
button.dataset.testId = 'submit-button';
button.style.padding = '12px 24px';
Expand All @@ -39,7 +39,6 @@ export function createDefaultSubmitButton(props: SubmitButtonProps): HTMLElement
button.style.cursor = 'pointer';
button.style.fontSize = '16px';
button.style.fontWeight = '500';
button.addEventListener('click', props.onSubmit);

container.appendChild(button);
return container;
Expand Down
27 changes: 27 additions & 0 deletions packages/factories/vanilla/src/components/FormField.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* Vanilla FormField Component
*/

export interface FormFieldProps {
children?: any[];
}

export function createFormField(props: FormFieldProps): HTMLElement {
const wrapper = document.createElement('div');
wrapper.style.marginBottom = '16px';

// Render children
if (props.children) {
props.children.forEach(child => {
if (child && typeof child === 'object') {
if ('element' in child && child.element instanceof HTMLElement) {
wrapper.appendChild(child.element);
} else if (child instanceof HTMLElement) {
wrapper.appendChild(child);
}
}
});
}

return wrapper;
}
31 changes: 31 additions & 0 deletions packages/factories/vanilla/src/components/FormSectionContainer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* Vanilla FormSectionContainer Component
*/

export interface FormSectionContainerProps {
children?: any[];
}

export function createFormSectionContainer(props: FormSectionContainerProps): HTMLElement {
const wrapper = document.createElement('div');
wrapper.style.marginBottom = '24px';
wrapper.style.padding = '20px';
wrapper.style.border = '1px solid #e5e7eb';
wrapper.style.borderRadius = '8px';
wrapper.style.background = '#fff';

// Render children
if (props.children) {
props.children.forEach(child => {
if (child && typeof child === 'object') {
if ('element' in child && child.element instanceof HTMLElement) {
wrapper.appendChild(child.element);
} else if (child instanceof HTMLElement) {
wrapper.appendChild(child);
}
}
});
}

return wrapper;
}
29 changes: 29 additions & 0 deletions packages/factories/vanilla/src/components/FormSectionGroup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* Vanilla FormSectionGroup Component
*/

export interface FormSectionGroupProps {
children?: any[];
}

export function createFormSectionGroup(props: FormSectionGroupProps): HTMLElement {
const wrapper = document.createElement('div');
wrapper.style.display = 'flex';
wrapper.style.flexDirection = 'column';
wrapper.style.gap = '8px';

// Render children
if (props.children) {
props.children.forEach(child => {
if (child && typeof child === 'object') {
if ('element' in child && child.element instanceof HTMLElement) {
wrapper.appendChild(child.element);
} else if (child instanceof HTMLElement) {
wrapper.appendChild(child);
}
}
});
}

return wrapper;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* Vanilla FormSectionGroupContainer Component
*/

export interface FormSectionGroupContainerProps {
children?: any[];
}

export function createFormSectionGroupContainer(props: FormSectionGroupContainerProps): HTMLElement {
const wrapper = document.createElement('div');
wrapper.style.display = 'grid';
wrapper.style.gridTemplateColumns = 'repeat(2, 1fr)';
wrapper.style.gap = '16px';
wrapper.style.marginBottom = '16px';

// Render children
if (props.children) {
props.children.forEach(child => {
if (child && typeof child === 'object') {
if ('element' in child && child.element instanceof HTMLElement) {
wrapper.appendChild(child.element);
} else if (child instanceof HTMLElement) {
wrapper.appendChild(child);
}
}
});
}

return wrapper;
}
20 changes: 20 additions & 0 deletions packages/factories/vanilla/src/components/FormSectionTitle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* Vanilla FormSectionTitle Component
*/

export interface FormSectionTitleProps {
title?: string;
children?: any[];
}

export function createFormSectionTitle(props: FormSectionTitleProps): HTMLElement {
const heading = document.createElement('h3');
heading.textContent = props.title || '';
heading.style.fontSize = '1.25rem';
heading.style.fontWeight = '600';
heading.style.marginTop = '0';
heading.style.marginBottom = '16px';
heading.style.color = '#111827';

return heading;
}
Loading