
November 19, 2025 08:11 by
Peter
Advanced text-editing features, such as inline formatting, auto-save drafts, mentions, and collaboration-ready structures, are becoming more and more necessary for modern workplace applications. Building your own lightweight Angular-based rich text editor gives you complete control, extensibility, and minimal dependency risk, whereas many teams rely on bulky third-party WYSIWYG editors.

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);
}
}
ANGULAR IMPLEMENTATION (FULL MODULE)
1. Module + Routing
editor.module.ts
@NgModule({
declarations: [
EditorComponent,
MentionDropdownComponent
],
imports: [
CommonModule,
FormsModule,
RouterModule.forChild([
{ path: '', component: EditorComponent }
])
]
})
export class EditorModule {}
2. Editor Component UI
editor.component.html
<div class="toolbar">
<button (click)="format('bold')">B</button>
<button (click)="format('italic')">I</button>
<button (click)="format('underline')">U</button>
</div>
<div class="editor-container">
<div class="editor"
contenteditable="true"
(keyup)="onKeyUp($event)"
(click)="closeMention()"
#editor>
</div>
<app-mention-dropdown
*ngIf="showMention"
[items]="mentionResults"
(select)="insertMention($event)">
</app-mention-dropdown>
</div>
3. Editor Component TS
editor.component.ts
export class EditorComponent implements OnInit, OnDestroy {
@ViewChild('editor') editor!: ElementRef;
autoSaveInterval: any;
showMention = false;
mentionResults: any[] = [];
mentionText = "";
cursorPos = 0;
constructor(private api: EditorService, private users: UsersService) {}
ngOnInit(): void {
this.loadDraft();
this.startAutoSave();
}
format(cmd: string) {
document.execCommand(cmd, false, '');
}
onKeyUp(event: KeyboardEvent) {
const value = this.getContent();
const char = event.key;
if (char === '@') {
this.showMention = true;
this.mentionText = "";
}
if (this.showMention && char !== '@') {
this.mentionText += char;
this.searchUsers();
}
}
searchUsers() {
this.users.search(this.mentionText).subscribe(res => {
this.mentionResults = res;
});
}
insertMention(user: any) {
document.execCommand("insertHTML", false, `<span class='mention'>@${user.displayName}</span> `);
this.showMention = false;
}
getContent() {
return this.editor.nativeElement.innerHTML;
}
loadDraft() {
this.api.getDraft(1).subscribe(r => {
if (r.content) this.editor.nativeElement.innerHTML = r.content;
});
}
startAutoSave() {
this.autoSaveInterval = setInterval(() => {
const content = this.getContent();
this.api.saveDraft({ documentId: 1, content }).subscribe();
}, 3000);
}
ngOnDestroy() {
clearInterval(this.autoSaveInterval);
}
closeMention() {
this.showMention = false;
}
}
4. Mention Dropdown Component
mention-dropdown.component.html
<div class="mention-box">
<div *ngFor="let u of items" (click)="onSelect(u)">
{{u.displayName}}
</div>
</div>
mention-dropdown.component.ts
@Component({
selector: 'app-mention-dropdown',
templateUrl: './mention-dropdown.component.html'
})
export class MentionDropdownComponent {
@Input() items: any[] = [];
@Output() select = new EventEmitter<any>();
onSelect(u: any) {
this.select.emit(u);
}
}
5. Services
editor.service.ts
@Injectable({ providedIn: 'root' })
export class EditorService {
constructor(private http: HttpClient) {}
saveDraft(model: any) {
return this.http.post('/api/editor/saveDraft', model);
}
getDraft(id: number) {
return this.http.get<any>(`/api/editor/getDraft/${id}`);
}
}
users.service.ts
@Injectable({ providedIn: 'root' })
export class UsersService {
constructor(private http: HttpClient) {}
search(q: string) {
return this.http.get<any[]>(`/api/users/search?q=${q}`);
}
}
UI Styling (CSS)
editor.component.css
.toolbar button {
margin-right: 6px;
padding: 4px;
}
.editor {
min-height: 300px;
border: 1px solid #ccc;
padding: 12px;
border-radius: 5px;
}
.mention {
background: #e3f2fd;
color: #0277bd;
padding: 2px 4px;
border-radius: 4px;
}
.mention-box {
position: absolute;
background: white;
border: 1px solid #ccc;
width: 200px;
z-index: 999;
border-radius: 4px;
}
.mention-box div {
padding: 6px;
cursor: pointer;
}
.mention-box div:hover {
background: #f1f1f1;
}