Nittro – error in Nextras/Datagrid

Barbarossa
Member | 74
+
0
-

Hi,

I am getting this error message from Nittro:
Dynamic snippet #snippet-articlesData-rows has no container

If I visit the same page via URL (without ajax), is everything aright.

code in default.latte (included from @layout)

{block content}
<div class="uk-grid" data-uk-grid-margin>
    <div class="uk-width-medium-1-1">
        <div class="md-card" >
            <div class="md-card-content">
                {control articlesData}
            </div>
        </div>
    </div>
</div>

is there any solution please, how can I fix it?

Thanks for any help

Nittro: 1.9.6
Nextras/Datagrid: v3.0.0-beta1

Last edited by Barbarossa (2017-02-13 23:49)

jahudka
Member | 71
+
0
-

Hi, I'm so sorry for not responding, I haven't been here on the Nette forums for a while. The issue is that Nextras/Datagrid doesn't currently support Nittro. I'm currently trying to find a solution to that, but it won't be easy, because the way Nextras/Datagrid handles snippets is non-standard and a little voodoo.

As a side note, I highly recommend you upgrade to Nittro 2.0 which came out just last Saturday – it won't help with this particular problem, but if there ever is a way to support Nextras/Datagrid, it will be as an extension to Nittro 2.0.

hrach
Member | 1834
+
0
-

@jahudka Could you provide more intel what's the voodoo? Datagrid behavior is pretty standard.

I've tried Datagrid 3.0@rc with Nittro 2.0 and it seems to work pretty well, with only exception: forms are not send by ajax. I have no idea how to force nitro to do that.

jahudka
Member | 71
+
0
-

hrach wrote:

Datagrid behavior is pretty standard.

Well, not really. You define a static snippet “rows” here – which means it's a wrapper for the whole table, but the dynamic snippets are the rows within <tbody>, ie. the dynamic snippets aren't direct child elements of the static container – so there is absolutely no way any front-end library could possibly know how to deal with the dynamic snippets, unless it's expressly written to support Nextras/Datagrid's specific DOM structure. What I was referring to as “voodoo” was the way the dynamic snippets are rendered – not using traditional {snippet ...} or n:snippet="...", but this – I'm not sure how snippets are implemented internally, so maybe this is just doing the same thing the snippet macro does, but to someone who's not well-versed in Nette's snippet internals this is voodoo :-)

And besides, while it's true that this simple approach should work, it's overkill to redraw the whole table each time – you could just as easily have the <thead>, <tbody> and <tfoot> be snippets and redraw them as needed (with the <tbody> serving as the static container for the row snippets and most of the time being the only thing that needs redrawing). It would not only shave a couple of bytes off the responses from the server, but more importantly it would mean you wouldn't need to refresh event handlers you might have attached to the form elements every time and it could also provide a better user experience (e.g. show a spinner when the next page is loading); you could also have fixed column sizes for a given use case and perhaps a container with a minimum height so that you reduce how much things jump around when the next page is loaded etc.

hrach wrote:

I've tried Datagrid 3.0@rc with Nittro 2.0 and it seems to work pretty well, with only exception: forms are not send by ajax. I have no idea how to force nitro to do that.

Weird, it does work for me out of the box without any special treatment. Do you still include nette.ajax.js or netteForms.js in your page? nette.ajax.js would probably conflict with Nittro pretty badly and netteForms.js is already included in all the Nittro builds, you only need to include nittro.min.js.

EDIT: Just to clarify, I'm trying this with the Datagrid demo and so far the only thing I've changed to make the full example work with Nittro is replace the contents of the layout head section with this:

<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" type="text/css">
<link rel="stylesheet" type="text/css" href="{$basePath}/css/nittro.min.css" />
<link rel="stylesheet" href="{$basePath}/proxy.php?f=bootstrap-style/bootstrap3.nextras.datagrid.css" type="text/css">
<script type="application/javascript" src="{$basePath}/js/nittro.min.js" async defer></script>

EDIT 2: Another thing is that the way dynamic snippets work means you shouldn't apply dynamic classes to the row snippets themselves, because when they're redrawn, only their inner content is sent in the payload – which means that the dynamic classes don't get applied at all when you add a new item and they're wrong when you update an item in a way that would change its class, like changing a person's gender in the Full example of the demo. It would be better to apply the class to an inner element of the snippet (like the ID cell in the example).

Last edited by jahudka (2017-03-03 15:37)

jahudka
Member | 71
+
0
-

The way I see it, what I'd need in order to be able to make a Nittro extension to fully support the Datagrid component is this:

  • Make the <thead>, <tbody> and <tfoot> tags snippets
  • Update the component's server-side code to only redraw them when needed (which means redraw the <thead> only when sorting changes and the <tfoot> only when the current page or filtering rules change)

Then I could make a JS extension / bridge that would add the proper data attributes to the <tbody> to make the dynamic snippets work (even with client-side sorting that would stay up to date with the current sorting mode), add transition effects, apply client-side dynamic snippet removals so that you don't need to redraw the whole table when a row is removed (this would also need server-side support, but that only means not rendering any snippets when rows are removed) and it should still be compatible with the current nette.ajax.js implementation, but also behave nicer with Nittro in the picture ;-)

hrach
Member | 1834
+
0
-

Well, not really. You define a static snippet “rows” here – which means it's a wrapper for the whole table, but the dynamic snippets are the rows within <tbody>

Imo this is wrong assumption. Latte doesn't force you to put dynamic snippets as direct child. See this check which travers indefinitely, until it finds static snippet.

so there is absolutely no way any front-end library could possibly know how to deal with the dynamic snippets

The problem may be here a fact, that your library does something which it shouldn't – or better, dynamic snippets aren't desinged for. You should update only that elements of which html id and contents is returned from backend.

I was referring to as “voodoo” was the way the dynamic snippets are rendered

Well, there is some voodoo in the backend side, but not in the HTTP interface side.
Datagrid works with ajax quite openly:

If you send ajax requires (x-requsted-with) → it returns array of snippets – their ids and contents, which should be replaced. nothing more. Sometimes it returns ids of rows – so it updates only specific rows, sometimes it returns only one snippet – the general table.

it's overkill to redraw the whole table each time

Please, double check the behavior. The whole datagrid is redrawn only when needed – e.g. pagination. There is no point to sned seprately each of td's instead of the whole table.

wouldn't need to refresh event handlers

Sorry, but this is so 2010. Nowadays, we add one event handler and check for the source when the event happend, don't we? ;) I mean $('.grid').on('click', 'a', function() {}).

Another thing is that the way dynamic snippets work means you shouldn't apply dynamic classes to the row snippets themselves,

Yes, this is wrong demo, this is just a little demonstration of some behaviour, which may make sence when do you don't need inline edit.


To the demo:

Aaaaah, it's solved! The magic is in async defer.


Resolution: Datagrid 3.0 & Nittro 2.0 works generally ok! The only missing is some custom javascript, which would close previously inline-edit-open rows. Simply, the logic here: https://github.com/….datagrid.js I wonder if it's simple to extend Nittro to do similar job.

Last edited by hrach (2017-03-03 20:40)

jahudka
Member | 71
+
0
-

hrach wrote:

Well, not really. You define a static snippet “rows” here – which means it's a wrapper for the whole table, but the dynamic snippets are the rows within <tbody>

Imo this is wrong assumption. Latte doesn't force you to put dynamic snippets as direct child. See this check which travers indefinitely, until it finds static snippet.

Yes, you're right about that – but if you take into account what dynamic snippets are usually used for (dynamic content which can also get added or deleted, apart from getting updated) then it's quite clear that the front end must be able to deal with that kind of use cases – and Nittro does that by requiring you to adhere to a specific DOM structure, in exchange for ease of use in that making dynamic snippets work is a matter of writing a single Latte macro (which itself translates to one data attribute and one class).

so there is absolutely no way any front-end library could possibly know how to deal with the dynamic snippets

The problem may be here a fact, that your library does something which it shouldn't – or better, dynamic snippets aren't desinged for. You should update only that elements of which html id and contents is returned from backend.

Yes, I'm doing that when you're just updating snippets (even for dynamic snippets), but if the backend returns a snippet which doesn't yet exist in the page then there's nothing to update – as is the case when the snippet is new. I don't know what dynamic snippets were designed for, but limiting their usage to updating existing content seems to be quite a drastic underestimation of what they could be used for ;-)

I was referring to as “voodoo” was the way the dynamic snippets are rendered

Well, there is some voodoo in the backend side, but not in the HTTP interface side.
Datagrid works with ajax quite openly:

If you send ajax requires (x-requsted-with) → it returns array of snippets – their ids and contents, which should be replaced. nothing more. Sometimes it returns ids of rows – so it updates only specific rows, sometimes it returns only one snippet – the general table.

Yeah, that's mostly true and you're quite right that I shouldn't care how it's done on the server side – and I mostly don't, except it made it harder for me to understand how it works, hence “voodoo” :-)

it's overkill to redraw the whole table each time

Please, double check the behavior. The whole datagrid is redrawn only when needed – e.g. pagination. There is no point to sned seprately each of td's instead of the whole table.

Okay, it might have been a misunderstanding on my end (might've been inspecting different requests in the DevTools).

wouldn't need to refresh event handlers

Sorry, but this is so 2010. Nowadays, we add one event handler and check for the source when the event happend, don't we? ;) I mean $('.grid').on('click', 'a', function() {}).

You don't have to explain event delegation to me, it's your end-users I'm worried about ;-)

Another thing is that the way dynamic snippets work means you shouldn't apply dynamic classes to the row snippets themselves,

Yes, this is wrong demo, this is just a little demonstration of some behaviour, which may make sence when do you don't need inline edit.

Ok

Aaaaah, it's solved! The magic is in async defer.

Glad it helped :-) some of the scripts probably need document.body which wouldn't be available if the script was executed synchronously in the <head> section. I'll get around to moving the DOM ready checking before that so that it works both ways..

Resolution: Datagrid 3.0 & Nittro 2.0 works generally ok! The only missing is some custom javascript, which would close previously inline-edit-open rows. Simply, the logic here: https://github.com/….datagrid.js I wonder if it's simple to extend Nittro to do similar job.

It most definitely should be – just tell me exactly what the original script is intended to do :-) I can read JavaScript, but as I've never really used nette.ajax.js I don't have the faintest idea how it's supposed to work :-)

EDIT: ohh, just found out – you probably mean that the DataGrid can only have one row open in inline edit mode and without AJAX that happens automatically, but with AJAX if you edit one row and then another without saving or cancelling the first edit, you'll have two rows active at the same time.. And if I understand it correctly, the current script is solving that by changing all the “edit” buttons' hrefs? Oh boy..

Okay, try this after the datagrid render:

<script type="application/javascript">
	(window._stack = window._stack || []).push([function (di, DOM) {
		DOM.getByClassName('grid').forEach(function (grid) {
			DOM.addListener(grid, 'click', function (evt) {
				var link = DOM.closest(evt.target, 'a'),
					frm = grid.getElementsByTagName('form').item(0);

				if (link && link.hasAttribute('data-datagrid-edit')) {
					evt.preventDefault();

					var btns = frm.elements.namedItem('edit[cancel]') || [],
						data = {};

					if (btns.tagName) {
						btns = [btns];
					}

					data[DOM.getData(grid, 'grid-name') + '-cancelEditPrimaryValue'] = btns
						.map(function (btn) {
							return DOM.getByClassName('grid-primary-value', DOM.closest(btn, 'tr'))[0].value;
						})
						.join(',');

					di.getService('page').open(link.href, 'get', data);
				}
			});
		});
	}, {
		DOM: 'Utils.DOM'
	}]);
</script>

If it works for you same as it does for me, I'll write it into an actual Nittro extension and add it to DataGrid via PR :-)

Last edited by jahudka (2017-03-03 22:15)

jahudka
Member | 71
+
0
-

… coming to think of it, you probably can use Nittro with dynamic snippets that aren't direct descendants of their static containers – Nittro doesn't really care how it's done on the backend, it only needs to know about the DOM containers for dynamic snippets – so you probably could just apply the nittro-snippet-container class and data-dynamic-mask attribute to the <tbody> without making it an actual snippet and it should work too.. I'll check it out ;-)

hrach
Member | 1834
+
0
-

You don't have to explain event delegation to me

Yeah, I know, I was just trolling :-D

I don't know what dynamic snippets were designed for, but limiting their usage to updating existing content seems to be quite a drastic underestimation of what they could be used for ;-)

I'm getting to understand what your lib also can do! +1 Still, some doc would be needed :) For now, fortunatelly, Nextras Datagrid does need this behavior, it doesn't do that (“the backend returns a snippet which doesn't yet exist in the page”).

just tell me exactly what the original script is intended to do

  • filtering form:
    • pressing enter in input “sends” the filtering form
    • choosing value in select “sends” the filtering form
  • clicking td with ctrl “clicks” the inline edit button
  • pressing enter in inline-edit input “savs” the inline-edit form
  • after each ajax change of the form it:
    • collects the ids of opened inline edit (should be just one)
    • updates inline-edit links to contain and id of row, which should be closed.
    • why: after clicking inline edit button, XHR sends a request to the backend with the id of the row. Backend recognize it wants only one row inline edit, therefore it renders only one row – dynamic snippets and send it only that to the frontend; however, because it's rendering only one row, the previously “inline-edited” tr stays “open”; that's wrong at least for now, because the whole grid is one html form and backend doesn't duplicate the inline-edit inputs for each row, so data would be rewritten with the last inline-edit row… I hope it's clear what is the desired behavior.

so you probably could just apply the nittro-snippet-container class and data-dynamic-mask attribute to the <tbody>

Hey, I've read the docs and I'm starting to get this. :-)

jahudka
Member | 71
+
0
-

Yeah, I know, I was just trolling :-D

Oh, that's all right then ;-)

I'm getting to understand what your lib also can do! +1 Still, some doc would be needed :) For now, fortunatelly, Nextras Datagrid does need this behavior, it doesn't do that (“the backend returns a snippet which doesn't yet exist in the page”).

Yeah, docs are still a work in progress, sorry about that.. But doesn't Nextras Datagrid have a “create new entry” feature? That would seem like quite the omission and should be quite easy to implement with what's already there.. From Nittro's standpoint the only thing needed to support that would be to have the new entry rendered the same way an updated snippet would be, except you'd need the proper class and data attribute on the <tbody> as mentioned (but a client-side script could easily take care of that). Also if there's a “delete row” or “delete selected rows” feature Nittro can help, the backend would only need to send the response payload without rendering any snippets (as Nittro can take care of dynamic snippet removal on the client side) – I guess that to maintain compatibility with existing users and implementations this could be controlled by an additional parameter in the request, what do you think?

  • filtering form:
    • pressing enter in input “sends” the filtering form
    • choosing value in select “sends” the filtering form
  • clicking td with ctrl “clicks” the inline edit button
  • pressing enter in inline-edit input “savs” the inline-edit form

Okay, easy enough

  • after each ajax change of the form it: (… takes care of closing previously open inline edit dialogs)

The script I posted earlier already does that, except it doesn't alter the attribute of the link itself but instead calls the appropriate Nittro handler directly

Hey, I've read the docs and I'm starting to get this. :-)

Great :-) Although as I said, the docs on the Wiki aren't yet completely up to date with Nittro 2.0, but I think the part about dynamic snippets is pretty much the same, at least from the user's point of view.

I'm working at the bar today, so I'm not sure I'll have time to put it together, but I have a feeling you can expect a pull request sometime tomorrow ;-)

Last edited by jahudka (2017-03-04 14:55)

jamesdev
Member | 1
+
0
-

jahudka wrote:

hrach wrote:

Well, not really. You define a static snippet “rows” here – which means it's a wrapper for the whole table, but the dynamic snippets are the rows within <tbody>

Imo this is wrong assumption. Latte doesn't force you to put dynamic snippets as direct child. See this check which travers indefinitely, until it finds static snippet.

Yes, you're right about that – but if you take into account what dynamic snippets are usually used for (dynamic content which can also get added or deleted, apart from getting updated) then it's quite clear that the front end must be able to deal with that kind of use cases – and Nittro does that by requiring you to adhere to a specific DOM structure, in exchange for ease of use in that making dynamic snippets work is a matter of writing a single Latte macro (which itself translates to one data attribute and one class).

so there is absolutely no way any front-end library could possibly know how to deal with the dynamic snippets

The problem may be here a fact, that your library does something which it shouldn't – or better, dynamic snippets aren't desinged for. You should update only that elements of which html id and contents is returned from backend.

Yes, I'm doing that when you're just updating snippets (even for dynamic snippets), but if the backend returns a snippet which doesn't yet exist in the page then there's nothing to update – as is the case when the snippet is new. I don't know what dynamic snippets were designed for, but limiting their usage to updating existing content seems to be quite a drastic underestimation of what they could be used for ;-)

I was referring to as “voodoo” was the way the dynamic snippets are rendered

Well, there is some voodoo in the backend side, but not in the HTTP interface side.
Datagrid works with ajax quite openly:

If you send ajax requires (x-requsted-with) → it returns array of snippets – their ids and contents, which should be replaced. nothing more. Sometimes it returns ids of rows – so it updates only specific rows, sometimes it returns only one snippet – the general table.

Yeah, that's mostly true and you're quite right that I shouldn't care how it's done on the server side – and I mostly don't, except it made it harder for me to understand how it works, hence “voodoo” :-)

it's overkill to redraw the whole table each time

Please, double check the behavior. The whole datagrid is redrawn only when needed – e.g. pagination. There is no point to sned seprately each of td's instead of the whole table.

Okay, it might have been a misunderstanding on my end (might've been inspecting different requests in the DevTools).

wouldn't need to refresh event handlers

Sorry, but this is so 2010. Nowadays, we add one event handler and check for the source when the event happend, don't we? ;) I mean $('.grid').on('click', 'a', function() {}).

You don't have to explain event delegation to me, it's your end-users I'm worried about ;-)

Another thing is that the way dynamic snippets work means you shouldn't apply dynamic classes to the row snippets themselves,

Yes, this is wrong demo, this is just a little demonstration of some behaviour, which may make sence when do you don't need inline edit.

Ok

Aaaaah, it's solved! The magic is in async defer.

Glad it helped :-) some of the scripts probably need document.body which wouldn't be available if the script was executed synchronously in the <head> section. I'll get around to moving the DOM ready checking before that so that it works both ways..

Resolution: Datagrid 3.0 & Nittro 2.0 works generally ok! The only missing is some custom java, which would close previously inline-edit-open rows. Simply, the logic here: https://github.com/….datagrid.js I wonder if it's simple to extend Nittro to do similar job.

It most definitely should be – just tell me exactly what the original script is intended to do :-) I can read JavaScript, but as I've never really used nette.ajax.js I don't have the faintest idea how it's supposed to work :-)

EDIT: ohh, just found out – you probably mean that the DataGrid can only have one row open in inline edit mode and without AJAX that happens automatically, but with AJAX if you edit one row and then another without saving or cancelling the first edit, you'll have two rows active at the same time.. And if I understand it correctly, the current script is solving that by changing all the “edit” buttons' hrefs? Oh boy..

Okay, try this after the datagrid render:

<script type="application/javascript">
	(window._stack = window._stack || []).push([function (di, DOM) {
		DOM.getByClassName('grid').forEach(function (grid) {
			DOM.addListener(grid, 'click', function (evt) {
				var link = DOM.closest(evt.target, 'a'),
					frm = grid.getElementsByTagName('form').item(0);

				if (link && link.hasAttribute('data-datagrid-edit')) {
					evt.preventDefault();

					var btns = frm.elements.namedItem('edit[cancel]') || [],
						data = {};

					if (btns.tagName) {
						btns = [btns];
					}

					data[DOM.getData(grid, 'grid-name') + '-cancelEditPrimaryValue'] = btns
						.map(function (btn) {
							return DOM.getByClassName('grid-primary-value', DOM.closest(btn, 'tr'))[0].value;
						})
						.join(',');

					di.getService('page').open(link.href, 'get', data);
				}
			});
		});
	}, {
		DOM: 'Utils.DOM'
	}]);
</script>

If it works for you same as it does for me, I'll write it into an actual Nittro extension and add it to DataGrid via PR :-)

I was getting the same issue earlier but it resolved after go through this thread conversation.