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

Todo 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

Related Content
Author
I’m a passionate full-stack software engineer, architect and Debian lover.