e-mission / e-mission-docs

Repository for docs and issues. If you need help, please file an issue here. Public conversations are better for open source projects than private email.
https://e-mission.readthedocs.io/en/latest
BSD 3-Clause "New" or "Revised" License
15 stars 32 forks source link

challenges with using embedded surveys #376

Open asiripanich opened 5 years ago

asiripanich commented 5 years ago

I recently found that the intercity branch uses qualtrics for collecting trip-end survey and the experience is much smoother than what we have implemented in rk-unsw branch (not having to mess with the base code of emission-phone is also a big advantage for us since we are not experienced enough to make big changes to the framework of emission). However, there are some challenges that we are facing.

Although Google forms is super easy to use and setup it lacks the ability to create a user-specific questionnaire. By this I mean, the ability to link user profile data such as their vehicles' information to the mode-of-travel questions, for example, if we want to know which of the vehicles the user's household own was used for the trip. We also need to think of a way to label trips that have been answered in the diary tab and ideally show an important part of their answers, for mode-of-travel questions this could be the mode they took, on to the survey button.

shankari commented 5 years ago

the ability to link user profile data such as their vehicles' information to the mode-of-travel questions, for example, if we want to know which of the vehicles the user's household own was used for the trip.

This is a really interesting use case. I am pretty sure it is possible to make other small hacks to accomplish this particular use case, but as the number of small hacks grows, at some point, you are better off writing a custom survey.

Couple of other notes:

For example, if I create a qualtrics form asking which vehicle you used for a particular trip, and then look at the resulting document through the inspector, I see that both the id and for elements indicate that this is a part of QID1. So theoretically, your survey launch javascript could go through and replace/delete/insert QID1 entries to get the set of selections that you want.

But this will only work with questions on the first page, and it won't return the values so that they can be reflected in the UI. Again, you could probably parse the page to figure out which value the user had selected, but that would only work for the last page.

the IAB plugin claims that it can also receive 'message' events from the page loaded in the browser. If we can get that to work, then potentially the survey page could return a message with the important value back to the main app. But I am not quite sure how a page generates a message and whether qualtrics will support arbitrary javascript to let us do that.

shankari commented 5 years ago

Maybe the embeddedData is an option https://api.qualtrics.com/docs/managing-survey-sessions#section-survey-session-request-object

shankari commented 5 years ago

Yes, it seems that embeddedData is a real option; in your case, you would use the option to set it by "appended to the end of the respondent’s survey link" https://www.qualtrics.com/support/survey-platform/survey-module/survey-flow/standard-elements/embedded-data/

https://www.qualtrics.com/support/survey-platform/survey-module/survey-flow/standard-elements/passing-information-through-query-strings/

However, the risk then is that this would potentially tie users to only use qualtrics. I am not sure whether there is similar functionality in Google Forms or SurveyMonkey

shankari commented 5 years ago

So another option is to look at open source survey authoring tools such as ODK https://opendatakit.org/

I believe that there is precedent for doing this - Andre Carrell's San Francisco Travel Quality Study (http://link.springer.com/article/10.1007/s11116-016-9732-4) integrated ODK surveys into a background tracking app. Alas, the implementation is lost since they never open sourced their work, but I think that an e-mission/ODK integration would be a great addition to the platform.

shankari commented 5 years ago

@asiripanich I thought some more about the options, and at least in terms of contributing to the platform, I am not convinced that writing a complex script to customize google forms is the right approach.

It is true that with google forms, users can store the form results for free without provisioning a different server. But that free comes at an implicit cost - you give Google permission to access your data. And at least so far, one of the main reasons that deployers have chosen e-mission is the ability to control their own data.

Berkeley actually has a paid subscription to Google that is supposed to ensure that data stored using our @berkeley.edu google accounts is private. That is how the TripAware study was able to get IRB approval for using a Google Form.

But when the current GreenTrip tried to use Google Forms for their surveys, the IRB actually pushed back and suggested that we use the recommended qualtrics.

This is not to say that you cannot use Google Forms on your own fork; of course you can. But if you are going to do a bunch of work to support Google Forms, and would like to submit it to the platform as a "canonical travel survey use case" for the platform, I think that something like ODK is a better option.

It has a visual form builder, and since it is open source, I am pretty sure we can adapt it to store the results to the e-mission server, which the user is already running.

shankari commented 5 years ago

Another option source form builder similar to ODK is https://www.kobotoolbox.org/ It looks really sophisticated (much closer to qualtrics) and may be a better architectural fit. I would need to poke into both ODK and Kobo Toolbox to check.

@asiripanich could you briefly experiment with the form designers for both ODK and kobo toolbox and give your feedback as a survey designer. If one is much better than the other, it will help me prioritize which code I should start looking at.

shankari commented 5 years ago

Ha! kobo toolbox collect is a fork of ODK collect :) but their form builder may be better, and enketo (their web-based solution) may be a better fit given that we use javascript (https://github.com/kobotoolbox/enketo-express)

shankari commented 5 years ago

it looks like both ODK and enketo (the backend to enketo express) use XForm. You can generate XForms using either Excel (http://xlsform.org/en/) or (I presume) the kobotoolbox survey builder. It's great that this is split out this way because it allows integrating with the parts that we care about and not the parts that we already have (wrt storage, etc).

shankari commented 5 years ago

After poking around on this some more, I think that Enketo looks very promising. Recall that the general idea was to allows survey builders to define their forms using XML or JSON, which would then be converted using scripts that you wrote to a google form.

Well, there is already a standard XML-based form definition (XForm) which can be generated using Excel or the kobotoolbox UI-based survey builder.

Once you have an XForm, it looks like Enketo Transformer can convert XForms to JSON (similar to the JSON generated by ODK, I assume). Enketo Core can then render this JSON as a HTML + JS + CSS form.

So theoretically, you should be able to adapt the enketo tools to allow users to build a .json file that represents the survey and then write some simple javascript that allows the .json to be displayed as a form. Looking at the enketo architecture (https://enketo.org/develop/#transformation) the storage is separate from the form, so we could just get the form result and store the data back into e-mission (e.g. as manual/trip_survey) or something.

I would strongly recommend exploring this approach rather than writing hacky code from scratch.

shankari commented 5 years ago

The enketo-core README (https://github.com/enketo/enketo-core) has examples of how to load a form and obtain the result. At least at first glance, it appears to be pretty straightforward (https://github.com/enketo/enketo-core). You could either load the survey from the e-mission server (e.g. under the webapp, sth like https://emission.myserver.com/survey/template/trip_survey.json) or push it to the client for local access. Ideally, you would make this an option for the end-user.

shankari commented 5 years ago

Here's what Enketo recommends for form building.

Enketo currently recommends using either XLSForm or the new KoBoForm to construct XForms. Another tool is ODK Build. XLSForm is a way to build forms in a spreadsheet. After an initial learning curve, it becomes very easy to build and maintain forms with this tool. A great advantage of this tool is that it makes it easy to share and discuss forms with others. A new tool by KoBoToolbox, that continued the earlier excellent work of Alex Dorey, provides a Graphical User Interface for XLSForm. This easy-to-use tool was launched on kobotoolbox.org in 2014 and is starting to revolutionize form building in the OpenRosa ecosystem. In 2017, Ona launched a fork of KoBo's formbuilder.

asiripanich commented 5 years ago

@asiripanich could you briefly experiment with the form designers for both ODK and kobo toolbox and give your feedback as a survey designer. If one is much better than the other, it will help me prioritize which code I should start looking at.

@shankari sure I will do that and report back to you this weekend.

shankari commented 5 years ago

@shankari after all my updates, I actually have a different ask for you. Can you focus only on "kobo toolbox", since it looks like that is the recommended solution, see if it can build the kinds of forms that you want, and then download the form specification? Then, we can try to stick them into a HTML page with the enketo example and get a quick sense of whether it will work.

ODK builder basically uses xlsform and kobotoolbox is the new and improved version of xlsform.

atton16 commented 5 years ago

Hi,

I am trying to test out our new method to deploy trip survey using in-app browser.

It is known that with cordova-plugin-inappbrowser we can embed the external survey easily with just a single line of code. But we wanted to extend it further.

We wanted to use inAppBrowserRef.executeScript function to embed the tiny script to detect whether the survey is completed then automatically dismiss the in-app browser window.

The script now can successfully detect the success state of the survey by looking into the content of the html on the page.

However, we are stuck when we wanted to report that success to our server using XHR method (Ajax).

Do you have any suggestion regarding executing XHR inside inAppBrowserRef.executeScript function scope?

Here is the code snippet inside inAppBrowserRef.executeScript function:

var post_url = "http://10.0.1.56:3000/board/?topic=test&message=success";
var surveySuccess = false;
var interval = setInterval(function(){
  if (surveySuccess) return;
  var elements = document.getElementsByClassName("vex-content");
  if (elements.length === 0) return;
  if (!elements[0].innerHTML.match(/Thank you for participating/)) return;
  surveySuccess = true;
  postMessage();
}, 50);
function postMessage() {
  var xhr = new XMLHttpRequest();
  xhr.onreadystatechange = function() {
    if (xhr.readyState === 4) {
      clearInterval(interval);
      if (xhr.status === 200) {
        alert(JSON.stringify(xhr.response));
      } else {
        alert(xhr.status);
      }
    }
  };
  xhr.open("POST", post_url, true);
  xhr.send('');
}
shankari commented 5 years ago

@atton16 the e-mission platform doesn't just display the external survey in an inappbrowser (IAB). The "one line of code" using SurveyLaunch is a wrapper that not only loads the URL in an IAB but also uses executeScript to fill in certain specified fields with values from the app.

But I am not sure what you mean by "our new method" at this point. I have heard many possible options :) I assume it is not customized google forms given that you are running your own server. Is it enketo? kobotoolbox?

Before getting into the weeds of how to use XHR in executeScript, I think that it would be good for you to outline your proposed design for discussion. I am not convinced that XHR from executeScript is even necessary.

asiripanich commented 5 years ago

@shankari We are exploring our option with KoBoToolbox.

shankari commented 5 years ago

Great! And just to clarify, my suggestion is that:

  1. you use KoBoToolbox to build the survey,
  2. download it as XLSForms (http://support.kobotoolbox.org/creating-forms/xls-forms/editing-forms-in-excel)
  3. convert it to HTML + XML (https://github.com/enketo/enketo-transformer)
  4. use enketo core to display it directly from the app (https://github.com/enketo/enketo-core)

Note that the enketo core library allows you to pass in external data and returns the result in record = getDataStr.

This (hopefully) allows you to customize the form with user data (passed in using external) and display selected results in the UI (from record) and store the result to the e-mission server just like all other data.

I assume that XLSForms allows you to customize the form flow using data passed in using external objects, but you would need to check.

If it works, this would allow you to have:

atton16 commented 5 years ago

Great! And just to clarify, my suggestion is that:

  1. you use KoBoToolbox to build the survey,
  2. download it as XLSForms (http://support.kobotoolbox.org/creating-forms/xls-forms/editing-forms-in-excel)
  3. convert it to HTML + XML (https://github.com/enketo/enketo-transformer)
  4. use enketo core to display it directly from the app (https://github.com/enketo/enketo-core)

Note that the enketo core library allows you to pass in external data and returns the result in record = getDataStr.

This (hopefully) allows you to customize the form with user data (passed in using external) and display selected results in the UI (from record) and store the result to the e-mission server just like all other data.

I assume that XLSForms allows you to customize the form flow using data passed in using external objects, but you would need to check.

If it works, this would allow you to have:

  • complex flows based on user profile data
  • result data displayed back to the user
  • without provisioning an additional server
  • and with unlimited usage

That sounds great. We need to see if the two libraries: enketo-core and enketo-transformer can run on mobile app or not.

shankari commented 5 years ago

You don't need to run enketo-transformer on the mobile app since you are only going to use a limited set of forms. you can run it somewhere else and just get the HTML + XML for use with enketo-core.

enketo-core is a pure javascript library, so there is really no reason why it shouldn't work. But of course, those are the famous last words :)

shankari commented 5 years ago

I think I'm almost done with a hacky solution, so I'm (belatedly) documenting the steps that I tried and the final design, at least for now.

First, there is an existing web service to convert XLSforms (which is what the kobotoolbox generates) -> xforms (which is what enketo core expects). This is http://xlsform.opendatakit.org/, which allows you to download the resulting form, AND to see a preview of it in enketo (e.g. https://odk.enke.to/preview?form=https://xlsform.opendatakit.org/downloads/ngzcr5fw/rkunsw-survey1.xml)

Unfortunately, the preview site cannot do the transforms - I tried https://odk.enke.to/transform?form=https://xlsform.opendatakit.org/downloads/ngzcr5fw/rkunsw-survey1.xml and it didn't work.

So I installed enketo-transform and converted the xform into the form and model. We could definitely consider making this available as a public web service in the future.

In [1]: import requests

In [2]: form_and_model = requests.get("http://localhost:8085/transform?xform=https://xls
   ...: form.opendatakit.org/downloads/ngzcr5fw/rkunsw-survey1.xml").json()

In [4]: import json

In [6]: json.dump(form_and_model["model"],
   ...: open("www/xml/survey/model1.xml", "w"))

In [7]: json.dump(form_and_model["form"],
   ...: open("www/xml/survey/form1.xml", "w"))

We now have the form and model stored locally. I then wrote some simple code to launch the form filling from the intro (just for testing).

test_survey_launch.patch.gz

shankari commented 5 years ago

At this point, the main question was how to include the form javascript files into our app. I first tried browserify, but it consistently failed while trying to read the enketo-core modules with this error.

https://stackoverflow.com/questions/40029113/syntaxerror-import-and-export-may-appear-only-with-sourcetype-module-w

I tried babelify, babelify-es6 extensions... and none of them worked. So I switched to webpack instead.

I created a wrapper that only included enketo-core - e.g.

/*
 * Since we are not using webpack for the entire app, and won't have time to
 * migrate it for now, let's make a separate wrapper just to use with webpack
 * so that we can get a script file to include manually in index.html
 * I tried putting this into `enketo_launch.js`, but then it created a
 * humungous file
 *
 * WARNING in asset size limit: The following asset(s) exceed the recommended size limit (244 KiB).
This can impact web performance.
Assets:
  main.js (602 KiB)
 * which included jquery, leaflet, datepicker,...
 */

import { Form } from 'enketo-core';

created a webpack.config.js

const path = require('path');

module.exports = {
  entry: './www/js/survey/enketo_webpack_wrapper.js',
  output: {
    filename: 'enketo-bundle.js',
    path: path.resolve(__dirname, 'www/lib/enketo')
  }
};

and then tried to run webpack

npx webpack --config webpack.config.js

which generated a file that I included, but I consistently ran into an error while using the Form, and I didn't see function Form() anywhere in the created bundle.js

So I abandoned all of this, cloned enketo-core separately and built it using grunt as recommended. That generated a separate build/js/enketo-bundle.js which does have function Form( formSelector, data, options ) {

Note that both enketo-core and enketo-transform need to be run from a python2.7 environment. So I installed a python 2.7 environment just for this

$ conda create --name py27 python=2.7
$ source activate py27
shankari commented 5 years ago

@asiripanich I think that the model could be used to fill in the options (similar to pulldata). When I was testing the local display module, I used the nested_repeats.xml form (https://github.com/enketo/enketo-core/blob/master/test/forms/nested_repeats.xml)

And this is what it looked like

Screen Shot 2019-04-30 at 5 47 34 PM

As you can see, the values are filled in.

I then pulled the form and model after transforming it, and got

4. Do you have any kids?</span>\n                    </legend><div class=\"option-wrapper\"><label class=\"\"><input type=\"radio\" name=\"/nested_repeats/kids/has_kids\" data-name=\"/nested_repeats/kids/has_kids\" value=\"0\" data-type-xml=\"select1\"/><span lang=\"\" class=\"option-label active\">no</span></label><label class=\"\"><input type=\"radio\" name=\"/nested_repeats/kids/has_kids\" data-name=\"/nested_repeats/kids/has_kids\"

while the model was

<kids>\n            <has_kids>1</has_kids>\n       ....      
<kids_name>Tom</kids_name>\n              <kids_age>2</kids_age>\n              <immunization_info>\n                <vaccine>Polio</vaccine>\n                <date/>\n              </immunization_info>\n              <immunization_info>\n                <vaccine>Rickets</vaccine>\n                <date/>\n              </immunization_info>\n            </kids_details>

so basically, you can fill in the value of the has_kids field by passing in a model xml that has it set, similarly for <kids_name> etc. and of course, you can manipulate the model xml in the app before initializing the form with it.

But I also wonder why you would want to do this. If you already know that the person has a kid named Tom, you don't need to fill that information into the form and make the user get tired of filling it out. What you really want is the ability to choose the selections for the form (e.g. the types of user cars), for which you want to use the external XML anyway (http://xlsform.org/en/#external-selects)

shankari commented 5 years ago

I originally tried to display the form in an ionicPopover, but I wasn't sure that was the desired UX, and the current example assumes that the loading javascript can modify the HTML that it controls. So switched to a different state, but reused the metrics view.

The example also uses jquery to insert the form into the HTML. I tried using angular {{}} instead, but only the raw text was displayed. Fell back to jquery, with the following element

   <ion-content>
    <div id="dummy-form-content" />
   </ion-content>

and the following javascript

            $( '#dummy-form-content' ).after( $scope.loaded_form );

After the usual futzing around to get the state to work right and to pass parameters correctly, I end up with the following UX

woo!

Unfortunately, the javascript is still failing with the error

index.html:133 ReferenceError: Form is not defined
    at enketo_launch.js:33
    at processQueue (ionic.bundle.js:29127)
    at ionic.bundle.js:29143
    at Scope.$eval (ionic.bundle.js:30395)
    at Scope.$digest (ionic.bundle.js:30211)
    at Scope.$apply (ionic.bundle.js:30503)
    at done (ionic.bundle.js:24824)
    at completeRequest (ionic.bundle.js:25022)
    at XMLHttpRequest.requestLoaded (ionic.bundle.js:24963)
shankari commented 5 years ago

So it looks like enketo-bundle.js does not export Form and is not wrapped as a commonjsmodule

(emission) C02KT61MFFT0:e-mission-phone shankari$ grep -r "export { Form, }" www/lib/enketo/
www/lib/enketo//enketo-bundle.js:   var jquery = createCommonjsModule(function (module) {
www/lib/enketo//enketo-bundle.js:   var mergexml = createCommonjsModule(function (module, exports) {
www/lib/enketo//enketo-bundle.js:   var jquery_touchSwipe_min = createCommonjsModule(function (module) {
www/lib/enketo//enketo-bundle.js:   var leafletSrc = createCommonjsModule(function (module, exports) {
www/lib/enketo//enketo-bundle.js:   var bootstrapDatepicker = createCommonjsModule(function (module, exports) {

Argh! We need to look at the build process for enketo-core again

shankari commented 5 years ago

ok so enketo-core uses rollup instead of webpack or browserify https://rollupjs.org/guide/en

Now we need to see why rollup only creates exports for some modules and not for others.

shankari commented 5 years ago

Tried using name: - didn't affect the output Tried using external - just removed jquery from the list of modules

www/lib/enketo//enketo-bundle.js:   var jquery = createCommonjsModule(function (module) {
shankari commented 5 years ago

ok so as I was beginning to suspect, this is expected behavior for rollup, which inlines the functions that the target javascript uses. I'm not quite sure why Leaflet et al are still retained as modules instead of being inlined as well. https://mariusschulz.com/blog/bundling-and-tree-shaking-with-rollup-and-ecmascript-2015-modules

Two options:

shankari commented 5 years ago

First option made no difference, trying second.

shankari commented 5 years ago

Second option also does not appear to help. Now I do see an enketocore variable.

var enketocore = (function () {
    'use strict';

But this variable is undefined

enketocore.Form
VM4714:1 Uncaught TypeError: Cannot read property 'Form' of undefined
    at <anonymous>:1:11
(anonymous) @ VM4714:1

And the // import { Form } from 'enketo-core'; line fails because browsers don't really support EC6 imports.

Finally, saw how moment defines this (as a global variable) and hardcoded the Form module

        return !!target;
    };

    window.FormModule = Form

    /**
     * Static method to obtain required enketo-transform version direct from class.
     */

Finally! The modules is defined

window.FormModule
function Form( formSelector, data, options ) {
        const $form = jquery( formSelector );

        this.$nonRepeats = {};
        this.$all = {};
        this.options = typeof options !== 'object' ? {} : options;
       …
shankari commented 5 years ago

That worked, finally! I can now see the form properly in the UX but it looks a bit ugly and it is not clear that the location options work.

shankari commented 5 years ago

Ah! The ugliness was because we were using the wrong css (the paper versus the screen version). After fixing that, it looks a lot better, but is still not perfect.

shankari commented 5 years ago

@asiripanich I have a PR out with the embedded survey option. Please see the PR and the associated caveats. Let me know how you would like to test this. Are you going to merge it to your channel? Should I merge it to one of the test channels (maybe martanetworksp18 since I am already using that to test mode and purpose.

Also, I assume that you and @atton16 can make the design of the survey template be beautiful, consistent with the app, etc and launch it from the places that you want to.

shankari commented 5 years ago

I got a response to the issue with the maps (https://github.com/enketo/enketo/issues/209) - it turned out that the map questions have to be marked with "appearance=maps". Since I didn't create the maps in kobotoolbox, I just edited the xml form directly to add the fields (https://github.com/enketo/enketo/issues/209) and the maps now work when the form is displayed directly in chrome (and presumably in the IAB at least).

@asiripanich I am not sure what kobotoolbox allows you to set when creating the questions, but if it is possible to ensure that the appearance is set properly, please do so!

shankari commented 5 years ago

although the maps now work in chrome, they don't work in the embedded survey. I see the map button, but clicking it does nothing.

shankari commented 5 years ago

Adding debugging logs to the click

            this.$map.find( '.show-map-btn' ).on( 'click', () => {
                console.log("Received click from show-map-btn");
                that.$widget.find( '.search-bar' ).removeClass( 'hide-search' );
                that.$widget.addClass( 'full-screen' );
                that._updateMap();
                return false;
            } );
            console.log("Finished registering "+JSON.stringify(this.$map.find( '.show-map-btn' ))+" for click");

we see that the click handler is registered but does not receive a click

Finished registering {"0":{"jQuery331058406024557150321":{"events":{"click":[{"type":"click","origType":"click","guid":17,"namespace":""}]}}},"length":1,"prevObject":{"0":{},"length":1,"prevObject":{"0":{},"length":1}}} for click
index.html:133 Finished registering {"0":{"jQuery331058406024557150321":{"events":{"click":[{"type":"click","origType":"click","guid":31,"namespace":""}]}}},"length":1,"prevObject":{"0":{},"length":1,"prevObject":{"0":{},"length":1}}} for click
...
<nothing more>
shankari commented 5 years ago

Exploring the event handlers for this button,

Screen Shot 2019-05-02 at 9 42 04 AM

the issue must be with one of the ionic listeners since this works fine in chrome. If we remove the inner-most listener (at line 2956), then the click is delivered.

This suggests that an iframe could be a good solution to isolate the form from the rest of the template. But let's spend an hour or so poking at what that handler does and whether we can fix it.

shankari commented 5 years ago

Removing the handler seems to break the radio button selection (see video). radio_clicks_not_working.mp4.gz

shankari commented 5 years ago

ok so the handler is the function tapClickGateKeeper

  //console.log('click ' + Date.now() + ' isIonicTap: ' + (e.isIonicTap ? true : false));
  if (e.target.type == 'submit' && e.detail === 0) {
    // do not prevent click if it came from an "Enter" or "Go" keypress submit
    return null;
  }

  // do not allow through any click events that were not created by ionic.tap
  if ((ionic.scroll.isScrolling && ionic.tap.containsOrIsTextInput(e.target)) ||
      (!e.isIonicTap && !ionic.tap.requiresNativeClick(e.target))) {
    //console.log('clickPrevent', e.target.tagName);
    e.stopPropagation();

    if (!ionic.tap.isLabelWithTextInput(e.target)) {
      // labels clicks from native should not preventDefault othersize keyboard will not show on input focus
      e.preventDefault();
    }
    return false;
  }

when using the radio box, none of the checks matched, so it went through.

when using the map button, it matched these checks, so the event was not propagated.

  if ((ionic.scroll.isScrolling && ionic.tap.containsOrIsTextInput(e.target)) ||
      (!e.isIonicTap && !ionic.tap.requiresNativeClick(e.target))) {
    //console.log('clickPrevent', e.target.tagName);
    e.stopPropagation();

Looking at the values, it looks like isIonicTap is undefined.

shankari commented 5 years ago

Actually, when we click on the button, we get two calls to tapClickGateKeeper. One does have e.isIonicTap set to true, but I am not sure which element it is for. The element does not have a click handler defined, though (onclick : null).

The outerHTML is

"<input type="text" name="/rkunsw-survey1/Where_is_your_workplace" data-type-xml="geopoint" style="display: none;">"

and the siblings are

nextElementSibling:div.geopicker.widget
nextSibling:div.geopicker.widget
shankari commented 5 years ago

looks like the element is this one

<input type="text" name="/rkunsw-survey1/Where_is_your_workplace" data-type-xml="geopoint" style="display: none;">

not sure why it should be triggered but the button should not

shankari commented 5 years ago

ok so the reason for the discrepancy is that the first call (with srcElement = input) invokes triggerMouseEvent and the second (with srcElement = button) does not.

shankari commented 5 years ago

ok so closer... In tapClick, e.target is the button, the container is the label.question.non-select.or-appearance-maps.clearfix.current and then ele becomes the input instead of the button.

function tapClick(e) {
  // simulate a normal click by running the element's click method then focus on it
  var container = tapContainingElement(e.target);
  var ele = tapTargetElement(container);

and that is because the container's control is set to the input.

function tapTargetElement(ele) {
  if (ele && ele.tagName === 'LABEL') {
    if (ele.control) return ele.control;

    // older devices do not support the "control" property
    if (ele.querySelector) {
      var control = ele.querySelector('input,textarea,select');
      if (control) return control;
    }
  }
  return ele;
}
shankari commented 5 years ago

I think that the option of embedding this into a screen in a more complex app doesn't work that well - the widgets don't play that well with each other and with other frameworks. Not just the extra invisible input at https://github.com/e-mission/e-mission-docs/issues/376#issuecomment-488764818 but also, if we pull out the footer code properly into an so that it doesn't overlap the content,

e.g.

Screen Shot 2019-05-02 at 12 15 03 PM

then the javascript can no longer find the buttons and the Next button doesn't work.

shankari commented 5 years ago

Copy-pasting everything in the HTML directly from the enketo-core sample without any ionic headers or footers works fine, except for the map button. The "Next" button is displayed instead of validate, the "Powered by" works properly...

shankari commented 5 years ago

Looking at the list of widgets (https://enke.to/::widgets), it looks like the hidden input is a fairly common pattern for more complex widgets; not just for maps, but also for the range widget, distress widget or rank widget. We need to solve this or go to an iframe.

shankari commented 5 years ago

ok! I know a fix for this! I tracked down that the tapClick functionality is in tap.js https://github.com/ionic-team/ionic-v1/blob/master/js/utils/tap.js

The documentation at the top says that:

So I manually added the attribute to the DOM in the emulator, and:

So now the only question is:

can we fix this in our code? if not:

  1. do we fix it in ionic?
  2. do we fix it in enketo?

Let's first try fixing it in our code.

asiripanich commented 5 years ago

Hi @shankari thanks for all the hard work you have done on this!

@asiripanich I have a PR out with the embedded survey option. Please see the PR and the associated caveats. Let me know how you would like to test this. Are you going to merge it to your channel? Should I merge it to one of the test channels (maybe martanetworksp18 since I am already using that to test mode and purpose.

We will merge it to our channel for testing over the weekend.

@asiripanich I am not sure what kobotoolbox allows you to set when creating the questions, but if it is possible to ensure that the appearance is set properly, please do so!

Ahh I found out this issue as well. Sorry that I couldn't reply sooner.

shankari commented 5 years ago

We can fix it in our code by adding data-tap-disabled="true" to one of the top tags - e.g.

        <article class="paper" data-tap-disabled="true">

with this change, the embedded survey is consistent with the version displayed in chrome

asiripanich commented 5 years ago

@asiripanich I think that the model could be used to fill in the options (similar to pulldata). When I was testing the local display module, I used the nested_repeats.xml form (https://github.com/enketo/enketo-core/blob/master/test/forms/nested_repeats.xml)

And this is what it looked like

Screen Shot 2019-04-30 at 5 47 34 PM

As you can see, the values are filled in.

I then pulled the form and model after transforming it, and got

4. Do you have any kids?</span>\n                    </legend><div class=\"option-wrapper\"><label class=\"\"><input type=\"radio\" name=\"/nested_repeats/kids/has_kids\" data-name=\"/nested_repeats/kids/has_kids\" value=\"0\" data-type-xml=\"select1\"/><span lang=\"\" class=\"option-label active\">no</span></label><label class=\"\"><input type=\"radio\" name=\"/nested_repeats/kids/has_kids\" data-name=\"/nested_repeats/kids/has_kids\"

while the model was

<kids>\n            <has_kids>1</has_kids>\n       ....      
<kids_name>Tom</kids_name>\n              <kids_age>2</kids_age>\n              <immunization_info>\n                <vaccine>Polio</vaccine>\n                <date/>\n              </immunization_info>\n              <immunization_info>\n                <vaccine>Rickets</vaccine>\n                <date/>\n              </immunization_info>\n            </kids_details>

so basically, you can fill in the value of the has_kids field by passing in a model xml that has it set, similarly for <kids_name> etc. and of course, you can manipulate the model xml in the app before initializing the form with it.

But I also wonder why you would want to do this. If you already know that the person has a kid named Tom, you don't need to fill that information into the form and make the user get tired of filling it out. What you really want is the ability to choose the selections for the form (e.g. the types of user cars), for which you want to use the external XML anyway (http://xlsform.org/en/#external-selects)

You are right we don't need "Tom" to fill his name in every time and that's definitely not what we want to do. The second example is what we really want to have. The first example is more useful for filling in trip id etc.

shankari commented 5 years ago

The first example is more useful for filling in trip id etc.

Just to clarify, you don't need to fill in the trip information into the form any more because the form save is not separate from the rest of the e-mission app. The form returns its results to e-mission, so you can fill in the trip information in the code and save it to the e-mission server. Hopefully that will make the form shorter and less intimidating for users.