Contributte multiplier – vytvoření elementu a nereagující snippet (resp. redraw)
- MikKuba
- Člen | 83
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! :)
- MikKuba
- Člen | 83
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
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
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
@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[]?
- MikKuba
- Člen | 83
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
@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.
- MikKuba
- Člen | 83
@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.
- MikKuba
- Člen | 83
@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
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
@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
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
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
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.