testuju nove formulare + Kdyby\Replicator a

před 5 lety

LeonardoCA
Člen | 297
+
0
-

Zkousim jak funguje nova verze formularu + Twitter Bootstrap + Kdyby\Replicator, jestli se nektere veci oproti drivejsku zjednodusily, atp. Aby me nematly me predchozi konstrukce, kde jsem mel vse vyresene, zacinam s cistym projektem, jako bych o formularich nic nevedel, podle navodu v dokumentaci a prikladu. Zkusim zdokumentovat poznatky a treba to nekomu pomuze.

Jednotlive kroky budu commitovat do projektu https://github.com/…/nplayground

Krok 1. Vytvoreni formulare primo v metode presenteru

Pridavam do projektu Kdyby\Replicator. Nechci jej pridavat pres bootstrap a chvili mi trva nez si vzpomenu na spravnou syntaxi zapisu pro config.ini v popisu doplnku na https://addons.nette.org/…s-replicator jsem to nenasel

extensions:
    formsReplicator: Kdyby\Replicator\DI\ReplicatorExtension

Kopiruju cast kodu z https://github.com/…endering.php a pridavam replicator podle https://addons.nette.org/…s-replicator

Zpracovani formulare zatim neresim, v tuto chvili mi jde o spravne vykresleni a funkcnost replikatoru.

Prvni verze formulare vypada takto:

<?php

namespace App\Presenters;

use Nette,
    Nette\Forms\Controls,
    Nette\Forms\Container,
    Nette\Forms\Controls\SubmitButton,
    App\Model;

/**
 * Homepage presenter.
 */
class HomepagePresenter extends BasePresenter
{

    public function renderDefault()
    {
        $this->template->anyVariable = 'any value';
    }



    public function createComponentTestForm()
    {
        $form = new Nette\Application\UI\Form();

        $form->addGroup('Personal data');
        $form->addText('name', 'Your name')
            ->setRequired('Enter your name');
        $form->addRadioList(
            'gender',
            'Your gender',
            array(
                'male',
                'female',
            )
        );
        $form->addCheckboxList(
            'colors',
            'Favorite colors:',
            array(
                'red',
                'green',
                'blue',
            )
        );
        $form->addSelect(
            'country',
            'Country',
            array(
                'Burundi',
                'Qumran',
                'Saint Georges Island',
            )
        );
        $form->addTextArea('note', 'Comment');

        $form->addGroup('Addresses');
        $removeEvent = callback($this, 'MyFormRemoveElementClicked');
        $users = $form->addDynamic(
            'users',
            function (Container $user) use ($removeEvent) {
                $user->addText('name', 'Name');
                $user->addText('surname', 'surname');
                $addresses = $user->addDynamic(
                    'addresses',
                    function (Container $address) use ($removeEvent) {
                        $address->addText('street', 'Street');
                        $address->addText('city', 'City');
                        $address->addText('zip', 'Zip');
                        $address->addCheckbox('send', 'Ship to address');
                        $removeBtn = $address->addSubmit('remove', '-')
                            ->setValidationScope(false);
                        $removeBtn->onClick[] = $removeEvent;
                    },
                    1
                );
                $addBtn = $addresses->addSubmit('add', '+')
                    ->setValidationScope(false);
                $addBtn->onClick[] = callback($this, 'MyFormAddElementClicked');
                $removeBtn = $user->addSubmit('remove', '-')
                    ->setValidationScope(false);
                $removeBtn->onClick[] = $removeEvent;
            },
            2
        );
        $users->addSubmit('add', '+')
            ->setValidationScope(false)
            ->onClick[] = callback($this, 'MyFormAddElementClicked');

        $form->addGroup();
        $form->addSubmit('submit', 'Send');
        $form->addSubmit('cancel', 'Cancel');

        // setup form rendering
        $renderer = $form->getRenderer();
        $renderer->wrappers['controls']['container'] = null;
        $renderer->wrappers['pair']['container'] = 'div class=form-group';
        $renderer->wrappers['pair']['.error'] = 'has-error';
        $renderer->wrappers['control']['container'] = 'div class=col-sm-9';
        $renderer->wrappers['label']['container'] =
            'div class="col-sm-3 control-label"';
        $renderer->wrappers['control']['description'] = 'span class=help-block';
        $renderer->wrappers['control']['errorcontainer'] =
            'span class=help-block';

        // make form and controls compatible with Twitter Bootstrap
        $form->getElementPrototype()->class('form-horizontal');

        foreach ($form->getControls() as $control) {
            if ($control instanceof Controls\Button) {
                $control->getControlPrototype()->addClass(
                    empty($usedPrimary) ? 'btn btn-primary' : 'btn btn-default'
                );
                $usedPrimary = true;
            } elseif ($control instanceof Controls\TextBase
                || $control instanceof Controls\SelectBox
                || $control instanceof Controls\MultiSelectBox
            ) {
                $control->getControlPrototype()->addClass('form-control');

            } elseif ($control instanceof Controls\Checkbox
                || $control instanceof Controls\CheckboxList
                || $control instanceof Controls\RadioList
            ) {
                $control->getSeparatorPrototype()->setName('div')->addClass(
                    $control->getControlPrototype()->type
                );
            }
        }

        return $form;
    }



    public function MyFormAddElementClicked(SubmitButton $button)
    {
        $button->parent->createOne();
    }



    public function MyFormRemoveElementClicked(SubmitButton $button)
    {
        // first parent is container
        // second parent is it's replicator
        $users = $button->parent->parent;
        $users->remove($button->parent, true);
    }

}

do sablony vkladam formular takto:

<div class="panel panel-default">
  <div class="panel-heading">
    <h3 class="panel-title">Test Form</h3>
  </div>
  <div class="panel-body">
    {control testForm}
  </div>
</div>

horni pulka se vyrenduje spravne

s druhou pulkou je to horsi

to be continued …

Editoval LeonardoCA (11. 10. 2014 14:58)

před 5 lety

LeonardoCA
Člen | 297
+
0
-

Krok 2. Oprava problemu s renderovanim formulare a funkcnosti replicatoru

1. problem, jako primary se zobrazuje jine tlacitko nez chci, upravuju podminku pro tridu tlacitka na

if ($control instanceof Controls\Button) {
    $control->getControlPrototype()->addClass(
        $control->getName() == 'submit' ? 'btn btn-primary'
            : 'btn btn-default'
    );

2. problem – po prozkoumani html kodu v prohlizeci zjistuji proc se spodni cast formulare zobrazuje podivne – tlacitka +/- nemaji spravne nastavenou css class, a vsechny polozky formulare uvnitr replikatoru maji mensi sirku nez by mely mit – to je take tim, ze jim chybi nastavit spravna class.

Zkousim si nechat vypsat pres, ktere elementy iteruje cyklus nastavujici tridy jednotlivym prvkum a zjistuji, ze $form->getControls() nevraci zadne prvky pridane pres $form->AddDynamic()

foreach ($form->getControls() as $control) {
    dump($control->getName());

Pravdepodobne proto, ze nejsou jeste pripojeny do formulare, jak to nejlepe vyresit?

Nebo je duvod jiny? jeste nevim …

Editoval LeonardoCA (22. 9. 2014 20:37)

před 5 lety

LeonardoCA
Člen | 297
+
0
-

Ano, iterovat nad prvky uvnitr containeru replikatoru nelze proto, ze replikatoru nastavujeme pouze tovarnicku a samotne containery a prvky formulare uvnitr replikatoru jsou vytvareny zavolanim tovarnicky az po pripojeni do prezenteru.

Opravit nastavovani trid se mi povedlo vyclenenim prislusneho kodu do funkce a jejim zavolanim uvnitr factory pro container replikatoru.

public function createComponentTestForm()
{
    ...
    $form->addGroup('Addresses');
    $removeEvent = callback($this, 'MyFormRemoveElementClicked');
    $users = $form->addDynamic(
        'users',
        function (Container $user) use ($removeEvent) {
            $user->addText('name', 'Name');
            $user->addText('surname', 'surname');
            $addresses = $user->addDynamic(
                'addresses',
                function (Container $address) use ($removeEvent) {
                    $address->addText('street', 'Street');
                    $address->addText('city', 'City');
                    $address->addText('zip', 'Zip');
                    $address->addCheckbox('send', 'Ship to address');
                    $removeBtn = $address->addSubmit('remove', '-')
                        ->setValidationScope(false);
                    $removeBtn->onClick[] = $removeEvent;
                    foreach ($address->getControls() as $control) {
                        $this->addBootstrapStylesToFormControl($control);
                    }
                },
                1
            );
            $addBtn = $addresses->addSubmit('add', '+')
                ->setValidationScope(false);
            $addBtn->onClick[] = callback($this, 'MyFormAddElementClicked');
            $removeBtn = $user->addSubmit('remove', '-')
                ->setValidationScope(false);
            $removeBtn->onClick[] = $removeEvent;
            foreach ($user->getControls() as $control) {
                $this->addBootstrapStylesToFormControl($control);
            }
        },
        2
    );
    $users->addSubmit('add', '+')
        ->setValidationScope(false)
        ->onClick[] = callback($this, 'MyFormAddElementClicked');

    $form->addGroup();
    $form->addSubmit('submit', 'Send');
    $form->addSubmit('cancel', 'Cancel');

    // setup form rendering
    $renderer = $form->getRenderer();
    $renderer->wrappers['controls']['container'] = null;
    $renderer->wrappers['pair']['container'] = 'div class=form-group';
    $renderer->wrappers['pair']['.error'] = 'has-error';
    $renderer->wrappers['control']['container'] = 'div class=col-sm-9';
    $renderer->wrappers['label']['container'] =
        'div class="col-sm-3 control-label"';
    $renderer->wrappers['control']['description'] = 'span class=help-block';
    $renderer->wrappers['control']['errorcontainer'] =
        'span class=help-block';

    // make form and controls compatible with Twitter Bootstrap
    $form->getElementPrototype()->class('form-horizontal');

    foreach ($form->getControls() as $control) {
        $this->addBootstrapStylesToFormControl($control);
    }

    return $form;
}



public function addBootstrapStylesToFormControl($control)
{
    if ($control instanceof Controls\Button) {
        $control->getControlPrototype()->addClass(
            $control->getName() == 'submit' ? 'btn btn-primary'
                : 'btn btn-default'
        );
    } elseif ($control instanceof Controls\TextBase
        || $control instanceof Controls\SelectBox
        || $control instanceof Controls\MultiSelectBox
    ) {
        $control->getControlPrototype()->addClass('form-control');

    } elseif ($control instanceof Controls\Checkbox
        || $control instanceof Controls\CheckboxList
        || $control instanceof Controls\RadioList
    ) {
        $control->getSeparatorPrototype()->setName('div')->addClass(
            $control->getControlPrototype()->type
        );
    }
}

coz se mi, ale vubec nelibi a stejne tak druhe reseni pripojeni k prezenteru uvnitr tovarnicky formulare takto

$this['testForm'] = $form;

if ($control instanceof Controls\Button) {
    $control->getControlPrototype()->addClass(
        $control->getName() == 'submit' ? 'btn btn-primary'
            : 'btn btn-default'
    );
} elseif ($control instanceof Controls\TextBase
    || $control instanceof Controls\SelectBox
    || $control instanceof Controls\MultiSelectBox
) {
    $control->getControlPrototype()->addClass('form-control');

} elseif ($control instanceof Controls\Checkbox
    || $control instanceof Controls\CheckboxList
    || $control instanceof Controls\RadioList
) {
    $control->getSeparatorPrototype()->setName('div')->addClass(
        $control->getControlPrototype()->type
    );
}

return $form;

No, ale stejne cilem bude vytvoreni samostatnych trid pro formular i containery replikatoru a pak by to melo jit vyresit lepe.

Na co jsem ale nejvic zvedavy je, jestli se mi nejak povede umistit lepe tlacitka +/- bez nutnosti manualniho renderovani formulare …

Editoval LeonardoCA (22. 9. 2014 21:59)

před 5 lety

LeonardoCA
Člen | 297
+
0
-

Nahodou jsem objevil, ze jiz existuje renderer, ktery vyse uvedeny problem elegantne resi https://github.com/…Renderer.php protoze nastavuje potrebne styly prvkum formulare opravdu az ve chvili, kdy se zacne renderovat…

Tim jsem mohl procistit kod formulare a pridal jsem jen tridy pro obarveni tlacitek +/-.

public function createComponentTestForm()
{
    $form = new Nette\Application\UI\Form();

    $form->addGroup('Personal data');
    $form->addText('name', 'Your name')
        ->setRequired('Enter your name');
    $form->addRadioList(
        'gender',
        'Your gender',
        array(
            'male',
            'female',
        )
    );
    $form->addCheckboxList(
        'colors',
        'Favorite colors:',
        array(
            'red',
            'green',
            'blue',
        )
    );
    $form->addSelect(
        'country',
        'Country',
        array(
            'Burundi',
            'Qumran',
            'Saint Georges Island',
        )
    );
    $form->addTextArea('note', 'Comment');

    $form->addGroup('Addresses');
    $removeEvent = callback($this, 'MyFormRemoveElementClicked');
    $users = $form->addDynamic(
        'users',
        function (Container $user) use ($removeEvent) {
            $user->addText('name', 'Name');
            $user->addText('surname', 'surname');
            $addresses = $user->addDynamic(
                'addresses',
                function (Container $address) use ($removeEvent) {
                    $address->addText('street', 'Street');
                    $address->addText('city', 'City');
                    $address->addText('zip', 'Zip');
                    $address->addCheckbox('send', 'Ship to address');
                    $removeBtn = $address->addSubmit('remove', '-')
                        ->setValidationScope(false);
                    $removeBtn->getControlPrototype()->addClass(
                        'btn-danger'
                    );
                    $removeBtn->onClick[] = $removeEvent;
                },
                1,
                true
            );
            $addBtn = $addresses->addSubmit('add', '+')
                ->setValidationScope(false);
            $addBtn->getControlPrototype()->addClass('btn-success');
            $addBtn->onClick[] = callback($this, 'MyFormAddElementClicked');
            $removeBtn = $user->addSubmit('remove', '-')
                ->setValidationScope(false);
            $removeBtn->getControlPrototype()->addClass('btn-danger');
            $removeBtn->onClick[] = $removeEvent;
        },
        1,
        true
    );
    $addBtn = $users->addSubmit('add', '+')
        ->setValidationScope(false);
    $addBtn->getControlPrototype()->addClass('btn-success');
    $addBtn->onClick[] = callback($this, 'MyFormAddElementClicked');

    $form->addGroup();
    $form->addSubmit('submit', 'Send');
    $form->addSubmit('cancel', 'Cancel');

    $form->onSuccess[] = $this->processTestForm;

    $form->setRenderer(new Bs3FormRenderer);

    return $form;
}

Bs3FormRenderer dokonce urcil spravne primary button, protoze v podmince pro jeho identifikaci ma, ze primarni tlacitko je prvni primy potomek formulare (v prikladu v examples nette se bralo prvni tlacitko, ktere v nasem pripadu bylo v containeru replikatoru)

ted uz jen si poradit s umistenim tlacitek +/-

Editoval LeonardoCA (24. 9. 2014 13:00)

před 5 lety

Michal Vyšinský
Člen | 614
+
0
-

Já teda nevím, ale nebylo by lepší pro takto specifický form manuální renderování?

před 5 lety

LeonardoCA
Člen | 297
+
0
-

manualnim renderovanim jsem to resil drive, ale rad bych to dokazal i jen pomoci rendereru

Mym dalsim cilem je aby polozky v replikatoru bylo mozne radit, pres jquery ui sortable.

A finalnim cilem je form builder typu http://www.wufoo.com/ pro nette formulare, ktery by se dal pouzit v cms pro vytvareni custom formularu, vcetne vsech pravidel

Reseni i trochu slozitejsiho html layutu s rendererem, pokud by se povedlo by se mi libilo vice, protoze s manualnim renderovanim jsem v jednom projektu skoncil s 5-ti velmi podobnymi sablonami, ktere jsou docela necitelne a musely se pro kazdy formular upravovat podle polozek…

<div n:snippet="navigationManagerForm" class="sortable-container row">
    {var $renderAsControl = 0}
    <?php $profiling = 0; if($profiling) \Panel\XDebugTrace::callStart(); ?>
    <?php \Nette\Diagnostics\Debugger::timer(); ?>
    {capture $navigationManagerForm}
        {if $renderAsControl}
            {control navigationManagerForm}
        {else}
            {form form}
            {form errors}
            <div class="bigContainer span9">
                {foreach $form['sections']->containers as $sid => $section}
                    <div class="row">
                        {if $navigationData['type']->use_sections}
                        <span class="btn pull-right"><i class="icon-move"></i></span>
                        {/if}
                        {if $navigationData['type']->use_sections}
                            <div class="span8">
                                <legend>{input sections-$sid-name}</legend>
                            </div>
                        {/if}
                        {input sections-$sid-id}
                        <div id="sections-{$sid}-menuItems"
                             class="navigation-section span8"
                             data-section-id="{$sid}">
                            {var $maxMid = 0}
                            {foreach $section['menuItems']->containers as $mid => $mitem}
                                {var $maxMid = max($mid, $maxMid)}
                                <div id='mitem-{$sid}-{$mid}' class="row" title="Drag to reorder.">
                                    <div
                                        class="span8 sortable-item controls-container controls-row">
                                        {***$form->render($form["sections-$sid-menuItems-$mid-title"])***}
                                        {if $navigationData['type']['ident'] != 'primary'}
                                            {input sections-$sid-menuItems-$mid-title}
                                            {input sections-$sid-menuItems-$mid-url}
                                        {else}
                                            {input sections-$sid-menuItems-$mid-item_id}
                                            {if $mitem->buildOptions['checkboxColumn']}
                                                {var $checkboxId = 'sections-'.$sid.'-'.$mitem->buildOptions['containerName'].'-'.$mid.'-'.$mitem->buildOptions['checkboxColumn']}
                                                <div class="span2 control-group pull-right"
                                                     style="text-align='right';">
                                                    <label class="control-label"
                                                           for="frmform-{$checkboxId}"
                                                           style="margin-left: 16px; width: auto;">Folded</label>
                                                    <div class="toggle-button controls"
                                                         style="margin-left: 16px">{input $checkboxId}</div>
                                                </div>
                                            {/if}
                                        {/if}
                                        <div
                                            class="span1">{input sections-$sid-menuItems-$mid-remove} {if $iterator->last}{input sections-$sid-menuItems-add}{/if}</div>
                                    </div>
                                </div>
                            {/foreach}
                            <span class="next-item-id" data-next-item-id="{$maxMid+1}"></span>
                        </div>
                    </div>
                {/foreach}
            </div>
            <div class="span9">{form buttons}</div>
        {/form}
        {/if}
    {/capture}
    <?php if($profiling) \Panel\XDebugTrace::callStop(); ?>
    {!$navigationManagerForm}
    {if false}
        <pre class="prettyprint linenums pre-scrollable">{$navigationManagerForm}</pre>
        <script>prettyPrint();</script>
    {/if}
    <script>
        $('.toggle-button').toggleButtons({
            width: 90,
            style: { enabled: "success", disabled: "warning"},
            label: { disabled: "No", enabled: "Yes"}
        });
        $('.select2').select2({ width: 'element' });
        $(function () {
            {if $navigationData['type']->use_sections}
            $(".bigContainer").sortable({
                handle: ".icon-move",
                forcePlaceholderSize: true,
                helper: "clone"
            });
            {/if}
            $(".navigation-section").sortable({
                forcePlaceholderSize: true,
                {if $navigationData['type']->move_between_sections}connectWith: ".navigation-section", {/if}
                receive: function (e, ui) {
                    var idSection = ui.item.parent().attr('data-section-id');
                    var nextId = ui.item.parent().find('.next-item-id').attr('data-next-item-id');
                    ui.item.find('input').each(function () {
                        $(this).attr('name', $(this).attr('name').replace(/sections\[\d\]\[menuItems\]\[\d\](?!\[remove\])/g, "sections[" + idSection + "][menuItems][" + nextId + "]"));
                    });
                    ui.item.find('[type="submit"][value="-"]').click();
                }
            });
        });
    </script>
</div>

Tohle reseni bylo funkcni, ale ted ho chci zjednodusit … (Simplicity is the ultimate sofistication.)

Byly i problemy s rychlosti renderovani ve spojeni s https://github.com/…FormRenderer a musel jsem si ho trosku upravovat.

Ale je mozne, ze to jen pomoci rendereru nepujde a pokusim se vytvorit alespon univerzalni sablonu.

Editoval LeonardoCA (24. 9. 2014 14:41)

před 5 lety

LeonardoCA
Člen | 297
+
0
-

Zatim jsem dospel k zaveru, ze pokud chci vyuzit renderer, budu muset but castecne prepsat chovani standard rendereru nebo radeji napsat uplne novy renderer.

Standartni render dela par veci, ktere jsou naprosto v poradku pro vetsinu formularu, ale neumozni mi napsat pravidla pro wrappers, ktere by dokazaly to co chci do markupu formulare pridat. Duvody proc nevyhovuje default render mym pozadavkum jsou hlavne dva:

  • renderuje vsechny tlacitka automaticky az na konci bud form nebo group (budu potrebovat byt trosku flexibilnejsi)
  • prestoze ma nejakou podporu pro container wrapper tak nefunguje, tak jak bych potreboval, protoze renderuje jednotlive prvky formulare predevsim podle groups a containery replicatoru vicemene ignoruje.

Proc se chci vyhnout pouziti vlastni template a radeji to resit rendererem? Duvody jsou tri:

  1. Reseni bez template by se mi libilo
  2. Napsat kod pro formular s vice replikatory a s do sebe zanorenymi replikatory je otazka chvilky, napsat zpracovani dat, uz je trosku vic prace, ale da se, napsat a odladit template s tim, aby podporovala jeste navic treba sortable je uz docela peklo, tak proc si to nezjedusit
  3. z predchozich zkusenosti vim, ze renderovani template u formularu, jiz s radove vice jak 50–100 prvky (coz u typickeho formulare s replikatorem neni az tak neobvykle) je uz pro php celkem celkem zaber a musel jsem zapnout profiler, hlavnim duvodem je, ze pri renderovani template se hojne vyuzivaji na processing time nejnarocnejsi php operace – callbacky a volani funkci.

Chtel bych dosahnout moznosti nadefinovat neco jako wrappers nebo „sablonu“ (nejen obalku (wrappers), ale mozna cely html markup – kvuli moznosti umistit nejak pohodlne tlacika – i kdyz to uz mozna pujde jen pres css) pro container.

A ted premyslim nad tim jak nejlepe, Container totiz nepodporuje getControlPrototype(), moznosti co me napadaji:

  1. Podedit Nette\Forms\Container a pridat funkcionalitu umoznujici pracovat s getControlPrototype() a definovat html reprezentaci containeru podobne jako pro Form Controls
  2. Rozsirit funkcionalitu wrappers rendereru o moznost pridavat vicenasobne definice pro container wrappers a pri potrebe ruznych definic wrappers pro ruzne containery to resit definici wrappers bud podle class containeru nebo nejakeho attributu

Render se musi upravit v obou pripadech, protoze potrebuju upravit princip jakym iteruje nad prvky formulare. Nejsem si jisty co bude lepsi cesta, mozna jeste uplne jinak. Ma nekdo nejaky tip?

Editoval LeonardoCA (26. 9. 2014 20:37)

před 5 lety

LeonardoCA
Člen | 297
+
0
-

formular jsem trochu upravil a prozatim pracuju s manualnim renderovanim, abych se dobral k tomu co bude potreba nadefinovat pro renderer

public function createComponentTestForm()
{
    $form = new Nette\Application\UI\Form();
    $addEventHandler = callback($this, 'handleAddItem');
    $removeEventHandler = callback($this, 'handleRemoveItem');
    $form->addGroup('Footer menu');
    $form->addDynamic(
        'sections',
        function (Container $column) use (
            $removeEventHandler, $addEventHandler
        ) {
            $column->addText('title', 'Column Title')->getControlPrototype()
                ->addClass('col-sm-5')->addAttributes(['placeholder' => 'Column Title']);
            $column->addDynamic(
                'menuItems',
                function (Container $menuItems) use ($removeEventHandler) {
                    $menuItems->addText('text', 'Text')
                        ->getControlPrototype()->addClass('col-sm-5')
                        ->addAttributes(['placeholder' => 'Text']);
                    $menuItems->addText('url', 'Url')->getControlPrototype()
                        ->addClass('col-sm-5')->addAttributes(
                        ['placeholder' => 'Url']
                    );
                    $menuItems->addSubmit('remove', '-')
                        ->setValidationScope(false)
                        ->setAttribute('class', 'btn btn-danger btn-sm')
                        ->setAttribute('data-replicator-item-remove', 'yes')
                        ->addRemoveOnClick($removeEventHandler);
                    $this->controlsInit($menuItems);
                },
                1,
                true
            )->addSubmit('add', '+')
                ->setValidationScope(false)
                ->setAttribute('class', 'btn btn-success btn-sm')
                ->addCreateOnClick(true, $addEventHandler);
            $column->addSubmit('remove', '-')
                ->setValidationScope(false)
                ->setAttribute('class', 'btn btn-sm btn-danger')
                ->setAttribute('data-replicator-item-remove', 'yes')
                ->addRemoveOnClick($removeEventHandler);
            $this->controlsInit($column);
        },
        2,
        true
    )->addSubmit('add', '+')
        ->setValidationScope(false)
        ->setAttribute('class', 'btn btn-sm btn-success')
        ->addCreateOnClick(true, $addEventHandler);
    $form->addGroup();
    $form->addSubmit('submit', 'Save');
    $form->addSubmit('cancel', 'Cancel');

    $this->controlsInit($form);
    $form->getElementPrototype()->addClass('form-horizontal');

    $form->onSuccess[] = $this->processTestForm;
    $form->setRenderer(new Bs3FormRenderer);
    return $form;
}

Krok 3 – Definovat univerzalni html markup pro containery sortable replicatoru

template se pomalu blizi stavu, ktery bude mozne popsat pomoci najakych pravidel rendereru

{form testForm}
{$form->render('errors')}
{var $replicatorHtmlId = $form['sections']->lookupPath()}
    <div id="{$replicatorHtmlId}"
         class="replicator-sortable"
         data-replicator-name="{$form['sections']->name}">
        {foreach $form['sections']->containers as $sid => $section}
            <div
                class="replicator-sortable-item"
                title="Drag to reorder."
                data-replicator-item-id="{$sid}">
                <div class="form-group">
                    <div
                        class="col-sm-5">
                        {input $section['title']}
                    </div>
                </div>
                <div
                    id="{$section['menuItems']->lookupPath()}"
                    class="replicator-sortable"
                    data-replicator-name="{$section['menuItems']->name}"
                    data-replicator-sortable-connectWith="{$replicatorHtmlId}-replicatorId-{$section['menuItems']->name}">
                    {foreach $section['menuItems']->containers as $rid => $mitem}
                        <div
                            class="replicator-sortable-item"
                            title="Drag to reorder."
                            data-replicator-item-id="{$rid}">
                            <div
                                class="form-group">
                                <div
                                    class="col-sm-5">
                                    {input $mitem['text']}
                                </div>
                                <div
                                    class="col-sm-5">
                                    {input $mitem['url']}
                                </div>
                                <div
                                    class="col-sm-2 pull-right">
                                    {input $mitem['remove']} {if $iterator->last}{input $section['menuItems']['add']}{/if}
                                </div>
                            </div>
                        </div>
                    {/foreach}
                </div>
                <div
                    class="form-group">
                    <div class="col-sm-4">
                        {input $section['remove']}
                        {if $iterator->last}{input $form['sections']['add']}{/if}
                    </div>
                </div>
            </div>
        {/foreach}
    </div>
    <div class="container-fluid">
        <div class="form-group">
            <div class="pull-right">
                {input cancel}
                {input submit}
            </div>
        </div>
    </div>
{/form}

Podstatna je tato konstrukce definujici „wrappers“ containeru replikatoru a containeru polozky replikatoru – ktere jsou potreba, pro funkcnost sortable

<div
    id="{$section['menuItems']->lookupPath()}"
    class="replicator-sortable"
    data-replicator-name="{$section['menuItems']->name}"
    data-replicator-sortable-connectWith="{$replicatorHtmlId}-replicatorId-{$section['menuItems']->name}">
    {foreach $section['menuItems']->containers as $rid => $mitem}
        <div
            class="replicator-sortable-item"
            title="Drag to reorder."
            data-replicator-item-id="{$rid}">

            /** rendering controls inside replicator **/
        </div>
    {/foreach}
</div>

v „data-replicator-“ attributech je ted vse potrebne, aby se dalo

Krok 4 – prepsat javascript tak, aby byl zavisly pouze na html markupu formulare

(tj v javascriptu nesmi byt pouzit napriklad zadny konkretni nazev inputu, apod.)

<script>
    $(function () {
        $(".replicator-sortable").sortable({
            forcePlaceholderSize: true,
            cursor: 'move',
            axis: 'y',
            helper: "clone",
            create: function (e, ui) {
                var sortableObj = $(e.target);
                if (sortableObj.attr('data-replicator-sortable-connectWith') !== undefined) {
                    sortableObj.sortable('option', {
                        connectWith: "[data-replicator-sortable-connectWith=" + sortableObj.attr('data-replicator-sortable-connectWith') + "]"
                    });
                    sortableObj.sortable({
                        receive: function (e, ui) {
                            var ids = ui.item.parent().find('.replicator-sortable-item').map(function () {
                                return $(this).attr('data-replicator-item-id');
                            }).get();
                            var nextId = Math.max.apply(Math, ids) + 1;
                            ui.item.find('input').each(function () {
                                var replicators = $(this).parents('[data-replicator-item-id]').map(function(){
                                    return {
                                        name: $(this).parent().attr('data-replicator-name'),
                                        id: $(this).attr('data-replicator-item-id')
                                    };
                                });
                                var index;
                                for (index = 0; index < replicators.length; ++index) {
                                    $(this).attr('name', $(this).attr('name').replace(
                                        new RegExp("(\\[?" + replicators[index].name + "]?)\\[\\d+](?!.*\\[remove]$)", "g"),
                                        "$1[" + (index == 0 ? nextId : replicators[index].id) + "]"
                                    ));
                                }
                            });
                            ui.item.find('[type="submit"][data-replicator-item-remove]').click();
                        }
                    });
                }
            }
        });
    });
</script>

tohle uz by se dalo jen s malou upravou dat do externiho souboru, pripadne zabalit jako plugin nette-ajax-js a pouzit na libovolny formular s replikatory vyrenderovany s markupem popsanym vyse

Vysledek

Treba formular pro editaci polozek menu ve vice sloupcich, jako byva typicky v paticce, ale i jinde.

Dvojnasobne zanoreny replikator, ktery diky jquery ui sortable umoznuje nejen razeni sloupcu (vnejsi replikator), razeni polozek menu v ramci sloupce (vnitrni replikator), ale take prohazovani polozek mezi sloupci (tj prohazovani polozek mezi replikatory)

(teoreticky by mel fungovat i s vice urovnemi zanoreni replikatoru, ale to jsem jeste nezkousel)

Editoval LeonardoCA (29. 9. 2014 20:22)

před 5 lety

LeonardoCA
Člen | 297
+
0
-

Proc tak slozite? Jak to funguje?

Princip replikatoru

Hodnoty, ktere nam formular vraci vypadaji takto

Replikator generuje replikovane containery s ciselnymi nazvy postupne od 0. Jak je ale videt z obrazku, neni podminkou aby byly serazeny ciselne za sebou, replikator data zpracuje a znovu vytvori svuj komponentovy model na zaklade dat v takovem poradi, jako prijdou data, na zaklade attributu „name“ polozek formulare input – ty jsou dane tim jak generuje jmena Form Controls Nette a ve standartni podobe muzou vypadat pro nas formular napr. takto:

sections[2]title
sections[2]menuItems[4][text]
sections[2]menuItems[4][url]
sections[1]title
sections[1]menuItems[3][text]
sections[1]menuItems[3][url]

// button remove name example:
sections[1]menuItems[3][remove]
Sortable v ramci jednoho replikatoru

Na zaklade toho jak replikator funguje staci obalit prvky nejakym html elementem do bloku, tak aby si s tim jquery ui sortable poradilo. Kdyz prehodime mezi sebou bloky v prohlizeci, zmeni se tim poradi inputu v post datech a to je vse. Po odeslani formulare replikator prevezme zmenene poradi. (razeni funkcni skoro bez prace)

Sortable na urovni vnorenych replikatoru mezi replikatory

To uz je uplne jina kava. Samotnym prehozenim poradi nejake skupiny inputu formulare, nelze prehodit inputy do jineho vnoreneho replikatoru. Aby byly zpracovany jinym vnorenym replikatorem (prestoze je to replikator na stejne urovni zanoreni, jedna se o uplne jinou komponentu a nazvy prvku v ni vytvorene maji odlisnou spolecnou cast „name“)

znovu priklad:

// first column block
sections[2]menuItems[4][text]
sections[2]menuItems[4][url]
sections[2]menuItems[3][text]
sections[2]menuItems[3][url]

// second column block
sections[1]menuItems[3][text]
sections[1]menuItems[3][url]

sections[1]menuItems[3][remove]

Chci prehodit vsechny jednu polozku z 2. slupce do 1.sloupce, tj vsechny inputy „sections[1]menuItems[3]“ do prvniho sloupce.

Co se musi stat na urovni formulare? V replikatoru pro 2. sloupec musi byt odstranena polozka a v replikatoru pro 1. sloupec musi byt ta sama polozka pridana.

ok, to vypada jednoduse, ale:

  1. zmenim jim vsem ve jmene sections[1] na sections[2]
  2. ale co s menu menuItems[3]? v prvnim sloupci uz prvky s casti jmena „menuItems[3]“ jiz jsou (tj container replikatoru se jmenem „3“ jiz existuje), co s tim? Musim dat prvkum index neexistujici polozky, jeste lepe takovy index, ktery by replikator sam polozce dal, kdyby ji sam vytvarel, tj vezmu max hodnotu indexu v 1. kontejneru + 1
  3. to by bylo, ale jak smazat blok (container) v puvodnim replikatoru – to je ta nejsnazsi cast, proste nasimulovat kliknuti na tlacitko delete v puvodnim umisteni…
  4. Temer vymysleno, jenze, co kdyz chci aby to fungovalo pro formular s libovolnymi nazvy replikatoru, libovolne zanorene a s libovolnou strukturou dat? nezbude nez pracovat s objektovou strukturou komponent na strane php a DOM na strane html…
Javascript s komentari
$(function () {
    $(".replicator-sortable").sortable({
        forcePlaceholderSize: true,
        cursor: 'move',
        axis: 'y',
        helper: "clone",

        // hlavni replikator nepotrebuje podporovat sortable mezi replikatory
        // i pro zanorene replikatory, by to mela byt volitelna moznost
        // proto dodatecne nastaveni pro sortable, az v eventu create

        create: function (e, ui) {
            var sortableObj = $(e.target);

            // pokud ma html kod reprezentujici container replikatoru nastaven atribut data-replicator-sortable-connectWith pak pridat podporu pro sortable mezi replikatory

            if (sortableObj.attr('data-replicator-sortable-connectWith') !== undefined) {
                sortableObj.sortable('option', {

                    // nastavit dodatecne html prvky kam lze container presouvat

                    connectWith: "[data-replicator-sortable-connectWith=" + sortableObj.attr('data-replicator-sortable-connectWith') + "]"
                });
                sortableObj.sortable({

                    // kdyz je blok prijat v jinem replikatoru (dropnut)
                    // zajisti pridani prvku do tohoto replikatoru
                    // a smaze blok v puvodnim replikatoru

                    receive: function (e, ui) {

                        // zjistit jiz vsechny ids jiz existujicich containeru

                        var ids = ui.item.parent().find('.replicator-sortable-item').map(function () {
                            return $(this).attr('data-replicator-item-id');
                        }).get();
                        var nextId = Math.max.apply(Math, ids) + 1;

                        // najit vsechny parent replikatory a ziskat k nim name a id
                        // tj zjisteni co odpovida sections[2] a menuItems[3]
                        // v nazvech inputu pro nase nove umisteni
                        // vlastne i puvodni, protoze script nazvy'sections' a 'menuItems' zatim nezna

                        ui.item.find('input').each(function () {
                            var replicators = $(this).parents('[data-replicator-item-id]').map(function(){
                                return {
                                    name: $(this).parent().attr('data-replicator-name'),
                                    id: $(this).attr('data-replicator-item-id')
                                };
                            });
                            var index;

                            // zmena attributu name vsech inputu v presunutem containeru

                            // na vsech urovnich zanoreni replikatoru provest nahrazeni id
                            // pro prvni replikator v seznamu
                            // - tj replikator na jehoz urovni radime
                            // nastavit predem zjistenou hodnotu nextId

                            // pro ostatni replikatory nahradit id puvodniho containeru
                            // id ktere ma aktulni container
                            // to vse pro vsechny inputy, krome inputu [remove]

                            for (index = 0; index < replicators.length; ++index) {
                                $(this).attr('name', $(this).attr('name').replace(
                                    new RegExp("(\\[?" + replicators[index].name + "]?)\\[\\d+](?!.*\\[remove]$)", "g"),
                                    "$1[" + (index == 0 ? nextId : replicators[index].id) + "]"
                                ));
                            }
                        });

                        // atribut 'name' u tlacitka remove zustal puvodni
                        // pro smazani containeru v puvodnim replikatoru proste odeslat
                        // replikator na strane php zpracuje odstraneni polozky
                        // a zaroven vytvori tu samou polozku v jinem umisteni

                        ui.item.find('[type="submit"][data-replicator-item-remove]').click();
                    }
                });
            }
        }
    });
});
Maly tip

zatim nejlepsi tester regularnich vyrazu (nejen js) na ktery jsem narazil – s paradnim zvyraznenim a vysvetlenim syntaxe http://regex101.com/#…

Editoval LeonardoCA (29. 9. 2014 20:33)

před 5 lety

akadlec
Člen | 1322
+
0
-

řek bych že to řešíš zbytečně moc složitě, včetně toho sortingu. Sorting se dá lehce a elegantně vyřešit pomocí dalšího hidden prvku např. „order“ kde budou prostě jen čísla podle kterých to seřadíš a v sort JS funkci si pouze s těmi čísly pohraješ, simply and easy.

a ad ty důvody proč ne vlastní šablona…nevim nevim, ale taky mi připadá že je hledáš formou kdo chce psa bít hůl si najde ;) Já renderuju všechny formy ručně, protože téměř u všech mám vždy něco navíc co by mě automatický renderer neudělal a navíc to zadrátovává do PHPka jeden jediný renderer. Kdyby se to definovalo až na úrovni šablony tak to by byla jiná.

před 5 lety

LeonardoCA
Člen | 297
+
0
-

Moc slozite, pokud beres v uvahu jen tento jediny priklad, tak mozna jo, ale nemam predstavu jak myslis, ze by melo fungovat, ze „v sort JS funkci si pouze s těmi čísly pohraješ, simply and easy“, ve spojeni s replikatory.

Proc ne vlastni sablona – jeste se uvidi, zatim co jsem v rychlosti zkousel, tak tenhle formular rychleji se o trosku rychleji vygeneruje se sablonou nez s rendererem, ale sablona jeste nepodporuje rekurzivni renderovani containeru a neprochazi component model, jsou tam stale jeste napevno definovane containery replikatoru pro muj pripad.

Kazdopadne chci nejak dosahnout bud definovani vlastnich pravidel pro renderer na urovni containeru a nebo vytvoreni sablon pro containery.

Já renderuju všechny formy ručně, protože téměř u všech mám vždy něco navíc co by mě automatický renderer neudělal.

s tim jsem se taky setkal a snazim se vymyslet jak to udelat abych nemusel, respektive neco se napsat musi – jednou pro celou aplikaci a nechci resit v kazdem formulari znovu ty same veci

Finalni vysledek by mel umoznit neco takoveho:

$form->addDynamic(....)
    ->setOptions([
        'sortableContainers' => true,
        'sortableAcrossContainers' => true
    ]);

hotovo

Editoval LeonardoCA (30. 9. 2014 13:13)

před 5 lety

akadlec
Člen | 1322
+
0
-

ad sortování, nevím co a jak tam přesně řešíš ale já vždy měl buď nějaké hidden pole co mě právě říkalo na jaké pozici položka má být a nebo se vycházelo z pořadí v jakém data přišly. Většina sortovacích funkcí v JS co sem vidělel/pracoval s nimi fungují na základně nějakého callbacku který se provede po dokončení sortování a v tomto callbacku stačí změnit hodnoty sortovacích elementů načtením všech a cyklickým updatem – simply and easy.

Sortování napříč kontajnery bych asi řešil už pak pomocí nějakého handle.

Ad to aspoň částečné vykreslování tak to už tu je, resp. viděl jsem to v původním bootstrap rendereru z kdyby kde jsi mohl renderovat ručně celý form a některé části nechat na rendereru.

Ale mě tam prostě pořád vadí že buď bude celý form jednotný dle daného layoutu a nebo budu muset cpát do definice formu classy které IMHO nepatří do phpka ale až do šablon.

Editoval akadlec (30. 9. 2014 15:11)

před 5 lety

LeonardoCA
Člen | 297
+
0
-

Sortování napříč kontajnery bych asi řešil už pak pomocí nějakého handle.

tohle prave tak jednoduse nejde a zakladni sortovani resim jeste jednoduseji, pouze aktivaci jquery ui sortable a nic vic netreba

s kdyby rendererem jsem prave mel drive vykonostni problemy, proto to chci zkusit jinak (jeste ho zkusim taky testnout, jestli se neco zmenilo)

Ale mě tam prostě pořád vadí že buď bude celý form jednotný dle daného layoutu a nebo budu muset cpát do definice formu classy které IMHO nepatří do phpka ale až do šablon.

nad tim prave premyslim jak to resit, chtel bych aby renderer umoznil definici sablony na urovni containeru nebo jednotlivych komponent, a nebo predani nejakemu „decoratoru“, ktery by co je treba provedl na urovni Nette\Html

před 4 lety

FJP
Člen | 121
+
0
-

Ahoj prosím o pomoc :) trošku jsem se v tomhle vlákně inspiroval, ale trošku jsem dojel na jiné věci: https://forum.nette.org/…olozky-kosik#…