Build a Markdown editor with Angular
Tags: angular, typescript, css, jiraclone
Published: 2020-09-22 03:42:19

If you notice, the current [jira.trungk18.com][jira] 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](https://dev-to-uploads.s3.amazonaws.com/i/3xjv7u0dr0wbfn8ow6wx.gif)

See [all tutorials for Jira clone](https://trungk18.com/tags/jira-clone/)

## Source code and demo

{% stackblitz angular-markdown-editor-jira view=preview %}

## 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](https://dev-to-uploads.s3.amazonaws.com/i/4ws2ppe2uugzrjvnhw4h.png)

There is not much code inside its module and component.

markdown-editor.component.ts

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

markdown-editor.module.ts

```ts
@NgModule({
  imports: [CommonModule],
  exports: [MarkdownEditorComponent],
  declarations: [MarkdownEditorComponent],
})
export class MarkdownEditorModule {}
```

No worry, we will add more code below.

## Github Markdown Toolbar

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

[@nartc][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

```bash
npm install --save @github/markdown-toolbar-element
```

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

```ts
import '@github/markdown-toolbar-element'
```

Then you can paste the below code into `MarkdownEditorComponent`.

markdown-editor.component.html

```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>
```

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](https://dev-to-uploads.s3.amazonaws.com/i/wia287og0rt7t4bxb7hc.png)

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

```ts
@NgModule({
  //code removed for brevity
  schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
```

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](https://dev-to-uploads.s3.amazonaws.com/i/4sv4l675h8hlgoy345bu.gif)

### 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][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.

```html
<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>
```

```css
$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;
    }
  }
}

```

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.

```html
<textarea id="textarea_id"
          class="text-editor">
</textarea>
```

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.

```css
$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;
  }
}
```

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](https://dev-to-uploads.s3.amazonaws.com/i/z4iiqnw70lpwdlj4ylc6.gif)

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][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][editable-textbox]. I also set the `cdkAutosizeMinRows` to 6 so that it will also have a certain minimum height.

```html
<textarea class="text-editor"
          (focus)="focus()"
          (blur)="blur()"
          [formControl]="control"
          id="MarkdownInput"
          cdkTextareaAutosize
          [cdkAutosizeMinRows]="6">
</textarea>
```

```ts
export class MarkdownEditorComponent implements OnInit {
  @HostBinding('class.focus') isFocus: boolean;
  focus() {
    this.isFocus = true;
  }
  blur() {
    this.isFocus = false;
  }
}
```

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](https://dev-to-uploads.s3.amazonaws.com/i/6e66ktfhsedoed5hupdu.gif)

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.

```ts
export class MarkdownEditorComponent implements OnInit {
  @Input() control: FormControl;
  ngOnInit(): void {
    this.control = this.control ?? new FormControl();
  }
}
```

And bind the control to the component HTML

```html
<textarea id="textarea_id"
          class="text-editor"
          [formControl]="control"
          (focus)="focus()"
          (blur)="blur()"
          cdkTextareaAutosize
          [cdkAutosizeMinRows]="6">
</textarea>
```

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

```ts
@NgModule({
  imports: [
    CommonModule,
    ReactiveFormsModule
  ],
  //code removed for brevity
})
export class MarkdownEditorModule { }
```

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

```ts
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
  }
}
```

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

```html
<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>
```

Sweet, everything seems working as expected.


![Angular Jira Clone Part 06 - Build a markdown text editor](https://dev-to-uploads.s3.amazonaws.com/i/z2rnqq021ir0nqcf0zce.gif)

### 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 :)

```html
<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>
```

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](https://dev-to-uploads.s3.amazonaws.com/i/ddba7y8plkgavpuh2snr.gif)

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!

[jira]: https://jira.trungk18.com/
[nartc]: https://github.com/nartc
[icon]: https://icons.getbootstrap.com/
[editable-textbox]: https://trungk18.com/experience/angular-jira-clone-tutorial-04-editable-textbox/
[text-field]: https://material.angular.io/cdk/text-field/api
[text-editor]: https://github.com/trungk18/jira-clone-angular/blob/feature/gql/apps/jira-clone/src/app/shared/text-editor/text-editor.component.html