Latte 2.10 a novinka {iterateWhile}

David Grudl
Nette Core | 8227
+
+6
-

Přidal jsem do Latte značku s pracovním názvem {iterateWhile} (původní název byl innerForeach, post jsem pak upravil). Hodí se na různé chytrystyky ve foreach cyklech.

Máme takovouto tabulku položek:

id catId name
1 1 Lorem
2 1 ipsum
3 2 dolor
4 3 sit
5 3 amet
6 3 consectetuer

Budu je vykreslovat v cyklu foreach, ale chtěl bych, aby každá kategorie byla v samostatné tabulce.

Tohle se dá řešit v různých šablonovacích systémem (dosaďte si jakýkoliv) zhruba tímto způsobem:

{var $prevCatId = null}
{foreach $items as $item}
	{if $item->catId !== $prevCatId}
		{* uzavřeme minulou kategorii *}
		{if $prevCatId !== null}
			</tbody>
		</table>
		{/if}

		{* otevřeme novou kategorii *}
		{do $prevCatId = $item->catId}
		<table>
			<thead>...</thead>
			<tbody>
	{/if}

	<tr>
		<td>{$item->name}
	</tr>
{/foreach}

{if $prevCatId !== null}  {* uzavřeme poslední kategorii *}
		</tbody>
	</table>
{/if}
</table>

Latte má výhodu v tom, že jeho $iterator zná následující položku, takže se dá odstranit duplicitní uzavírání tabulky a výsledek vypadá srozumitelněji.

{var $prevCatId = null}
{foreach $items as $item}
    {* otevřeme novou kategorii *}
	{if $item->catId !== $prevCatId}
		{do $prevCatId = $item->catId}
		<table>
			<thead>...</thead>
			<tbody>
	{/if}

	<tr>
		<td>{$item->name}
	</tr>

    {* uzavřeme minulou kategorii *}
	{if !$iterator->hasNext() || $iterator->nextValue->catId !== $item->catId}
			</tbody>
		</table>
	{/if}
{/foreach}

Ale stále nevypadá tak, abych s tím byl spokojen :)

Vzniká tu nerovnovážná situace, kdy se HTML značky otevírají v bloku podmínky {if}, ale zavírají se v jiném bloku. Kvůli tomu nepůjde ani ve značce <table> používat n:atributy. Celkově to nepůsobí moc blbuvzdorně.

{iterateWhile}

Pomocí iterateWhile můžeme nechat iteraci probíhat po určitou dobu (např. dokud je stejná kategorie) ve vnitřním cyklu:

{foreach $items as $item}
    {* otevřeme novou kategorii *}
	<table>
		<thead>...</thead>
		<tbody>

		{iterateWhile}
			<tr>
				<td>{$item->name}
			</tr>
		{/iterateWhile $iterator->nextValue->catId === $item->catId}

	    {* uzavřeme minulou kategorii *}
		</tbody>
	</table>
{/foreach}

Nepotřebujeme žádné pomocné proměnné jako $prevCatId, není tu nerovnováha v podobě otevíracích značek v jiném bloku než jsou uzavírací, hezky sedí odsazení, je to blbuvzdorné.

Samozřejmě je potřeba se naučit tento zápis číst, bez toho může vypadat komplikovaně. Možná by pomohl i lepší název než iterateWhile, napadá vás?

Pokud uvedeme podmínku v otevírací značce {iterateWhile}, tak se chování mění obdobně jako v cyklech do {} while a while {}. Tedy podmínka (a přechod na další prvek) se vykoná na začátku inner cyklu. Zatímco do {iterateWhile} se vstoupí vždy, do {iterateWhile $cond} jen při splnění podmínky a $item se mění.

Takže v tomto příkladě se první položka kategorie vykreslí uvnitř <thead> a další až v <tbody>:

{foreach $items as $item}
    {* otevřeme novou kategorii *}
	<table>
		<thead>
			<tr>
				<td>{$item->name}
			</tr>
		</thead>
		<tbody>

		{* pokracujeme následujícím prvek pokud je ve stejné kategorii *}
		{iterateWhile $iterator->nextValue->catId === $item->catId}
			<tr>
				<td>{$item->name}
			</tr>
		{/iterateWhile}

	    {* uzavřeme minulou kategorii *}
		</tbody>
	</table>
{/foreach}

Aby se v případě jediného prvku v kategorii nevypsalo prázdné <tbody></tbody>, lze mu doplnit podmínku do n:if.

Také je možné podmínku vůbec neuvádět. Začíná se tedy značkou {foreach}, pak celý cyklus proběhne v {iterateWhile} ... {/iterateWhile}, pak se pokračuje k {/foreach} a konec.

{foreach $items as $item}
	<table>
		<thead>...{$item->name}...</thead>
		<tbody>

		{iterateWhile}
			<tr>
				<td>{$item->name}
			</tr>
		{/iterateWhile}

		</tbody>
	</table>
{/foreach}

Jak se to liší od toho, že bychom použili uvnitř rovnou {foreach}? Jednak tím, že pokud je pole $items prázdné, nevykreslí se tabulka. A dále $item s prvním prvek máme k dispozici už dříve, viz <thead>

Vnořené smyčky

V rámci jednoho cyklu můžeme vytvářet více inner-smyček a dokonce je zanořovat. Takže by se daly ohraničit třeba podkategorie atd.

Představme si, že v tabulce je ještě sloupec subCatId. Každá kategorie bude v samostatném <table>, každá podkategorie v samostatném <tbody>:

{foreach $items as $item}
	<table>
		{iterateWhile}
			<tbody>
				{iterateWhile}
					<tr>
						<td>{$item->name}
					</tr>
				{/iterateWhile $iterator->nextValue->subCatId === $item->subCatId}
			</tbody>
		{/iterateWhile $iterator->nextValue->catId === $item->catId}
	</table>
{/foreach}

Filtr |batch

Seskupování lineárních položek řeší i filtr batch, který je spojuje po určitém počtu:

<table>
{foreach ($items|batch:3) as $row}
    <tr>
        {foreach $row as $column}
            <td>{$column}</td>
        {/foreach}
    </tr>
{/foreach}
</table>

Lze jej nahradit za iterateWhile tímto způsobem:

<table>
{foreach $items as $item}
    <tr>
        {iterateWhile}
            <td>{$item}</td>
        {/iterateWhile $iterator->counter0 % 3}
    </tr>
{/foreach}
</table>

Rozdíl je opět v tom, že $item s prvním prvek máme k dispozici vně vnitřního cyklu, na rozdíl od $column.

Gappa
Nette Blogger | 208
+
+3
-

Přidal jsem do Latte značku s pracovním názvem {innerForeach}. Hodí se na různé chytrystyky ve foreach cyklech.

Čistě co se názvu týče, tak ten pracovní by se „třískal“ s n:inner-foreach (atributovou variantou zápisu „obyčejného“ foreach).

Návrhy:

  • subForeach
  • partialForeach
  • localForeach
hrach
Člen | 1838
+
0
-

groupForeach/groupingForeach?

David Grudl
Nette Core | 8227
+
+5
-

nestedLoop?

Felix
Nette Core | 1245
+
+1
-

David Grudl napsal(a):

nestedLoop?

Take me to napadlo, ale myslel jsem, ze se drzime foreach v nazvu. NestedLoop se mi libi moc.

David Grudl
Nette Core | 8227
+
0
-

V názvu by spíš foreach byt vůbec nemělo, protože to „each“ není pravda, to spíš forsome :-)

filsedla
Člen | 101
+
+3
-

Mně taky přijde, že by to mohlo názvem napovídat, že to pomáhá při seskupování, forGroup?

Marek Bartoš
Nette Blogger | 1274
+
0
-

Kolega kodér tohle prý už v pár desítkách případů potřeboval a z navrhnutých variant se mu zdá nejjasnější nestedLoop. Ideální název

filsedla
Člen | 101
+
0
-

Ve všech příkladech je chyba, je tam vždy </table> navíc, ne?

filsedla
Člen | 101
+
+1
-

Proč se to vlastně nejmenuje do…while (nebo něco od toho odvozeného)?

Mám spíš na mysli se zeptat, v jakých konkrétních aspektech se to liší od té klasické konstrukce do-while?

David Grudl
Nette Core | 8227
+
0
-

filsedla napsal(a):

Ve všech příkladech je chyba, je tam vždy </table> navíc, ne?

V jednom nebylo :-) Díky, opravil jsem.

David Grudl
Nette Core | 8227
+
0
-

Přemýšlím, do jaké míry brainstorming ovlivňuje to, že většina příkladů se týká seskupování. Takhle to používám, ale možná je užití mnohem širší, jen mě teď nenapadají příklady.

David Grudl
Nette Core | 8227
+
0
-

Btw, je lepší $iterator nebo $loop?

Pavel Kravčík
Člen | 1195
+
-1
-

Kratší je lepší. ♀️

GEpic
Člen | 566
+
0
-

Mohlo by to řešit i případ, kdy chcete vypisovat obrázky od shora dolů do sloupců a né do řádků.
Výsledkem je poté masonry grid:

{varType Nette\Database\Table\ActiveRow[] $images}
{default int $columns = 4}

{* foreach + nestedLoop *}
<div class="row row-cols-{$columns}">
	{foreach $images as $image}
		<div>
			{nestedLoop}
				<img src="{$image->path}" alt="{$image->name}" class="img-fluid">
			{/nestedLoop $iterator->counter % ceil(count($images) / $columns) === 0)}
		</div>
	{/foreach}
</div>

I přesto, že podmínka je matematicky trošku složitější (a snad jsem jí zapsal správně), výsledný kód je daleko přehlednější.

Editoval GEpic (18. 1. 2021 16:16)

David Grudl
Nette Core | 8227
+
0
-

GEpic napsal(a):

I přesto, že podmínka je matematicky trošku složitější (a snad jsem jí zapsal správně), výsledný kód je daleko přehlednější.

Mělo by tam být spíš $iterator->counter0 % xxx !== 0.

Na tu dělitelnost by se tam mohla dát i nějaká srozumitelnější funkci. Něco jako {nestedLoop $iterator->step(xxx)} nebo tak.

GEpic
Člen | 566
+
0
-

@DavidGrudl smazal jsem ten příklad, jak bych to řešil teď – byl totiž špatně. A upřímně ať přemýšlím, jak přemýšlím tak nevím, jak bych ten případ bez nestedLoop vyřešil bez využítí funkce jako např. array_chunk

Editoval GEpic (18. 1. 2021 16:18)

dakur
Člen | 493
+
0
-

filsedla napsal(a):

Proč se to vlastně nejmenuje do…while (nebo něco od toho odvozeného)?

Mám spíš na mysli se zeptat, v jakých konkrétních aspektech se to liší od té klasické konstrukce do-while?

Mě napadlo totéž – funguje to jako (do) while, vypadá to jako (do) while, proč ne (do) while? 🙂

filsedla
Člen | 101
+
0
-

Tohle není nested loop. Počet iterací se nijak nenásobí, zůstává stejný jako kdyby tam byl jenom ten jednoúrovňový foreach.

David Matějka
Moderator | 6445
+
+3
-

v jakých konkrétních aspektech se to liší od té klasické konstrukce do-while?

samo to posouvá iterátor.

Tak co iterateWhile nebo prostě jen iterate?

filsedla
Člen | 101
+
+3
-

iterateWhile

David Grudl
Nette Core | 8227
+
+7
-

iterateWhile je dobrý, proože tam je jasně naznačená ta existence podmínky