neumorphic-peripheral
Version:
A lightweight, framework-agnostic JavaScript/TypeScript library for beautiful neumorphic styling
720 lines (706 loc) • 26.9 kB
JavaScript
;
/**
* Joi Validation Adapter for Neumorphic Peripheral
*
* This adapter allows you to use Joi schemas for validation
* Install: npm install joi
*
* Usage:
* import Joi from 'joi'
* import { joiAdapter } from 'neumorphic-peripheral/adapters/joi'
*
* const schema = Joi.string().email().min(5)
* np.input(element, { validate: joiAdapter(schema) })
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.examples = exports.joiUtils = exports.joiIntegration = exports.JoiFormValidator = exports.joiSchemas = void 0;
exports.joiAdapter = joiAdapter;
exports.joiAdapterDetailed = joiAdapterDetailed;
exports.joiAdapterAsync = joiAdapterAsync;
exports.createJoiFormValidator = createJoiFormValidator;
/**
* Creates a validation function from a Joi schema
*/
function joiAdapter(schema) {
return (value) => {
const result = schema.validate(value);
if (result.error) {
// Return first error message from Joi
return result.error.details[0]?.message || 'Invalid value';
}
return null; // Valid
};
}
/**
* Creates a comprehensive validation function that returns all errors
*/
function joiAdapterDetailed(schema) {
return (value) => {
const result = schema.validate(value, { abortEarly: false });
if (result.error) {
const errors = result.error.details.map((detail) => detail.message);
return { isValid: false, errors };
}
return { isValid: true, errors: [] };
};
}
/**
* Async validation adapter for Joi schemas
*/
function joiAdapterAsync(schema) {
return async (value) => {
try {
await schema.validateAsync(value, { abortEarly: false });
return { isValid: true, errors: [] };
}
catch (error) {
const errors = [];
if (error.details && Array.isArray(error.details)) {
errors.push(...error.details.map((detail) => detail.message));
}
else if (error.message) {
errors.push(error.message);
}
else {
errors.push('Invalid value');
}
return { isValid: false, errors };
}
};
}
/**
* Schema builder utilities for common patterns
*/
exports.joiSchemas = {
email: () => {
return Promise.resolve().then(() => __importStar(require('joi'))).then(Joi => Joi.default.string().email({ tlds: { allow: false } }).required().messages({
'string.email': 'Please enter a valid email address',
'any.required': 'Email is required'
}));
},
password: (minLength = 8) => {
return Promise.resolve().then(() => __importStar(require('joi'))).then(Joi => Joi.default.string()
.min(minLength)
.pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/)
.required()
.messages({
'string.min': `Password must be at least ${minLength} characters`,
'string.pattern.base': 'Password must contain uppercase, lowercase, number and special character',
'any.required': 'Password is required'
}));
},
phone: () => {
return Promise.resolve().then(() => __importStar(require('joi'))).then(Joi => Joi.default.string()
.pattern(/^\+?[1-9]\d{1,14}$/)
.required()
.messages({
'string.pattern.base': 'Please enter a valid phone number',
'any.required': 'Phone number is required'
}));
},
url: () => {
return Promise.resolve().then(() => __importStar(require('joi'))).then(Joi => Joi.default.string().uri().required().messages({
'string.uri': 'Please enter a valid URL',
'any.required': 'URL is required'
}));
},
required: (message = 'This field is required') => {
return Promise.resolve().then(() => __importStar(require('joi'))).then(Joi => Joi.default.string().required().messages({
'any.required': message
}));
},
number: (min, max) => {
return Promise.resolve().then(() => __importStar(require('joi'))).then(Joi => {
let schema = Joi.default.number();
if (min !== undefined) {
schema = schema.min(min);
}
if (max !== undefined) {
schema = schema.max(max);
}
return schema.required().messages({
'number.base': 'Must be a number',
'number.min': `Must be at least ${min}`,
'number.max': `Must be no more than ${max}`,
'any.required': 'This field is required'
});
});
},
/**
* Date validation schemas
*/
date: () => {
return Promise.resolve().then(() => __importStar(require('joi'))).then(Joi => ({
birthDate: Joi.default.date()
.max('now')
.min('1900-01-01')
.messages({
'date.max': 'Birth date cannot be in the future',
'date.min': 'Birth date must be after 1900'
}),
futureDate: Joi.default.date()
.min('now')
.messages({
'date.min': 'Date must be in the future'
}),
pastDate: Joi.default.date()
.max('now')
.messages({
'date.max': 'Date must be in the past'
}),
ageRange: (minAge, maxAge) => Joi.default.date()
.max(new Date(Date.now() - minAge * 365.25 * 24 * 60 * 60 * 1000))
.min(new Date(Date.now() - maxAge * 365.25 * 24 * 60 * 60 * 1000))
.messages({
'date.max': `Must be at least ${minAge} years old`,
'date.min': `Must be no more than ${maxAge} years old`
})
}));
}
};
/**
* Form-level validation using Joi
*/
class JoiFormValidator {
constructor(schema) {
this.fields = new Map();
this.schema = schema;
}
/**
* Register a field with its schema key
*/
registerField(element, key) {
this.fields.set(element, key);
}
/**
* Validate a specific field
*/
validateField(element) {
const key = this.fields.get(element);
if (!key) {
return { isValid: true, errors: [] };
}
const value = this.getElementValue(element);
try {
// Try to extract field schema from object schema
let fieldSchema;
if (this.schema.extract) {
fieldSchema = this.schema.extract(key);
}
else {
// If extract is not available, create a simple test
const testData = { [key]: value };
const result = this.schema.validate(testData, { abortEarly: false, allowUnknown: true });
if (result.error) {
const fieldErrors = result.error.details
.filter((detail) => detail.path[0] === key)
.map((detail) => detail.message);
return { isValid: fieldErrors.length === 0, errors: fieldErrors };
}
return { isValid: true, errors: [] };
}
const result = fieldSchema.validate(value, { abortEarly: false });
if (result.error) {
const errors = result.error.details.map((detail) => detail.message);
return { isValid: false, errors };
}
return { isValid: true, errors: [] };
}
catch (error) {
// Fallback: validate the whole object with just this field
const testData = { [key]: value };
const result = this.schema.validate(testData, { abortEarly: false, allowUnknown: true });
if (result.error) {
const fieldErrors = result.error.details
.filter((detail) => detail.path[0] === key)
.map((detail) => detail.message);
return { isValid: fieldErrors.length === 0, errors: fieldErrors };
}
return { isValid: true, errors: [] };
}
}
/**
* Async field validation
*/
async validateFieldAsync(element) {
const key = this.fields.get(element);
if (!key) {
return { isValid: true, errors: [] };
}
const value = this.getElementValue(element);
try {
// Try async validation
let fieldSchema;
if (this.schema.extract) {
fieldSchema = this.schema.extract(key);
await fieldSchema.validateAsync(value, { abortEarly: false });
}
else {
// Fallback to full object validation
const testData = { [key]: value };
const result = await this.schema.validateAsync(testData, { abortEarly: false, allowUnknown: true });
return { isValid: true, errors: [] };
}
return { isValid: true, errors: [] };
}
catch (error) {
const errors = [];
if (error.details && Array.isArray(error.details)) {
errors.push(...error.details.map((detail) => detail.message));
}
else if (error.message) {
errors.push(error.message);
}
else {
errors.push('Invalid value');
}
return { isValid: false, errors };
}
}
/**
* Validate entire form
*/
validateForm() {
const formData = {};
const errors = {};
// Collect all field values
this.fields.forEach((key, element) => {
formData[key] = this.getElementValue(element);
});
const result = this.schema.validate(formData, { abortEarly: false });
if (result.error) {
result.error.details.forEach((detail) => {
const path = detail.path.join('.');
if (!errors[path]) {
errors[path] = [];
}
errors[path].push(detail.message);
});
return { isValid: false, errors };
}
return { isValid: true, errors: {}, data: result.value };
}
/**
* Async form validation
*/
async validateFormAsync() {
const formData = {};
const errors = {};
// Collect all field values
this.fields.forEach((key, element) => {
formData[key] = this.getElementValue(element);
});
try {
const result = await this.schema.validateAsync(formData, { abortEarly: false });
return { isValid: true, errors: {}, data: result };
}
catch (error) {
if (error.details && Array.isArray(error.details)) {
error.details.forEach((detail) => {
const path = detail.path.join('.');
if (!errors[path]) {
errors[path] = [];
}
errors[path].push(detail.message);
});
}
return { isValid: false, errors };
}
}
getElementValue(element) {
if (element instanceof HTMLInputElement) {
if (element.type === 'checkbox') {
return element.checked;
}
if (element.type === 'number') {
return element.value ? Number(element.value) : undefined;
}
if (element.type === 'date') {
return element.value ? new Date(element.value) : undefined;
}
return element.value;
}
if (element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement) {
return element.value;
}
return element.textContent || '';
}
}
exports.JoiFormValidator = JoiFormValidator;
/**
* Helper function to create form validator
*/
function createJoiFormValidator(schema) {
return new JoiFormValidator(schema);
}
/**
* Integration with neumorphic components
*/
exports.joiIntegration = {
/**
* Setup form validation with Joi schema
*/
setupForm(formElement, schema, components) {
const validator = new JoiFormValidator(schema);
// Register all components with their field names
Object.entries(components).forEach(([key, component]) => {
if (component && component.element) {
validator.registerField(component.element, key);
// Setup real-time validation
component.element.addEventListener('blur', () => {
const result = validator.validateField(component.element);
if (!result.isValid && component.clearErrors && component.validate) {
component.clearErrors();
// Create a validation function that returns the Joi result
component.validate = () => result;
component.updateValidationState?.();
}
});
}
});
// Setup form submission validation
formElement.addEventListener('submit', (e) => {
e.preventDefault();
const result = validator.validateForm();
if (result.isValid) {
// Form is valid, can submit
const event = new CustomEvent('np:form-valid', {
detail: { data: result.data }
});
formElement.dispatchEvent(event);
}
else {
// Show field-specific errors
Object.entries(result.errors).forEach(([fieldName, fieldErrors]) => {
const component = components[fieldName];
if (component && component.clearErrors) {
component.clearErrors();
// Set custom validation result
component._validationResult = {
isValid: false,
errors: fieldErrors
};
component.updateValidationState?.();
}
});
const event = new CustomEvent('np:form-invalid', {
detail: { errors: result.errors }
});
formElement.dispatchEvent(event);
}
});
return validator;
},
/**
* Setup async form validation
*/
setupAsyncForm(formElement, schema, components) {
const validator = new JoiFormValidator(schema);
// Register all components with their field names
Object.entries(components).forEach(([key, component]) => {
if (component && component.element) {
validator.registerField(component.element, key);
// Setup real-time async validation
component.element.addEventListener('blur', async () => {
const result = await validator.validateFieldAsync(component.element);
if (!result.isValid && component.clearErrors && component.validate) {
component.clearErrors();
component.validate = () => result;
component.updateValidationState?.();
}
});
}
});
// Setup async form submission validation
formElement.addEventListener('submit', async (e) => {
e.preventDefault();
const result = await validator.validateFormAsync();
if (result.isValid) {
const event = new CustomEvent('np:form-valid', {
detail: { data: result.data }
});
formElement.dispatchEvent(event);
}
else {
// Show field-specific errors
Object.entries(result.errors).forEach(([fieldName, fieldErrors]) => {
const component = components[fieldName];
if (component && component.clearErrors) {
component.clearErrors();
component._validationResult = {
isValid: false,
errors: fieldErrors
};
component.updateValidationState?.();
}
});
const event = new CustomEvent('np:form-invalid', {
detail: { errors: result.errors }
});
formElement.dispatchEvent(event);
}
});
return validator;
},
/**
* Setup conditional validation based on other fields
*/
setupConditionalValidation(component, schemaBuilder, getFormData) {
if (!component || !component.element)
return;
component.element.addEventListener('blur', () => {
try {
const formData = getFormData();
const schema = schemaBuilder(formData);
const fieldName = component.element.name || component.element.id;
const value = component.getValue ? component.getValue() : component.element.value;
let fieldSchema;
if (schema.extract) {
fieldSchema = schema.extract(fieldName);
}
else {
// Use the full schema
fieldSchema = schema;
}
const result = fieldSchema.validate(value);
if (result.error) {
if (component.clearErrors && component.validate) {
component.clearErrors();
component._validationResult = {
isValid: false,
errors: [result.error.details[0]?.message || 'Invalid value']
};
component.updateValidationState?.();
}
}
else {
if (component.clearErrors) {
component.clearErrors();
}
}
}
catch (error) {
console.warn('Conditional validation error:', error.message);
}
});
}
};
/**
* Advanced Joi utilities for neumorphic components
*/
exports.joiUtils = {
/**
* Create a cross-field validation schema
*/
createCrossFieldValidation: async (dependencies) => {
const Joi = await Promise.resolve().then(() => __importStar(require('joi')));
return Joi.default.object().custom((value, helpers) => {
// Example: password confirmation
if (dependencies.includes('password') && dependencies.includes('confirmPassword')) {
if (value.password !== value.confirmPassword) {
return helpers.error('any.invalid', {
message: 'Passwords must match'
});
}
}
return value;
});
},
/**
* Create async validation (e.g., for username availability)
*/
createAsyncValidation: (checkFunction, errorMessage = 'Value is not available') => {
return Promise.resolve().then(() => __importStar(require('joi'))).then(Joi => Joi.default.string().external(async (value) => {
if (!value)
return value; // Let required handle empty values
try {
const isValid = await checkFunction(value);
if (!isValid) {
throw new Error(errorMessage);
}
return value;
}
catch (error) {
throw new Error(errorMessage);
}
}));
},
/**
* Create conditional validation based on other fields
*/
createConditionalValidation: (condition, trueSchema, falseSchema) => {
return Promise.resolve().then(() => __importStar(require('joi'))).then(Joi => Joi.default.alternatives().conditional(Joi.default.ref('$context'), {
is: condition,
then: trueSchema,
otherwise: falseSchema || Joi.default.any()
}));
},
/**
* Create date validation with relative constraints
*/
createDateValidation: () => {
return Promise.resolve().then(() => __importStar(require('joi'))).then(Joi => ({
birthDate: Joi.default.date()
.max('now')
.min('1900-01-01')
.messages({
'date.max': 'Birth date cannot be in the future',
'date.min': 'Birth date must be after 1900'
}),
futureDate: Joi.default.date()
.min('now')
.messages({
'date.min': 'Date must be in the future'
}),
pastDate: Joi.default.date()
.max('now')
.messages({
'date.max': 'Date must be in the past'
}),
ageRange: (minAge, maxAge) => Joi.default.date()
.max(new Date(Date.now() - minAge * 365.25 * 24 * 60 * 60 * 1000))
.min(new Date(Date.now() - maxAge * 365.25 * 24 * 60 * 60 * 1000))
.messages({
'date.max': `Must be at least ${minAge} years old`,
'date.min': `Must be no more than ${maxAge} years old`
})
}));
},
/**
* Create file validation schemas
*/
createFileValidation: () => {
return Promise.resolve().then(() => __importStar(require('joi'))).then(Joi => ({
imageFile: Joi.default.object({
mimetype: Joi.default.string().valid('image/jpeg', 'image/png', 'image/gif', 'image/webp')
.messages({
'any.only': 'Only JPEG, PNG, GIF, and WebP images are allowed'
}),
size: Joi.default.number().max(5 * 1024 * 1024) // 5MB
.messages({
'number.max': 'File size must be less than 5MB'
})
}),
documentFile: Joi.default.object({
mimetype: Joi.default.string().valid('application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document')
.messages({
'any.only': 'Only PDF and Word documents are allowed'
}),
size: Joi.default.number().max(10 * 1024 * 1024) // 10MB
.messages({
'number.max': 'File size must be less than 10MB'
})
})
}));
}
};
// Example usage documentation
exports.examples = {
basicField: `
import Joi from 'joi'
import { joiAdapter } from 'neumorphic-peripheral/adapters/joi'
const emailSchema = Joi.string().email().required()
np.input(emailElement, {
validate: joiAdapter(emailSchema)
})
`,
asyncValidation: `
import { joiUtils } from 'neumorphic-peripheral/adapters/joi'
const checkUsernameAvailability = async (username) => {
const response = await fetch(\`/api/check-username/\${username}\`)
return response.ok
}
const usernameSchema = await joiUtils.createAsyncValidation(
checkUsernameAvailability,
'Username is already taken'
)
np.input(usernameElement, {
validate: joiAdapterAsync(usernameSchema)
})
`,
complexForm: `
import Joi from 'joi'
import { joiIntegration } from 'neumorphic-peripheral/adapters/joi'
const formSchema = Joi.object({
email: Joi.string().email().required(),
password: Joi.string().min(8).required(),
confirmPassword: Joi.string()
.valid(Joi.ref('password'))
.required()
.messages({
'any.only': 'Passwords must match'
}),
age: Joi.number().min(18).required().messages({
'number.min': 'Must be at least 18 years old'
})
})
const components = {
email: np.input(emailEl),
password: np.password(passwordEl),
confirmPassword: np.password(confirmEl),
age: np.input(ageEl)
}
const validator = joiIntegration.setupForm(
formElement,
formSchema,
components
)
`,
dateValidation: `
import { joiUtils } from 'neumorphic-peripheral/adapters/joi'
const dateSchemas = await joiUtils.createDateValidation()
// Birth date validation
np.input(birthDateEl, {
validate: joiAdapter(dateSchemas.birthDate)
})
// Age range validation (18-65 years old)
np.input(dobEl, {
validate: joiAdapter(dateSchemas.ageRange(18, 65))
})
`,
fileValidation: `
import { joiUtils } from 'neumorphic-peripheral/adapters/joi'
const fileSchemas = await joiUtils.createFileValidation()
// Image file validation
np.input(imageUploadEl, {
validate: joiAdapter(fileSchemas.imageFile)
})
// Document file validation
np.input(documentUploadEl, {
validate: joiAdapter(fileSchemas.documentFile)
})
`
};