Creating a to-do app using Blazor - Part 2
Table of contents
The Part 2 of this series covers editing, adding and removing to-do entries. You can access the first part that covers planning and rendering the to-do application:
Creating a to-do app using Blazor and Bootstrap - part 1.
Preview
Editing To-do's
To edit entries, we'll start by doing something similar to what we did for allowing collapsing and expanding to-do entries:
In the TodoEntry.razor, we have already a EditMode
parameter, add the OnEditClicked
parameterized callback.
[Parameter]
public EventCallback OnEditClicked { get; set; }
Toggling "Edit Mode"
At the top of the TodoEntry.razor, we'll use @using Microsoft.AspNetCore.Components.Forms
, it's a library that provides useful components that make the process of implementing forms in Razor easier. Let's use the InputText
component that the library offers. To learn more about these components, check out ASP.NET Core Blazor forms and Input components.
Now, implement the "onclick" event near the element that renders the pencil, and use the callback OnEditClicked
and move this HTML block out of the if block so that we can toggle the "Edit Mode":
<div class="col-1">
<button class="btn btn-link" @onclick="OnEditClicked">
<span class="oi oi-pencil" aria-hidden="true"></span>
</button>
</div>
Now we need to handle the click event in the parent component and define the respective edit state as true or false depending on the previous state. For convenience, we'll expand the to-do entry when the user clicks on the edit button (the pencil icon). In your TodoList.razor, we added the following method:
async Task OnEntryEditClicked(EntryRenderData entryData)
{
// Toggle editMode
var editMode = RenderData[entryData.Data.Id].IsEditMode;
RenderData[entryData.Data.Id].IsEditMode = !editMode;
// Toggle collapse state
RenderData[entryData.Data.Id].IsCollapsed = false;
}
This will toggle the IsEditMode
value and set the "IsCollapsed" to true to expand the entry.
Finally, assign the parameters related to the edit rendering process, such as the EditMode
flag and the callback, in the TodoEntry
HTML part:
EditMode=@todoEntry.IsEditMode
OnEditClicked="async () => await OnEntryEditClicked(todoEntry)"
With this setup, the render state can easily be set to both edit and non-edit modes, and a button will appear when the to-do item is in edit mode. This is the save button, which currently does nothing. Let's handle it.
Saving edited to-do entries
Now let's create a data transfer object that is used when editing an entry, so that the updated information can be saved to the database. This can be achieved by using a record
:
namespace BlazorTodoApp.Data;
public record EntryDataDto (string Title, string Content);
It is also necessary to create a method in the "TodoListService.cs" that updates an entry in the database.
/// <summary>
/// Updates a To-do entry
/// </summary>
/// <param name="entryId">Id of the entry.</param>
/// <param name="updatedData">The data to update</param>
/// <returns></returns>
public bool UpdateEntry(int entryId, EntryDataDto dataDto)
{
var index = TodoEntries.FindIndex(x => x.Id == entryId);
if (index != -1)
{
TodoEntries[index].Content = dataDto.Content;
TodoEntries[index].Title = dataDto.Title;
return true;
}
return false;
}
Next, in the TodoEntry.razor component, the value of the input component field is bound to the Title
, like this @bind-Value="@Title"
. The input for editing the title appears as follows:
<InputText type="text" class="form-control" placeholder="Title" @bind-Value="@Title" />
Similar to the above, it is also needed to include functionality for editing the content of the entry within the InputTextArea
:
<InputTextArea class="form-control" rows="3" @bind-Value="@Content"/>
Finally, it is necessary to provide a parameterized callback with the previously declared DTO data. When a user clicks on the "Save" button, the event callback will be invoked with an EntryDataDto
object as a parameter, which contains the updated data of a to-do entry. This event callback method will handle the event and persist the updated data to the database.
[Parameter]
public EventCallback<EntryDataDto> OnSaveClicked { get; set; }
Now assign the event callback when the user clicks the save button:
<button class="btn btn-link" @onclick="async () => await OnSaveClicked.InvokeAsync(new EntryDataDto(Title, Content))">
<span class="oi oi-collapse-down" aria-hidden="true"></span>
</button>
Here, the callback is invoked with the EntryData DTO, which includes the Title and Content properties that were previously bound to the respective input values. Now, these properties can be handled in the TodoList.razor file.
When the save happens, the following actions will be taken:
- The updated data is saved to the database.
- The edit mode is set to false.
- The method is assigned as a parameterized callback in each TodoEntry.razor file.
This method is named OnEntrySaveClicked
and it is implemented as follows:
async Task OnEntrySaveClicked(int id, EntryDataDto dataDto)
{
// Check if exists
var item = ListService.GetEntryById(id);
if (item != null)
{
// Update data in the "database"
ListService.UpdateEntry(id, dataDto);
}
// Set edit mode to false
RenderData[id].IsEditMode = false;
}
Finally, assign the method that was just created to the parameterized OnSaveClicked
callback. (OnSaveClicked="async (entryEditData) => await OnEntrySaveClicked(todoEntry.Data.Id, entryEditData)"
).
The TodoEntry component with all parameters at this point should look like this:
<TodoEntry
Title=@todoEntry.Data.Title
Content=@todoEntry.Data.Content
Collapsed=@todoEntry.IsCollapsed
OnEntryClicked="async () => await OnEntryClicked(todoEntry)"
EditMode=@todoEntry.IsEditMode
OnEditClicked="async () => await OnEntryEditClicked(todoEntry)"
OnSaveClicked="async (entryEditData) => await OnEntrySaveClicked(todoEntry.Data.Id, entryEditData)"
/>
Perfect. Now the user can edit and save the changes. The changes are persisted even if the webpage is reloaded.
Adding entries
To add a new entry, let's add a method to the TodoListService.cs in order to add an entry to the in-memory database:
/// <summary>
/// Adds a To-Do entry to TodoEntries list.
/// </summary>
/// <param name="entry">Entry to add</param>
public TodoEntryData AddEntry(EntryDataDto entry)
{
// Create new entry
var newEntry = new TodoEntryData
{
Id = GetEntriesCount(),
Content = entry.Content,
Title = entry.Title
};
TodoEntries.Add(newEntry);
return newEntry;
}
Start by editing the TodoEntry.razor file and adding the CreateMode property decorated with the [Property]
attribute:
[Parameter]
public bool CreateMode { get; set; }
To avoid a bug when the user clicks in the edit mode without saving it first, the Edit button can be hidden when the item is in create mode:
@if (!CreateMode)
{
<div class="col-1">
<button class="btn btn-link" @onclick="OnEditClicked">
<span class="oi oi-pencil" aria-hidden="true"></span>
</button>
</div>
}
For the rendering part, let's add an empty item for editing. To achieve this, the IsEditMode
flag will be activated, and we also have a property called IsCreateMode
which will be activated to deactivate the edit button in the TodoEntry.razor file. Then, when the user clicks the save button, we'll check if the rendering data is in "CreateMode" and create an entry instead of updating it.
To begin, we will implement a method that is invoked when the "Add entry" button is clicked. The intended outcome is to have an empty entry item rendered without it being saved yet. The to-do entry will be saved when the user clicks the save icon:
async Task OnAddEntryClicked()
{
var id = ListService.TodoEntries.Count;
var temporaryEntryData = new EntryData()
{
Id = id,
Title = "",
Content = ""
};
RenderData[id] = new EntryRenderData(temporaryEntryData)
{
IsEditMode = true,
IsCreateMode = true,
IsCollapsed = false,
};
}
Then, assign the callback to the add button in the TodoList.razor
file:
<button
type="button"
class=" btn btn-primary px-sm-4 m-1 align-content-center"
@onclick="OnAddEntryClicked">Add item
</button>
Assign the "CreateMode" to the TodoItem component inside the foreach loop CreateMode="@todoEntry.IsCreateMode"
.
Now, it is necessary to change the OnEntrySaveClicked
method to handle when the entry is being added;
async Task OnEntrySaveClicked(int id, EntryDataDto dataDto)
{
// Check if exists
var item = ListService.GetEntryById(id);
if (item != null)
{
// Update data in the "database"
ListService.UpdateEntry(id, dataDto);
}
else // Entry is being created
{
var newEntry = ListService.AddEntry(dataDto);
RenderData[newEntry.Id] = new EntryRenderData(newEntry);
// Let the entry expanded
RenderData[newEntry.Id].IsCollapsed = false;
}
// Set edit mode to false
RenderData[id].IsEditMode = false;
}
The method has been modified to handle when the item is null, in that case, it is known that a new entry needs to be created. First, the entry is added using the ListService
, then the render data is updated with the actual entry data. The IsCollapsed
property is set to false, so that the entry is displayed in an expanded state. And that's it!
Removing entries
To remove a entry let's add a DeleteEntry
method the TodoListService.cs
:
public bool DeleteEntry(int entryId)
{
var index = TodoEntries.FindIndex(x => x.Id == entryId);
if (index != -1)
{
TodoEntries.RemoveAt(index);
return true;
}
return false;
}
In the TodoEntry.razor
, include a callback when the user clicks in the "X" button. Add the following parameterized callback into the@code
block:
[Parameter]
public EventCallback OnRemoveClicked { get; set; }
Then assign the parameterized callback to the button with the icon "oi-x":
<button class="btn btn-link" @onclick="OnRemoveClicked">
<span class="oi oi-x" aria-hidden="true"></span>
</button>
Now back to the TodoList.razor
add a OnRemoveEntryClicked
method to remove a entry:
async Task OnRemoveEntryClicked(int id)
{
// Add a new entry to the render data and assign a id
ListService.DeleteEntry(id);
// Delete from the rendering data
RenderData.Remove(id);
}
First we remove the data from the database using the service, secondly we remove the actual rendering data.
Finally handle removing the data by assigning the "OnRemoveClicked" callback in the TodoEntryComponent
:
OnRemoveClicked="async () => await OnRemoveEntryClicked(todoEntry.Data.Id)"
That's it! We can now remove entries. With these changes, it is now possible to manipulate to-do entries and persist them while the application is running.
Sample Code
You can access the full code used in this series here: Github - BlazorTodoApp