Δυναμικά αποσπάσματα

Αρκετά συχνά κατά την ανάπτυξη εφαρμογών υπάρχει η ανάγκη εκτέλεσης λειτουργιών AJAX, για παράδειγμα, σε μεμονωμένες γραμμές ενός πίνακα ή σε στοιχεία λίστας. Ως παράδειγμα, μπορούμε να επιλέξουμε να απαριθμήσουμε άρθρα, επιτρέποντας στον συνδεδεμένο χρήστη να επιλέξει μια βαθμολογία “like/dislike” για καθένα από αυτά. Ο κώδικας του παρουσιαστή και του αντίστοιχου προτύπου χωρίς AJAX θα μοιάζει κάπως έτσι (παραθέτω τα πιο σημαντικά αποσπάσματα, ο κώδικας προϋποθέτει την ύπαρξη μιας υπηρεσίας για τη σήμανση των αξιολογήσεων και τη λήψη μιας συλλογής άρθρων – η συγκεκριμένη υλοποίηση δεν είναι σημαντική για τους σκοπούς αυτού του σεμιναρίου):

public function handleLike(int $articleId): void
{
	$this->ratingService->saveLike($articleId, $this->user->id);
	$this->redirect('this');
}

public function handleUnlike(int $articleId): void
{
	$this->ratingService->removeLike($articleId, $this->user->id);
	$this->redirect('this');
}

Template:

<article n:foreach="$articles as $article">
	<h2>{$article->title}</h2>
	<div class="content">{$article->content}</div>
	{if !$article->liked}
		<a n:href="like! $article->id" class=ajax>I like it</a>
	{else}
		<a n:href="unlike! $article->id" class=ajax>I don't like it anymore</a>
	{/if}
</article>

Ajaxization

Ας φέρουμε τώρα το AJAX σε αυτή την απλή εφαρμογή. Η αλλαγή της βαθμολογίας ενός άρθρου δεν είναι αρκετά σημαντική ώστε να απαιτεί ένα αίτημα HTTP με ανακατεύθυνση, οπότε ιδανικά θα πρέπει να γίνεται με AJAX στο παρασκήνιο. Θα χρησιμοποιήσουμε το σενάριο χειρισμού από τα πρόσθετα με τη συνήθη σύμβαση ότι οι σύνδεσμοι AJAX έχουν την κλάση CSS ajax.

Ωστόσο, πώς να το κάνουμε συγκεκριμένα; Η Nette προσφέρει 2 τρόπους: τον τρόπο με τα δυναμικά αποσπάσματα και τον τρόπο με τα συστατικά. Και οι δύο έχουν τα πλεονεκτήματα και τα μειονεκτήματά τους, γι' αυτό θα τους παρουσιάσουμε έναν προς έναν.

Ο τρόπος των δυναμικών αποσπασμάτων

Στην ορολογία Latte, ένα δυναμικό απόσπασμα είναι μια ειδική περίπτωση χρήσης της ετικέτας {snippet} όπου μια μεταβλητή χρησιμοποιείται στο όνομα του αποσπάσματος. Ένα τέτοιο snippet δεν μπορεί να βρεθεί οπουδήποτε στο πρότυπο – πρέπει να είναι τυλιγμένο από ένα στατικό snippet, δηλαδή ένα κανονικό, ή μέσα σε ένα {snippetArea}. Θα μπορούσαμε να τροποποιήσουμε το πρότυπό μας ως εξής.

{snippet articlesContainer}
	<article n:foreach="$articles as $article">
		<h2>{$article->title}</h2>
		<div class="content">{$article->content}</div>
		{snippet article-{$article->id}}
			{if !$article->liked}
				<a n:href="like! $article->id" class=ajax>I like it</a>
			{else}
				<a n:href="unlike! $article->id" class=ajax>I don't like it anymore</a>
			{/if}
		{/snippet}
	</article>
{/snippet}

Κάθε άρθρο ορίζει τώρα ένα μόνο snippet, το οποίο έχει ένα αναγνωριστικό άρθρου στον τίτλο. Όλα αυτά τα αποσπάσματα τυλίγονται στη συνέχεια σε ένα ενιαίο απόσπασμα που ονομάζεται articlesContainer. Εάν παραλείψουμε αυτό το απόσπασμα περιτύλιξης, το Latte θα μας ειδοποιήσει με μια εξαίρεση.

Το μόνο που απομένει να κάνουμε είναι να προσθέσουμε ανασχεδίαση στον παρουσιαστή – απλά ανασχεδιάζουμε το στατικό περιτύλιγμα.

public function handleLike(int $articleId): void
{
	$this->ratingService->saveLike($articleId, $this->user->id);
	if ($this->isAjax()) {
		$this->redrawControl('articlesContainer');
		// $this->redrawControl('article-' . $articleId); -- δεν χρειάζεται
	} else {
		$this->redirect('this');
	}
}

Τροποποιήστε την αδελφή μέθοδο handleUnlike() με τον ίδιο τρόπο, και η AJAX είναι έτοιμη και λειτουργεί!

Ωστόσο, η λύση έχει ένα μειονέκτημα. Αν ερευνήσουμε περισσότερο τον τρόπο με τον οποίο λειτουργεί η αίτηση AJAX, θα διαπιστώσουμε ότι παρόλο που η εφαρμογή φαίνεται αποτελεσματική εμφανισιακά (επιστρέφει μόνο ένα απόσπασμα για ένα συγκεκριμένο άρθρο), στην πραγματικότητα απεικονίζει όλα τα αποσπάσματα στον διακομιστή. Έχει τοποθετήσει το επιθυμητό απόσπασμα στο ωφέλιμο φορτίο μας και έχει απορρίψει τα υπόλοιπα (έτσι, εντελώς περιττά, τα έχει ανακτήσει και από τη βάση δεδομένων).

Για να βελτιστοποιήσουμε αυτή τη διαδικασία, θα πρέπει να προβούμε σε ενέργειες όπου θα περνάμε τη συλλογή $articles στο πρότυπο (π.χ. στη μέθοδο renderDefault() ). Θα εκμεταλλευτούμε το γεγονός ότι η επεξεργασία του σήματος πραγματοποιείται πριν από την render<Something> μεθόδους:

public function handleLike(int $articleId): void
{
	// ...
	if ($this->isAjax()) {
		// ...
		$this->template->articles = [
			$this->connection->table('articles')->get($articleId),
		];
	} else {
		// ...
}

public function renderDefault(): void
{
	if (!isset($this->template->articles)) {
		$this->template->articles = $this->connection->table('articles');
	}
}

Τώρα, όταν το σήμα επεξεργάζεται, αντί για μια συλλογή με όλα τα άρθρα, μόνο ένας πίνακας με ένα μόνο άρθρο περνάει στο πρότυπο – αυτό που θέλουμε να αποδώσουμε και να στείλουμε ως payload στο πρόγραμμα περιήγησης. Έτσι, το {foreach} θα γίνει μόνο μία φορά και δεν θα αποδοθούν επιπλέον αποσπάσματα.

Τρόπος συστατικού

Μια εντελώς διαφορετική λύση χρησιμοποιεί μια διαφορετική προσέγγιση για την αποφυγή δυναμικών αποσπασμάτων. Το κόλπο είναι να μεταφέρουμε όλη τη λογική σε ένα ξεχωριστό συστατικό – από εδώ και πέρα, δεν έχουμε έναν παρουσιαστή που φροντίζει για την εισαγωγή της βαθμολογίας, αλλά ένα ειδικό LikeControl. Η κλάση θα μοιάζει με την ακόλουθη (επιπλέον, θα περιέχει και τις μεθόδους render, handleUnlike, κ.λπ:)

class LikeControl extends Nette\Application\UI\Control
{
	public function __construct(
		private Article $article,
	) {
	}

	public function handleLike(): void
	{
		$this->ratingService->saveLike($this->article->id, $this->presenter->user->id);
		if ($this->presenter->isAjax()) {
			$this->redrawControl();
		} else {
			$this->presenter->redirect('this');
		}
	}
}

Πρότυπο του συστατικού:

{snippet}
	{if !$article->liked}
		<a n:href="like!" class=ajax>I like it</a>
	{else}
		<a n:href="unlike!" class=ajax>I don't like it anymore</a>
	{/if}
{/snippet}

Φυσικά θα αλλάξουμε το πρότυπο προβολής και θα πρέπει να προσθέσουμε ένα εργοστάσιο στον παρουσιαστή. Δεδομένου ότι θα δημιουργήσουμε το συστατικό τόσες φορές όσες φορές λαμβάνουμε άρθρα από τη βάση δεδομένων, θα χρησιμοποιήσουμε την κλάση Multiplier για να το “πολλαπλασιάσουμε”.

protected function createComponentLikeControl()
{
	$articles = $this->connection->table('articles');
	return new Nette\Application\UI\Multiplier(function (int $articleId) use ($articles) {
		return new LikeControl($articles[$articleId]);
	});
}

Το πρότυπο προβολής μειώνεται στο ελάχιστο απαραίτητο (και εντελώς απαλλαγμένο από αποσπάσματα!):

<article n:foreach="$articles as $article">
	<h2>{$article->title}</h2>
	<div class="content">{$article->content}</div>
	{control "likeControl-$article->id"}
</article>

Η εφαρμογή θα λειτουργεί τώρα με AJAX. Και εδώ πρέπει να βελτιστοποιήσουμε την εφαρμογή, επειδή λόγω της χρήσης της βάσης δεδομένων Nette, η επεξεργασία σήματος θα φορτώσει άσκοπα όλα τα άρθρα από τη βάση δεδομένων αντί για ένα. Ωστόσο, το πλεονέκτημα είναι ότι δεν θα υπάρξει rendering, επειδή μόνο το δικό μας συστατικό θα αποδοθεί πραγματικά.