Hello and thanks for your time,
I am having an issue with the onValueChanged hook where i am losing the last character typed (or selected)
TypeScriptonValueChanged: (
question: QuestionCompositeModel,
name: string,
newValue: any,
) => {
const panel = <PanelModel>question["contentPanel"];
if (!panel) return;
let multipleChoiceQuestionArray: ProductsSurveyJSQuestionProductValue[] =
[];
const selectedProducts = question.value as Record<string, number[]>;
if (question.multipleChoice) {
const multipleChoiceProductPanel = <QuestionPanelDynamicModel>(
question.contentPanel.getQuestionByName("multipleChoiceProductPanel")
);
panel.elements.forEach((element, index) => {
if (element instanceof PanelModel) {
const productId = element.getPropertyValue("productId");
const quantityQuestion = element.getQuestionByName(
`quantity_${index}`,
);
if (quantityQuestion) {
const isProductSelected = Object.values(selectedProducts).some(
(products) =>
Array.isArray(products) && products.includes(productId),
);
if (isProductSelected) {
quantityQuestion.visible = true;
multipleChoiceQuestionArray.push({
productId,
price: element.getPropertyValue("price"),
quantity: quantityQuestion.value ?? 1,
});
} else {
quantityQuestion.visible = false;
multipleChoiceQuestionArray =
multipleChoiceQuestionArray.filter(
(p) => p.productId !== productId,
);
}
}
}
multipleChoiceProductPanel.value = multipleChoiceQuestionArray;
console.log(multipleChoiceQuestionArray);
console.log(multipleChoiceProductPanel.value);
});
}
JavaScriptquantityQuestion.textUpdateMode = "onTyping";
im logging the multipleChoiceProductPanel.value and the array it is set to, which all contain the correct values.
when i hit complete on the form and log sender.data in the model.onComplete i get a truncated value missing the last input.
any thoughts on how this could be happening?
Hello Alex,
I reviewed the code and your screenshots, however, I didn't quite understand what you're trying to achieve. Would you please elaborate on your usage scenario? Feel free to share a survey JSON definition and illustrate the target output.
Thank you
yes i have a quantity question that appears when a product is selected from a panel dynamic checkbox. i am creating a composite question to store the array of values entered
multipleChoiceQuestionArray : [ { "productId": 1, "quantity": 23 }, { "productId": 15, "quantity": 3 } ]
in the second product object i am not getting the last character typed in ("i typed 34"), so my last value isnt getting recorded. similarly, if select the second product without the quantity, the product selection is missing. therefore my last value is always missing. i gave you some screenshots where i am console logging the question values after they are set, and the array that i am setting the values to, which both contain the correct values. when i hit complete on the form. i lose the last value.
here is the full code snippet for my products quesiton
/* eslint-disable @typescript-eslint/no-explicit-any */ import { SessionView } from "admin/src/server/mappers/session/session"; import { generateIteratedQuestionName } from "admin/src/ui/components/surveyJS/utils/geIteratedQuestionName"; import { formatUsdString } from "admin/src/utils/helpers/invoice/formatUsdString"; import { ComponentCollection, ItemValue, PanelModel, Question, QuestionCheckboxModel, QuestionCompositeModel, QuestionPanelDynamicModel, QuestionRadiogroupModel, QuestionTextModel, Serializer, } from "survey-core"; import { QuestionAddedEvent, SurveyCreatorModel } from "survey-creator-core"; export const ProductsSurveyJSQuestionName = "products"; export type ProductsSurveyJSQuestionProps = { session?: SessionView; creator?: SurveyCreatorModel; }; export type ProductsSurveyJSQuestionProductOption = { value: number; price?: number; showPrice: boolean; userPriceOverride: boolean; text: string; visibleIf?: string; enableIf?: string; }; export type ProductsSurveyJSQuestionProductValue = { productId: number; price?: number; invoiceId?: number; quantity?: number; }; export type ProductsSurveyJSQuestionValue = { products?: ProductsSurveyJSQuestionProductValue[]; }; enum ProductsSurveyJSQuestionProperties { multipleChoice = "multipleChoice", options = "options", } Serializer.addProperty("panel", { name: "productId:number", default: 0, }); export const ProductsSurveyJSQuestionRegisterProperties = ( session?: SessionView, ) => { Serializer.addProperty(ProductsSurveyJSQuestionName, { name: ProductsSurveyJSQuestionProperties.multipleChoice, displayName: "Multiple Choice", type: "boolean", categoryIndex: 0, visibleIndex: 0, category: "Product Question", default: false, }); Serializer.addClass( "productitemvalue", [ { name: "value", type: "dropdown", choices: session?.society?.products?.map((product) => ({ value: product.id, text: product.name, })), }, { name: "text", type: "string", showMode: "form" }, { name: "price", type: "number", showMode: "form" }, { name: "showPrice", type: "boolean", showMode: "form" }, { name: "userPriceOverride", type: "boolean", showMode: "form" }, ], undefined, "itemvalue", ); Serializer.addProperty(ProductsSurveyJSQuestionName, { name: ProductsSurveyJSQuestionProperties.options, type: "productitemvalue[]", visible: true, categoryIndex: 0, visibleIndex: 1, category: "Product Question", }); }; export const ProductsSurveyJSQuestion = async ({ session, creator, }: ProductsSurveyJSQuestionProps) => { ProductsSurveyJSQuestionRegisterProperties(session); creator?.onQuestionAdded.add( (sender: SurveyCreatorModel, options: QuestionAddedEvent) => { if (options.question?.getType() == ProductsSurveyJSQuestionName) { options.question.name = generateIteratedQuestionName( ProductsSurveyJSQuestionName, sender, ); } }, ); const ProductsSurveyJSQuestionDefinition = { name: ProductsSurveyJSQuestionName, title: "Products", onInit: async () => { ProductsSurveyJSQuestionRegisterProperties(); }, onItemValuePropertyChanged( question: Question, options: { obj: ItemValue; propertyName: string; name: string; newValue: any; }, ): void { if ( options.propertyName === ProductsSurveyJSQuestionProperties.options && options.name === "value" && options.obj["text"] == options.newValue ) { const product = session?.society?.products?.find( (product) => product.id === options.newValue, ); options.obj["text"] = product?.name ?? "ERROR: No Product Name"; } buildProductQuestionElements(question); }, onValueChanged: ( question: QuestionCompositeModel, name: string, newValue: any, ) => { const panel = <PanelModel>question["contentPanel"]; const { multipleChoice } = question; if (!panel) return; let multipleChoiceQuestionArray: ProductsSurveyJSQuestionProductValue[] = []; if (multipleChoice) { panel.elements.forEach((element, index) => { if (element instanceof PanelModel) { const productId = element.getPropertyValue("productId"); const quantityQuestion = element.getQuestionByName( `quantity_${index}`, ); const isProductSelected = Object.values(question.value).some( (products) => Array.isArray(products) && products.includes(productId), ); if (isProductSelected) { quantityQuestion.visible = true; multipleChoiceQuestionArray.push({ productId, price: element.getPropertyValue("price"), quantity: quantityQuestion.value, }); } else { quantityQuestion.visible = false; multipleChoiceQuestionArray = multipleChoiceQuestionArray.filter( (p) => p.productId !== productId, ); } } }); question.contentPanel.getQuestionByName( "multipleChoiceProductPanel", ).value = multipleChoiceQuestionArray; } else { let panelIndex = 0; panel.elements.forEach((element) => { if (!(element instanceof PanelModel)) return; const productId = element.getPropertyValue("productId"); const quantityQuestion = <QuestionTextModel>( element.getQuestionByName(`quantity_${panelIndex}`) ); const productQuestion = <QuestionRadiogroupModel>( element.getQuestionByName(`singleChoiceProduct_${panelIndex}`) ); const singleChoiceProductQuestion = <QuestionPanelDynamicModel>( question.contentPanel.getQuestionByName( "singleChoiceProductQuestion", ) ); if (name.includes("singleChoiceProduct_")) { if (productId === newValue) { quantityQuestion.visible = true; singleChoiceProductQuestion.value = { productId: productQuestion.value, price: element.getPropertyValue("price"), }; } else { quantityQuestion.visible = false; productQuestion.clearValue(); quantityQuestion.clearValue(); } } if ( name.includes("quantity") && singleChoiceProductQuestion.value[panelIndex] ) { singleChoiceProductQuestion.value[panelIndex].quantity = newValue ?? 1; } panelIndex++; }); } }, onLoaded: (question: Question) => { buildProductQuestionElements(question); }, createElements: () => { return []; }, }; const buildProductQuestionChoiceText = ( option: ProductsSurveyJSQuestionProductOption, ) => { const product = session?.society?.products?.find( (product) => product.id === option.value, ); const formattedPrice = formatUsdString(option.price ?? product?.price ?? 0); if (option.showPrice) { return `${ option.text ?? product?.name ?? "ERROR: No Product Name OR Description" } - ${formattedPrice}`; } return ( option.text ?? product?.name ?? "ERROR: No Product Name OR Description" ); }; const buildProductQuestionElements = (question: Question) => { const panel = <PanelModel>question["contentPanel"]; if (!panel) { return; } for (const element of panel.elements) { panel.removeElement(element); } buildProductQuestionElementsMultipleChoice(question); buildProductQuestionElementsSingleChoice(question); }; const buildProductQuestionElementsMultipleChoice = (question: Question) => { const panel = <PanelModel>question["contentPanel"]; if (!panel || question.multipleChoice !== true) return; const options = <ProductsSurveyJSQuestionProductOption[]>question.options; options.forEach((option, index) => { const productPanel = new PanelModel(`productPanel_${index}`); productPanel.setPropertyValue("productId", option.value); const multipleChoiceProductQuestion = new QuestionCheckboxModel( `multipleChoiceProduct_${index}`, ); multipleChoiceProductQuestion.titleLocation = "hidden"; multipleChoiceProductQuestion.textUpdateMode = "onTyping"; multipleChoiceProductQuestion.isEditableTemplateElement = false; multipleChoiceProductQuestion.isInteractiveDesignElement = false; multipleChoiceProductQuestion.choices = [ new ItemValue(option.value, option.text), ]; const quantityQuestion = new QuestionTextModel(`quantity_${index}`); quantityQuestion.visible = false; quantityQuestion.title = `${option.text} Quantity`; quantityQuestion.inputType = "number"; quantityQuestion.textUpdateMode = "onTyping"; productPanel.addElement(multipleChoiceProductQuestion); productPanel.addElement(quantityQuestion); panel.addElement(productPanel); }); if ( !panel.elements.some( (element) => element.name === "multipleChoiceProductPanel", ) ) { const multipleChoiceProductPanel = new QuestionPanelDynamicModel( "multipleChoiceProductPanel", ); multipleChoiceProductPanel.visible = false; panel.addElement(multipleChoiceProductPanel); } }; const buildProductQuestionElementsSingleChoice = (question: Question) => { const panel = <PanelModel>question["contentPanel"]; if (!panel) { return; } const { multipleChoice } = question; const options = <ProductsSurveyJSQuestionProductOption[]>question.options; if (multipleChoice !== true) { options.forEach((option, index) => { const productPanel = new PanelModel(`productPanel_${index}`); productPanel.setPropertyValue("productId", option.value); const singleChoiceProductQuestion = new QuestionRadiogroupModel( `singleChoiceProduct_${index}`, ); singleChoiceProductQuestion.titleLocation = "hidden"; singleChoiceProductQuestion.isEditableTemplateElement = false; singleChoiceProductQuestion.isInteractiveDesignElement = false; singleChoiceProductQuestion.textUpdateMode = "onTyping"; singleChoiceProductQuestion.choices = [ new ItemValue(option.value, buildProductQuestionChoiceText(option)), ]; productPanel.addElement(singleChoiceProductQuestion); const quantityQuestion = new QuestionTextModel(`quantity_${index}`); quantityQuestion.visible = false; quantityQuestion.title = `${option.text} Quantity`; quantityQuestion.textUpdateMode = "onTyping"; quantityQuestion.inputType = "number"; productPanel.addElement(quantityQuestion); panel.addElement(productPanel); if ( !panel.elements.some( (element) => element.name === "singleChoiceProductQuestion", ) ) { const singleChoiceProductQuestion = new QuestionCheckboxModel( "singleChoiceProductQuestion", ); singleChoiceProductQuestion.visible = false; panel.addElement(singleChoiceProductQuestion); } }); if (!panel.elements.find((element) => element.name == "productId")) { const productQuestion = new QuestionTextModel("productId"); productQuestion.clearIfInvisible = "none"; productQuestion.visible = false; panel.addElement(productQuestion); } if (!panel.elements.find((element) => element.name == "price")) { const priceQuestion = new QuestionTextModel("price"); priceQuestion.clearIfInvisible = "none"; priceQuestion.visible = false; panel.addElement(priceQuestion); } if (!panel.elements.find((element) => element.name == "invoiceId")) { const invoiceQuestion = new QuestionTextModel("invoiceId"); invoiceQuestion.clearIfInvisible = "none"; invoiceQuestion.visible = false; panel.addElement(invoiceQuestion); } } }; if ( !ComponentCollection.Instance.getCustomQuestionByName( ProductsSurveyJSQuestionName, ) ) { ComponentCollection.Instance.add(ProductsSurveyJSQuestionDefinition); } };
Hello,
Thank you for the update. Is it possible to share a simplified code sandbox demo / a survey JSON definition so that I can recreate the usage scenario and test it?