Jak zaslat AJAX POST požadavek na funkci v Presenteru ?

FilipStudený
Člen | 8
+
0
-

Zdravím, narazil jsem na menší problém se kterým si nemohu poradit.

Mám jednoduchou blogovou aplikaci, kde uživatelé mohou vytvořit příspěvky, komentáře a následně je „Liknout“ (tlačíkto líbí se).

Mám ProfilePresenter, který se stará o vykreselení příspěvků, komentářů a liknutých příspěvků a komentářu.
Tlařítko Like mám vytvořený přes formulář, ale chtěl bych jej nahradit přes Ajax.

Mám ProfilePresenter, který má šablonu default.latte v adresáři Profile v adresáři templates, mám také vytvořenou šablonu @post.latte, které se předávají získaná data z databáze o příspěvku a zobrazuje samotný příspěvek. Toto je má struktura /app adresáře

/app
     /templates
            /Profile
				- default.latte
- @post.latte
- ProfilePresenter

Toto je zkácený kód @post.latte:

<div class="w-full mx-auto rounded-lg overflow-hidden shadow-lg m-2 bg-white">
    <a n:href="Profile:default $post->username" class="px-6 py-4 flex justify-between items-center hover:bg-gray-300">
        <div class="flex items-center">
            <img class="w-12 h-12 rounded-full mr-4" src="https://placehold.co/40x40" alt="Avatar">
            <div>
                <p class="text-gray-900 text-lg font-semibold">{$post->username}</p>
                <p class="text-gray-600 text-sm"></p>
            </div>
        </div>
    </a>
    <zbytek příspěvku>
    <div class="w-full mt-2 p-2 flex justify-end">
        <form n:name="likeForm">
            <input type="hidden" n:name="entityId" value="{$post->id}">
            <button n:name="like" class="flex flex-row justify-center items-center text-gray-900 text-xl p-2 rounded-full hover:bg-red-500 border-none hover:text-white {if $post->liked}text-red-500{else}text-gray-900{/if}" type="submit">
                {if $post->liked}<i class="fa-solid fa-heart"></i>{else}<i class="fa-regular fa-heart"></i>{/if}
            </button>

        </form>
    </div>

    <button
            class="like-button flex flex-row justify-center items-center text-gray-900 text-xl p-2 rounded-full hover:bg-red-500 border-none hover:text-white {if $post->liked}text-red-500 liked{else}text-gray-900{/if}"
            data-entity-id="{$post->id}"
            data-type="post"
    >
        {if $post->liked}<i class="fa-solid fa-heart"></i>{else}<i class="fa-regular fa-heart"></i>{/if}
    </button>

</div>

Toto je můj Ajax kód, přes který zasílám data o akci na tlačítko:

	<script>
		// like.js

		// Attach a click event handler to the like button
		$(document).ready(function () {
			$('.like-button').click(function (e) {
				e.preventDefault();
				var entityId = $(this).data('entity-id');
				var type = $(this).data('type');

				// Send an AJAX request to the server
				$.ajax({
					type: 'POST',
					url: "/profile/like", // Replace with the URL of your like action endpoint
					data: {
						entityId: entityId,
						type: type,
					},
					success: function (response) {
						// Handle the response, e.g., update the like count or button appearance
						if (response.liked) {
							// Post/comment is now liked, update UI
							$(this).addClass('liked');
						} else {
							// Post/comment is unliked, update UI
							$(this).removeClass('liked');
						}
					},
				});
			});
		});
	</script>

A toto je můj ProfilePresenter:

class ProfilePresenter extends Presenter
{
    private PostModel $posts;
    private Nette\Utils\Paginator $paginator;

    public function __construct(private Nette\Database\Explorer $database, public CustomAuthenticator $authenticator,
                                private UserModel $userModel, PostModel $postModel,
                                private LikeModel $likeModel, private CommentsModel $commentsModel){
        $this->posts = $postModel;
        $this->paginator = new Nette\Utils\Paginator();
        $this->paginator->setItemsPerPage(10);
    }

    public function renderDefault(string $username, int $page = 1, string $type = 'Posts'): void{
        $user = $this->userModel->getUser($username);
        $this->template->type = $type;

        if (!$user) {
            throw new Nette\Application\BadRequestException('User not found');
        }

        $userId = $user->id;
        $this->paginator->setPage($page);
        $totalItemCount = null;

        $this->paginator->setItemCount($totalItemCount);
        if ($type === 'Posts') {
            $totalItemCount = $this->posts->getTotalCountByUser($username);
            $this->template->posts = $this->posts->getAllPaginated($userId,$this->paginator->getOffset(), $this->paginator->getLength());
            $this->template->likes = [];
            $this->template->comments = [];
        } elseif ($type === 'Comments') {
            $totalItemCount = $this->commentsModel->getTotalCountByUser($username);
            $this->template->comments = $this->commentsModel->getAllPaginated($userId, $this->paginator->getOffset(), $this->paginator->getLength());
            $this->template->posts = [];
            $this->template->likes = [];
        }else{
            $totalItemCount = $this->likeModel->getTotalCountByUser($userId);
            $this->template->posts = [];
            $this->template->comments = [];
            $this->template->likes = $this->likeModel->getAllPaginated($userId, $this->paginator->getOffset(), $this->paginator->getLength());
        }

        $this->paginator->setItemCount($totalItemCount);
        $this->template->paginator = $this->paginator;
        $this->template->profile = $user;
    }

    protected function createLikeForm(string $type): Form
    {
        $form = new Form();
        $form->addHidden('entityId');
        $form->addSubmit('like', 'Like');
        $form->onSuccess[] = function (Form $form, \stdClass $values) use ($type) {
            $this->likeFormSucceeded($form, $values, $type);
        };
        return $form;
    }

    protected function createComponentLikeForm(): Form
    {
        return $this->createLikeForm('post');
    }

    protected function createComponentCommentLikeForm(): Form
    {
        return $this->createLikeForm('comment');
    }

    /**
     * @throws AbortException
     */
    #[NoReturn] public function likeFormSucceeded(Form $form, \stdClass $values, string $type): void{
        $userId = $this->getUser()->getId();
        $targetId = $values->entityId;
        $createdAt = new \DateTime();

        $likeModelMethod = $type === 'post' ? 'hasUserLikedPost' : 'hasUserLikedComment';
        $existing_like = $this->likeModel->$likeModelMethod($userId, $targetId);

        if ($existing_like) {
            $this->likeModel->deleteLike($userId, $type, $targetId);
        } else {
            $this->database->table('Likes')->insert([
                'user_id' => $userId,
                'type' => $type,
                'liked_entity_id' => $targetId,
                'created_at' => $createdAt
            ]);
        }

        $this->redirect('this');
    }

    public function actionLike(): void {
        $userId = $this->getUser()->getId();
        $entityId = $this->getParameter('entityId');
        $type = $this->getParameter('type');

        $likeModelMethod = $type === 'post' ? 'hasUserLikedPost' : 'hasUserLikedComment';
        $existingLike = $this->likeModel->$likeModelMethod($userId, $entityId);

        if ($existingLike) {
            $this->likeModel->deleteLike($userId, $type, $entityId);
            $liked = false;
        } else {
            $this->database->table('Likes')->insert([
                'user_id' => $userId,
                'type' => $type,
                'liked_entity_id' => $entityId,
                'created_at' => new \DateTime(),
            ]);
            $liked = true;
        }

        //$this->sendResponse(new JsonResponse(['liked' => $liked]));
    }
}

Problém je v tom, že nevím jak zaslat Ajax požadavek tak aby se spustila funkce actionLike() a přčedali se ji data jako je ID příspěvku a typ příspěvku (‚post‘ | ‚comment‘).

Při stisknutí tlačítka se zašle požadavek na tuto adresu a spadne mi to chybou:

XHRPOST
http://localhost/profile/like
[HTTP/1.1 500 Internal Server Error 272ms]
Nette\Application\BadRequestException #404
Page not found. Missing template 'C:\Users\Filda\Desktop\PHP\facefook\app\Presenters\templates\Profile\like.latte'. create file► search►
...\nette\application\src\Application\UI\Presenter.php:506
...\nette\application\src\Application\UI\Presenter.php:487
...\nette\application\src\Application\UI\Presenter.php:257
...\vendor\nette\application\src\Application\Application.php:163
...\vendor\nette\application\src\Application\Application.php:90
C:\Users\Filda\Desktop\PHP\facefook\www\index.php:10

 1:    <?php
 2:
 3:    declare(strict_types=1);
 4:
 5:    require __DIR__ . '/../vendor/autoload.php';
 6:
 7:    $configurator = App\Bootstrap::boot();
 8:    $container = $configurator->createContainer();
 9:    $application = $container->getByType(Nette\Application\Application::class);
10:    $application->run();

Zkoušel jsem požadavek zasílat na /actionLike ` , `/profile/actionLike, `/profile/action ` to ale nefunguje.

Existuje tedy nějaký způsob jak to opravit ?

Kamil Valenta
Člen | 771
+
+2
-

Odpověď je ukryta v routeru, který sice nevidíme, ale ani nemusíme. Nech to na něm.

$.ajax({
					type: 'POST',
					url: {plink Profile:like},
nightfish
Člen | 480
+
+3
-

FilipStudený napsal(a):

Existuje tedy nějaký způsob jak to opravit ?

Pro vyřešení chyby, že nelze najít šablonu pro akci like (like.latte), by mohlo stačit odkomentovat řádek //$this->sendResponse(new JsonResponse(['liked' => $liked]));.

m.brecher
Generous Backer | 794
+
+1
-

@FilipStudený

…uživatelé mohou vytvořit příspěvky, komentáře a následně je „Liknout“ (tlačíkto líbí se).

Tlařítko Like mám vytvořený přes formulář, ale chtěl bych jej nahradit přes Ajax.

Nejjednodušší je použít na odeslání dat obyčejný Nette signál, tlačítko v html realizovat obyčejným odkazem <a…>, potom Nette vytvoří url pro signál a parametry signálu a nemusíš složitě manipulovat v javascriptu s daty, místo jquery je dnes jednodušší použít javascript (fetchAPI + promisy) a i když je tlačítko jednoduché, vyplatí se napsat ho jako UI\Control komponentu, API modelu bych zjednodušil na jedinou metodu toggle(), protože v podstatě potřebujeme tlačítko přepínat.

Zde je nástin strukturovaného řešení tlačítka, které jsem testoval a funguje s javascriptem i bez něj:

Tlačítko like jako UI\Control komponenta

class LikeButtonControl extends Nette\Application\UI\Control
{
    public function __construct(private LikeModel $likeModel)
    {}

    public function handleToggle(int $id, string $table)
    {
        $responseData = $this->likeModel->toggle($id, $table);  // měníme stav tlačítka v db
        if($this->presenter->isAjax()){
            $this->presenter->sendJson($responseData);  // ajaxový response

        }else{
            $this->redirect('this');  // standardní response (vypnutý javascript)
                                      // přesměrujeme (zamezíme tím opakované toggle() při F5)
                                      // po přesměrování se automaticky odešle celá html stránka
        }
    }

    public function render(int $id, string $table, ?string $like)
    {
        $this->template->id = $id;
        $this->template->table = $table;
        $this->template->like = $like;
        $this->template->render(__DIR__ . '/likeButtonControl.latte');
    }
}

Šablona tlačítka

    <a n:class="like-button, $like" n:href="toggle!, id: $id, table: $table" data-like-button>Like</a>
  • signál toggle! volá handler komponenty handleToggle()
  • místo POST parametrů id a table posíláme parametry signálu
  • atribut data-like-button označuje like tlačítko pro javascript

Tlačítko vložíme do šablony akce

    <table>
		.....
        {foreach $posts as $post}
            {var $like = $post->related('like')->where('like.user_id', $user->id)->count('*') > 0 ? 'liked'}
            <tr>
                <td>.....</td>
                <td>
                    {control 'likeButton', id: $post->id, table: 'post', like: $like}
                </td>
            </tr>
        {/foreach}
    </table>

Interface factory komponenty
+ zaregistrujeme factory jako službu di, Nette automaticky vygeneruje z interface factory

interface LikeButtonFactory
{
    public function create(): LikeButtonControl;
}

Vytvoření komponenty pomocí factory

class ProfilePresenter extends UI\Presenter
{
    public function __construct(
        .......
        private readonly LikeButtonFactory $likeButtonFactory,
    )
    {}

    public function createComponentLikeButton(): LikeButtonControl
    {
        return $this->likeButtonFactory->create();  // vytvoříme tlačítko pomocí factory komponenty
    }

Modelová třída


class LikeModel
{
    public function __construct(
        private Nette\Database\Explorer  $database,
        private Nette\Security\User      $user,
    )
    {}

    public function toggle(int $id, string $table): array
    {
        try{
            $column = match($table){
                'post' => 'post_id',
                'comment' => 'comment_id',
                default => throw new Exception('invalid value of parameter table'),
            };
            $data = [$column => $id, 'user_id' => $this->user->id];
            $this->database->table('like')->insert($data);    // pokus vytvořit nový like
            $responseData = ['ok' => true, 'like' => true];

        }catch(Nette\Database\UniqueConstraintViolationException){
            $this->database->table('like')->where($data)->fetch()->delete();  // pokud like existoval, smažeme ho
            $responseData =  ['ok' => true, 'like' => false];

        }catch (\Exception){
            $responseData =  ['ok' => false, 'like' => null];
        }
        return $responseData;       // response vrací a) zda se akce povedla, b) nový stav like
    }
}

v tabulce like musí být nastaven UNIQUE KEY post_id_user_id (post_id,user_id), ten vyhodí UniqueConstraintViolationException při pokusu o duplicitní zápis

Javascript pro ajaxovou funkci tlačítka

<style>
    .like { color:green;}
</style>

<script>
    class LikeButton
    {
        handleClick(event)
        {
            const element = event.target
            if(element.dataset.likeButton === ''){           // test, zda se kliklo na like tlačítko
                event.preventDefault()                      // blokujeme standardní request html odkazu
                this.#toggle(element)
            }
        }

        #toggle(button)
        {
            fetch(
                   button.href,                                          // použijeme url html odkazu
                   { headers: { 'X-Requested-With': 'XMLHttpRequest' }}  // označíme request jako Ajax
                 )
                .then(response => response.json())                      // zpracování response Nette
                .then(data => {
                    if(!data.ok){
                        throw new Error('server error')
                    }
                    data.like
                        ? button.classList.add('like')
                        : button.classList.remove('like')
                })
                .catch(error => console.log(error))
        }
    }
    const likeButton = new LikeButton()
    document.addEventListener('click', (event) => likeButton.handleClick(event))
</script>

Editoval m.brecher (27. 10. 2023 15:58)

FilipStudený
Člen | 8
+
0
-

Díky za pomoc, řešení od m.brecher funguje.