Search code examples
javascriptreactjskeyboard-eventssetstate

Limiting keyboard inputs to A-Z as opposed to any keyboard input on React component


I've just come across a component I'm struggling to get my head around, but basically the value I care about is allowForClassification, which is a boolean value that is passed down into a child component and determines whether or not we display a button. (Basically meaning that only after a user has typed a letter will the "Find" button appear - currently even a spacebar will trigger the button to appear).

However I'm struggling to understand in this component where exactly that check is being made, I understand that at the bottom allowForClassification is set to true if canClassify & !classifyInProgress are returned but I can't find where they are making the check for the keyboard entry, any advice would be really helpful.

const getCcceValues = (object?: FormObjectModel | null) => {
  const ccceInput: $Shape<CcceInput> = {};

  if (!object) {
    return {};
  }

  const ccceValues = object.attributeCollection.questions.reduce(
    (acc, attribute) => {
      const fieldEntry = ccceBeInformedFieldMap.get(attribute.key);

      if (fieldEntry) {
        acc[fieldEntry] = attribute.value;
      }

      return acc;
    },
    ccceInput
  );

  // ready to perfom classification based on user input
  const canClassify = Object.values(ccceValues).every(Boolean);

  return { canClassify, ccceValues };
};

export const useCcceEmbed = (
  ccceResultAttribute: AttributeType,
  onChange: Function
): CcceHook => {
  const { object, form } = useFormObjectContext();
  const [resultCode, setResultCode] = useState<string | null>(null);

  const { canClassify, ccceValues } = getCcceValues(object);

  const { handleSubmit } = useFormSubmit();

  const [showModal, setShowModal] = useState<boolean>(false);

  const handleCloseModal = useCallback(() => setShowModal(false), []);
  const handleShowModal = useCallback(() => setShowModal(true), []);

  // state value to keep track of a current active classification
  const [classifyInProgress, setClassifyInProgress] = useState<boolean>(false);

  const handleResult = useCallback(
    (result) => {
      if (result?.hsCode) {
        onChange(ccceResultAttribute, result.hsCode);

        setResultCode(result.hsCode);
        setClassifyInProgress(false);

        handleSubmit(form);
      }
    },
    [ccceResultAttribute, form, handleSubmit, onChange]
  );

  const handleCancelClassify = useCallback(() => {
    setClassifyInProgress(false);
    handleCloseModal();
  }, [handleCloseModal]);

  const handleClassify = useCallback(
    (event?: SyntheticEvent<any>) => {
      if (event) {
        event.preventDefault();
        console.log("scenario 1");
      }

      if (classifyInProgress || !canClassify) {
        console.log("scenario 2");
        return;
      }

      const ccce = window.ccce;

      if (!ccceValues || !ccce) {
        throw new Error("Unable to classify - no values or not initialised");
        console.log("scenario 3");
      }

      setClassifyInProgress(true);

      const classificationParameters = {
        ...ccceValues,
        ...DEFAULT_EMBED_PROPS,
      };

      ccce.classify(
        classificationParameters,
        handleResult,
        handleCancelClassify
      );
    },
    [
      classifyInProgress,
      canClassify,
      ccceValues,
      handleResult,
      handleCancelClassify,
    ]
  );

  return {
    allowForClassification: canClassify && !classifyInProgress,
    classifyInProgress,
    dataProfileId,
    embedID: EMBED_ID,
    handleCancelClassify,
    handleClassify,
    handleCloseModal,
    handleShowModal,
    isDebugMode,
    resultCode,
    shouldShowModal: showModal,
  };
};


Solution

  • Solution

    There's a lot that's missing in this code and there may be a better way to do this. But here's what I came up with.

    Instead of evaluating whether or not the input value is empty, we can evaluate whether or not it contains a character.

    Replace this line:

    const canClassify = Object.values(ccceValues).every(Boolean);
    

    With these two lines:

    const regex = new RegExp('\w', 'g');
    
    const canClassify = Object.values(ccceValues).every(value => regex.test(value));
    

    or this one line:

    const canClassify = Object.values(ccceValues).every(value => /\w/g.test(value))
    

    Why That Line?

    I arrived at that solution by working backwards. Here's all the steps that I took through the code to arrive at the right place.

    allowForClassification: canClassify && !classifyInProgress,
    

    allowForClassification is true if canClassify is true and classifyInProgress is false. We don't want to classify if we are already performing a classification. That makes sense. So it's the canClassify value that we care about.

    const { canClassify, ccceValues } = getCcceValues(object);
    

    canClassify comes from the getCcceValues function, and it's value may depend on the value of object.

    const { object, form } = useFormObjectContext();
    

    object comes from some external hook which is a dead end for me because I can't see that code. But note that there might be some alternative solution here.

    const canClassify = Object.values(ccceValues).every(Boolean);
    

    .every(Boolean) has the same meaning as writing it out longform .every(value => Boolean(value)). canClassify is true if every value of ccceValues is true when cast to a Boolean. Which means that the value is anything other than: false, 0, -0, 0n, "", null, undefined, NaN. (source)

    const ccceInput: $Shape<CcceInput> = {};
    
    const ccceValues = object.attributeCollection.questions.reduce(
        (acc, attribute) => {
          const fieldEntry = ccceBeInformedFieldMap.get(attribute.key);
    
          if (fieldEntry) {
            acc[fieldEntry] = attribute.value;
          }
    
          return acc;
        },
        ccceInput
    );
    

    ccceValues is bit complex. It created by an array .reduce() operation. It starts with an initial value of ccceInput which is an empty object {}. At each step of the iteration, it maybe sets one property on that object. The final value is the object after a bunch of properties have been set.

    The arguments in the callback are acc -- the object that we are building, and attribute -- the current element of the questions array. The key that we are setting is fieldEntry and the value is attribute.value.

    Remember that canClassify checks if all values are "truthy". So the key doesn't matter. We only care about the value (attribute.value).


    Why That Code?

    But working backwards, what we learning is that when canClassify/allowForClassifiation is true in cases where you want it to be false, it's because there are values where Boolean(attribute.value) is true when you want it to be false. Why is that?

    I'm going to make a logical assumption that the attribute.value variable contains the string that the user has typed in the box.

    Remember what I said earlier about Boolean casting. The empty string '' is false and all other strings are true. So if there is at least one box in object.attributeCollection.questions where the user hasn't entered anything, then allowForClassification will be false because attribute.value will be ''.

    As soon as the user hits the spacebar in the last box, the attribute.value becomes ' '. This string has a Boolean value of true, so allowForClassification is now true.

    But you want to apply a different ruleset. You want to consider a value as true if and only if it contains at least one letter. We can check that using a simple regular expression. I'm not sure of your exact ruleset (Are numbers ok? What about underscores? What about accented/non-Latin characters?), so you might need to tweak the regex. But this should get you 99% of the way there.

    const regex = new RegExp('\w', 'g');
    // can also be written as: const regex = /\w/g;
    

    I am using the \w alphanumeric character class and the g global match flag to match any alphanumeric character anywhere in a string.

    To check if a string matches this regex, use the .test() method.

    const hasCharacter = regex.test(someString);
    

    We want to use this function in the .every() callback of canClassify instead of the Boolean constructor function. So we write:

    const canClassify = Object.values(ccceValues).every(value => regex.test(value));