Ajaxové stránkování: základy

Wed, 29 Apr 2020 21:59:33 +1100

Úpravy vlastního webu mi daly podklady pro nový článek, který se bude věnovat Ajaxovému stránkování. Tedy přesněji, jak nalepit Ajax na existující výstup. Budu předpokládat, že nějaký výpis článků z databáze už máte hotový a podívám se detailněji na úpravy, kterými musí systém projít. Složité úpravy to určitě nebudou. Článek bude směrovaný k začínajícím kodérům.

Pošlu request, dostanu výstup, vepíšu výstup: nejkratší možná cesta, nic víc. Až v závěru článku nastíním, jak by se daná funkcionalita mohla (měla) řešit lépe. Ale věřím, že pro začátek to takhle stačí.

Co musí být hotové

Abych nemusel rozebírat úplné a naprosté základy, naopak zase předpokládám, že to hlavní už je hotové. Musíme mít výpis článků z databáze. K tomu stránkování. Jak bude vypadat a jak se bude chovat, na tom nezáleží: musí prostě jen fungovat. Také bychom měli mít aspoň minimálně oddělený kontroler od šablony, abychom mohli využít podmíněné tahání jednotlivých bloků stránky.

Co musíme upravit

V prvé řadě šablonu tak, abychom měli jasně ohraničený cíl, kam se bude vpisovat obsah. Dále kontrolery. Pokud posílám Ajax request na výpis článků, nechci přece zbytečně načítat celé menu nebo novinky někde v pravém sloupci. Přidáme funkci na detekci Ajax requestu, abychom to nemuseli podmiňovat GET proměnnou. (Kterou sice stále využijeme, ale k jinému účelu.) Dále loadovací kolečko: můžeme přidat, nemusíme. Tedy úprava stylů. No a na závěr samozřejmě celá JavaScriptová obsluha.

Úprava kontrolerů je přímo úměrná složitosti systému, ale tož nemusíme to osekat úplně všechno. Někdy je lepší provést pár operací navíc, než abych podmiňoval každou akci v každém layeru systému, aby pak vznikl nějaký
takovýto kód...

Detekce Ajax requestu

function isAjax() {
	return isset($_SERVER['HTTP_X_REQUESTED_WITH']) && (strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest');
}

Příklad načtení obsahu

Jak vyřešit architekturu systému a routování Ajaxových a běžných requestů, o tom dnešní článek také není. Buď vyřešeno máte, nebo se s tím nebudete trápit. Právě takové řešení uvedu v následujícím příkladu. Jednoduchou podmínkou oddělíme volání funkcí respektive metod, které mají být vykonané. Třeba nějak takhle. Pokud bychom tuto úpravu neprovedli, samotný Ajax pak ztratí tak trochu smysl.

function loadStuff(){

	$ajax = Server::isAjax();

	loadArticles();
	loadTagsForArticles();

	createPaging();
	getPageTitle();

	if(empty($ajax)){
		loadCategoryMenu();
		loadArchiveMenu();
		loadTagCloud();

		// ...
	}

	include_once('template.phtml');

}

Šablona

V šabloně potřebujeme vyřešit reálné zobrazení. Určitě je jednodušší dostat celý kód a někam ho fláknout, než parsovat bůhvíjaké pole a někam ho pochybně generovat. Pořád se bavíme o webu náročnosti toho mého, nikoli o komplexní JavaScriptové aplikaci. Takže z šablony vyjmeme části, které se měnit nebudou. Včetně containeru, kam se vloží výsledek requestu.

<?php
	$ajax = isAjax();
?>
<?php if(empty($ajax)){ ?>

	<?php include_once('_header.phtml'); ?>




<?php } ?>

		<?php // foreach co vypise clanky ?>

		<?php // strankovani ?>

<?php if(empty($ajax)){ ?>





	<?php include_once('_footer.phtml'); ?>

<?php } ?>

Styly

Než se podíváme na nejdůležitější část kódu, JavaScript, sfoukneme ještě styly. Přidáme si loadovací kolečko tak, aby bylo vždy vycentrované ve viditelné oblasti a zároveň překrývalo celý container. Samozřejmě je potřeba, aby náš "js-ajax-main-container" byl obalen ještě jediným divem s relativní pozicí, aby se absolutní chytila správně. Pozici kolečka pak zařídí výška relativně k viewportu a position: sticky. (Fixní alternativa je fallback pro IE11.)

.hidden {display:none !important;}

.article-list {position:relative;}

.ajax-loader {
	position:absolute;
	top:0;
	left:0;
	width:100%;
	height:100%;
	z-index:20000;
}

.ajax-loader .circle {
	position:fixed;
	position:sticky;
	top:0;
	left:0;
	width:100%;
	height:100%;
	max-height:100vh;
	background:rgba(255,255,255,0.75) url(loading.gif) no-repeat 50% 50%/150px auto;
}

JavaScriptová obsluha

A to nejdůležitější na závěr. Kontroler máme upravený, načítá jen nutná data. Šablona zobrazí přesně to, co potřebujeme. Takže teď se jenom napojíme na stránkování a namísto přechodu na cílovou URL provedeme request. Ještě připomenu, že stránkování je také potřeba přepsat po requestu, protože pokud ho například vypisujeme nějak takto:

 1 ... 7 | 8 | 9 ... 12

samotné přehození active class nestačí. A opět je jednodušší vepsat celý HTML kód, než tuhle hrůzu generovat přes JS. Když už ji umíme vypsat přes PHP. Což také znamená, že po requestu musíme znovu namapovat akce na odkazy.

var hookActions = function(){
	$('.paging').find('a').on('click touch', function(){
		var url = $(this).attr('href');
		contentRequest(url, false);
		return false;
	});

	// plus vse ostatni, co se nejak napojuje na elementy ve vypisu clanku
};

hookActions();

Samotná funkce contentRequest je mega easy. Pošle request, dostane html výstup, přepíše innerHTML a konec. Ale abychom si to udělali víc cool a sexy, provedeme i změnu URL bez jejího znovunačtení. K tomu slouží metoda history.pushState. Takže pokud kliknu např. na stránku 2, stisknu F5, už na ní zůstanu. A nemusím to ohýbat přes kotvy a podmíněné volání funkce. Server prostě vrátí normální output.

var loader = $('.js-ajax-loader');
var target = $('.js-ajax-main-container');
var currentUrl = window.location.href.split('#')[0];

var contentRequest = function(url, historyMove){
	loader.removeClass('hidden');

	var requestUrl = url + (url.indexOf('?')+1 ? '&' : '?') + 'ajax=1';

	$.ajax({
		url: requestUrl
		success: function(data){
			target.html(data);

			if(!historyMove){
				$('html,body').stop(true,true).animate({ scrollTop: 0 }, 0);
				history.pushState(null, null, url);
			}

			currentUrl = url;

			hookActions();
			loader.addClass('hidden');
		},
		error: function(){
			window.location.href = url;
		}
	});
};

Jenže když už jednou šáhnu na history.pushState, měl bych vyřešit i nativní funkce prohlížeče zpět a vpřed. Pokud uživatel přijde například z Googlu na druhou, třetí stránku výpisu, párkrát si dole klikne na paging, tlačítko zpět by ho vrátilo zase na Google. Protože reálně se po stránce nepohyboval. Ale to lze vyřešit přidáním jediné události:

$(window).bind('popstate', function(e){
	var nextUrl = window.location.href.split('#')[0];

	if(nextUrl != currentUrl){
		contentRequest(window.location.href, true, true);
	}

	currentUrl = nextUrl;
});

Tady jsem měl trochu problém s odpálením popstate při změně window.location.hash. Přes hash totiž zaměřuji jednotlivé komentáře, a on se na to popstate také chytá. Proto parsování url a jedna malá podmínka: otestuji, jestli se změnila celá adresa nebo jenom kotva.

V tuto chvíli je vám také určitě jasné, k čemu slouží druhý parametr u funkce contentRequest: jakmile se hýbu v historii, nebudu znovu vyvolávat její změnu, nebo bych se z toho zacyklil.

A co se vlastně stane při vypnutém JavaScriptu? No přece to samé, co před nasazením Ajaxu. Stránka se načte přes normální request. Na závěr ještě přiložím link na jednotlivé části kódu v samostatných souborech, pokud byste měli zájem si je uložit.

Lepší řešení

JSON, samozřejmě. Pořád budu vracet celý HTML kód, ale díky poli dostanu možnost měnit třeba titulek. Nebo si do patičky zapsat čas trvání reqeustu. Pro účely vývoje. Ale to už bych nemohl článek oštítkovat jako "Základy", navíc by šlo o příliš individuální úpravy, které těžko zobecním jedním tutoriálem.

Také bychom mohli přidat rozdílné chování pro telefon: stránkování se nahradí jenom za odkaz "Následující" a funkce obsah přidá, nepřepíše. Ale to už by pro změnu bylo na nový článek. A protože druhý díl už mám nachystaný a chci v něm ukázat funkční miniaplikaci, o mobilním stránkování třeba zase jindy.

func.php
loadstuff.php
template.php
style.css
main.js

ZIP archív ke stažení