svarog-ui-core
Version:
Core components of Svarog UI without themes
326 lines (281 loc) • 9.8 kB
JavaScript
// src/components/Form/FormGroup.js
import {
createComponent,
createElement,
} from '../../utils/componentFactory.js';
import { createBaseComponent } from '../../utils/baseComponent.js';
import { validateRequiredProps } from '../../utils/validation.js';
import Typography from '../Typography/Typography.js';
// CSS injection imports
import { createStyleInjector } from '../../utils/styleInjection.js';
import { formGroupStyles } from './FormGroup.styles.js';
import { formValidationStyles } from './FormValidation.styles.js';
// Create style injectors for FormGroup component
const injectFormGroupStyles = createStyleInjector('FormGroup');
const injectFormValidationStyles = createStyleInjector('FormValidation');
/**
* Creates a FormGroup component for grouping form controls with labels
* @param {Object} props - FormGroup properties
* @returns {Object} FormGroup component API
*/
const createFormGroup = (props) => {
// Inject styles on component creation
injectFormGroupStyles(formGroupStyles);
injectFormValidationStyles(formValidationStyles);
// Migrate legacy props to standardized props
const migrateLegacyProps = (props) => {
const migrated = { ...props };
// Standardize labelPosition with defaultValue alias
if ('defaultValue' in props && !('labelPosition' in props)) {
console.warn('[FormGroup] defaultValue is being used for labelPosition');
migrated.labelPosition = props.defaultValue;
delete migrated.defaultValue;
}
return migrated;
};
// Normalize props - but keep original props for validation
const normalizedProps = migrateLegacyProps(props);
// Validate required props - use original props to maintain backward compatibility with tests
validateRequiredProps(
props,
{
label: { required: true, type: 'string' },
field: { required: true },
labelPosition: {
required: false,
type: 'string',
allowedValues: ['top', 'left', 'right', 'bottom'],
validator: (position) => {
if (
position &&
!['top', 'left', 'right', 'bottom'].includes(position)
) {
return 'must be one of: top, left, right, bottom';
}
return true;
},
},
},
'FormGroup'
);
// Initialize state from normalized props with defaults
const state = {
label: normalizedProps.label,
field: normalizedProps.field,
helpText: normalizedProps.helpText,
id: normalizedProps.id,
required: normalizedProps.required || false,
className: normalizedProps.className || '',
labelPosition: normalizedProps.labelPosition || 'top',
fieldType: normalizedProps.fieldType || 'text',
};
// Reference to the field component
let fieldComponent = null;
let validationMessageElement = null;
// Create base component with render function
const component = createBaseComponent((componentState) => {
return createFormGroupContainer(componentState);
})(state);
/**
* Creates the form group container
* @private
* @param {Object} state - Current state
* @returns {HTMLElement} The form group container element
*/
function createFormGroupContainer(state) {
const { label, field, helpText, id, required, className, labelPosition } =
state;
// Create container div with has-help-text class if helpText is provided
const container = createElement('div', {
classes: [
'form-group',
`form-group--${labelPosition}`,
helpText ? 'has-help-text' : '',
className,
],
});
// Create label directly without using Typography
const labelId = id ? `${id}-label` : null;
const labelElement = createElement('label', {
classes: [
'form-group__label',
required ? 'form-group__label--required' : '',
],
attributes: {
for: id,
id: labelId,
},
text: label,
});
// Create field container
const fieldContainer = createElement('div', {
classes: ['form-group__field'],
});
// Process field - either use the provided component or create an Input from string
let fieldElement;
if (typeof field === 'string') {
// If field is a string, create an Input component (would need to be imported)
throw new Error(
'FormGroup: field as string is not supported in this refactored version'
);
} else if (typeof field.getElement === 'function') {
// If field is a component with getElement method
fieldElement = field.getElement();
fieldComponent = field;
} else if (field instanceof HTMLElement) {
// If field is already an HTMLElement
fieldElement = field;
fieldComponent = null;
} else {
throw new Error(
'FormGroup: field must be a component with getElement method or an HTMLElement'
);
}
// Add field to container
fieldContainer.appendChild(fieldElement);
// Create validation message element
validationMessageElement = createElement('div', {
classes: ['form-group__validation-message'],
attributes: { 'aria-live': 'polite' },
// No textContent initially to ensure it doesn't take up space
});
// Create help text if provided - using Typography for this
let helpTextElement = null;
if (helpText) {
helpTextElement = Typography({
children: helpText,
as: 'div',
className: 'form-group__help-text',
}).getElement();
}
// Assemble the component based on label position
switch (labelPosition) {
case 'left':
container.appendChild(labelElement);
container.appendChild(fieldContainer);
container.appendChild(validationMessageElement);
if (helpTextElement) {
container.appendChild(helpTextElement);
}
break;
case 'right':
container.appendChild(fieldContainer);
container.appendChild(labelElement);
container.appendChild(validationMessageElement);
if (helpTextElement) {
container.appendChild(helpTextElement);
}
break;
case 'bottom':
container.appendChild(fieldContainer);
container.appendChild(labelElement);
container.appendChild(validationMessageElement);
if (helpTextElement) {
container.appendChild(helpTextElement);
}
break;
case 'top':
default:
container.appendChild(labelElement);
container.appendChild(fieldContainer);
container.appendChild(validationMessageElement);
if (helpTextElement) {
container.appendChild(helpTextElement);
}
break;
}
return container;
}
// Get the container element
const containerElement = component.getElement();
/**
* Updates the validation message for the field
* @param {string} message - The validation message to display
* @param {boolean} isValid - Whether the field is valid
* @returns {Object} FormGroup component for chaining
*/
function updateValidation(message, isValid) {
if (validationMessageElement) {
validationMessageElement.textContent = message || '';
// Always update the validation state classes based on isValid
// regardless of whether there's a message
containerElement.classList.remove('has-error', 'has-success');
containerElement.classList.add(isValid ? 'has-success' : 'has-error');
}
return api;
}
// Public API
const api = {
/**
* Gets the form group element
* @returns {HTMLElement} The form group container element
*/
getElement() {
return containerElement;
},
/**
* Gets the field component or element
* @returns {Object|HTMLElement} The field component or element
*/
getField() {
return (
fieldComponent ||
containerElement.querySelector('.form-group__field > *')
);
},
/**
* Updates the validation message for the field
* @param {string} message - The validation message to display
* @param {boolean} isValid - Whether the field is valid
* @returns {Object} FormGroup component for chaining
*/
updateValidation,
/**
* Validates the field if it has a validate method
* @returns {boolean} Whether the field is valid
*/
validate() {
if (fieldComponent && typeof fieldComponent.validate === 'function') {
const isValid = fieldComponent.validate();
// Get validation message from the field component if available
let message = '';
if (fieldComponent.validationMessageElement) {
message = fieldComponent.validationMessageElement.textContent;
}
// Update our own validation message
this.updateValidation(message, isValid);
return isValid;
}
return true;
},
/**
* Updates component with new props
* @param {Object} newProps - New properties
* @returns {Object} FormGroup component for chaining
*/
update(newProps) {
// Apply prop standardization to new props
const normalizedNewProps = migrateLegacyProps(newProps);
// Update state
Object.assign(state, normalizedNewProps);
// Update component
component.update(state);
return this;
},
/**
* Clean up resources
*/
destroy() {
// Clean up references
fieldComponent = null;
validationMessageElement = null;
// Call base component's destroy
component.destroy();
},
};
return api;
};
// Define required props for validation
createFormGroup.requiredProps = ['label', 'field'];
// Export as a component factory
export default createComponent('FormGroup', createFormGroup);