Implementação Manual de Máscaras com Typescript e React

A aplicação de máscaras em campos de input é uma funcionalidade comum em muitos formulários. Embora existam várias bibliotecas externas que podem facilitar essa tarefa, muitas vezes uma implementação manual básica é suficiente para atender às necessidades do projeto.

Optar por implementar máscaras manualmente, em vez de usar bibliotecas externas, oferece algumas vantagens significativas:

• Redução do Tamanho do Bundle: Ao evitar bibliotecas externas, o tamanho do bundle de código é reduzido. Isso resulta em tempos de carregamento mais rápidos e melhor desempenho geral da aplicação. Em projetos onde o desempenho é crucial, cada kilobyte conta.

• Controle Total: Implementar máscaras manualmente dá ao desenvolvedor controle total sobre o comportamento do input. Isso permite customizações específicas que podem não ser suportadas por bibliotecas genéricas.

• Simplicidade: Para casos de uso simples, a implementação de máscaras pode ser feita com algumas linhas de código JavaScript ou TypeScript, eliminando a necessidade de adicionar dependências externas.

Exemplos:

Input com máscara de CPF :

Input com máscara de CNPJ :

Input com máscara de telefone celular :

Input com máscara genérica de três caracteres :

Após ver a implementação você pode adaptar os caracteres da forma que quiser. O segredo dessa máscara genérica está na forma como você lida com o evento para apagar todos os caracteres da máscara. Se não tratar corretamente o tamanho da máscara, você terá resultados indesejados ao tentar apagar os caracteres do input. Argumentarei melhor sobre isso na devida sessão dessa máscara.

Input com máscara de porcentagem :

Fluxo de desenvolvimento dos algoritmos:

1 - Ajuste seu componente de Input para receber suas funções de máscara:

<input
    type={type}
    onChange={handleChange}
    onKeyDown={handleKeyDown}
    {...props}
/>

Não há estilização alguma nesse componente, pois o intuito do tutorial é mostrar o comportamento das funções apenas.

2 - Adicione uma função para lidar com o evento de mudança de valor do input:

const handleChange = React.useCallback(
      (event: React.ChangeEvent<HTMLInputElement>) => {
        if (mask && maskHandlers[mask]) {
          maskHandlers[mask](event);
        }
        if (props.onChange) {
          props.onChange(event);
        }
      },
      [mask, props]
);

A função handleChange é usada como um manipulador de eventos para o evento onChange de um elemento input. Ela é criada usando React.useCallback para memorização, o que ajuda a otimizar o desempenho evitando a criação de uma nova função em cada renderização.

3 - Adicione uma função que lida com o evento de pressionamento de teclas enquanto o input está focado.

const handleKeyDown = React.useCallback(
      (event: React.KeyboardEvent<HTMLInputElement>) => {
        if (props.onKeyDown) {
          props.onKeyDown(event);
        }
      },
      [props]
);

De forma análoga ao handleChange, também utilizamos o hook useCallback para otimizar essa função. O propósito desta função nos nossos inputs é permitir que o usuário apague os caracteres.

3.1 - Caso queira adicionar outros handles para os exemplos de máscara XYZ e porcentagem, não se esqueça de adicioná-los aqui. São essas chamadas de funções que farão que nosso usuário possa apagar corretamente os dados inseridos no input.

const handleKeyDown = React.useCallback(
      (event: React.KeyboardEvent<HTMLInputElement>) => {
        if (mask === "xyz") {
          xyzKeyDown(event);
        }
        if (mask === "percentage") {
          percentageKeyDown(event);
        }
        if (props.onKeyDown) {
          props.onKeyDown(event);
        }
      },
    [mask, props]
);

3.2 - Caso você queira utilizar as máscaras de porcentagem ou a máscara genérica XYZ, não se esqueça de adicionar as funções de manipulação do input para que o usuário possa apagar os caracteres de forma correta.

export const xyzKeyDown = (event: { key: string; target: any }) => {
  if (event.key === "Backspace" || event.key === "Delete") {
    const target = event.target;
    const value = target.value;

    const position = value.length - 4;

    if (target.selectionStart === value.length) {
      target.selectionStart = target.selectionEnd = position;
    }
  }
};

export const percentageKeyDown = (event: { key: string; target: any }) => {
  if (event.key === "Backspace" || event.key === "Delete") {
    const target = event.target;
    const value = target.value;

    const position = value.length - 1;

    if (target.selectionStart === value.length) {
      target.selectionStart = target.selectionEnd = position;
    }
  }
};

Perceba como ambas as funções iguais, mudando apenas o valor que é subtraído da constante position. Esse valor está relacionado ao tamanho dos caracteres da sua máscara. Na máscara de desconto temos 1 caractere na nossa máscara, portanto nós retiramos uma unidade do valor do input do usuário. Mantenha isso em mente quando for criar funções similares.

4 - Defina um objeto para mapear todos os tipos de máscara de entrada para funções manipuladoras de máscara.

const maskHandlers: Record<InputMasks, MaskHandler> = {
    cpf: handleCPFMask,
};

Record é um utilitário do TypeScript que constrói um tipo de objeto cujas chaves são do tipo InputMasks e cujos valores são do tipo MaskHandler.

InputMasks é um tipo que define as máscaras de entrada disponíveis (neste caso, a de CPF).
export type InputMasks = "cpf";
MaskHandler é um tipo que define a assinatura de uma função que lida com eventos de input e aplica a máscara apropriada.
export type MaskHandler = (event: React.ChangeEvent<HTMLInputElement>) => void;

5 - Adicionamos nossa função que irá chamar a função de formatação:

export const handleCPFMask = (event: { target: any }) => {
  const { value } = event.target;
  const formattedValue = maskCPF(value);
  event.target.value = formattedValue;
};

6 - Adicionamos nossa função para formatação de CPF:

Esperamos receber uma string de entrada e retorná-la no formato "XXX.XXX.XXX-XX"

Função de formatação para CPF:

export function maskCPF(cpf: string) {
    const numbersOnly = cpf.replace(/\D/g, '');

    if (numbersOnly.length <= 3) {
        return numbersOnly;
    } else if (numbersOnly.length <= 6) {
        return `${numbersOnly.slice(0, 3)}.${numbersOnly.slice(3)}`;
    } else if (numbersOnly.length <= 10) {
        return `${numbersOnly.slice(0, 3)}.${numbersOnly.slice(3, 6)}.${numbersOnly.slice(6)}`;
    } else {
        return `${numbersOnly.slice(0, 3)}.${numbersOnly.slice(3, 6)}.${numbersOnly.slice(6, 9)}-${numbersOnly.slice(9, 11)}`; 
    }
}

6.1 - Função para formatação de CNPJ:

Esperamos receber uma string de entrada e retorná-la no formato "XX.XXX.XXX/XXXX-XX"

export function maskCNPJ(cnpj: string) {
  const numbersOnly = cnpj.replace(/D/g, "");

  if (numbersOnly.length <= 2) {
    return numbersOnly;
  } else if (numbersOnly.length <= 5) {
    return `${numbersOnly.slice(0, 2)}.${numbersOnly.slice(2)}`;
  } else if (numbersOnly.length <= 8) {
    return `${numbersOnly.slice(0, 2)}.${numbersOnly.slice(2, 5)}.${numbersOnly.slice(5)}`;
  } else if (numbersOnly.length <= 12) {
    return `${numbersOnly.slice(0, 2)}.${numbersOnly.slice(2, 5)}.${numbersOnly.slice(5, 8)}/${numbersOnly.slice(8)}`;
  } else {
    return `${numbersOnly.slice(0, 2)}.${numbersOnly.slice(2, 5)}.${numbersOnly.slice(5, 8)}/${numbersOnly.slice(8, 12)}-${numbersOnly.slice(12, 14)}`;
  }
}

6.2 - Função para formatação de porcentagem:

Esperamos receber um numérico de entrada e retorná-la no formato "X %" a "XXX %". Lembrando que também limitamos o limite numérico a 100 para evitar que o usuário insira um dado acima de 100%.

export function maskPercentage(value: string) {
  const number = Number(value);
  return number < 100 ? `${number}%` : "100%";
}

6.2 - Função para formatação de telefone:

Esperamos receber um numérico de entrada e retorná-la no formato "(XX) X XXXX-XXXX".

export function maskPhone(phone: string) {
  const numbersOnly = phone.replace(/D/g, "");

  if (numbersOnly.length <= 2) {
    return numbersOnly;
  } else if (numbersOnly.length <= 7) {
    return `(${numbersOnly.slice(0, 2)}) ${numbersOnly.slice(2)}`;
  } else if (numbersOnly.length <= 11) {
    return `(${numbersOnly.slice(0, 2)}) ${numbersOnly.slice(2, 3)} ${numbersOnly.slice(3, 7)}-${numbersOnly.slice(7)}`;
  } else {
    return `(${numbersOnly.slice(0, 2)}) ${numbersOnly.slice(2, 3)} ${numbersOnly.slice(3, 7)}-${numbersOnly.slice(7, 11)}`;
  }
}

6.2 - Função para formatação da máscara genérica XYZ:

Esperamos receber um numérico de entrada e retorná-la no formato "XX xyz".

export function maskXYZ(value: string) {
  const number = Number(value);
  return `${number} xyz`;
}

© Pedro Melo - 2024. Todos os direitos reservados.