Δυναμικά snippets

Αρκετά συχνά κατά την ανάπτυξη εφαρμογών προκύπτει η ανάγκη εκτέλεσης λειτουργιών AJAX, για παράδειγμα, σε μεμονωμένες γραμμές πίνακα ή στοιχεία λίστας. Ως παράδειγμα, μπορούμε να επιλέξουμε την εμφάνιση άρθρων, όπου για κάθε ένα από αυτά επιτρέπουμε στον συνδεδεμένο χρήστη να επιλέξει βαθμολογία “μου αρέσει/δεν μου αρέσει”. Ο κώδικας του presenter και του αντίστοιχου template χωρίς 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>μου αρέσει</a>
	{else}
		<a n:href="unlike! $article->id" class=ajax>δεν μου αρέσει πια</a>
	{/if}
</article>

Ajaxification

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

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

Ο δρόμος των δυναμικών snippets

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

{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>μου αρέσει</a>
			{else}
				<a n:href="unlike! $article->id" class=ajax>δεν μου αρέσει πια</a>
			{/if}
		{/snippet}
	</article>
{/snippet}

Κάθε άρθρο ορίζει τώρα ένα snippet, το οποίο έχει στο όνομά του το ID του άρθρου. Όλα αυτά τα snippets είναι στη συνέχεια ομαδοποιημένα μαζί με ένα snippet με το όνομα articlesContainer. Αν παραλείπαμε αυτό το περιβάλλον snippet, το Latte θα μας ειδοποιούσε με μια εξαίρεση.

Μας μένει να συμπληρώσουμε την επανασχεδίαση στον presenter – αρκεί να επανασχεδιάσουμε το στατικό περιτύλιγμα.

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, θα διαπιστώναμε ότι παρόλο που εξωτερικά η εφαρμογή φαίνεται οικονομική (επιστρέφει μόνο ένα μοναδικό snippet για το συγκεκριμένο άρθρο), στην πραγματικότητα στον server σχεδίασε όλα τα snippets. Το επιθυμητό snippet τοποθετήθηκε στο payload, και τα υπόλοιπα απορρίφθηκαν (εντελώς άσκοπα τα απέκτησε επίσης από τη βάση δεδομένων).

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

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

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

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

Ο δρόμος των components

Ένας εντελώς διαφορετικός τρόπος λύσης αποφεύγει τα δυναμικά snippets. Το κόλπο έγκειται στη μεταφορά ολόκληρης της λογικής σε ένα ξεχωριστό component – από τώρα και στο εξής, η εισαγωγή βαθμολογίας δεν θα γίνεται από τον presenter, αλλά από ένα εξειδικευμένο 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');
		}
	}
}

Το template του component:

{snippet}
	{if !$article->liked}
		<a n:href="like!" class=ajax>μου αρέσει</a>
	{else}
		<a n:href="unlike!" class=ajax>δεν μου αρέσει πια</a>
	{/if}
{/snippet}

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

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

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

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

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