Simplifier les commentaires de VitePress avec Pinia Colada
Fait partie de la série Améliorer votre blog VitePress avec une API Laravel
L'authentification et la connexion à l'API sont en place. Nous pouvons maintenant intégrer le système de commentaires au front-end.
Le back-end sait déjà lister, créer, modifier et supprimer des commentaires. Notre tâche consiste à exposer ces opérations à travers un ensemble clair de fonctions API, de queries et de mutations Pinia Colada, ainsi que de petits composants Vue.
Plusieurs éléments doivent fonctionner ensemble, mais chacun suit les patterns introduits plus tôt dans la série. À la fin de cet article, le blog pourra afficher, créer, modifier et supprimer des commentaires.
Les bases
Le système de commentaires est une nouvelle fonctionnalité front-end. Nous allons donc créer un dossier dans .vitepress/theme pour ses composants, ses queries, ses mutations et ses appels API.
mkdir -p .vitepress/theme/commentsComme pour la fonctionnalité utilisateur, plaçons les appels API dans un fichier api.ts :
export function fetchComments(postId: number) {
return api(`api/posts/${postId}/comments`)
}
export function postComment(postId: number, content: string) {
return api(`api/posts/${postId}/comments`, {
method: 'POST',
body: { content },
})
}
export function putComment(commentId: number, content: string) {
return api(`api/comments/${commentId}`, {
method: 'PUT',
body: { content },
})
}
export function deleteComment(commentId: number) {
return api(`api/comments/${commentId}`, {
method: 'DELETE',
})
}Ces quatre fonctions correspondent aux quatre endpoints Laravel :
Route::get('/posts/{post}/comments', [PostCommentController::class, 'index']);
Route::post('/posts/{post}/comments', [PostCommentController::class, 'store']);
Route::put('/comments/{comment}', [CommentController::class, 'update']);
Route::delete('/comments/{comment}', [CommentController::class, 'destroy']);La première fonction récupère les commentaires d'un article avec GET, la deuxième crée un commentaire avec POST, la troisième modifie un commentaire avec PUT et la dernière en supprime un avec DELETE.
Comme expliqué dans l'article précédent, les fonctions de query commencent par fetch, tandis que les fonctions de mutation commencent par le verbe HTTP. Cette convention rend leur rôle évident.
Définissons ensuite le type Comment. Il reflète la ressource retournée par l'API Laravel.
import type { User } from '@/user/types/user'
export interface Comment {
id: number
content: string
author: User
}Afficher les commentaires
Les fonctions API et le type sont prêts. Créons maintenant une query qui récupère les commentaires de l'article courant. Nous l'utiliserons dans BlogShow.vue pour les afficher.
export const useComments = defineQuery(() => {
const { frontmatter } = useData()
return useQuery<Comment[]>({
key: () => ['comments', frontmatter.value.id],
query: () =>
fetchComments(frontmatter.value.id).then(response => response.data),
enabled: typeof document !== 'undefined',
})
})defineQuery crée un composable qui s'exécute dans le contexte Vue. Nous pouvons ainsi utiliser useData pour lire le frontmatter de la page courante et demander uniquement les commentaires de cet article.
Dans l'article consacré à l'automatisation, nous avons ajouté une propriété id au frontmatter de chaque article. Cette valeur unique relie l'article du front-end à son enregistrement du back-end, ce qui nous permet de demander ses commentaires.
Le front-end appelle cette valeur
id, tandis que la base de données l'appellemeta_id.
Lorsque le front-end demande /api/posts/2/comments, 2 désigne meta_id, et non l'id interne de la base de données. Si l'enregistrement possède l'ID interne 1 et le meta_id 2, le binding de modèle par défaut cherchera l'ID interne 2. Il retournera donc le mauvais article, ou aucun article.
Note
Le binding de modèle des routes permet à Laravel d'injecter le modèle correspondant à un paramètre de route. Pour /posts/{post}, Laravel utilise le nom du paramètre et le type de l'argument du contrôleur pour déterminer quel enregistrement Post récupérer.
Pour corriger cela, nous devons personnaliser le nom de la clé de route dans le modèle Post. Nous indiquerons ainsi à Laravel d'utiliser la colonne meta_id plutôt que la colonne id par défaut.
final class Post extends Model
{
/**
* Get the route key for the model.
*/
public function getRouteKeyName(): string
{
return 'meta_id';
}
}Ajoutons deux tests pour nous assurer que les articles sont toujours récupérés avec meta_id :
test('route key name', function () {
expect((new Post())->getRouteKeyName())->toBe('meta_id');
});it('returns comments by using the post meta id', function () {
Comment::factory()
->for(Post::factory()->create(['id' => 1, 'meta_id' => 5]))
->create();
$this->getJson(route('posts.comments', ['post' => 5]))
->assertJson(
fn (AssertableJson $json) => $json
->has(
'data',
1,
)
);
});Maintenant que la query utilise le bon article, affichons les commentaires dans BlogShow.vue.
Créez trois composants dans .vitepress/theme/comments/components :
Comment.vue: Affiche un commentaire.CommentList.vue: Affiche une liste de commentaires.CommentSection.vue: Affiche la section de commentaires et sa liste.
Comment.vue est un composant de présentation. Il reçoit un commentaire dans ses props et l'affiche.
<script lang="ts">
import type { Comment } from '@/comments/types/comment'
const blogComment = tv({
slots: {
base: 'space-y-4',
content: '',
author: 'flex items-center gap-2 font-semibold',
authorAvatar: 'size-8 rounded-full',
},
})
export interface BlogCommentProps {
comment: Comment
class?: any
ui?: Partial<typeof blogComment.slots>
}
export interface BlogCommentEmits {}
export interface BlogCommentSlots {}
</script>
<script lang="ts" setup>
const props = defineProps<BlogCommentProps>()
defineEmits<BlogCommentEmits>()
defineSlots<BlogCommentSlots>()
const { mutate: deleteComment } = useDeleteComment()
const update = ref(false)
const ui = computed(() => blogComment())
</script>
<template>
<article :class="ui.base({ class: [props.class, props.ui?.base] })">
<p>
{{ props.comment.content }}
</p>
<div :class="ui.author({ class: props.ui?.author })">
<img
v-if="props.comment.author.avatar"
:src="props.comment.author.avatar"
:class="ui.authorAvatar({ class: props.ui?.authorAvatar })"
>
<template v-if="props.comment.author.name">
{{ props.comment.author.name }} ({{ props.comment.author.username }})
</template>
<template v-else>
{{ props.comment.author.username }}
</template>
</div>
</article>
</template>CommentList.vue est également un composant de présentation. Il reçoit une liste et rend chaque élément avec Comment.vue.
<script lang="ts">
import type { Comment } from '@/comments/types/comment'
const blogComments = tv({
slots: {
base: 'space-y-8',
empty: '',
},
})
export interface BlogCommentsProps {
comments: Comment[]
class?: any
ui?: Partial<typeof blogComments.slots>
}
export interface BlogCommentsEmits {}
export interface BlogCommentsSlots {}
</script>
<script lang="ts" setup>
const props = defineProps<BlogCommentsProps>()
defineEmits<BlogCommentsEmits>()
defineSlots<BlogCommentsSlots>()
const ui = computed(() => blogComments())
</script>
<template>
<ol :class="ui.base({ class: props.ui?.base })">
<li
v-if="props.comments.length === 0"
:class="ui.empty({ class: props.ui?.empty })"
>
Aucun commentaire pour le moment.
</li>
<template v-else>
<li v-for="comment in props.comments" :key="comment.id">
<Comment :comment="comment" />
</li>
</template>
</ol>
</template>Enfin, CommentSection.vue affiche la section titrée et récupère les commentaires :
<script lang="ts">
const blogCommentsSection = tv({
slots: {
base: 'space-y-12',
title: 'text-2xl font-bold',
list: 'space-y-8',
},
})
export interface BlogCommentsSectionProps {
class?: any
ui?: Partial<typeof blogCommentsSection.slots>
}
export interface BlogCommentsSectionEmits {}
export interface BlogCommentsSectionSlots {}
</script>
<script lang="ts" setup>
const props = defineProps<BlogCommentsSectionProps>()
defineEmits<BlogCommentsSectionEmits>()
defineSlots<BlogCommentsSectionSlots>()
const { state: commentsState } = useComments()
const ui = computed(() => blogCommentsSection())
</script>
<template>
<section :class="ui.base({ class: [props.class, props.ui?.base] })">
<h2 :class="ui.title({ class: props.ui?.title })">
Commentaires
</h2>
<div v-if="commentsState.status === 'pending'">
Chargement des commentaires...
</div>
<div v-else-if="commentsState.status === 'error'">
Erreur lors du chargement des commentaires
</div>
<template v-else>
<CommentList
:comments="commentsState.data"
:class="ui.list({ class: props.ui?.list })"
/>
</template>
</section>
</template>Utilisez CommentSection dans BlogShow.vue pour afficher les commentaires de l'article.
<script lang="ts" setup>
const { frontmatter } = useData()
</script>
<template>
<Page>
<BlogArticle :title="frontmatter.title" />
<CommentSection />
<PageFooter :back-to="{ href: '/blog', label: 'Retour au blog' }" />
</Page>
</template>Découper la fonctionnalité en composants ciblés facilite l'ajout de fonctionnalités, les changements de design, la correction de bugs et l'écriture de tests.
Démarrez le front-end et le back-end avec la base de données remplie pour voir les commentaires d'un article.
Ajouter un commentaire
Pour ajouter un commentaire, nous avons besoin d'un formulaire avec une zone de texte. Le formulaire utilisera la mutation postComment.
Créez un composant CommentForm.vue pour le formulaire.
Commençons par le template :
<template>
<form
:class="ui.base({ class: [props.class, props.ui?.base] })"
@submit.prevent="onSubmit"
>
<textarea
v-model="content"
placeholder="Écrire un commentaire..."
:class="ui.textarea({ class: props.ui?.textarea })"
/>
<span v-if="error" class="text-red-500">{{ error.data.message }}</span>
<Button
type="submit"
label="Commenter"
:class="ui.button({ class: props.ui?.button })"
:disabled="isLoading"
/>
</form>
</template>Définissons ensuite la mutation dans le script :
<script lang="ts" setup>
const { frontmatter } = useData()
const queryCache = useQueryCache()
const content = ref('')
const {
mutate: storeComment,
isLoading,
error,
} = useMutation<
{
data: Comment
},
{ postId: number, content: string, parentId?: number },
{
data: {
message: string
errors: {
content: string[]
}
}
}
>({
mutation: ({ postId, content }) => postComment(postId, content),
onSuccess: () => {
content.value = ''
},
onSettled: () =>
queryCache.invalidateQueries({
key: ['comments', frontmatter.value.id],
exact: true,
}),
})
</script>La mutation expose l'action, l'état de chargement et l'erreur. Nous pouvons les utiliser pour afficher une erreur lorsque la requête échoue et désactiver le bouton pendant son exécution. Un indicateur de chargement pourrait être ajouté au même endroit.
La forme de l'erreur correspond à la réponse de validation de Laravel. Désactiver le bouton évite les soumissions en double pendant l'exécution de la requête.
Note
Pour garder l'exemple simple, le formulaire désactive uniquement le bouton d'envoi lorsqu'il est occupé.
Gérons l'événement d'envoi en appelant la mutation :
<script lang="ts" setup>
// ...
function onSubmit() {
storeComment({ postId: frontmatter.value.id, content: content.value })
}
</script>Note
Je sais que ce formulaire finira par créer et modifier des commentaires, je le conçois donc dès maintenant pour les deux cas. Cela évitera de dupliquer le code, comme nous le verrons dans la section suivante.
Voici le formulaire complet :
<script lang="ts">
import type { Comment } from '@/comments/types/comment'
const commentForm = tv({
slots: {
base: 'flex flex-col gap-4',
textarea:
'border-4 border-black px-8 py-4 shadow-[4px_4px_0_black] focus:outline-none',
button: '',
},
})
export interface CommentFormProps {
comment?: Comment
class?: any
ui?: Partial<typeof commentForm.slots>
}
export interface CommentFormEmits {
submit: [void]
}
export interface CommentFormSlots {}
</script>
<script lang="ts" setup>
const props = defineProps<CommentFormProps>()
const emit = defineEmits<CommentFormEmits>()
defineSlots<CommentFormSlots>()
const { frontmatter } = useData()
const queryCache = useQueryCache()
const content = ref(props.comment?.content ?? '')
const {
mutate: storeComment,
isLoading,
error,
} = useMutation<
{
data: Comment
},
{ postId: number, content: string, parentId?: number },
{
data: {
message: string
errors: {
content: string[]
}
}
}
>({
mutation: ({ postId, content }) => postComment(postId, content),
onSuccess: () => {
content.value = ''
},
onSettled: () =>
queryCache.invalidateQueries({
key: ['comments', frontmatter.value.id],
exact: true,
}),
})
function onSubmit() {
storeComment({ postId: frontmatter.value.id, content: content.value })
}
const ui = computed(() => commentForm())
</script>
<template>
<form
:class="ui.base({ class: [props.class, props.ui?.base] })"
@submit.prevent="onSubmit"
>
<textarea
v-model="content"
placeholder="Écrire un commentaire..."
:class="ui.textarea({ class: props.ui?.textarea })"
/>
<span v-if="error" class="text-red-500">{{ error.data.message }}</span>
<Button
type="submit"
label="Commenter"
:class="ui.button({ class: props.ui?.button })"
:disabled="isLoading"
/>
</form>
</template>Placez le formulaire dans un composant AddComment.vue avec un titre. N'affichez le formulaire qu'aux utilisateurs authentifiés afin que les visiteurs ne voient pas un contrôle qu'ils ne peuvent pas utiliser :
<script lang="ts">
const addComment = tv({
slots: {
base: 'space-y-4',
header: 'flex items-center gap-2',
avatar: 'size-8 rounded-full',
title: 'text-lg font-semibold',
},
})
export interface AddCommentProps {
class?: any
ui?: Partial<typeof addComment.slots>
}
export interface AddCommentEmits {}
export interface AddCommentSlots {}
</script>
<script lang="ts" setup>
const props = defineProps<AddCommentProps>()
defineEmits<AddCommentEmits>()
defineSlots<AddCommentSlots>()
const { state: userState } = useUser()
const loginUrl = `${import.meta.env.VITE_API_URL}/auth/github/redirect`
const ui = computed(() => addComment())
</script>
<template>
<div :class="ui.base({ class: [props.class, props.ui?.base] })">
<div :class="ui.header({ class: props.ui?.header })">
<img
v-if="userState.data && userState.data.avatar"
:src="userState.data.avatar"
:class="ui.avatar({ class: props.ui?.avatar })"
>
<h3 :class="ui.title({ class: props.ui?.title })">
Ajouter un commentaire
</h3>
</div>
<CommentForm v-if="userState.data" />
<div v-else class="flex justify-center">
<Button label="Se connecter pour commenter" :href="loginUrl" />
</div>
</div>
</template>Les utilisateurs non authentifiés voient un bouton qui les redirige vers GitHub. Nous utilisons useUser comme dans l'en-tête. Pinia Colada déduplique la requête et partage le résultat, nous n'avons donc pas besoin de faire passer les props ni d'effectuer la récupération depuis un composant éloigné.
Utilisez enfin AddComment dans CommentSection.vue, sous la liste :
<template>
<section :class="ui.base({ class: [props.class, props.ui?.base] })">
<!-- ... -->
<!-- eslint-disable-next-line vue/valid-v-else -->
<template v-else>
<!-- ... -->
<AddComment />
</template>
</section>
</template>Modifier un commentaire
Les utilisateurs doivent pouvoir modifier leurs propres commentaires. Nous devons donc gérer cet état dans l'interface.
Comment savoir quels commentaires l'utilisateur courant peut modifier ? Le back-end utilise CommentPolicy pour prendre cette décision. Le front-end ne doit pas afficher de bouton de modification lorsque la policy refuse l'accès.
Nous pourrions dupliquer la policy dans le front-end, mais cela créerait deux sources de vérité. Chaque changement de la policy du back-end nécessiterait alors une modification correspondante du front-end, et cette duplication deviendrait plus difficile à maintenir à mesure que la policy grandirait.
Retournons plutôt une propriété can avec chaque commentaire. Le back-end peut la remplir en réutilisant la policy et fournir au front-end les permissions nécessaires sans dupliquer les règles.
Dans le back-end, nous vérifions déjà si l'utilisateur peut modifier ou supprimer un commentaire dans le contrôleur :
final class CommentController extends Controller
{
public function destroy(Comment $comment): Response
{
Gate::authorize('delete', $comment);
}
}Nous devons également exposer le droit de supprimer le commentaire, nous allons donc gérer les deux permissions ensemble.
Dans CommentResource, créons une propriété can qui contiendra les permissions de l'utilisateur sur le commentaire :
final class CommentResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
// ...
'can' => [
'update' => $request->user()?->can('update', $this->resource) ?? false,
'delete' => $request->user()?->can('delete', $this->resource) ?? false,
],
];
}
}Le front-end peut maintenant vérifier les deux permissions. La réponse ressemblera à ceci :
{
"data": {
"id": 1,
"content": "This is a comment",
"author": {
"id": 1,
"name": "John Doe",
"username": "johndoe",
"avatar": null
},
"can": {
"update": true,
"delete": true
}
}
}Le front-end peut utiliser ces valeurs dans des conditions v-if pour afficher les actions appropriées.
Mettez à jour les tests pour vérifier que can est correctement rempli.
it('returns the comment', function () {
// ...
$this->actingAs($user)
->putJson(route('comments.update', $comment), [
'content' => 'Updated content',
])
->assertJson(
fn (AssertableJson $json) => $json
->has(
'data',
fn (AssertableJson $json) => $json
// ...
->has(
'can',
fn (AssertableJson $json) => $json
->where('update', true)
->where('delete', true)
)
)
);
});it('returns comments', function () {
// ...
$this->getJson(route('posts.comments', $post))
->assertJson(
fn (AssertableJson $json) => $json
->has(
'data',
2,
fn (AssertableJson $json) => $json
// ...
->has(
'can',
fn (AssertableJson $json) => $json
->where('update', false)
->where('delete', false)
)
)
);
});it('returns the created comment', function () {
// ...
$response
->assertJson(
fn (AssertableJson $json) => $json
->has(
'data',
fn (AssertableJson $json) => $json
// ...
->has(
'can',
fn (AssertableJson $json) => $json
->where('update', true)
->where('delete', true)
)
)
);
});Avec la réponse API en place, ajoutons un bouton de modification à Comment.vue.
Avant de modifier le composant, mettons à jour l'interface Comment pour y inclure can :
import type { User } from '@/user/types/user'
export interface Comment {
id: number
content: string
author: User
can: {
update: boolean
delete: boolean
}
}Le bouton ne sera affiché que lorsque le back-end accordera la permission update.
<script lang="ts">
const blogComment = tv({
slots: {
footer: 'flex items-center justify-between',
actions: 'flex items-center gap-2',
},
})
</script>
<script lang="ts" setup>
// ...
</script>
<template>
<article :class="ui.base({ class: [props.class, props.ui?.base] })">
<!-- ... -->
<div :class="ui.footer({ class: props.ui?.footer })">
<div :class="ui.author({ class: props.ui?.author })">
<!-- ... -->
</div>
<div :class="ui.actions({ class: props.ui?.actions })">
<button v-if="props.comment.can.update">
Modifier
</button>
</div>
</div>
</article>
</template>La policy du back-end contrôle maintenant l'affichage de ce bouton.
Lorsque l'utilisateur clique sur le bouton, affichons CommentForm.vue à la place du contenu du commentaire afin de le modifier directement dans la liste.
Ajoutons une ref à Comment.vue pour suivre le mode de modification :
<script lang="ts">
// ...
</script>
<script lang="ts" setup>
const update = ref(false)
</script>
<template>
<article :class="ui.base({ class: [props.class, props.ui?.base] })">
<CommentForm
v-if="update"
:comment="props.comment"
@submit="update = false"
/>
<p v-else>
{{ props.comment.content }}
</p>
<div :class="ui.footer({ class: props.ui?.footer })">
<!-- ... -->
<div :class="ui.actions({ class: props.ui?.actions })">
<button v-if="props.comment.can.update" @click="update = !update">
Modifier
</button>
</div>
</div>
</article>
</template>Mettons enfin CommentForm.vue à jour pour le mode modification. Passons le commentaire en prop et initialisons la zone de texte avec son contenu. La présence de cette prop indique au formulaire le mode à utiliser.
<script lang="ts">
import type { Comment } from '@/comments/types/comment'
const commentForm = tv({
slots: {
base: 'flex flex-col gap-4',
textarea:
'border-4 border-black px-8 py-4 shadow-[4px_4px_0_black] focus:outline-none',
button: '',
},
})
export interface CommentFormProps {
comment?: Comment
class?: any
ui?: Partial<typeof commentForm.slots>
}
export interface CommentFormEmits {
submit: [void]
}
export interface CommentFormSlots {}
</script>
<script lang="ts" setup>
const props = defineProps<CommentFormProps>()
const emit = defineEmits<CommentFormEmits>()
defineSlots<CommentFormSlots>()
const { frontmatter } = useData()
const queryCache = useQueryCache()
const content = ref(props.comment?.content ?? '')
const {
mutate: storeComment,
isLoading: isStoreCommentLoading,
error: storeCommentError,
} = useMutation<
{
data: Comment
},
{ postId: number, content: string, parentId?: number },
{
data: {
message: string
errors: {
content: string[]
}
}
}
>({
mutation: ({ postId, content }) => postComment(postId, content),
onSuccess: () => {
content.value = ''
},
onSettled: () =>
queryCache.invalidateQueries({
key: ['comments', frontmatter.value.id],
exact: true,
}),
})
const {
mutate: updateComment,
isLoading: isUpdateCommentLoading,
error: updateCommentError,
} = useMutation<
{
data: Comment
},
{ commentId: number, content: string },
{
data: {
message: string
errors: {
content: string[]
}
}
}
>({
mutation: ({ commentId, content }) => putComment(commentId, content),
onSuccess: (data) => {
content.value = ''
const comments
= queryCache.getQueryData<Comment[]>(['comments', frontmatter.value.id])
?? ([] as Comment[])
const index = comments.findIndex(
(comment: Comment) => comment.id === data.data.id,
)
if (index !== -1) {
comments[index] = data.data
queryCache.setQueryData(['comments', frontmatter.value.id], comments)
}
emit('submit')
},
onSettled: () =>
queryCache.invalidateQueries({
key: ['comments', frontmatter.value.id],
exact: true,
}),
})
function onSubmit() {
if (props.comment) {
updateComment({
commentId: props.comment.id,
content: content.value,
})
}
else {
storeComment({ postId: frontmatter.value.id, content: content.value })
}
}
const error = computed(
() => storeCommentError.value || updateCommentError.value,
)
const isLoading = computed(
() => isStoreCommentLoading.value || isUpdateCommentLoading.value,
)
const ui = computed(() => commentForm())
</script>
<template>
<form
:class="ui.base({ class: [props.class, props.ui?.base] })"
@submit.prevent="onSubmit"
>
<textarea
v-model="content"
placeholder="Écrire un commentaire..."
:class="ui.textarea({ class: props.ui?.textarea })"
/>
<span v-if="error" class="text-red-500">{{ error.data.message }}</span>
<Button
type="submit"
:label="props.comment ? 'Modifier' : 'Commenter'"
:class="ui.button({ class: props.ui?.button })"
:disabled="isLoading"
/>
</form>
</template>La zone de texte commence avec le contenu du commentaire lorsque la prop comment est définie. Nous combinons les états de chargement de la création et de la modification afin de désactiver le bouton pendant l'une ou l'autre requête, et nous combinons leurs erreurs pour afficher un seul message. Le libellé du bouton change selon le mode. Lors de l'envoi, le formulaire vérifie si comment existe et appelle updateComment ou storeComment.
Tip
Réutiliser CommentForm.vue fonctionne bien ici, car la création et la modification ne diffèrent que par la valeur initiale et l'endpoint API.
Supprimer un commentaire
Pour supprimer un commentaire, créons une mutation useDeleteComment. Elle utilisera une mise à jour optimiste afin de fournir un retour immédiat pendant l'exécution de la requête.
import type { Comment } from '@/comments/types/comment'
export const useDeleteComment = defineMutation(() => {
const { frontmatter } = useData()
const queryCache = useQueryCache()
return useMutation({
mutation: ({ commentId }: { commentId: number }) =>
deleteComment(commentId),
onMutate: ({ commentId }) => {
const oldComments
= queryCache.getQueryData<Comment[]>([
'comments',
frontmatter.value.id,
]) || ([] as Comment[])
const newComments = JSON.parse(JSON.stringify(oldComments)) as Comment[]
newComments.splice(
newComments.findIndex(c => c.id === commentId),
1,
)
queryCache.setQueryData(['comments', frontmatter.value.id], newComments)
queryCache.cancelQueries({
key: ['comments', frontmatter.value.id],
exact: true,
})
return { oldComments }
},
onError: (_, __, { oldComments }) => {
if (oldComments) {
queryCache.setQueryData(
['comments', frontmatter.value.id],
oldComments,
)
}
},
onSettled: () =>
queryCache.invalidateQueries({
key: ['comments', frontmatter.value.id],
exact: true,
}),
})
})Lorsqu'elle est appelée, la mutation lit les commentaires actuels dans le cache, retire le commentaire sélectionné et réécrit la nouvelle liste. Le commentaire disparaît immédiatement au lieu d'attendre la réponse du serveur.
Si la requête échoue, elle restaure la liste précédente afin que le commentaire réapparaisse. Dans tous les cas, elle invalide la query pour que le prochain résultat reflète le serveur.
Utilisons maintenant cette mutation dans les actions du composant Comment.vue :
<script lang="ts">
// ...
</script>
<script lang="ts" setup>
const { mutate: deleteComment } = useDeleteComment()
</script>
<template>
<article :class="ui.base({ class: [props.class, props.ui?.base] })">
<!-- ... -->
<div :class="ui.footer({ class: props.ui?.footer })">
<!-- ... -->
<div :class="ui.actions({ class: props.ui?.actions })">
<!-- ... -->
<button
v-if="props.comment.can.delete"
@click="deleteComment({ commentId: props.comment.id })"
>
Supprimer
</button>
</div>
</div>
</article>
</template>Dernières réflexions
La série s'achève ici. Nous sommes partis de la structure et des outils qualité d'un projet Laravel, puis nous avons ajouté l'authentification GitHub et une API de commentaires, synchronisé les métadonnées des articles, connecté le front-end au back-end et enfin construit une interface de commentaires interactive.
L'implémentation reste volontairement simple, mais l'architecture est réutilisable. La même séparation entre fonctions API, queries, mutations et composants peut accueillir de futures fonctionnalités sans transformer un seul composant en fourre-tout.
Si vous poursuivez cet exemple, je serais ravi de découvrir ce que vous créerez. Voici quelques extensions possibles :
- Prise en charge du Markdown : Permettre aux utilisateurs d'écrire leurs commentaires en Markdown.
- Réponses : Permettre aux utilisateurs de répondre à un commentaire existant.
- Likes : Permettre aux utilisateurs de réagir à un commentaire.
Quelle fonctionnalité aimeriez-vous voir dans une future série ? Dites-le-moi dans les commentaires.
Bon code ! 👨💻
Merci de me lire ! Je m'appelle Estéban, et j'adore écrire sur le développement web et le parcours humain qui l'entoure.
Je code depuis plusieurs années maintenant, et j'apprends encore de nouvelles choses chaque jour. J'aime partager mes connaissances avec les autres, car j'aurais aimé avoir accès à des ressources aussi claires et complètes lorsque j'ai commencé à apprendre la programmation.
Si vous avez des questions ou souhaitez discuter, n'hésitez pas à commenter ci-dessous ou à me contacter sur Bluesky, X, et LinkedIn.
J'espère que vous avez apprécié cet article et appris quelque chose de nouveau. N'hésitez pas à le partager avec vos amis ou sur les réseaux sociaux, et laissez un commentaire ou une réaction ci-dessous, cela me ferait très plaisir ! Si vous souhaitez soutenir mon travail, vous pouvez me sponsoriser sur GitHub !
Discussions
Ajouter un commentaire
Vous devez être connecté pour accéder à cette fonctionnalité.