Contributte multiplier – vytvoření elementu a nereagující snippet (resp. redraw)

MikKuba
Člen | 83
+
0
-

Ahoj,
Možná to bude nějaká maličkost, ale už nějakou dobu bojuju s tím, že můj funkční multiplier umí správně přidávat nové multipliery (otázky) anebo i vnořené multipliery (odpovědi). To vše ale bez ajaxu a tedy s nepěkným refreshem formuláře (data se neztratí, ale refresh stránky je na nic).
Když jsem přidal logiku pro ajax, tak se sám o sobě formulář nepřekreslí (resp. se nepřidá nová položka v multiplieru) viditelně, jen na pozadí cosi dobře proběhne. Proběhne to, že se odešle dle Networks request a v odpovědi je celá HTML stránka, v níž už je správně přidaná nová otázka / odpověď. Na webu ale navenek se nic nezmění.

layout.latte

<body>
  {include #content}
<script type="text/javascript" src="https://code.jquery.com/jquery-2.2.4.min.js"></script>
<script type="text/javascript" src="https://code.jquery.com/ui/1.12.0/jquery-ui.min.js"></script>
<script src="{$basePath}/js/netteForms.js"></script>
<script src="{$basePath}/js/nette.ajax.js"></script>
<script>
                        $(function () {
                        $.nette.init();
                    });
</script>

Potom šablona napojená na presenter:

{extends form.latte}
{block title}Upravit test{/block}
{block form}

	{control form}
{/block}

Jako komponenta se volá FormFactory ve které se vytváří formulář s multiplierem:

protected function createComponentForm(){
  $form = new Form;
  $questions = $form->addMultiplier('questions', function (\Nette\Forms\Container $container, Form $form) {
                                                $container->addText('question', 'Otázka')
                                                        ->setAttribute('class', 'form-control');

                                                $answers = $container->addMultiplier('answers', function (\Nette\Forms\Container $container, Form $form) {
                                                        $container->addText('answer', 'Odpověď')->setAttribute('class', 'form-control');
                                                }, 1, 10);
                                                $answers->addRemoveButton('X', function (Nette\Forms\Controls\SubmitButton $submitter) {
                                                                $submitter->setValidationScope(false);
                                                });
                                                $answers->addCreateButton('+ Přidat odpověď');

                                            }, 1, 10);
                        $questions->addRemoveButton('Smazat otázku', function (Nette\Forms\Controls\SubmitButton $submitter) {
                                        $submitter->setValidationScope(false);
                        });
                        $questions->addCreateButton('+ Přidat otázku',1, function (Nette\Forms\Controls\SubmitButton $submitter) {
                                                        $submitter->onAnchor[] = function () {
                                                                $this->redrawControl('wrapper');
                                                                $this->redrawControl('questions');

                                                        };
                                                        $submitter->setHtmlAttribute('class', 'ajax');

                                                        });
}

A potom ještě šablona, ve které se vykresluje form:

{block content}
 {snippetArea wrapper}
  {form form class => 'form-horizontal'}
  //...
  {snippet questions}
        <div n:multiplier="questions" class="form-group questions">

                {input id }
                <div class="col-sm-3 control-label">{label question /}</div>
                <div class="col-sm-9">{input question }</div>
                <div n:multiplier="answers" class="form-group answers">
                        <div class="col-sm-3 control-label">{label answer /}</div>
                        <div class="col-sm-7 answer">{input answer }</div>
                                {btnRemove answers, class => "btn btn-xs btn-danger"}
                </div>

                <div style="clear: both;"></div>
                <div class="form-group">
                        <div class="col-sm-6">
                                {btnCreate answers, class => "btn btn-xs btn btn-secondary ajax"}
                        </div>
                </div>

                {btnRemove questions, class => "btn btn-xs btn-danger"}
        </div>
        {/snippet}
	{btnCreate questions, class => "btn btn-xs btn btn-secondary ajax"}

Testuji to zatím hlavně na tom {btnCreate questions}. Ale jak jsem psal na začátku, podle response se mi vrátí celá HTML stránka, v níž už ale ten formulář má přidanou danou část multiplieru, ale snippet se mi neaktualizuje.

Předem díky! :)

Martk
Člen | 661
+
0
-

Vypadá to, že máš starší verzi multiplieru. SubmitButton má event $submitter->onAnchor[]? Neviděl jsem ho tam nikdy, vždy jsem používal $submitter->onClick[]

MikKuba
Člen | 83
+
0
-

Martk napsal(a):

Vypadá to, že máš starší verzi multiplieru. SubmitButton má event $submitter->onAnchor[]? Neviděl jsem ho tam nikdy, vždy jsem používal $submitter->onClick[]

@Martk
To onAnchor[] jsem tam zapomněl, našel jsem to v jiném příspěvku ohledně multiplieru. Ale ani s onClick[] to nefungovalo a nefunguje.
Multiplier mám `contributte/forms-multiplier: dev-master #f2cf, Nette 3.0.5 a na PHP 7.4.6

Z nějakého důvodu pořád podezřívám nefunkčnost snippetu, resp. Redrawu. V odpovědi serveru po kliku na ten createButton se mi vrátí celá HTML stránka a je v ní i to nově přidané pole správně, ale web se mi nijak nerefreshne.
Ani do toho snippetu, který je třeba jen nad multiplierem, se mi nenačte ta celá HTML odpověd (to by se samozřejmě form rozpadl jak nemá, ale proběhl by tam očividně ten redraw, což se nejspíš neděje)

Editoval MikKuba (21. 10. 2020 15:22)

Martk
Člen | 661
+
0
-

V tom případě v addCreateButton neexistuje 3 argument, musíš použít toto:

$multiplier->addCreateButton('Add')
			->addOnCreateCallback(function (Submitter $submitter) {
				$submitter->onClick[] = function (): void {
					$this->redrawControl(...);
				};
			})
			->addClass('ajax');

Editoval Martk (21. 10. 2020 16:51)

MikKuba
Člen | 83
+
0
-

Martk napsal(a):

V tom případě v addCreateButton neexistuje 3 argument, musíš použít toto:

$multiplier->addCreateButton('Add')
			->addOnCreateCallback(function (Submitter $submitter) {
				$submitter->onClick[] = function (): void {
					$this->redrawControl(...);
				};
			})
			->addClass('ajax');

Ach, tohle jsem přehlídl, že to umí jen dva parametry. Za to řešení addOnCreateCallback() moc díky, funguje!

MikKuba
Člen | 83
+
0
-

@Martk
Ještě možná jedna věc, v třídě RemoveButton je callback addOnCreateCallback, který mi ale pro mazání nefunguje. Resp. stejný problém jako předtím, že datově se v response vrátí umazaná otázka, ale neproběhne ten redraw.
Nechybí tam spíš místo onCreate[] něco jako onDelete[]?

Martk
Člen | 661
+
0
-

Tam se to použije stejně jako v addCreateButton

$multiplier->addRemoveButton('Remove')
			->addOnCreateCallback(function (SubmitButton $submitter) {
				$submitter->onClick[] = function (): void {
					$this->redrawControl(...);
				};
			})

Editoval Martk (21. 10. 2020 18:43)

MikKuba
Člen | 83
+
0
-

Martk napsal(a):

Tam se to použije stejně jako v addCreateButton

$multiplier->addRemoveButton('Remove')
			->addOnCreateCallback(function (SubmitButton $submitter) {
				$submitter->onClick[] = function (): void {
					$this->redrawControl(...);
				};
			})

@Martk Mám právě nastaveno stejně jako create, jen pro remove, ale po kliknutí mi zase nefunguje ten redraw, odpověď serveru je správně:

$answers->addRemoveButton('X')
        ->addOnCreateCallback(function (\Nette\Forms\Controls\SubmitButton $submitter) {
				$submitter->onClick[] = function (): void {
					$this->redrawControl('wrapper');
                    $this->redrawControl('questions');
				};
});

A v šabloně:

{snippetArea wrapper}
{form form class => 'form-horizontal'}
....
{snippet questions}
        <div n:multiplier="questions" class="form-group questions">

                {input id }
                <div class="col-sm-3 control-label">{label question /}</div>
                <div class="col-sm-9">{input question }</div>
                <div n:multiplier="answers" class="form-group answers">
                        <div class="col-sm-3 control-label">{label answer /}</div>
                        <div class="col-sm-7 answer">{input answer }</div>
                        <div class="col-sm-3 right">{input right }</div>
                                {btnRemove answers, class => "btn btn-xs btn-danger ajax"}
                </div>

                <div style="clear: both;"></div>
                <div class="form-group">
                        <div class="col-sm-6">
                                {btnCreate answers, class => "btn btn-xs btn btn-secondary ajax"}
                        </div>
                </div>
                <div class="form-group question-settings">
                        <div class="col-sm-3 control-label">{label points /}</div>
                        <div class="col-sm-6">{input points }</div>
                </div>
                <div class="form-group question-settings">
                        <div class="col-sm-9 checkbox-inline">{input required }</div>
                </div>
                <div style="clear: both;"></div>
                {btnRemove questions, class => "btn btn-xs btn-danger ajax"}
        </div>
{/snippet}
....
{/form}
{/snippetArea}
MikKuba
Člen | 83
+
0
-

@Martk
Navíc ta metoda $questions->addRemoveButton('Smazat otázku')->addOnCreateCallback() se skutečně nespustí, i když z ní volám neexistující funkci, nic se nestane. Když stejnou neexistující dám do $questions->addCreateButton('+ Přidat otázku')->addOnCreateCallback() tak to normálně vyhodí chybu, že volám neexistující funkci.

S tímto jsem teď trochu v koncích, protože zjišťuju že při tom mazacím callbacku potřebuju i danou položku smazat z DB pokud tam je, což teď nemůžu, protože se mi na to nezavolá funkce.

Martk
Člen | 661
+
0
-

Toto by mělo být opraveno v tomto fixu: https://github.com/…8b3c7f1540a0

MikKuba
Člen | 83
+
0
-

@Martk
Zpětně děkuju za ten fix, pomohlo to :)

Po nějakém čase jsem narazil na další oříšek a nevím, jak s ním naložit.

Formulář, v němž kromě několika polí je i multiplier, mám obalenný snippetem, abych při vytvoření/smazání nějakého pole to měl ihned obnovené.
Do textového pole uvnitř multiplieru jsem nastavil načítání mceEditoru (Tiny 5.0.6), mám jej stažený, nenačítám skrz CDN.
Mám trochu problém s rychlostí načítání. V momentě, kdy mám v multiplieru vytvořených třeba už 5 kontejnerů, každý v jednom svém textovém poli obsahuje TinyMce, který se na to pole vlastně volá při každém ajax refreshi a už to zabírá viditelné množství času == neproběhne nějaký hladký refresh a přidání nového kontejneru v rámci třeba sekundy, ale třeba to trvá už 4 sekundy.
Prvně jsem neměl žádnou preloader animaci, protože vše běželo rychle. Ale jakmile toto trvá 4 sekundy, uživatel si může myslet, te kliknul špatně a zavolá znovu createButton a tím to ještě více zaseká. Proto jsem během ajaxu přidal zobrazení loading kolečka a na web nejde dočasně klikat. Ale to neřeší problém, že při každém kliknutí na addCreateButton překresluji snippet s multipliery a tím překreslením na ně znovu volám inicializaci tiny.

Není na to nějaká šikovná fíčurka prosím?

{snippetArea wrapper}
{form form class => 'form-horizontal', autocomplete=>"off"}

        {snippet questions}
                {var $counter = 0}
                <div n:multiplier="questions" class="form-group questions">
                        {input id }
                        <div class="col-sm-6 float-right mb-2 select-question-type">{input type id => 'type-'. $counter, onchange => "changeType(this)"}</div>
                        <div class="col-sm-6 control-label">{label question /}</div>
                        <div class="col-sm-12 mt-4">{input question class => "form-control mceEditorSmall"}</div>
                        <div class="form-group question-settings">
                                <div class="col-sm-3 control-label">{label points /}</div>
                                <div class="col-sm-6">{input points }</div>
                        </div>
                        <div class="form-group question-settings">
                                <div class="col-sm-9 checkbox-inline">{input required }</div>
                        </div>
                        <div style="clear: both;"></div>
                        {btnRemove questions, class => "btn btn-xs btn-danger ajax"}

                        <script n:syntax="double" defer>
                                $('#preloader').show("fast", function(){
                                        if (typeof checkType === "function") {
                                                checkType(document.getElementById('type-' + {{$counter}}));
                                        }
                                        if (typeof initMCE === "function") {
                                                initMCE();
                                        }
                                        $('#preloader').delay(100).fadeOut('slow', function() {
                                          $(this).hide();
                                        });
                                });

                        </script>
                        {var $counter = $counter +1}
                </div>
        {/snippet}
	{btnCreate questions, class => "btn btn-xs btn btn-info ajax"}
{/form}
{/snippetArea}

No a to JSko s tiny.init() je dole v šabloně:

<script defer>
                        function initMCE(){
                                tinymce.remove();
                                tinyMCE.init({
                                        selector: ".mceEditorSmall",
                                        height: 150,
                                        plugins: '',
                                        toolbar1: 'bold superscript subscript',
                                        menubar: "",
                                        entity_encoding: "raw",
                                        setup: function (editor) {
                                                editor.on('change blur', function () {
                                                    tinymce.triggerSave();
                                                });
                                        }
                                });

                                tinyMCE.init({
                                        mode: "specific_textareas",
                                        editor_selector: "mceEditor",
                                        height: 350,
                                        plugins: 'media autolink directionality visualblocks visualchars image link table charmap hr anchor toc insertdatetime advlist lists textcolor wordcount imagetools contextmenu colorpicker textpattern help',
                                        toolbar1: 'media | formatselect | bold underline italic strikethrough forecolor backcolor | link | alignleft aligncenter alignright alignjustify  | numlist bullist outdent indent  | removeformat ',
                                        menubar: "",
                                        entity_encoding: "raw"
                                });
                        }
                </script>

Ten .mceEditor se používá jen na jiný textový blok mimo multiplier, na začátku formu.
Reset pomocí tinymce.remove(); tam je kvůli tomu, že bez toho se mi provede ajax poměrně rychle, ale ten tinyEditor se aplikuje jen na posledně přidaný multiplier a jeho textArea a všechny předešlé containery zůstanou jen s obyčejnou textArea bez editoru.

Martk
Člen | 661
+
0
-

Na tohle bych už použil spíše něco jako Vue, menší alternativa reactu je preactjs. Ale dá se to ještě hacknout tak, že si snippet budeš překreslovat vlastní cestou a staré kontejnery si tam necháš s instancí tinymce.

MikKuba
Člen | 83
+
0
-

@Martk
Asi bych chtěl mít víc pod kontrolou překreslování snippetu, protože když jsem si přidal do kontejneru ještě upload pro obrázek, vložím obrázek a v momentě kliknutí na addCreateButton se mi při redrawu ten zvolený soubor z addUpload smaže.
Nepodařilo se mi vygooglovat ale něco, z čeho bych pochopil jakým způsobem můžu ručně ovlivnit to, co se ve snippetu překreslí. Protože hádám že v multiplieru není nějaká možnost že addCreateButton provede jen přidání a ty předtím vytvořené kontejnery nechá bez manipulace?

Martk
Člen | 661
+
0
-

Třeba takto:

<div n:multiplier="questions" class="form-group questions" data-container="{$_multiplier->getName()}" data-replaceable=".replaceable">
                        {input id }
                        <div class="col-sm-6 float-right mb-2 select-question-type">{input type id => 'type-'. $counter, onchange => "changeType(this)"}</div>
                        <div class="col-sm-6 control-label">{label question /}</div>
                        <div class="col-sm-12 mt-4">{input question class => "form-control mceEditorSmall"}</div>
                        <div class="form-group question-settings">
                                <div class="col-sm-3 control-label">{label points /}</div>
                                <div class="col-sm-6">{input points }</div>
                        </div>
                        <div class="form-group question-settings">
                                <div class="col-sm-9 checkbox-inline">{input required }</div>
                        </div>
                        <div style="clear: both;"></div>
                        <div class="replaceable">{btnRemove questions, class => "btn btn-xs btn-danger ajax"}</div>

                        <script n:syntax="double" defer>
                                $('#preloader').show("fast", function(){
                                        if (typeof checkType === "function") {
                                                checkType(document.getElementById('type-' + {{$counter}}));
                                        }
                                        if (typeof initMCE === "function") {
                                                initMCE();
                                        }
                                        $('#preloader').delay(100).fadeOut('slow', function() {
                                          $(this).hide();
                                        });
                                });

                        </script>
                        {var $counter = $counter +1}
                </div>
const naja = require('naja');

function createElementFromTemplate(content) {
	const template = document.createElement('template');
	template.innerHTML = content;

	return template.content;
}

naja.snippetHandler.addEventListener('beforeUpdate', (event) => {
	if (event.detail.snippet.id !== 'snippet--questions') {
		return;
	}
	const target = event.detail.snippet;

	const replaceableClass = target.dataset.replaceable;

	const containers = {};
	target.querySelectorAll('[data-container]').forEach((container) => {
		containers[container.dataset.container] = container;
	});

	const source = createElementFromTemplate(event.detail.content);
	source.querySelectorAll('[data-container]').forEach((container) => {
		const id = container.dataset.container;

		if (!containers.hasOwnProperty(id)) {
			// new element
			target.appendChild(container);
		} else {
			// old element - replace replaceable
			const original = containers[id].querySelectorAll(replaceableClass);
			const actual = container.querySelectorAll(replaceableClass);

			if (original.length !== actual.length) {
				throw new Error('Something gone wrong...');
			}

			original.forEach((el, index) => {
				el.replaceWith(actual[index]);
			});

			delete containers[id];
		}
	});

	// remove containers
	const values = Object.values(containers);
	if (values.length) {
		values.forEach(container => {
			container.remove();
		});
	}

	event.preventDefault();
});

document.addEventListener('DOMContentLoaded', () => naja.initialize());

V komentáři // new element je nově přidaný element, tam můžeš inicializovat tinymce

Editoval Martk (12. 11. 2020 16:15)

MikKuba
Člen | 83
+
0
-

@Martk
Wow, to je super. Zkusil jsem, nasadil ten replaceable class na všechny btnCreate nebo remove a přidal ten skript s Najou dolů do šablony (importuji přímo naja.js soubor, nemám takhle žádný JS modul ve kterým by to šlo importnout – takže ten řádek s require(‚naja‘) jsem vyhodil).

Potom jsem zjistil že to ID snippetu s question se jmenuje snippet-form-questions, refresh už probíhá svižně, jen teda se mi tam nenačte ten tinyMce :/ Tam mi to hlásí Uncaught Error: Node cannot be null or undefined, což mi přijde, že se mi zavolá ta funkce ale nemá k sobě načtený všechny potřebný JS.

Takhle to mám na konci šablony:

<script>
                        function createElementFromTemplate(content) {
                            const template = document.createElement('template');
                            template.innerHTML = content;

                            return template.content;
                        }

                        naja.snippetHandler.addEventListener('beforeUpdate', (event) => {
                            if (event.detail.snippet.id !== 'snippet-form-questions') {
                                return;
                            }

                            const target = event.detail.snippet;

                            const replaceableClass = target.dataset.replaceable;

                            const containers = {};
                            target.querySelectorAll('[data-container]').forEach((container) => {
                                containers[container.dataset.container] = container;
                            });

                            const source = createElementFromTemplate(event.detail.content);
                            source.querySelectorAll('[data-container]').forEach((container) => {
                                const id = container.dataset.container;

                                if (!containers.hasOwnProperty(id)) {
                                    // new element
                                    initMCE();
                                    target.appendChild(container);
                                } else {
                                    // old element - replace replaceable
                                    const original = containers[id].querySelectorAll(replaceableClass);
                                    const actual = container.querySelectorAll(replaceableClass);

                                    if (original.length !== actual.length) {
                                        throw new Error('Something gone wrong...');
                                    }

                                    original.forEach((el, index) => {
                                        el.replaceWith(actual[index]);
                                    });

                                    delete containers[id];
                                }
                            });

                            // remove containers
                            const values = Object.values(containers);
                            if (values.length) {
                                values.forEach(container => {
                                    container.remove();
                                });
                            }

                            event.preventDefault();
                        });

                        document.addEventListener('DOMContentLoaded', () => naja.initialize());

                </script>

Nad tím je <script> s initem toho tinyMCE a někde víš pak import toho JS pro tiny.

EDIT: Tak jsem do té části //new element hodil místo volání funkce samotný ten blok s inicializací toho „mini“ editoru a už se to načte, jen teda pro posledně přidaný container, a všechny předešlé ho tím ztratí :/

Editoval MikKuba (12. 11. 2020 21:01)

MikKuba
Člen | 83
+
0
-

Tak už problém snad vyřešen. Trochu to zlobilo, tak jsem nakonec ulehčil layoutu a zatím konkrétně soubor naja.js a tinymce.js přesunul přímo do šablony s formulářem a pod tyto dvě volání dávám to zpracování s Najou.
Docela mě překvapuje, že když mám tyto scripty uvnitř multiplieru, tak se vypisují do stránky Xkrát, ale i tak to jede jako blesk :)

Ještě bych možná potřeboval poradit, jak řešit addUpload uvnitř multiplieru? Protože když vyberu obrázek, tak je standardně vypsaný ve formě cesty vedle toho „vybrat soubor“ tlačítku. Jenže jakmile provedu jakýkoliv refresh snippetu (přidání/odebrání containeru) tak se tato cesta vynuluje a tedy při následném odeslání nemám ten soubor vybraný :/

Martk
Člen | 661
+
0
-

Ty scripty uvnitř multiplieru jsem tam nechtěně nechal, ty by tam být neměly… máš tam jen loader a ten se dá navěsit na naja události (https://naja.js.org/#…)

addUpload je stejný problém jako u tinyMce, tudíž stejné možnosti řešení

Klidně ten script můžeš upravit z

if (event.detail.snippet.id !== 'snippet--questions') {
{snippet questions}

na

if (event.detail.snippet.classList.has('js-multiplier')) {
<div n:snippet="questions" class="js-multiplier">

a pak to můžeš použít na kterýkoliv multiplier.

MikKuba
Člen | 83
+
0
-

Ještě tady budu s tímto chvíli otravovat. Naja i obecně práce se všemi JS callbacky na frontu je pro mě komplikovaná.

Nicméně, na vyřešení problému s addUpload jsem nepřišel a když jsem si trochu hrál s tou Najou, zjistil jsem že mi ten blok s Najou nechybí, stačí když jen před tím voláním initMce() mám importovaný tinymce.js, s tím ten refresh je rychlý a editory se aplikují.

V Naje jsem kdekoliv zkusil vypsat do console nějaké proměnné, ale nijak to nereagovalo.

Takže mám klidně jen:

{snippet questions}
 <div n:multiplier="questions" class="form-group questions js-multiplier" data-container="{$_multiplier->getName()}" data-replaceable=".replaceable">
   <div class="col-sm-6 control-label">{label question /}</div>
   <div class="col-sm-12 mt-4">{input question class => "form-control mceEditorSmall"}</div>
   <div class="form-group">
     <div class="col-sm-9 control-label">{label image /}</div>
     <div class="col-sm-9">{input image class => "question-upload-image"}</div>
   </div>
   <script src="{$basePath}/tinymce/tinymce.min.js"></script>
   <script n:syntax="double"  async>
      $('#preloader').show("fast", function(){
        if (typeof checkType === "function") {
          checkType(document.getElementById('type-' + {{$counter}}));
        }
        if (typeof initMCE === "function") {
          initMCE();
        }
        function createElementFromTemplate(content) {
          const template = document.createElement('template');
        template.innerHTML = content;

        return template.content;
     }
     //vynechany naja.eventListener(beforeupdate)

     document.addEventListener('DOMContentLoaded', () => naja.initialize());
     $('#preloader').delay(100).fadeOut('slow', function() {
       $(this).hide();
     });
   });
  </script>
 </div>
{/snippet}
{btnCreate questions}

A funguje to ajaxově dobře, načtení i při vyšším počtu containerů je v rozumně rychlé odezvě a krátkým refreshem.
Jen teda když cokoliv zvolím do addUpload (input image) tak to po jakémkoliv ajaxu zmizí, skrz tu Naju jsem nedokázal to nijak ovládnout.

Martk
Člen | 661
+
0
-

@MikKuba ten <script> s naja by neměl být v tom snippetu, ale úplně mimo. Klidně v patičce hned. Myslím si, že naja.eventListener beforeUpdate se vůbec nespustí, když se překreslí i upload