Some client apps make use of Turbolinks-Android to provide a native Android experience for their Rails web apps. This is fine until you try to use something like an HTML5 date field. When the user taps on the field they get… nothing. No datepicker, not even a keyboard, just a flashing cursor trapped in a forever-empty field.

This is a known issue with Turbolinks-Android. It appears that most HTML5 fields will work, but date/time and autofill will not.

What Turbolinks-Android gives us

One of the nice things Turbolinks-Android provides us is the ability to send messages to the app via javascript.

On the Rails-side, we can just add this snippet:

  if (this.JsNativeDatePicker) {
    $(document).on('click', '[type~=date]', function(event) {
        JsNativeDatePicker.showDatePicker(event.target.id, event.target.value);
        return false;
    });
  }

For the most part, this is fairly clear. We’re adding an on-click handler to date fields that fires off the id and value of the field to JsNativeDatePicker.

But what about this JsNativeDatePicker? Where did that come from?

Turbolinks-Android uses a @JavaScriptInterface annotation for public methods that are then magically available for us to call from Javascript. So in our JsNativeDatePicker.java we have something like this:

  public class JsNativeDatePicker
  ...
    
      @JavascriptInterface
      public void showDatePicker (String id, String value) {
          elementId = id;
          Calendar cal = Calendar.getInstance();
          SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH);
          try {
              cal.setTime(sdf.parse(value));
          } catch (ParseException p) {
              cal = null;
          }
          DialogFragment newFragment = new DatePickerFragment(this, cal);
          newFragment.show(activity.getFragmentManager(), "datePicker");
      }

Here we are taking the id and value passed in from the Javascript call and using it to create a new datepicker.

The DatepickerFragment

To display a DatePicker, we are wrapping it in a new DatePickerFragment. The code for this is pretty straight-forward, based on the documentation at https://developer.android.com/guide/topics/ui/controls/pickers

public class DatePickerFragment extends DialogFragment {
    private DatePickerDialog.OnDateSetListener listener;
    private Calendar calendar;
    
    public DatePickerFragment(DatePickerDialog.OnDateSetListener listener, Calendar calendar) {
        this.listener = listener;
        if (calendar == null) {
            calendar = Calendar.getInstance();
        }
        this.calendar = calendar;
    }
    
    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        int year = calendar.get(Calendar.YEAR);
        int month = calendar.get(Calendar.MONTH);
        int day = calendar.get(Calendar.DAY_OF_MONTH);
        // Create a new instance of DatePickerDialog and return it
        return new DatePickerDialog(getActivity(), listener, year, month, day);
    }
} 

Fairly self-explanatory. We take an OnDateSetListener to listen for updates when the user selects a date, and a Calendar of the current value to use (defaulting to today via Calendar.getInstance()).

Updating the value

Here the JsNativeDatePicker class is also going to function as our OnDateSetListener to receive updates when the user selects a value.

public class JsNativeDatePicker
  implements DatePickerDialog.OnDateSetListener {
  
      public void onDateSet(DatePicker view, int year, int month, int day) {
        webView.evaluateJavascript(getUpdateJavascript(elementId, year, month, day),
                new ValueCallback<String>() {
                    @Override
                    public void onReceiveValue(String s) {
                    }
                });
    }

    private String getUpdateJavascript(String id, int year, int month, int day) {
        return String.format(Locale.ENGLISH, "(function() {" +
                    "var input = document.getElementById('%s');" +
                    "input.value = '%d-%02d-%02d';" +
                    "})();",
                id, year, month+1, day);
    }
}

When the user has picked a date, our onDateSet method is hit. Now we have the value, but we still need to update the html field. To do this we inject some javascript straight into the WebView. Note that the month is in the range 0..11 for compatibility with Calendar.MONTH, so we simply +1 it here.

Trouble managing your Github Pull Requests?

GitArborist was created to simplify Pull Request management on Github
Mark PR dependencies, automatic merging, scheduled merges, and more →

Hooking it up

Now that the pieces are in place we just need to register our shiny new datepicker so it’s available from Javascript on the page. When setting up Turbolinks (in my case this was in TurbolinksHelper.java#setupTurbolinks) simply add this line:

// WebView webView = TurbolinksSession.getDefault(context).getWebView();
webView.addJavascriptInterface(new JsNativeDatePicker(activity, webView), "JsNativeDatePicker");

addJavascriptInterface will take any class as a parameter, then any public methods with the @JavaScriptInterface annotation will be available to call. The second parameter ("JsNativeDatePicker" here) is what you will call from javascript - in our case JsNativeDatePicker.<annotated method>. I’ve used the class name here for clarity, but you can actually use any name you want as long as it is unique.

Summary

Turbolinks native wrappers provide some very handy functionality, and allow you provide some mobile-native elements without having to duplicate business logic in both the native app and the backend - you just have to be aware of the rough edges where you have to provide the Rails-to-native bridging manually.