Modern enterprise applications increasingly require advanced text-editing features—inline formatting, auto-save drafts, mentions, and collaboration-ready structures. While many teams rely on heavy third-party WYSIWYG editors, building your own lightweight Angular-based rich text editor gives you full control, extensibility, and zero dependency risk.
This article walks you through creating a custom rich text editor with:
✔ Bold/Italic/Underline, Headings
✔ Auto-Save every X seconds
✔ Restore draft on reload
✔ Mention support (@user) with dropdown
✔ Save content to SQL Server
✔ ASP.NET Core API for managing drafts and users
System Architecture (High-Level)
Angular App
├── RichTextEditor Module
│ ├── Editor UI (contenteditable)
│ ├── Auto-Save Service (interval-based)
│ ├── Mention Service (user lookup)
│
├── API Service (HTTP)
│
.NET Core API
├── /api/editor/saveDraft
├── /api/editor/getDraft
├── /api/users/search
│
SQL Server
├── DocumentDrafts
├── Users
DATABASE SCRIPT (SQL Server)
1. Users Table
CREATE TABLE Users (
UserId INT IDENTITY PRIMARY KEY,
DisplayName VARCHAR(100),
Email VARCHAR(150)
);
2. DocumentDrafts Table
CREATE TABLE DocumentDrafts (
DraftId INT IDENTITY PRIMARY KEY,
DocumentId INT NOT NULL,
Content NVARCHAR(MAX),
LastSaved DATETIME DEFAULT GETDATE()
);
.NET BACKEND (ASP.NET Core 8 API)
1. Model
public class DocumentDraft
{
public int DocumentId { get; set; }
public string Content { get; set; }
}
2. Controller: EditorController.cs
[ApiController]
[Route("api/[controller]")]
public class EditorController : ControllerBase
{
private readonly IConfiguration _config;
public EditorController(IConfiguration config)
{
_config = config;
}
[HttpPost("saveDraft")]
public async Task<IActionResult> SaveDraft([FromBody] DocumentDraft draft)
{
using SqlConnection con = new(_config.GetConnectionString("Default"));
using SqlCommand cmd = new("IF EXISTS (SELECT 1 FROM DocumentDrafts WHERE DocumentId=@Id)
UPDATE DocumentDrafts SET Content=@C, LastSaved=GETDATE() WHERE DocumentId=@Id
ELSE
INSERT INTO DocumentDrafts(DocumentId, Content) VALUES(@Id, @C)", con);
cmd.Parameters.AddWithValue("@Id", draft.DocumentId);
cmd.Parameters.AddWithValue("@C", draft.Content);
con.Open();
await cmd.ExecuteNonQueryAsync();
return Ok(new { message = "Draft saved" });
}
[HttpGet("getDraft/{documentId}")]
public async Task<IActionResult> GetDraft(int documentId)
{
using SqlConnection con = new(_config.GetConnectionString("Default"));
using SqlCommand cmd = new("SELECT Content FROM DocumentDrafts WHERE DocumentId=@Id", con);
cmd.Parameters.AddWithValue("@Id", documentId);
con.Open();
string? content = (string?)await cmd.ExecuteScalarAsync();
return Ok(new { content });
}
}
3. UsersController.cs for mentions
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
private readonly IConfiguration _config;
public UsersController(IConfiguration config)
{
_config = config;
}
[HttpGet("search")]
public async Task<IActionResult> Search([FromQuery] string q)
{
using SqlConnection con = new(_config.GetConnectionString("Default"));
using SqlCommand cmd = new("SELECT TOP 10 UserId, DisplayName FROM Users WHERE DisplayName LIKE @Q + '%'", con);
cmd.Parameters.AddWithValue("@Q", q);
con.Open();
List<object> users = new();
using SqlDataReader dr = await cmd.ExecuteReaderAsync();
while (await dr.ReadAsync())
{
users.Add(new
{
UserId = dr.GetInt32(0),
DisplayName = dr.GetString(1)
});
}
return Ok(users);
}
}

Join the conversation! Your thoughts help the community grow.