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,
};
};
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))
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
).
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));