Loading Gizmos with ajax

26 views
Skip to first unread message

sdc50

unread,
Aug 4, 2016, 10:30:42 AM8/4/16
to tethysp...@googlegroups.com
I'm attempting to create a form using the result of an ajax call. I have a template that renders the form content which includes several gizmos. I noticed that some of the gizmos didn't look/act correctly because the css and JS dependencies didn't get loaded. My current solution for this is to load the dependencies manually after the ajax call like this:


$.get(url, data, function(result){
        if(result.success){
            $('#ajax-form').html(result.html);
        }
    })
    .done(function() {
        // initialize the select input
        $('.select2').select2();  //NOTE: this only works because I have previously loaded a select_input gizmo on another form.

       // get dependencies for date pickers
        $.getScript("/static/tethys_gizmos/vendor/bootstrap_datepicker/js/bootstrap_datepicker.js", function(){});
        $("<link/>", {
           rel: "stylesheet",
           type: "text/css",
           href: "/static/tethys_gizmos/vendor/bootstrap_datepicker/css/datepicker3.css"
        }).appendTo("head");
    })


My form currently includes a select_input and date_pickers, but I'm guessing any gizmo with dependencies would have similar issues.

Has anyone else run into this issue? Is there a better solution that what I'm doing here (NOTE: I could improve this slightly by passing the dependencies back as part of the ajax call rather than hard coding them)? 

swainn

unread,
Aug 4, 2016, 11:01:37 AM8/4/16
to Tethys Platform
From what I understand, $.get() is just shorthand for $.ajax({'method': 'get',...}), so I assume your data.html is just a string of the html you intend to insert. Is that correct? One possible approach would be to use $.load(). This method will replace the contents of the element selected with whatever content is returned by the URL you give it. For example:

$('#target').load('/apps/my-first-app/some-endpoint/', function() { // callback });

To load a gizmo, you make a new controller and template just like you normally would, but the template would only contain the gizmo and probably not inherit from other templates. Since you are not inheriting from the base template, you would also need to manually load the gizmo dependencies using the gizmo_dependencies tag. So it would look something like this:

{% load tethys_gizmos %}

{% gizmo some_gizmo gizmo_options %}

{% gizmo_dependencies %}

The only trick then will be initializing the gizmos after the $.load() call is finished (in the callback function you give it). I believe Alan recently added methods to the MapView (reInitializeMap()) and Highcharts plots (initHighchartsPlot()) for this purpose. But we need to add similar methods to the other gizmos and probably settle on a naming convention for these types of methods (reInitialize vs. init...)


alansnow21

unread,
Aug 4, 2016, 12:08:23 PM8/4/16
to Tethys Platform
What Nathan suggests looks great. I would be wary of items like the map/plots as they are finicky in their appearances and load timing. 

As Nathan said, I just loaded the required JS in the main page to prevent reloading it every AJAX call and then added an initializer to the gizmos JS: http://docs.tethysplatform.org/en/dev/tethys_sdk/gizmos/plot_view.html#tethys-plot-view-inithighchartsplot-jquery-element


sdc50

unread,
Aug 4, 2016, 12:13:07 PM8/4/16
to tethysp...@googlegroups.com
Thank you @swainn and @alansnow21 for the suggestions. I've tried several things and eventually came up with something that is satisfactory, but I would like to propose some changes to Tethys to make this a little cleaner.  I'll explain the process that I went through (for posterity):

The step that I had missed was adding the {% gizmo_dependencies %} tag to the bottom of my template. At first when I added this I got the following error:

Traceback (most recent call last):
  File "/usr/lib/tethys/local/lib/python2.7/site-packages/django/core/handlers/base.py", line 149, in get_response
    response = self.process_exception_by_middleware(e, request)
  File "/usr/lib/tethys/local/lib/python2.7/site-packages/django/core/handlers/base.py", line 147, in get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/usr/lib/tethys/local/lib/python2.7/site-packages/django/contrib/auth/decorators.py", line 23, in _wrapped_view
    return view_func(request, *args, **kwargs)
  File "/usr/lib/tethys/src/tethys_apps/tethysapp/data_browser/controllers.py", line 271, in get_download_options
    html = render_to_string('data_browser/options.html', download_options)
  File "/usr/lib/tethys/local/lib/python2.7/site-packages/django/template/loader.py", line 97, in render_to_string
    return template.render(context, request)
  File "/usr/lib/tethys/local/lib/python2.7/site-packages/django/template/backends/django.py", line 95, in render
    return self.template.render(context)
  File "/usr/lib/tethys/local/lib/python2.7/site-packages/django/template/base.py", line 206, in render
    return self._render(context)
  File "/usr/lib/tethys/local/lib/python2.7/site-packages/django/template/base.py", line 197, in _render
    return self.nodelist.render(context)
  File "/usr/lib/tethys/local/lib/python2.7/site-packages/django/template/base.py", line 992, in render
    bit = node.render_annotated(context)
  File "/usr/lib/tethys/local/lib/python2.7/site-packages/django/template/base.py", line 959, in render_annotated
    return self.render(context)
  File "/usr/lib/tethys/src/tethys_gizmos/templatetags/tethys_gizmos.py", line 173, in render
    gizmos_rendered = context['gizmos_rendered']
  File "/usr/lib/tethys/local/lib/python2.7/site-packages/django/template/context.py", line 77, in __getitem__
    raise KeyError(key)
KeyError: 'gizmos_rendered'

I don't fully understand the reasons for getting this error because the way that the gizmo dependencies are resolved is still blackbox magic to me, however the cause of the error was because I was trying to use render_to_string() to render the template.

html = render_to_string('template_name.html', context)

I was able to avoid the error by changing this to:

html = render(request, 'template_name.html', context).content

I was able to get this to work using both the $.get and the $('#target').load() methods:

$.get(url, data, function(result){
    if(result.success){
        $('#target').html(result.html);
    }
})
.done(function() {
    $('.select2').select2();
});

$('#download-options-content').load(url, $.param(data), function(e){
    $('.select2').select2();
});
 
Note: for the $.get method the controller returns an json object, whereas for the $('#target').load() method the controller returns raw html. Also, note that in the load() method I had to serialize the data object with $.param(data) so that the request would use GET rather than POST.
 
However, with this approach the html that is returned by the call has all of the <script> and <link> tags in it which was a little weird to embed into a form in the middle of the page. Besides causing me a little angst (which would be forgivable) it also causes the following javascript warning:
 
 Synchronous XMLHttpRequest on the main thread is deprecated because of its detrimental effects to the end user's experience. For more help, check https://xhr.spec.whatwg.org/.

which is caused by jQuery trying to load all of the scripts and styles after the html has been loaded (see http://stackoverflow.com/questions/24639335/javascript-console-log-causes-error-synchronous-xmlhttprequest-on-the-main-thr?answertab=votes#28478146)

I ended up essentially doing what @alansnow21 suggests which is to load the gizmo dependencies in the main page manually, however I propose that we come up with a way to force gizmo dependencies to load even though they haven't been rendered. Perhaps something like this:

{% load_gizmo_dependencies [select_input, date_picker, etc.] %}


(again this is still blackbox magic to me so I'll have to dig down to see how this could actually be done).

As for initializing gizmos that are loaded later through ajax I think there are different ways we should handle that depending on the gizmo. As far as I can tell the date_picker doesn't require any initialization as long as the required js and css files are loaded. The select_input currently requires calling $('.select2').select2(); to initialize after it's loaded, but this could be avoided by changing this line of the Tethys code to:

$('.select2').live(function() {

(see http://stackoverflow.com/questions/4742835/re-run-application-javascript-on-ajax-loaded-content?answertab=votes#4742845). It looks like this would also work for Toggle Switch and the Highcharts Plot View. However, some gizmos like the MapView probably do need an initialize function, and I agree with @swainn that the naming of these functions should be standardized. 


In summary, I propose the following: 
  • a new template tag to manually load gizmo dependencies
  • refactoring code to automatically initialize gizmo that are loaded with ajax (where possible)
  • add (and standardize) initialize functions for gizmos (where needed)

sdc50

unread,
Aug 4, 2016, 12:47:38 PM8/4/16
to Tethys Platform
For those looking at this thread in their email note that I made some important edits to my previous post.

sdc50

unread,
Aug 4, 2016, 2:14:51 PM8/4/16
to Tethys Platform
The .live() jQuery function is deprecated. We should use .on() instead.

swainn

unread,
Aug 5, 2016, 11:33:00 AM8/5/16
to Tethys Platform
There is a lot of black magic that happens with gizmos and their dependencies. Basically, if I remember right, every time the gizmo tag is called, the name/type of gizmo is registered with a special context variable (as the page is being rendered). The gizmo_dependencies tag looks at this context variable and loads dependencies for each type of gizmo that has been added to it using the gizmo dependency functions that are defined when creating the gizmo. This points out a limitation of the gizmo dependencies tag: it needs to be called after all gizmo tags, or some dependencies may get missed.

I like the idea of having a manual way to load the gizmos (load_gizmo_dependency). +1

I also like the idea of using $.on(). I had no idea that was a thing. We should do some experimenting though. If it is called immediately after the element is created, but before the script/link elements are loaded for the dependencies, then it probably won't work too well for the normal use case with gizmos... We could get around it with the ajax load case because we can have the dependencies loaded before. Of course if it is nested in the document ready callback then that could work... Does it immediately execute on elements that have already been loaded when it is called, or only on elements added after it is called? We may need both $.each() and $.on() to handle the initial page load case and the ajax load case. Any way, these details could be worked out. +1

Along with standardizing the initialize functions for gizmos, perhaps we should standardize how all gizmos are loaded. e.g. instead of TETHYS_MAP_VIEW handling the initialization of the map, it provides the initialization method, which we call with either $.on() and $.each(). Then every gizmo can be handled the same. +1
Reply all
Reply to author
Forward
0 new messages