Undo v JavaScriptu

Školení, která pořádám

Nedařilo se mi najít aplikaci na telefon pro počítání baseballového skóre, která by mi zcela vyhovovala. A tak jsem si zcela v duchu Not Invented Here během půl hodinky spíchnul vlastní (což byla kratší doba, než jsem předtím strávil hledáním).

Aplikaci jsem napsal v JavaScriptu a není na ní dohromady nic zajímavého. Tedy kromě jedné věci. Některé aplikace mají kromě tlačítek na přidání skóre také tlačítko na jeho odebrání. To pro případ, že se člověk uklepne. Stejně tak vedle přidání autu je jeho odebrání. A tak dále. Uživatelské rozhraní se tím příšerně komplikuje a naopak svádí k chybám, protože se někdy uklepnu a místo přidání skóre ho umylem uberu, takže pak ho musím přidat dvakrát, abych chybu napravil. Některé operace (jako změna směny po třech autech) navíc nejdou vrátit vůbec.

Ne, ne, já potřebuji jednu funkci zastrčenou někde pěkně v rohu, která bude schopná vrátit cokoliv. Prostě klasické undo. Jeho implementace v JavaScriptu je jednoduchá díky tomu, že funkce jsou first-class citizens. Jak vypadá základní myšlenka?

var undoStack = [];
var score = 0;

function addScore() {
	score++;
	undoStack.push(function () {
		score--;
	});
}

function undo() {
	if (undoStack.length) {
		undoStack.pop()();
	}
}

Díky viditelnosti proměnných můžeme funkce i snadno skládat dohromady:

var team = 'visitor';
var outs = 0;
var inning = 1;

function addOut() {
	var old = outs;
	var undoTeam = function () { };
	outs++;
	if (outs >= 3) {
		outs = 0;
		if (team == 'visitor') {
			team = 'home';
			undoTeam = function () {
				team = 'visitor';
			};
		} else {
			team = 'visitor';
			inning++;
			undoTeam = function () {
				team = 'home';
				inning--;
			};
		}
	}
	undoStack.push(function () {
		outs = old;
		undoTeam();
	});
}

Přijďte si o tomto tématu popovídat na školení JavaScript a AJAX.

Jakub Vrána, Řešení problému, 14.5.2012, diskuse: 9 (nové: 0)

Diskuse

NoxArt:

Před časem jsem implementoval něco takového, kontrolu undo/redo (https://gist.github.com/2692147). Hlavní pochybnost mám (a u zde nastíněného skriptu to asi bude přítomno také) - co pokud bude uživatel mačkat undo velmi rychle, první undo akce se bude ještě provádět když se spustí druhá. Resp. pokud v nějaké undo akci bude asynchroní volání, případně více ... teoreticky by se mohla aplikace dostat do neplatného stavu.

ikona Jakub Vrána OpenID:

Rychlé klikání naštěstí není potřeba řešit, protože JavaScript se provádí v jednom vláknu, takže dokud nedoběhne jeden kód, tak se další nespustí. Není to jen nějaké omezení prohlížečů, ale návrh jazyka.

Správnou funkci s asynchronními operacemi samozřejmě řešit musíme, obdobně jako u jakéhokoliv jiného JS kódu. Ale pro undo (případně i redo) si moc nedovedu představit, na co bych je potřeboval – předchozí stav si můžu uložit do zásobníku a nemusím ho načítat asynchronně.

NoxArt:

Aha, díky, to je pravda, tím je to v pořádku. Jelikož má redo obnovit stav a ne provést znovu akci, znovuodeslání požadavku by vlastně naopak mohl být nekorektní postup.

Herbee:

Nebylo by v takto jednoduchých případech lepší do zásobníku ukládat stav proměnných ?

Tedy něco jako:
undoStack.push( {'team':team, 'score':score, 'outs':outs, 'inning':inning} );

Je to prosté a mnohem méně náchylné k chybám (nemusím psát "undo" funkce).
Samozřejmě si dovedu představit situace, kdy to nepůjde...pak by možná šlo kombinovat oba způsoby.

ikona Jakub Vrána OpenID:

Možná by to v některých případech šlo, ale já kromě změny proměnných provádím i nějaké operace (např. přidávání políček do HTML). To by se proměnnou dalo také vyjádřit, ale univerzální Undo funkce by se tím dost zkomplikovala. Navíc by dělala zbytečně moc práce.

Ale jinak to považuji za dobrý nápad a smysluplné alternativní řešení.

ikona Patrik:

Zaujímavý problém. Ako prvé by ma asi napadlo nie moc optimálne riešenie s klonovaním celej vetvy DOM formuláru na iné miesto v dokumente:

$('form .has_undo').bind('change', function() {
        $('#my_form').clone().appendTo("#undo_container");
});

Mohol by si objasniť konštrukciu: undoStack.pop()();
Prečo tá druhá skupina zátvoriek?

Honza T.:

undoStack.pop() vrátí tu funkci, které provede krok zpět. Ty další závorky tu funkci pak rovnou spustí. Šlo by to rozepsat třeba jako var fnc = undoStack.pop(); fnc();

ikona Patrik:

Jasné, vďaka za objasnenie.
Skúsil som trocha rozviesť ten nápad s klonovaním celého formulára, no trocha sa to skomplikovalo a v konečnom dôsledku to nie je také minimalistické ako sa zdalo: http://www.kvalitne.sk/sample/js_undo_test/ Skúsil som pridať aj redo funkciu. Nie je to skvost no na pamäťovo nenáročné formuláre to môže fungovať.

myf:

S použitím něčeho takového jsem kdysi zkusil implementovat cosi jako „stromovou“ historii, ve které redo neruší řetěz stavů ale vytváří paralelní větev. Technicky se tam sice cloneNode nepoužívá (ke své ostudě jsem to tehdy nezal) ale princip (ukládat "snapshot" worksetu přímo v DOMu) je stejný.

http://eldar.cz/myf/lab/multilife/v0.3/multilife.html

Vložit komentář

Používejte diakritiku. Vstup se chápe jako čistý text, ale URL budou převedeny na odkazy a PHP kód uzavřený do <?php ?> bude zvýrazněn. Pokud máte dotaz, který nesouvisí s článkem, zkuste raději diskusi o PHP, zde se odpovědi pravděpodobně nedočkáte.

Jméno: URL:

avatar © 2005-2018 Jakub Vrána. Publikované texty můžete přetiskovat pouze se svolením autora. Ukázky kódu smíte používat s uvedením autora a URL tohoto webu bez dalších omezení Creative Commons. Můžeme si tykat. Skripty předpokládají nastavení: magic_quotes_gpc=Off, magic_quotes_runtime=Off, error_reporting=E_ALL & ~E_NOTICE a očekávají předchozí zavolání mysql_set_charset. Skripty by měly být funkční v PHP >= 4.3 a PHP >= 5.0.