Form.io in native Android application

Ivan Trogrlić
Team Lead
Development

What is Form.io?

A few months ago I had a request from a client to implement Form.io into their native Android application.

Without going into too much detail, one of their web application features was allowing users to create custom forms, for which they decided to use Form.io.

After some research, I realized that Form.io doesn’t have libraries for native Android and iOS applications so I started looking into what other options there were.

At this point you might be wondering what Form.io is? Let me quote the pitch from their website:

Form.io is an enterprise class combined form and API data management platform for developers who are building their own complex form-based business process applications”. 

In a nutshell, Form.io enables you to drag and drop elements to create custom forms and easily connect it to your API. 

Rendering the most basic form

The client’s requirement was to:

  • enable rendering
  • prefilling
  • validating
  • collecting user-entered data

from the Form.io forms.

So basically, the application would receive JSON data from the client’s API to render and prefill the forms, and once users fill out the form we send JSON’s back to clients API.

Form.io has that all supported in their JS library and I just needed a way to make use of it somehow. Since, as pointed out already, I didn’t have a native library to work with, implementing my own solution was the only way to go. 

Form.io developed the front-end JavaScript renderer library which you can find here. I already knew Android’s WebView is capable of executing Javascript so I started playing around with it. Created a WebView and started testing their sample cases. 

Started simply – render a most basic case from their github webpage.

If you’re gonna be trying this yourself, make sure to add the internet permission in the manifest. Also, I’m only gonna be pasting important snippets of code here. Head over to my github for a full solution.

settings.javaScriptEnabled = true
settings.javaScriptCanOpenWindowsAutomatically = false

val html = "<html>\n" +
       "  <head>\n" +
       "    <link rel='stylesheet' href='https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css'>\n" +
       "    <link rel='stylesheet' href='https://unpkg.com/formiojs@latest/dist/formio.full.min.css'>\n" +
       "    <script src='https://unpkg.com/formiojs@latest/dist/formio.full.min.js'></script>\n" +
       "    <script type='text/javascript'>\n" +
       "      window.onload = function() {\n" +
       "        Formio.createForm(document.getElementById('formio'), 'https://examples.form.io/example');\n" +
       "      };\n" +
       "    </script>\n" +
       "  </head>\n" +
       "  <body>\n" +
       "    <div id='formio'></div>\n" +
       "  </body>\n" +
       "</html>"

loadData(html, "text/html", "UTF-8")

Built it, ran it… aaand it doesn’t work, of course.

Fails saying “Uncaught (in promise) SecurityError: Failed to read the ‘cookie’ property from ‘Document”.

To fix it you could disable cookies, or simply use loadDataWithBaseURL method instead with whichever url, for example

loadDataWithBaseURL("https://yoursite.com", html, "text/html; charset=utf-8", "UTF-8", null)

And voilà! 

Making FormIo render custom JSON

Now that wasn’t so bad, but I need to render JSON directly instead of referencing the embedded URL from Form.io since that’s what client requested.

Application will only receive “components” part of the JSON from the API. Ok, let’s try that instead:

Formio.createForm(document.getElementById('formio'), {
  components: [
    {
      type: 'textfield',
      key: 'firstName',
      label: 'First Name',
      placeholder: 'Enter your first name.',
      input: true
    },
    {
      type: 'textfield',
      key: 'lastName',
      label: 'Last Name',
      placeholder: 'Enter your last name',
      input: true
    },
    {
      type: 'button',
      action: 'submit',
      label: 'Submit',
      theme: 'primary'
    }
  ]
});

Great, that works too! Or at least until you disconnect from the internet… and the client’s biggest request is to make this feature work offline.

Let’s make it work offline

So how do I make it work, is it even possible? Well, as the matter of fact it is!

First, I’ll need access to Form.io javascript library locally. After some research I figured out a way to import javascript into a WebView from the local file system.

Basically, I had to go ahead and download the whole javascript library and move it to the assets folder (the whole library is huge so I removed like ~80% of none required files).

That way you can access javascript files in the WebView by using file:///android_asset/ as a base url. Of course, you need to change the path to the javascript files to reference files in the local storage instead of the embedded URL so here is what I ended up doing:

val html = "<html> \n" +
        <head>\n" +
           <link rel='stylesheet' href='formio/app/bootstrap/css/bootstrap.min.css'>\n" +
           <link rel='stylesheet' href='formio/dist/formio.full.min.css'>\n" +
           <script src='formio/app/jquery/jquery.min.js'></script>\n" +
           <script src='formio/app/jquery/jquery.slim.min.js'></script>\n" +
           <script src='formio/app/bootstrap/js/bootstrap.bundle.min.js'></script>\n" +
           <script src='formio/dist/formio.full.min.js'></script>\n" +
           …this part doesn’t change, it's the same FormIo.createForm… " +
         </head>\n" +
         <body>\n" +
           <div id='formio'></div>\n" +
         </body>\n" +
       </html>"

loadDataWithBaseURL(“file:///android_asset/”, html, "text/html", "UTF-8", "")

Awesome, I have it working offline too (btw, if you want your forms in another bootstrap, check this out).

But what if you need to prefill the form with some data? Turns out it’s not a problem at all, you just need a data JSON object which keys match form’s keys:

Formio.createForm(document.getElementById('formio'), 'https://examples.form.io/example').then(function(form) {
  form.submission = {
    data: {
      firstName: 'Joe',
      lastName: 'Smith',
      email: 'joe@example.com'
    }
  };
});

After building this you can see that first name, last name and email are prefilled with provided data. 

Validating and collecting form data

At this point I got the rendering part working, but I still need to run form validation and collect user entered data from the form to pass it back to the API.

That’s pretty easy to do as well with the javascript library itself but I need a way to pass it back and forth between native code and a WebView.

Turns out there’s a neat way to do this. In order to enable WebView to call native code you just need an interface:

private class WebViewJavaScriptInterface internal
constructor() {
   @JavascriptInterface
   fun submissionData(submissionData: String?) {
   	// Do whatever you need 
   }

   @JavascriptInterface
   fun submissionChanged(submissionData: String?) {
   	// Do whatever you need 
   }

   @JavascriptInterface
   fun validityChecked(isValid: Boolean) {
   	// Do whatever you need 
   }

   @JavascriptInterface
   fun fieldFocused(fieldName: String) {
   	// Do whatever you need 
   }
}

To make it work call this method before calling loadUrl to inject it into the WebView:

addJavascriptInterface(WebViewJavaScriptInterface(), "formio_interface")

“formio_interface” will be used to reference this interface from the javascript. 

As you can see, I have 4 methods in that interface above so let’s go through them one by one.

The first one, “submissionData”, is called on demand. I want to be able to retrieve user input from the form (Form.io references it as submission data) whenever I want. For that I need a way to call the javascript method from the native code which can be done by calling the loadUrl method: 

loadUrl("javascript:getSubmissionData()")

All I need now is to declare the getSubmissionData JS function:

...
   <script type='text/javascript'>
       ...
       function getSubmissionData(){
          let jsonData = JSON.stringify(formVar.submission.data);
         ‘formio_interface’.submissionData(jsonData);
       }
       ...
     </script>
...

So how does this work?

Once you call the loadUrl(“javascript:getSubmissionData()”), getSubmissionData function gets called, it takes the submission data and passes it back to the WebViewJavaScriptInterface though the submissionData method.

There we go, I have access to the form data JSON in the native code and I’m able to pass it back to the API. 

But what If I wanted to observe changes in the form so any time the form changes I get notified with a fresh new data? Form.io thought of that too:

...
<script type='text/javascript'>
       window.onload = function() {
       	Formio.createForm(document.getElementById('formio'),
            	${formIoModel.formConfig},
                      .then(function(form) {
          		form.on('change', (component, value) => {
             		let jsonData = JSON.stringify(form.submission.data);
              		‘formio_interface’.submissionChanged(jsonData)
              		let isValid = form.checkValidity(form.submission.data);
              		‘formio_interface’.validityChecked(isValid);
          		});
                      })
           }
           </script>
...

So on(‘change’) is what I needed here. Also, as you can see, FormIo provides the checkValidity method on the form which checks if the form is valid based on the conditions you set when building the form.

Now, every time a user changes something I get notified of the form validity and new data in the form through WebViewJavaScriptInterface. 

Tracking focused FormIo field

And last, but not least, I needed to track which input field in the form is currently focused.

My client had an interesting feature request; they wanted me to enable users to scan a barcode, take that value and paste it into the currently focused field (whether it’s a WebView’s form input field or some of Android’s native input fields).

Not gonna go deep into that now since there’s no need to go that wide; I’m just gonna show you how I managed to set data on the form’s input field. For this I took advantage of jQuery to find a name of the currently focused field:

...
    <script type='text/javascript'>
           window.onload = function() {
                Formio.createForm(document.getElementById('formio'),
                  ${formIoModel.formConfig},
                      .then(function(form) {
           		$('#formio').on('focusin', (event) => {
              		var target = $(event.target);
              		if (target.is('input') || target.is('textarea')) {
                  			‘formio_interface’.fieldFocused(target.prop('name'));
              		}
          		});
                      })
           }
   </script>
...

Then, using that information I was able to set the field value with this function:

...
    <script type='text/javascript'>
       ...
 	function setInputValue(inputName, inputValue){
              formVar.getAllComponents().forEach(component => {
                      component.inputs.forEach(input => {
                          if (input.name == inputName){
                              component.setValue(inputValue, false);
                          }
                      });
              });
           }
       ...
  </script>
…

And similarly to fetching the submission data from the form, I just had to call this method to set a field value:

loadUrl("javascript:setInputValue(‘inputName’, ‘someValue’)")

Conclusion

And that’s it for this one folks! I found this interesting to implement and hope you learned something from it. For a plug and play solution please go to my github here

There are a lot of cons to this approach, one of them being an ugly solution to add the Form.io javascript dependency, ‘cause every time you want to update the library you need to explicitly download it and paste it into the assets folder.

So if you’re in need of Form.io in your mobile application I highly recommend you build it in React Native to avoid all these shenanigans since Form.io developed a library for React Native specifically. 

For more Tech blogs written by our senior developers visit our development section.

Feel free to contact us at business@decode.agency or jobs@decode.agency if you want to become part of our team.

Hope you have a great one!