DEV Community

Cover image for Build a Markdown editor with Angular
Trung Vo
Trung Vo

Posted on • Edited on • Originally published at trungk18.com

Build a Markdown editor with Angular

If you notice, the current jira.trungk18.com is using an HTML text editor. I am replacing it with a Markdown text editor for the upcoming features of #jiraclone.

In this post, I will guide you through the process of building a Markdown editor with Angular.

That's how a Markdown text editor looks.

Angular Jira Clone Part 06 - Build a markdown text editor

See all tutorials for Jira clone

Source code and demo

Markdown Editor Module

A markdown text editor might be reused in many places on a web application. So that I will create a brand new module MarkdownEditorModule for that purpose. At the moment, it will have only one component MarkdownEditorComponent and it will be exported as well.

Angular Jira Clone Part 06 - Build a markdown text editor

There is not much code inside its module and component.

markdown-editor.component.ts

@Component({
  selector: 'markdown-editor',
  templateUrl: './markdown-editor.component.html',
  styleUrls: ['./markdown-editor.component.css'],
})
export class MarkdownEditorComponent implements OnInit {
  ngOnInit() {}
}
Enter fullscreen mode Exit fullscreen mode

markdown-editor.module.ts

@NgModule({
  imports: [CommonModule],
  exports: [MarkdownEditorComponent],
  declarations: [MarkdownEditorComponent],
})
export class MarkdownEditorModule {}
Enter fullscreen mode Exit fullscreen mode

No worry, we will add more code below.

Github Markdown Toolbar

Install @github/markdown-toolbar-element and use it inside our Angular component

@nartc suggested me to use that package to enable a markdown toolbar. I had a look and really like that tiny package, plus It came from Github itself 😊

To add that to an Angular application, simply run

npm install --save @github/markdown-toolbar-element
Enter fullscreen mode Exit fullscreen mode

Second, you need to import @github/markdown-toolbar-element into MarkdownEditorComponent.

import '@github/markdown-toolbar-element'
Enter fullscreen mode Exit fullscreen mode

Then you can paste the below code into MarkdownEditorComponent.

markdown-editor.component.html

<markdown-toolbar for="textarea_id">
  <md-bold>bold</md-bold>
  <md-header>header</md-header>
  <md-italic>italic</md-italic>
  <md-quote>quote</md-quote>
  <md-code>code</md-code>
  <md-link>link</md-link>
  <md-image>image</md-image>
  <md-unordered-list>unordered-list</md-unordered-list>
  <md-ordered-list>ordered-list</md-ordered-list>
  <md-task-list>task-list</md-task-list>
  <md-mention>mention</md-mention>
  <md-ref>ref</md-ref>
</markdown-toolbar>
<textarea id="textarea_id"></textarea>
Enter fullscreen mode Exit fullscreen mode

Because markdown-toolbar is a custom web element tag and it looks like an Angular component selector. Angular couldn't find the declaration elsewhere, that's why you are seeing that error.

Angular Jira Clone Part 06 - Build a markdown text editor

To fix it, follow the error on the screen to add CUSTOM_ELEMENTS_SCHEMA into the MarkdownEditorModule

@NgModule({
  //code removed for brevity
  schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
Enter fullscreen mode Exit fullscreen mode

Now something is rendering on the UI and the textarea gets updated upon selection on the toolbar, but it hasn't looked good just yet.

Angular Jira Clone Part 06 - Build a markdown text editor

Styling the markdown toolbar

To make styling easier, I set a button with class .btn and wrap the text into a button. I also use Boostrap Icon to make it look like a real toolbar. markdown-editor.component.html is getting pretty long because all of the icons are SVG, I won't paste all of them here. Take a look at one bold icon and you will understand.

<markdown-toolbar for="textarea_id">
  <md-bold>
    <button class="btn">
    <svg width="1em"
          height="1em"
          viewBox="0 0 16 16"
          class="bi bi-type-bold"
          fill="currentColor"
          xmlns="http://www.w3.org/2000/svg">
        <path d="M8.21 13c2.106 0 3.412-1.087 3.412-2.823 0-1.306-.984-2.283-2.324-2.386v-.055a2.176 2.176 0 0 0 1.852-2.14c0-1.51-1.162-2.46-3.014-2.46H3.843V13H8.21zM5.908 4.674h1.696c.963 0 1.517.451 1.517 1.244 0 .834-.629 1.32-1.73 1.32H5.908V4.673zm0 6.788V8.598h1.73c1.217 0 1.88.492 1.88 1.415 0 .943-.643 1.449-1.832 1.449H5.907z" />
    </svg></button>
  </md-bold>
  <!-- code removed for brevity -->
</markdown-toolbar>
Enter fullscreen mode Exit fullscreen mode
$hover-color: #06c;

markdown-toolbar {
  padding: 8px;

  .btn {
    background: none;
    border: none;
    cursor: pointer;
    display: inline-block;
    height: 24px;
    padding: 3px 5px;
    width: 28px;
    color: #222;

    i {
      display: flex;
    }

    &:hover {
      color: $hover-color;
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

After styling the textarea as below, you will see a quite satisfying result 😊

Styling the textarea

I will do the styling for the textarea as well.

First, I assign a class text-editor to that textarea.

<textarea id="textarea_id"
          class="text-editor">
</textarea>
Enter fullscreen mode Exit fullscreen mode

For the CSS, I wanted:

  • No border for textarea
  • Have border for the container around the markdown toolbar and textarea
  • On hovering the textarea, set a different border color for the container

I hope my CSS will express itself :) But if you have any questions about CSS, let me know in the comment box below.

$border-color: #d9d9d9;

:host {
  border: 1px solid $border-color;
  box-shadow: 0 0 0 1px $border-color;
  border-radius: 3px;
  outline: none;
  background: #fff;
  display: flex;
  flex-direction: column;  

  .text-editor {
    padding-left: 15px;
    padding-right: 15px;
    resize: none;
    border-color: transparent;
    width: 100%;
    overflow-y: hidden;

    &:focus {
      outline: none;
      border: transparent;
    }
  }

  &.focus {
    border: 1px solid $hover-color;
    box-shadow: 0 0 0 1px $hover-color;
  }
}
Enter fullscreen mode Exit fullscreen mode

I have a result now, looks pretty good. But the border color didn't change when I select the textarea.

Angular Jira Clone Part 06 - Build a markdown text editor

Why? Because we need to set an extra class to the parent of the textarea. We need to:

  • Handle focus event of the textarea to add a class named .focus to the parent container.
  • Also handle blur event to remove this class from the parent container.

I also added cdkTextareaAutosize from @angular/cdk/text-field package to make the textarea auto-expand its height when the content is too long. By default, textarea will have a scrollbar visible and won't auto expand. See more on my previous tutorial - Build an editable textbox. I also set the cdkAutosizeMinRows to 6 so that it will also have a certain minimum height.

<textarea class="text-editor"
          (focus)="focus()"
          (blur)="blur()"
          [formControl]="control"
          id="MarkdownInput"
          cdkTextareaAutosize
          [cdkAutosizeMinRows]="6">
</textarea>
Enter fullscreen mode Exit fullscreen mode
export class MarkdownEditorComponent implements OnInit {
  @HostBinding('class.focus') isFocus: boolean;
  focus() {
    this.isFocus = true;
  }
  blur() {
    this.isFocus = false;
  }
}
Enter fullscreen mode Exit fullscreen mode

What the HostBinding does is check if isFocus is true, then Angular will add a class name focus to the component selector. It look like <markdown-editor class="focus. If the value is false, then remove this class then.

Angular Jira Clone Part 06 - Build a markdown text editor

I think we are almost there, looks excellent now. The last thing is to connect this component with a form.

Link the markdown editor component to a form

Usually, the Markdown editor will be used into a form with some additional form input and you wanted to see its value in the form instance.

To do it, simply set the MarkdownEditorComponent to accept an input which is a FormControl. So that the control can be passed into the component from the parent component form instance.

The component will initial a default FormControl if there is no input passed.

export class MarkdownEditorComponent implements OnInit {
  @Input() control: FormControl;
  ngOnInit(): void {
    this.control = this.control ?? new FormControl();
  }
}
Enter fullscreen mode Exit fullscreen mode

And bind the control to the component HTML

<textarea id="textarea_id"
          class="text-editor"
          [formControl]="control"
          (focus)="focus()"
          (blur)="blur()"
          cdkTextareaAutosize
          [cdkAutosizeMinRows]="6">
</textarea>
Enter fullscreen mode Exit fullscreen mode

To be able to do that, you have to import ReactiveFormsModule into MarkdownEditorModule

@NgModule({
  imports: [
    CommonModule,
    ReactiveFormsModule
  ],
  //code removed for brevity
})
export class MarkdownEditorModule { }
Enter fullscreen mode Exit fullscreen mode

To test it with a form, I will create a simple form with two inputs by FormBuilder:

  • Title as the normal textbox
  • Description as the markdown editor
export class AppComponent implements OnInit {
  form: FormGroup;
  constructor(private _fb: FormBuilder) {}

  ngOnInit() {
    this.form = this._fb.group({
      title: ["Hello, I am Trung", Validators.required],
      description: ["This is a markdown text editor for - http://jira.trungk18.com/"]
    });
  }

  get descriptionControl(){
    return this.form.controls.description as FormControl
  }
}
Enter fullscreen mode Exit fullscreen mode

I also get the description control from my form and then send it to the MarkdownEditorComponent

<form [formGroup]="form">
  <div class="form-group">
    <label for="Title">Title</label>
    <input formControlName="title" class="form-control" id="Title" aria-describedby="Title">
  </div>
  <div class="form-group">
    <label>Description</label>
    <markdown-editor [control]="descriptionControl"></markdown-editor>
  </div>
</form>
<div class="alert alert-info">
  {{ form.value | json }}
</div>
Enter fullscreen mode Exit fullscreen mode

Sweet, everything seems working as expected.

Angular Jira Clone Part 06 - Build a markdown text editor

Accessibility

Last but not least, remember to add the aria-label and title for all of the icons. Otherwise, If users are not familiar with the text edit icon, they might find it difficult to understand the meaning. The aria-label is for people with disability can have an easy navigation through your website :)

<markdown-toolbar for="textarea_id">
  <md-bold>
    <button class="btn" title="Bold" aria-label="Bold">
    <svg width="1em"
          height="1em"
          viewBox="0 0 16 16"
          class="bi bi-type-bold"
          fill="currentColor"
          xmlns="http://www.w3.org/2000/svg">
        <path d="M8.21 13c2.106 0 3.412-1.087 3.412-2.823 0-1.306-.984-2.283-2.324-2.386v-.055a2.176 2.176 0 0 0 1.852-2.14c0-1.51-1.162-2.46-3.014-2.46H3.843V13H8.21zM5.908 4.674h1.696c.963 0 1.517.451 1.517 1.244 0 .834-.629 1.32-1.73 1.32H5.908V4.673zm0 6.788V8.598h1.73c1.217 0 1.88.492 1.88 1.415 0 .943-.643 1.449-1.832 1.449H5.907z" />
    </svg></button>
  </md-bold>
  <!-- code removed for brevity -->
</markdown-toolbar>
Enter fullscreen mode Exit fullscreen mode

Now when you hover over the icon for sometimes, the browser will display the title.

Angular Jira Clone Part 06 - Build a markdown text editor

That's all for building a Markdown Editor with Angular. Any questions, you can leave it on the comment box below or reach me on Twitter. Thanks for stopping by!

Top comments (2)

Collapse
 
amplanetwork profile image
Ampla Network • Edited

Good starting point for a Markdown editor.
Thank you. I will try with Marks rendering engine.

Collapse
 
trungk18 profile image
Trung Vo

Thanks for stopping by! Let me know if it is working for you or if you have any problems. Cheers 🍺