enketo / enketo

Enketo web forms monorepo
Apache License 2.0
9 stars 15 forks source link

enketo-core Form object doesn't see user changes (form.getDataStr() is always the same) #1300

Closed ChasNelson1990 closed 2 months ago

ChasNelson1990 commented 3 months ago

Hi there,

Apologies if this should go on the ODK Forum somewhere - it wasn't quite clear to me where to post for something like this (not really a bug, not really a feature request).

I am developing an Electron & React app for a client and they want to be able to render XLSForms via this app.

I have one of their XLSForms and it is marked as valid and can render fine using https://getodk.org/xlsform/.

The XML generated by our server also passes the validation tests at https://validate.enketo.org/.

Our Electron app then stores this XML in a local DB (again I can copy and paste straight from the DB into validate.enketo.org and it passes).

Inside the frontend of our app, we then run that ODK XML through enketo-transformer/web. This seems successful because I can pass the output HTML and XML strings to the below React functional component.

As you can see from my in-line comments, the result is that my app renders the form :ta-da:, and I can select radio boxes or pick from drop down options.

But :disappointed: whatever I do, none of the form logic seems to happen, e.g. I select Option A, which should reveal Question 6 (and does when rendered via getodk.org/xlsform)... but it doesn't.

And, even thought I can select radio boxes, or drop downs, in the app - when I hit my submit button (which calls form.getDataStr()) the data string is always the same (my form's default values and everything else empty).

I've spent that last 4 hours trawling the issues and source code and seemingly this is not an issue others have had... so... question reveal time... what on earth am I doing wrong?! I am presuming I have mishandled something either at init or injection but I can't workout what.

We are working to a stupid tight deadline on this project and this is the last component in the chain for us to have our MVP so any guidance would be massively appreciated!

import React, { useEffect, useRef, useState } from "react";
import { Form } from "enketo-core";
import { Button } from "@material-ui/core";
import "./theme-kobo.css";

interface EnketoFormProps {
  XML: string; // The XML output of enketo-transformer/web
  HTML: string; // The HTMl output of enketo-transformer/web
}

export const EnketoForm: React.FC<EnketoFormProps> = ({ XML, HTML }) => {
  const formRef = useRef<HTMLDivElement>(null);
  const [form, setForm] = useState<Form | null>(null);

  useEffect(() => {
    // inject the HTML into the form container
    if (!HTML) return;
    if (formRef.current === null) return;

    formRef.current.innerHTML = HTML;
  }, [formRef, HTML]);

  useEffect(() => {
    // create the enketo-core Form object
    if (!HTML) return;
    if (!XML) return;

    const data = {
      modelStr: XML,
      instanceStr: "",
      submitted: false,
      session: {
        username: "test",
      },
    };

    const options = {};

    setForm(new Form(formRef.current, data, options));
  }, [XML, HTML]);

  useEffect(() => {
    // once the Enketo Form object is created, init the form
    // this "works" in that I render the form with default fields filled in
    // however, when I make changes to the form no "relevant" or "constraint" seems to be applied
    try {
      const loadErrors = form.init();
      console.info("Enketo form initialized successfully");
      console.warn("Load errors:");
      console.warn(loadErrors); // One load error: TypeError: Cannot read properties of null (reading 'classList')
    } catch (error) {
      console.error("Error initializing Enketo form:");
      console.error(error);
    }
  }, [form]);

  const onSubmit = () => {
    if (form) {
      const data = form.getDataStr();
      console.log(data); // This never changes even though I can see the changes I have made in the rendered form and they hang about after any other changes are made of the Submit button (this one) is pressed
    }
  };

  const onReset = () => {
    // Works! When I click the button the form resets - including the default data
    if (form) {
      form.resetView();
    }
  };

  return (
    <>
      <div ref={formRef}></div>
      <Button onClick={onReset}>Reset</Button>
      <Button onClick={onSubmit}>Submit</Button>
    </>
  );
};
lognaturel commented 3 months ago

I know your end goal is to work within a React context but you might consider first making sure you can get things working in plain JS. I don’t see any obvious issue and I suspect this is more of a React question. You could try the forum to see whether anyone else has used Enketo Core this way. One thing to keep in mind is that all state and logic is driven from the page markup so I imagine there could be issues if there are conflicting element or CSS class names.

Another idea that comes to mind is that you could try calling validate on submit to see whether that has any effect. It certainly won’t make a difference when it comes to triggering logic at form fill time but it could possibly give you some clues.

MartijnR commented 2 months ago

// One load error: TypeError: Cannot read properties of null (reading 'classList')

That's the issue to investigate, I think. Some exception that occurs after loading the default values into the view during initialization. If there are loadErrors, the form did not initialize correctly.

ChasNelson1990 commented 2 months ago

@lognaturel @MartijnR thanks for the pointers - I got it working!

Turned out because setting React's ref.current.innerHTML is injecting the form as a child node I needed to do setForm(new Form(formRef.current?.children[0], data, options)); instead! Basically Enketo was trying to parse the parent div of the form and was confused as to why there was no className on it.

I will close this ticket now but I am hoping I get time to parcel my little React component up and, if I do, I'll post a link here for future searchers of React knowledge.

Thanks again for replying, it's really appreciated!