Jak zaslat AJAX POST požadavek na funkci v Presenteru ?
- FilipStudený
- Člen | 8
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 | 815
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},
- m.brecher
- Generous Backer | 863
@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)