Skip to content

Commit f738d29

Browse files
committed
added Repeater WIP
1 parent 46b1455 commit f738d29

File tree

15 files changed

+2276
-0
lines changed

15 files changed

+2276
-0
lines changed

REPEATER.md

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
# Repeater - Dynamic Form Containers
2+
3+
Repeater umožňuje vytvářet dynamické kontejnery ve formulářích, které lze přidávat a odebírat na straně klienta pomocí JavaScriptu.
4+
5+
## Základní použití
6+
7+
```php
8+
use Nette\Forms\Form;
9+
10+
$form = new Form;
11+
12+
// Přidání repeateru
13+
$persons = $form->addRepeater('persons', function (Nette\Forms\Container $person) {
14+
$person->addText('firstname', 'Jméno:')->setRequired();
15+
$person->addText('surname', 'Příjmení:')->setRequired();
16+
});
17+
18+
// Konfigurace počtu položek
19+
$persons->setBounds(min: 1, max: 5, default: 2);
20+
21+
// Tlačítka pro UI (volitelné)
22+
$persons->addCreateButton('Přidat osobu')
23+
->setHtmlAttribute('class', 'btn btn-success');
24+
25+
$persons->addRemoveButton('Odebrat')
26+
->setHtmlAttribute('class', 'btn btn-danger');
27+
```
28+
29+
## Latte šablona
30+
31+
```latte
32+
{form myForm}
33+
{formRepeater persons}
34+
<div class="person-item">
35+
<label>
36+
Jméno: {input firstname}
37+
</label>
38+
<label>
39+
Příjmení: {input surname}
40+
</label>
41+
42+
{* Remove button *}
43+
<button n:repeater-remove>Odebrat</button>
44+
</div>
45+
{/formRepeater}
46+
47+
{* Create button *}
48+
<button n:repeater-add="persons">Přidat osobu</button>
49+
50+
{input submit}
51+
{/form}
52+
```
53+
54+
## Vnořené repeatery
55+
56+
```php
57+
$persons = $form->addRepeater('persons', function (Container $person) {
58+
$person->addText('firstname', 'Jméno:');
59+
60+
// Vnořený repeater pro emaily
61+
$person->addRepeater('emails', function (Container $email) {
62+
$email->addEmail('email', 'Email:');
63+
})->setItemsCount(min: 1, max: 3);
64+
});
65+
```
66+
67+
```latte
68+
{formRepeater persons}
69+
{input firstname}
70+
71+
{formRepeater emails}
72+
{input email}
73+
<button n:repeater-remove>×</button>
74+
{/formRepeater}
75+
76+
<button n:repeater-add="emails">+ email</button>
77+
{/formRepeater}
78+
```
79+
80+
## API
81+
82+
### `addRepeater(string $name, callable $factory): Repeater`
83+
84+
Přidá repeater do formuláře nebo kontejneru.
85+
86+
**Parametry:**
87+
- `$name` - název repeateru
88+
- `$factory` - callable, který vytvoří strukturu jednoho itemu
89+
90+
```php
91+
$form->addRepeater('items', function (Container $item) {
92+
$item->addText('name', 'Název:');
93+
$item->addInteger('quantity', 'Počet:');
94+
});
95+
```
96+
97+
### `setItemsCount(int $min = 0, ?int $max = null, ?int $default = null): static`
98+
99+
Nastaví minimální, maximální a výchozí počet položek.
100+
101+
**Parametry:**
102+
- `$min` - minimální počet položek (výchozí 0)
103+
- `$max` - maximální počet položek (null = neomezeno)
104+
- `$default` - výchozí počet položek (výchozí = min)
105+
106+
```php
107+
$repeater->setItemsCount(min: 1, max: 10, default: 3);
108+
```
109+
110+
### `addCreateButton(string|Stringable $caption): Html`
111+
112+
Přidá konfiguraci pro tlačítko "Přidat položku". Vrací Html element pro nastavení atributů.
113+
114+
```php
115+
$repeater->addCreateButton('Přidat')
116+
->setHtmlAttribute('class', 'btn btn-primary')
117+
->setHtmlAttribute('data-custom', 'value');
118+
```
119+
120+
### `addRemoveButton(string|Stringable $caption): Html`
121+
122+
Přidá konfiguraci pro tlačítko "Odebrat položku". Vrací Html element pro nastavení atributů.
123+
124+
```php
125+
$repeater->addRemoveButton('×')
126+
->setHtmlAttribute('class', 'btn btn-sm btn-danger');
127+
```
128+
129+
### `getControlPart(?string $name): ?Html`
130+
131+
Vrací virtuální control part pro tlačítka.
132+
133+
```php
134+
// V Latte:
135+
<button n:repeater-remove>Odebrat</button>
136+
<button n:repeater-add="persons">Přidat osobu</button>
137+
```
138+
139+
## Nastavení výchozích hodnot
140+
141+
```php
142+
$form->setDefaults([
143+
'persons' => [
144+
[
145+
'firstname' => 'Jan',
146+
'surname' => 'Novák',
147+
],
148+
[
149+
'firstname' => 'Marie',
150+
'surname' => 'Nováková',
151+
],
152+
],
153+
]);
154+
```
155+
156+
## Získání hodnot
157+
158+
```php
159+
if ($form->isSuccess()) {
160+
$values = $form->getValues();
161+
// $values->persons je pole objektů/polí s daty jednotlivých položek
162+
163+
foreach ($values->persons as $person) {
164+
echo $person->firstname . ' ' . $person->surname;
165+
}
166+
}
167+
```
168+
169+
## Validace
170+
171+
Repeater automaticky přidává validační pravidla:
172+
173+
```php
174+
$repeater->setItemsCount(min: 2, max: 5);
175+
// Automaticky přidá: Form::Count s rozsahem [2, 5]
176+
```
177+
178+
## JavaScript
179+
180+
Pro funkčnost přidávání/odebírání položek je potřeba JavaScript. Použijte připravený soubor:
181+
182+
```html
183+
<script src="path/to/repeater.js"></script>
184+
```
185+
186+
Soubor se nachází v `src/assets/repeater.js` a obsahuje třídu `RepeaterManager`, která automaticky inicializuje všechny repeatery na stránce.
187+
188+
### Funkce
189+
190+
**Automatická detekce změn:** Při pokusu o odstranění položky s upravenými hodnotami se uživatel automaticky dotáže na potvrzení. RepeaterManager porovnává aktuální hodnoty všech formulářových prvků s jejich výchozími hodnotami (ty, které by se obnovily po `reset()`).
191+
192+
**Podpora min/max limitů:** JavaScript automaticky kontroluje min/max atributy na repeateru a zabraňuje přidání/odebrání položek mimo tyto limity.
193+
194+
### Manuální inicializace
195+
196+
Pokud potřebujete inicializovat repeatery manuálně (např. po AJAXovém načtení):
197+
198+
```javascript
199+
const manager = new RepeaterManager();
200+
// nebo inicializovat jen v konkrétním kontextu
201+
manager.init(document.getElementById('my-container'));
202+
```
203+
204+
Viz kompletní příklad v `examples/repeater-template.latte`.
205+
206+
### Testování
207+
208+
JavaScript testy pomocí Karma/Jasmine se nacházejí v `tests/netteForms/spec/repeaterSpec.js`.
209+
210+
## HTML struktura
211+
212+
Repeater generuje následující HTML:
213+
214+
```html
215+
<div data-nette-repeater="persons">
216+
<!-- Existující položky -->
217+
<div data-repeater-index="0">
218+
<input name="persons[0][firstname]" id="frm-persons-0-firstname">
219+
<!-- ... -->
220+
</div>
221+
222+
<div data-repeater-index="1">
223+
<input name="persons[1][firstname]" id="frm-persons-1-firstname">
224+
<!-- ... -->
225+
</div>
226+
227+
<!-- Template pro nové položky -->
228+
<template
229+
id="frm-persons-template"
230+
data-nette-repeater-path="persons[0]">
231+
<input name="persons[0][firstname]" id="frm-persons-0-firstname">
232+
<!-- ... -->
233+
</template>
234+
</div>
235+
```
236+
237+
## Data atributy
238+
239+
- `data-nette-repeater="name"` - označuje repeater kontejner
240+
- `data-repeater-index="0"` - index položky (na wrapper elementu)
241+
- `data-nette-repeater-create="name"` - cílový repeater pro create button
242+
- `data-nette-repeater-max="5"` - maximální počet položek
243+
- `data-nette-repeater-min="1"` - minimální počet položek
244+
245+
## Registrace Latte Extension
246+
247+
V bootstrapu nebo konfiguraci:
248+
249+
```php
250+
$latte->addExtension(new Nette\Bridges\FormsLatte\FormsExtension);
251+
```
252+
253+
Nebo v `common.neon`:
254+
255+
```neon
256+
latte:
257+
extensions:
258+
- Nette\Bridges\FormsLatte\FormsExtension
259+
```
260+
261+
## Známá omezení
262+
263+
- JavaScript pro přidávání/odebírání je třeba implementovat samostatně
264+
- Indexy v HTTP datech musí být numerické
265+
- Při odeslání formuláře se items renumerují na souvislou řadu od 0
266+
267+
## Viz také
268+
269+
- `examples/repeater-example.php` - základní příklad
270+
- `examples/repeater-template.latte` - příklad s Latte a JavaScriptem

examples/latte/form-rep.latte

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
{* Generic form template *}
2+
3+
{define form, $name}
4+
<form n:name=$name>
5+
6+
<fieldset n:foreach="$form->getGroups() as $group" n:attr="id => $group->getOption(id)">
7+
<label n:ifcontent>{$group->getOption(label)}</label>
8+
{include controls $group->getControls()}
9+
</fieldset>
10+
11+
{include block-controls $form->getControls()}
12+
</form>
13+
{/define}
14+
15+
16+
{define block-controls, array $controls}
17+
<table>
18+
<tr n:foreach="$controls as $control"
19+
n:if="!$control->getOption(rendered) && $control->getOption(type) !== hidden"
20+
n:class="$control->required ? required">
21+
22+
<th>{label $control /}</th>
23+
24+
<td>
25+
{if $control->getOption(type) === repeater}
26+
{formRepeater $control}
27+
{* vykreslení obsahu *}
28+
{input firstname}
29+
{input lastname}
30+
31+
{formRepeater emails}
32+
{input email}
33+
{/formRepeater}
34+
35+
<button n:name="email:create">přidat email</button>
36+
37+
<button n:name="repeater:remove">X</button>
38+
{/formRepeater}
39+
40+
<button n:name="repeater:create">přidat osobu</button>
41+
42+
43+
44+
{else}
45+
{input $control}
46+
47+
<span class=info n:ifcontent>{$control->getOption(description)}</span>
48+
<span class=error n:ifcontent>{$control->error}</span>
49+
{/if}
50+
</td>
51+
</tr>
52+
</table>
53+
{/define}

0 commit comments

Comments
 (0)