Saturday, August 30, 2014

I am working on a frustrating problem with Javascript/ExtJS.

How to reproduce it

To show the context without requiring you to install Lino Welfare, I extended lino.projects.min1 (a minimal demo application used for certain tests):

  • it now installs lino_xl.lib.cal (a basic calendar module)

  • it now defines a lino.core.site.Site.get_admin_main_items() method which yields ml.cal.MyEvents:

    def get_admin_main_items(self, ar):
        yield self.modules.cal.MyEvents
    

    The lino.core.site.Site.get_admin_main_items() method is expected to yield a list of tables to be displayed in the body of the main page using “plain html”.

Here is how lino.projects.min1 now looks after Robin logged in:

../../_images/0830a.png

You should be able to reproduce the above by installing the latest development version of Lino (as explained in Install your Lino developer environment) and executing:

$ cd ~/repositories/lino/lino/projects/min1
$ python manage.py initdb_demo
$ runserver

Now please note the link symbol after the table’s title (“My events”). Clicking on this symbol opens the given table in a “real” window with an editable ExtJS grid.

The initial problem report then was:

  • When I add an event in the “My events” table, then that event isn’t displayed in main page after closing the window.

This was an easy pick, the problem came because Lino didn’t refresh the main page. I just needed to add a call to Lino.viewport.refresh() when the last window has been closed. Summary of the relevant JavaScript code:

Lino.close_window = function(status_update, norestore) {
  var ww = Lino.window_history.pop();
  ...
  if (ww) {
    ...
    Lino.current_window = ww.window;
  } else {
      Lino.current_window = null;
      Lino.viewport.refresh();       // new since 20140829
  }
  ...
};

That worked. But it has a side effect:

Once the main page has been refreshed that way (by closing the last window, not by reloading the whole page), the link doesn’t work anymore. The window just doesn’t open, and there is no error message.

If you were able to follow until here, then please try help me to solve this problem!

A first explanation

Joe suggested the following explanation:

I think your JS problem is pretty standard. When you replace some HTML content, script tags included are not executed and of course all event bindings are discarded together with old HTML code. Possibilities to fix:

  1. Make the html-replacing code execute script tags and bind the actions in replaced HTML code. Ext JS has built-in support. My first pull request was just about it: https://gitlab.com/lino-framework/lino/pull/1/files

  2. Use “correct” way of action binding using top-level element catching bubbling events from child elements. See http://api.jquery.com/on/ that explains it. The essence is to bind the event on top element that is not replaced. After the child elements are replaces all bindings are still there and events bubbles-up normally.

  3. Manually re-bind actions after reload.

My answer: I don’t understand this explanation. Because where are those event bindings that might get discarded? In fact I even believe that the reason must be somewhere else. Two observations to explain why:

Observation 1

The Lino.Viewport.refresh() function sends an AJAX request and receives the following html fragment as response:

<div class="htmlText" style="margin:5px">
<p>Quick links:
[<a href="javascript:Lino.contacts.Persons.detail.run(null)">Detail Persons</a>]...</p>
<p>Hi, Robin! </p>
<p>This is a Lino demo site. ... </p>
<h2>My events
  <a href="javascript:Lino.cal.MyEvents.grid.run(null)">
  <img src="...link.png"/>
</a></h2>
<table bgcolor="#ffffff" ...>...</table>
</div>

Then it updates the main_area with this fragment:

if (result.html) {
    var cmp = Ext.getCmp('main_area');
    cmp.update(result.html);
}

I tried to specify true for the Component.update() method:

cmp.update(result.html, true);

Which didn’t solve the problem.

AFAICS the HTML of main_area does not use script tags, just a javascript: anchor. And the specified code (Lino.cal.MyEvents.grid.run(null)) continues to be executed when I click on the symbol.

Observation 2

Here is the definition of Lino.WindowAction whose run() method is being called:

Lino.WindowAction = Ext.extend(Lino.WindowAction,{
    window : null,
    get_window : function() {
      if (this.window == null)  {
      // if (true)  {
          this.windowConfig.main_item = this.main_item_fn();
          this.window = new Lino.Window(this.windowConfig);
      }
      return this.window;
    },
    run : function(requesting_panel, status) {
      Lino.open_window(this.get_window(), status, requesting_panel);
    }
});

This code does a kind of “caching” of the Ext.Window object, and this caching is related to our problem because when I disable it (by writing if (true) instead of if (this.window == null)), then the problem does not occur.