To recap my last post, I added the ability to add a blog post. I wanted the writing experience to be clean and efficient so I added Markdown support. I also removed the hard coded data I’d been using. In this post I’m going to add the ability to edit and delete posts. Lets get started.
The Server
I’m going to start by adding two new method to the BlogPostService, UpdateBlogPost
and DeleteBlogPost
.
public void UpdateBlogPost(int postId, string updatedPost, string updateTitle)
{
var originalBlogPost = _blogPosts.Find(x => x.Id == postId);
originalBlogPost.Post = updatedPost;
originalBlogPost.Title = updateTitle;
}
public void DeleteBlogPost(int postId)
{
var blogPost = _blogPosts.Find(x => x.Id == postId);
_blogPosts.Remove(blogPost);
}
With these in place I have something to call from my controller. Before I add new endpoints though I’m going to add the routes in the Urls class in the shared project.
public const string UpdateBlogPost = "api/blogpost/{id}";
public const string DeleteBlogPost = "api/blogpost/{id}";
I know the are the same, but in the future if I wanted to adjust the routes independently, I could. Now the routes are taken care of I need to add two new endpoints on my API controller.
[HttpPut(Urls.UpdateBlogPost)]
public IActionResult UpdateBlogPost(int id, [FromBody]BlogPost updatedBlogPost)
{
_blogPostService.UpdateBlogPost(id, updatedBlogPost.Post, updatedBlogPost.Title);
return Ok();
}
[HttpDelete(Urls.DeleteBlogPost)]
public IActionResult DeleteBlogPost(int id)
{
_blogPostService.DeleteBlogPost(id);
return Ok();
}
That’s all I need on the server, time to get going on the client-side.
The Client
The first thing I want to do is to correct a bug which I spotted after writing the last post. I’d been getting some weird issues with routing on the client. If I tried to view a blog post directly by typing the URL in the address bar nothing would load. Also when clicking the home link from say, the add post page, the entire app would hard refresh.
After a bit of checking I realised that when I implemented the theme I forgot to add the <base />
tag to the <head>
section. This is used by Blazor as the base URI for requests and for the router to understand what routes to handle. I just need the following to sort the issue.
<head>
...
<title>WordDaze - Blazor Powered Blogging App</title>
<base href="/" />
...
</head>
Now that’s fixed, I can get going with the changes for editing and deleting.
Editing Posts
For the purpose of this demo app I just want the ability to edit the title and the post itself. I’ve already got the UI for this in the AddPost component. In fact, with a few small upgrades it should serve for both adding and editing posts.
I’m going to start by changing it’s name to be a better representation of it’s responsibilities. PostEditor seems a better fit to me.
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Blazor;
using Microsoft.AspNetCore.Blazor.Components;
using Microsoft.AspNetCore.Blazor.Services;
using Microsoft.JSInterop;
using WordDaze.Shared;
namespace WordDaze.Client.Features.PostEditor
{
public class PostEditorModel : BlazorComponent
{
[Inject] private HttpClient _httpClient { get; set; }
[Inject] private IUriHelper _uriHelper { get; set; }
[Parameter] protected string PostId { get; set; }
protected string Post { get; set; }
protected string Title { get; set; }
protected int CharacterCount { get; set; }
protected BlogPost ExistingBlogPost { get; set; } = new BlogPost();
protected bool IsEdit => string.IsNullOrEmpty(PostId) ? false : true;
protected ElementRef editor;
protected override async Task OnInitAsync()
{
if (!string.IsNullOrEmpty(PostId))
{
await LoadPost();
}
}
public async Task UpdateCharacterCount() => CharacterCount = await JSRuntime.Current.InvokeAsync<int>("wordDaze.getCharacterCount", editor);
public async Task SavePost()
{
var newPost = new BlogPost() {
Title = Title,
Author = "Joe Bloggs",
Post = Post,
Posted = DateTime.Now
};
var savedPost = await _httpClient.PostJsonAsync<BlogPost>(Urls.AddBlogPost, newPost);
_uriHelper.NavigateTo($"viewpost/{savedPost.Id}");
}
public async Task UpdatePost()
{
await _httpClient.PutJsonAsync(Urls.UpdateBlogPost.Replace("{id}", PostId), ExistingBlogPost);
_uriHelper.NavigateTo($"viewpost/{ExistingBlogPost.Id}");
}
private async Task LoadPost()
{
ExistingBlogPost = await _httpClient.GetJsonAsync<BlogPost>(Urls.BlogPost.Replace("{id}", PostId));
CharacterCount = ExistingBlogPost.Post.Length;
}
}
}
Let me break down the changes above. I’ve added PostId
parameter. This will be used if the component is going to be in edit mode. It will be populated from a PostId in the URL.
I’ve added an ExistingBlogPost
property which will be populated when LoadPost
is called when editing. I’ve also added a IsEdit
property which I will use to show and hide UI in the component.
Finally, I’ve added an UpdatePost
method which makes the call to the update API endpoint I built earlier.
Now for the component.
@page "/addpost"
@page "/editpost/{PostId}"
@layout MainLayout
@inherits PostEditorModel
@if (IsEdit)
{
<WdHeader Heading="WordDaze" SubHeading="Edit Post"></WdHeader>
}
else
{
<WdHeader Heading="WordDaze" SubHeading="Add Post"></WdHeader>
}
<div class="container">
<div class="row">
<div class="col-md-12">
@if (IsEdit)
{
<div class="editor">
<input @[email protected] placeholder="Title" class="form-control" />
<textarea @ref="editor" @[email protected] @onkeyup="@UpdateCharacterCount" placeholder="Write your post (Supports Markdown)" rows="25"></textarea>
<div class="character-count text-blaxk-50 float-left">@CharacterCount Characters</div>
<button class="btn btn-primary float-right" onclick="@UpdatePost">Update</button>
</div>
}
else
{
<div class="editor">
<input @bind=@Title placeholder="Title" class="form-control" />
<textarea @ref="editor" bind=@Post @onkeyup="@UpdateCharacterCount" placeholder="Write your post (Supports Markdown)" rows="25"></textarea>
<div class="character-count text-blaxk-50 float-left">@CharacterCount Characters</div>
<button class="btn btn-primary float-right" onclick="@SavePost">Post</button>
</div>
}
</div>
</div>
</div>
The first thing I’ve done is declared two @page
directives. In Blazor, components can be accessed via multiple routes. In this case the component will be in edit mode if accessed via a route such as /editpost/1. But it will be in add mode if accessed from a route of /addpost.
I’ve used the IsEdit
property to show different UI depending on which mode the component is in. If in edit mode, I’m binding the controls to the ExistingBlogPost
property. In add mode, I’m binding to the original Title
and Post
properties.
Delete Post
With all the changes done to the PostEditor component I just need to add a delete button and have it call a method on the component model and I should be done.
public async Task DeletePost()
{
await _httpClient.DeleteAsync(Urls.DeleteBlogPost.Replace("{id}", ExistingBlogPost.Id.ToString()));
_uriHelper.NavigateTo("/");
}
@if (IsEdit)
{
<div class="editor">
<input @[email protected] placeholder="Title" class="form-control" />
<textarea @ref="editor" @[email protected] @onkeyup="@UpdateCharacterCount" placeholder="Write your post (Supports Markdown)" rows="25"></textarea>
<div class="character-count text-blaxk-50 float-left">@CharacterCount Characters</div>
<button class="btn btn-primary float-right" @onclick="@DeletePost">Delete</button>
<button class="btn btn-primary float-right" @onclick="@UpdatePost">Update</button>
</div>
}
One last thing
I nearly forgot, I need a link to edit the post. On the ViewPost component I’m going to add a NavLink
component in a new row under where I currently display the blog post.
<div class="row">
<div class="col-md-12">
<NavLink class="btn btn-primary float-right" href="@($"/editpost/{BlogPost.Id}")">Edit</NavLink>
</div>
</div>
Wrapping up
With that, the penultimate post of this series comes to an end. In the last post I’m going to be putting all the add, edit and delete functionality behind a login. Some may argue I should have done that first but that would be boring.
Enjoying things so far? If you have any questions or suggestions then please let me know in the comments below. As always all the source code to accompany this series is available on GitHub.