Some JS Callbacks not working inside Loader


#1

Hi,

I have this problem for the second time now, so I’m starting to think it might be a bug:
I started using the Loader example https://github.com/atk4/ui/blob/665f16c51e47d48162bd464b3b985e2a32bbf3eb/demos/loader2.php to have a list on the left and a form on the right to edit one of the list items.

The first thing that did not work was a JsReload() inside the Loader, which worked well if used somewhere outside the loader:

This made me use the proposed workaround.

Now I stumble into simething similar using the (very nice) AutoComplete Field. If I define it inside a form inside the Loader, it does not work:

 $middlecolumn = $c->addColumn(6);
//middle column content: Loader (in which Form is loaded)
$middlecolumn_form = $middlecolumn->add(['Loader', 'loadEvent' => true, 'shim' => ['Text', 'Select a G to edit']]);

$middlecolumn_form->set(function($p) use ($app, $leftcolumn) {
	//Add Guest Form
	$guestform = $p->add('Form');
	//TODO: Label weight with kg, height with cm, see http://agile-ui.readthedocs.io/en/latest/field.html
	$guest = $guestform->setModel(new Guest($app->db));
        ...
	
	//if model is loaded, show all associated groups and add possibility to delete/add new
	if($guest->loaded()) {
		$group_form = $p->add('Form');
		$group_form->setModel(new Group($app->db), false);
		
		...

		//have Autocomplete button to add Guest to an existing group.
		$add_to_existing_group = $group_form->addField('add_to_group', [
			'AutoComplete',
			'model'       => new Group($app->db),
			'placeholder' => 'Search for existing group',
			'search'      => ['name', 'id'],
		]);
		...
	}
});

If I just have the form without the Loader at all, it works:

$middlecolumn = $c->addColumn(6);
//middle column content: Guest Form
$guestform = $middlecolumn->add('Form');
//TODO: Label weight with kg, height with cm, see http://agile-ui.readthedocs.io/en/latest/field.html
$guest = $guestform->setModel(new Guest($app->db));

...

//if model is loaded, show all associated groups and add possibility to delete/add new
if($guest->loaded()) {
	$group_form = $middlecolumn->add('Form');
	$group_form->setModel(new Group($app->db), false);
	
	...

	//have Autocomplete button to add Guest to an existing group.
	$add_to_existing_group = $group_form->addField('add_to_group', [
		'AutoComplete',
		'model'       => new Group($app->db),
		'placeholder' => 'Search for existing group',
		'search'      => ['name', 'id'],
	]);
	
    ...
}

So Loader causes me trouble this way 2 times now, what makes me think this might be a bug.

What also makes me wonder if I could implement all this without the loader as I always want the Form to show, so no changing content on the right side. The only thing I need to achieve is to get all the onRowClick() functionality without using the loader…

Best regards
Philipp


#2

Here is a short code for you:

class ViewTester extends \atk4\ui\View
{
    public function init()
    {
        parent::init();
        $label = $this->add(['Label', 'CallBack', 'detail' => 'fail', 'red']);
        $reload = new \atk4\ui\jsReload($this, [$this->name => 'ok']);
        if (isset($_GET[$this->name])) {
            $label->class[] = 'green';
            $label->detail = 'success';
        } else {
            $this->js(true, $reload);
            $this->js(true, new \atk4\ui\jsExpression('var s = Date.now(); var i=setInterval(function() { var p = Date.now()-s; var el=$[]; el.find(".detail").text(p+"ms"); if(el.is(".green")) { clearInterval(i); }}, 100)', [$label]));
        }
    }
}

This creates a component which you can place in various places and it will try and call back to itself and if it’s successful, it will set it’s label accordingly.

You can see this component in action here:

http://ui.agiletoolkit.org/demos/loader.php

but placing it inside your application at various location can help you identify where exactly callbacks no longer able to occur and thus isolate the problem.


#3

Hi Romans,

thanks for the code. I put it next to the non-working AutoComplete button and it works…
But the AutoComplete button shows some strange behaviour which might help to find the problem:
Have a look at this screen where I use the Loader like the loader2 example: a Grid on the Left, a Form inside the Loader on the right:

The Autocomplete does nothing, seems to reload and reload when clicking it.
When clicking the “+ new Group” button, A Modal opens which should have a form with the group model. Instead, the currently loaded Guest is displayed in there:

To my amateur eye it looks as if it gets the callback that the Loader got before…
Best Regards
Philipp


#4

I don’t think i can help further without the code. Can you contribute it into “demos/**” an an additional file and I will then be able to get it working?

In general Callbacks will try and have unique identifier for a callback, but that relies on the sequence in which you have added them on.

If incorrect callback is executed, it’s possible that a subsequent callback have used an extra callback object somewhere.


#5

Hi Romans, sry for the late answer, I was busy coding other stuff.

I am putting together a simplified version of my non-working code to make your life easier and will upload as soon as its finished.

I “managed” to create another non-working nested jsReload which is inside a section which gets jsReloaded itself so I am even more sure there’s a problem with nested Callbacks.

Does any of you have some good reads about proper debugging of JS Callbacks, some tips what to use for debugging etc? As I will have more JS Callback work todo, I must get into that anyway.

Best regards
Philipp


#6

Hi,

I committed 2 files into /demos:
demojsnestedreloadfail.php
demojsnestedreloadfail.sql

I can post a screencast of this not working here if you like.
Also, I have 2 other cases where I had failing nested reloads. If you need them, I can quickly add another demo file with that code.

Thanks
Philipp


#7

Hi. No I don’t have any better way to debug roloader other than inspector. If you could isolate the problem and describe how to reproduce, it would be very helpful.

Try removing all non-relevant elements / code lines then share.


#8

Hi there,

will add a screencast of the demo code later, the problem is pretty obvious when you see it.

Best regards


#9

In one of the 3 cases I coded non-working reloads, Trial & Error helped: It apparently is important where in the code the reload is placed.

Ok, lets try to explain:
I have a section A which reloads section B, its a list, as soon as an item is clicked, B reloads displaying a form for the item.
If B is saved, it reloads A to show the changes in the list as well.

So, the non-working code was structured like:

....
Section A
    definition and logic
....
Section B
    definition and logic
    trigger reload of A onSubmit
....
Section A
    trigger reload of B onRowClick
...

After loading, the A onSubmit reload worked, but as soon as A was reloaded by B, the A onSubmit didn’t work any more.

Changing the code a bit it works:

....
definition of Section A and Section B
Section A
    logic
    trigger reload of B onRowClick
...
Section B
    logic
    trigger reload of A onSubmit
...

So for reloads inside reloaded content, it seems important where in the code the reload is defined.
My real code is messy at the moment and needs refactoring, it has 6 different reload scenarios reloading different parts of the page - but now it works!. I can post it once its really readable.

Thanks again for this great integration of JS actions into the PHP code, I haven’t found another Framework making this so easy.

Best regards
Philipp


#10

Yes, generating jsReload inside callbacks often include additional triggers. This makes it easier for native elements such as Form to be able to reach itself, but if you reload other section jsReload should be created at appropriate level. I’ll try to explain this more in the documentation. I’m also assuming that once you figured this, it started to work fine.

Well said. Tell your friends :slight_smile:


#11

I feel like Britney Spears - oops I did it again!

I will start looking deeper into this now, I have the feeling I will stumble across this more often :slight_smile:

Again, after jsReload a section which has another callback, the callback URL is broken:
Before: "uri": "groups.php?atk_admin_virtualpage_3_callbacklater=cut",
After: "uri": "groups.php?atk_admin_columnlayout_view_callbacklater=callback\x26atk_admin_virtualpage_3_callbacklater=cut",

The interesting thing in this case is that the problem only occurs when I add the button that triggers the reload to a form field as FormField->action. If I use the very same code and just add the button to the form layout it works.

Will investigate, if I am stuck will post a more detailed description.

Best regards
Philipp


#12

Narrowed it down to line 426 to 440 of App.php->url(): When i comment this block everything works as it should:

    //add sticky arguments
    if (is_array($sticky) && !empty($sticky)) {
        foreach ($sticky as $key => $val) {
            if ($val === true) {
                if (isset($_GET[$key])) {
                    $val = $_GET[$key];
                } else {
                    continue;
                }
            }
            if (!isset($result[$key])) {
                $result[$key] = $val;
            }
        }
    }

So, the reloaded content has url which triggered the reload as GET argument in App->sticky_get_arguments. This array is filled by App->stickyGet() only as far as I can see…

EDIT:
The place where this stickyGet is set is Callpback.php line 88 - 90 in set():

            $this->app->stickyGet($this->name);
            $ret = call_user_func_array($callback, $args);
           $this->app->stickyForget($this->name);

commenting the first line makes my callbacks work. Reading the complete code block:

            if (isset($_GET[$this->name])) {
            $this->triggered = $_GET[$this->name];

            $t = $this->app->run_called;
            $this->app->run_called = true;
            $this->app->stickyGet($this->name);
            $ret = call_user_func_array($callback, $args);
            $this->app->stickyForget($this->name);
            $this->app->run_called = $t;

            return $ret;
        }

So in my example from above that means that $_GET[‘atk_admin_columnlayout_view_callbacklater’] = ‘callback’ exists. Seems a strange $_GET parameter to me…

My brain is done for now, will try to find where that $_GET parameter is set in the first way. For now I leave it to commenting the 2 stickyGet and stickyForget lines in Callback.php and leave App.php in its original state, as everything works then.

Best regards
Philipp


#13

callbacks sets sticky get during execution of your function, because if you Add any view inside it and it wants to generate URL() in needs to be able to reach itself through a callback. Mainly that’s used for having dynamic elements on VirtualPage.

There is a proposal how to improve this “sticky” situation, I have outlined it here: https://github.com/atk4/ui/issues/307. Until that’s in-place, you should be careful with the stickies, and if you’re stuck, simply move nested logic into a different PHP file.


#14

Hi Romans,

thanks for the feedback.

The funny thing is: If I move the nested logic to a different php file (and class to reuse it), it does not work. If I code the very same functionality inline, it works. Its about opening the jsModal/VirtualPage for Email sending:

So my limited understanding is:

  1. The callback needs its view id (like atk_admin_columnlayout_view_callbacklater) set as GET/POST parameter to trigger the callback function.
  2. The stickyGet causes this view id to be added to other callback URIs created inside the callback function, which results in a non-working callbacks…

I will post an example of my current case as its pretty odd later, now my kids need me.
Best regards
Philipp


#15

well you can’t just “move” it, it needs to be refactored. You would need to pass “id” of some sort, but the idea to try and avoid multiple nested virtual pages when you can help it.


#16

Oh yes this definitely does :slightly_smiling_face:

Still thinking about:

Thats exactly what I do, at least I think I do :slight_smile:

In the examples I saw so far, VirtualPages are added directly to the $app object, not to a deeper part of the render tree. Like $app->add($vp);
So at least in this case, there wouldn’t be a need for the VP to know the ID of the view it gets created in, right?

So at least in my case, commenting the 2 sticky lines in Callback->set() worked, but it breaks the atk Loader, which I do not use at the moment. So definitely not a perfect solution.

Best regards
Philipp


#17

remember to make note of WHERE url is created. If you can make it outside of the call-back, then pass it through use($url) it’s can solve the problem.