Creating Custom Validators
Creating Custom Validators
Section titled “Creating Custom Validators”While Phaser Hooks provides built-in validators, you can create your own custom validators to meet specific validation needs.
Validator Function Signature
Section titled “Validator Function Signature”A validator function must follow this signature:
type StateValidator = (value: unknown) => boolean | string;- Input: value- The value to validate
- Output: trueif valid,falseorstringerror message if invalid
Basic Custom Validator
Section titled “Basic Custom Validator”import { withLocalState } from 'phaser-hooks';
// Custom validator for email formatfunction emailValidator(value: unknown): boolean | string {  const email = value as string;
  if (typeof email !== 'string') {    return 'Value must be a string';  }
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;  if (!emailRegex.test(email)) {    return 'Value must be a valid email address';  }
  return true;}
// Custom validator for positive numbersfunction positiveNumberValidator(value: unknown): boolean | string {  const num = value as number;
  if (typeof num !== 'number' || Number.isNaN(num)) {    return 'Value must be a number';  }
  if (num <= 0) {    return 'Value must be a positive number';  }
  return true;}
export class GameScene extends Phaser.Scene {  create() {    // Use custom validators    const playerEmail = withLocalState(this, 'email', 'player@game.com', {      validator: emailValidator    });
    const playerScore = withLocalState(this, 'score', 100, {      validator: positiveNumberValidator    });  }}import { withLocalState } from 'phaser-hooks';
// Custom validator for email formatfunction emailValidator(value) {  const email = value;
  if (typeof email !== 'string') {    return 'Value must be a string';  }
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;  if (!emailRegex.test(email)) {    return 'Value must be a valid email address';  }
  return true;}
// Custom validator for positive numbersfunction positiveNumberValidator(value) {  const num = value;
  if (typeof num !== 'number' || Number.isNaN(num)) {    return 'Value must be a number';  }
  if (num <= 0) {    return 'Value must be a positive number';  }
  return true;}
export class GameScene extends Phaser.Scene {  create() {    // Use custom validators    const playerEmail = withLocalState(this, 'email', 'player@game.com', {      validator: emailValidator    });
    const playerScore = withLocalState(this, 'score', 100, {      validator: positiveNumberValidator    });  }}Advanced Custom Validators
Section titled “Advanced Custom Validators”Validator Factory Functions
Section titled “Validator Factory Functions”// Validator factory for string lengthfunction stringLengthValidator(minLength: number, maxLength?: number) {  return (value: unknown): boolean | string => {    const str = value as string;
    if (typeof str !== 'string') {      return 'Value must be a string';    }
    if (str.length < minLength) {      return `String must be at least ${minLength} characters long`;    }
    if (maxLength !== undefined && str.length > maxLength) {      return `String must be at most ${maxLength} characters long`;    }
    return true;  };}
// Validator factory for object propertiesfunction objectPropertyValidator(requiredProps: string[]) {  return (value: unknown): boolean | string => {    if (typeof value !== 'object' || value === null) {      return 'Value must be an object';    }
    const obj = value as Record<string, any>;
    for (const prop of requiredProps) {      if (!(prop in obj)) {        return `Object must have property: ${prop}`;      }    }
    return true;  };}
export class GameScene extends Phaser.Scene {  create() {    // Use validator factories    const playerName = withLocalState(this, 'playerName', 'Player', {      validator: stringLengthValidator(3, 20)    });
    const playerData = withLocalState(this, 'playerData', { name: 'Player', level: 1 }, {      validator: objectPropertyValidator(['name', 'level'])    });  }}// Validator factory for string lengthfunction stringLengthValidator(minLength, maxLength) {  return (value) => {    const str = value;
    if (typeof str !== 'string') {      return 'Value must be a string';    }
    if (str.length < minLength) {      return `String must be at least ${minLength} characters long`;    }
    if (maxLength !== undefined && str.length > maxLength) {      return `String must be at most ${maxLength} characters long`;    }
    return true;  };}
// Validator factory for object propertiesfunction objectPropertyValidator(requiredProps) {  return (value) => {    if (typeof value !== 'object' || value === null) {      return 'Value must be an object';    }
    const obj = value;
    for (const prop of requiredProps) {      if (!(prop in obj)) {        return `Object must have property: ${prop}`;      }    }
    return true;  };}
export class GameScene extends Phaser.Scene {  create() {    // Use validator factories    const playerName = withLocalState(this, 'playerName', 'Player', {      validator: stringLengthValidator(3, 20)    });
    const playerData = withLocalState(this, 'playerData', { name: 'Player', level: 1 }, {      validator: objectPropertyValidator(['name', 'level'])    });  }}Complex Validation Logic
Section titled “Complex Validation Logic”// Complex validator for game coordinatesfunction coordinateValidator(maxX: number, maxY: number) {  return (value: unknown): boolean | string => {    if (typeof value !== 'object' || value === null) {      return 'Value must be an object';    }
    const coord = value as { x: number; y: number };
    if (typeof coord.x !== 'number' || typeof coord.y !== 'number') {      return 'Coordinates must have numeric x and y properties';    }
    if (coord.x < 0 || coord.x > maxX) {      return `X coordinate must be between 0 and ${maxX}`;    }
    if (coord.y < 0 || coord.y > maxY) {      return `Y coordinate must be between 0 and ${maxY}`;    }
    return true;  };}
// Validator for inventory itemsfunction inventoryItemValidator() {  return (value: unknown): boolean | string => {    if (typeof value !== 'object' || value === null) {      return 'Value must be an object';    }
    const item = value as { id: string; name: string; quantity: number };
    if (typeof item.id !== 'string' || item.id.trim() === '') {      return 'Item must have a valid ID';    }
    if (typeof item.name !== 'string' || item.name.trim() === '') {      return 'Item must have a valid name';    }
    if (typeof item.quantity !== 'number' || item.quantity < 0) {      return 'Item quantity must be a non-negative number';    }
    return true;  };}
export class GameScene extends Phaser.Scene {  create() {    const playerPosition = withLocalState(this, 'position', { x: 0, y: 0 }, {      validator: coordinateValidator(800, 600)    });
    const inventoryItem = withLocalState(this, 'item', { id: 'sword', name: 'Iron Sword', quantity: 1 }, {      validator: inventoryItemValidator()    });  }}// Complex validator for game coordinatesfunction coordinateValidator(maxX, maxY) {  return (value) => {    if (typeof value !== 'object' || value === null) {      return 'Value must be an object';    }
    const coord = value;
    if (typeof coord.x !== 'number' || typeof coord.y !== 'number') {      return 'Coordinates must have numeric x and y properties';    }
    if (coord.x < 0 || coord.x > maxX) {      return `X coordinate must be between 0 and ${maxX}`;    }
    if (coord.y < 0 || coord.y > maxY) {      return `Y coordinate must be between 0 and ${maxY}`;    }
    return true;  };}
// Validator for inventory itemsfunction inventoryItemValidator() {  return (value) => {    if (typeof value !== 'object' || value === null) {      return 'Value must be an object';    }
    const item = value;
    if (typeof item.id !== 'string' || item.id.trim() === '') {      return 'Item must have a valid ID';    }
    if (typeof item.name !== 'string' || item.name.trim() === '') {      return 'Item must have a valid name';    }
    if (typeof item.quantity !== 'number' || item.quantity < 0) {      return 'Item quantity must be a non-negative number';    }
    return true;  };}
export class GameScene extends Phaser.Scene {  create() {    const playerPosition = withLocalState(this, 'position', { x: 0, y: 0 }, {      validator: coordinateValidator(800, 600)    });
    const inventoryItem = withLocalState(this, 'item', { id: 'sword', name: 'Iron Sword', quantity: 1 }, {      validator: inventoryItemValidator()    });  }}Error Handling with Custom Validators
Section titled “Error Handling with Custom Validators”function customEmailValidator(value: unknown): boolean | string {  const email = value as string;
  if (typeof email !== 'string') {    return 'Value must be a string';  }
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;  if (!emailRegex.test(email)) {    return 'Value must be a valid email address';  }
  return true;}
export class GameScene extends Phaser.Scene {  create() {    const playerEmail = withLocalState(this, 'email', 'player@game.com', {      validator: customEmailValidator    });
    function updateEmail(newEmail: string) {      try {        playerEmail.set(newEmail);        console.log('Email updated successfully');        return true;      } catch (error) {        console.error('Email validation failed:', error.message);        // Handle the error appropriately        this.showErrorMessage('Please enter a valid email address');        return false;      }    }  }}function customEmailValidator(value) {  const email = value;
  if (typeof email !== 'string') {    return 'Value must be a string';  }
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;  if (!emailRegex.test(email)) {    return 'Value must be a valid email address';  }
  return true;}
export class GameScene extends Phaser.Scene {  create() {    const playerEmail = withLocalState(this, 'email', 'player@game.com', {      validator: customEmailValidator    });
    function updateEmail(newEmail) {      try {        playerEmail.set(newEmail);        console.log('Email updated successfully');        return true;      } catch (error) {        console.error('Email validation failed:', error.message);        // Handle the error appropriately        this.showErrorMessage('Please enter a valid email address');        return false;      }    }  }}Best Practices
Section titled “Best Practices”1. Clear Error Messages
Section titled “1. Clear Error Messages”// ✅ Good - Clear error messagefunction goodValidator(value: unknown): boolean | string {  if (typeof value !== 'string') {    return 'Value must be a string';  }  return true;}
// ❌ Bad - Unclear error messagefunction badValidator(value: unknown): boolean | string {  if (typeof value !== 'string') {    return 'Invalid';  }  return true;}2. Type Safety
Section titled “2. Type Safety”// ✅ Good - Type-safe validationfunction typeSafeValidator(value: unknown): boolean | string {  const obj = value as { id: string; name: string };
  if (typeof obj.id !== 'string') {    return 'ID must be a string';  }
  if (typeof obj.name !== 'string') {    return 'Name must be a string';  }
  return true;}3. Reusable Validators
Section titled “3. Reusable Validators”// ✅ Good - Reusable validator factoryfunction createRangeValidator(min: number, max: number) {  return (value: unknown): boolean | string => {    const num = value as number;    if (typeof num !== 'number') {      return 'Value must be a number';    }    if (num < min || num > max) {      return `Value must be between ${min} and ${max}`;    }    return true;  };}
// Usageconst healthValidator = createRangeValidator(0, 100);const scoreValidator = createRangeValidator(0, 999999);Common Custom Validator Patterns
Section titled “Common Custom Validator Patterns”URL Validator
Section titled “URL Validator”function urlValidator(value: unknown): boolean | string {  const url = value as string;
  if (typeof url !== 'string') {    return 'Value must be a string';  }
  try {    new URL(url);    return true;  } catch {    return 'Value must be a valid URL';  }}Date Validator
Section titled “Date Validator”function dateValidator(value: unknown): boolean | string {  const date = value as string;
  if (typeof date !== 'string') {    return 'Value must be a string';  }
  const parsedDate = new Date(date);  if (isNaN(parsedDate.getTime())) {    return 'Value must be a valid date';  }
  return true;}Password Strength Validator
Section titled “Password Strength Validator”function passwordValidator(minLength: number = 8) {  return (value: unknown): boolean | string => {    const password = value as string;
    if (typeof password !== 'string') {      return 'Value must be a string';    }
    if (password.length < minLength) {      return `Password must be at least ${minLength} characters long`;    }
    if (!/[A-Z]/.test(password)) {      return 'Password must contain at least one uppercase letter';    }
    if (!/[a-z]/.test(password)) {      return 'Password must contain at least one lowercase letter';    }
    if (!/\d/.test(password)) {      return 'Password must contain at least one number';    }
    return true;  };}Next Steps
Section titled “Next Steps”- Validators Overview - Return to validators overview
- Number Range Validator - Learn about number validation
- Non-Empty String Validator - Learn about string validation