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>&nbsp;`);
    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;
}