Ngx Markdown Editor is a Angular library providing a WYSIWYG markdown editor, which is especially intended for users unfamiliar with the Markdown syntax. However, it is also well-suited for advanced users as it provides efficient ways to write Markdown, e.g. by using shortcuts or utilizing a preview-like markup theme to get immediate visual response of how the result will look like.
In addition, this markdown editor provides high extensibility and customizability as well as broad and simple options for internationalization.
Last but not least, by containing an opt-in material theme, this component will perfectly fit into your Angular Material application. If you do not use Angular Material you can easily integrate your own theme.
Demo available @ https://mdefy.github.io/ngx-markdown-editor/
MarkdownEditorComponent
This library depends on Markdown Editor Core and Ngx Markdown.
Markdown Editor Core is a JS library based on CodeMirror and was developed together with Ngx Markdown Editor. It provides the text editor and an extensive API for markdown-related actions and everything required to interact conveniently with the editor.
Ngx Markdown is used to provide a preview feature, that renders the Markdown text written in the editor.
Run
npm i @mdefy/ngx-markdown-editor
or
yarn add @mdefy/ngx-markdown-editor
Include MarkdownEditorModule
into your Angular module and include <ngx-markdown-editor></ngx-markdown-editor>
into your HTML template.
Make sure to load Material Font, e.g. the header of your index.html
file:
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
In order to use the material theme of Ngx Markdown Editor in combination with your global material theme (especially Angular Material), import the theme file into your styles.scss
and include the material mixin, where you should pass your app's primary color.
You can select from different material styles like in Angular Material's MatFormField
using the materialStyle
input property.
@import '~ngx-markdown-editor/themes/material';
@include mde-material(mat-color($your-primary-color));
In order to set specific dimension for Ngx Markdown Editor, simply apply any of the dimensional CSS properties to the component. Example:
ngx-markdown-editor {
min-height: 100px;
max-height: 500px;
}
In general, all configuration of the editor can be done dynamically through input bindings. However, as Ngx Markdown Editor utilizes the Ngx Markdown library for its preview feature and as the latter can be configured statically via NgModule import, MarkdownEditorModule
also implements Angular's forRoot()
and forChild()
paradigma to make Ngx Markdown's configuration options available. For this, import MarkdownEditorModule
as follows:
@NgModule({
imports: [
NgxMarkdownEditorModule.forRoot({
previewConfig: {
sanitize: ...
markedOptions: ...
}
})
]
})
For detailed instructions how to configure Ngx Markdown, visit its dedicated documentation section on GitHub.
Note, that we do not forward the loader
option of MarkdownModuleConfig
, as the [src]
property of <ngx-markdown>
is irrelevant for us.
If you like to use the same configuration for other MarkdownEditorComponent
instances in sub modules, import the module with
@NgModule({
imports: [ NgxMarkdownEditorModule.forChild() ]
})
Due to the fact that the
MarkdownService
Ngx Markdown is a singleton object, we cannot provide different preview configurations within one application so far. The most possible is to injectMarkdownService
an make adjustments at run-time (see ngx-markdown#177). Apart from that, however, there are no dependencies between different Ngx Markdown Editor instances.
Input | Description | Default value |
---|---|---|
data: string |
Data string to set as content of the editor. | '' |
options: Options |
Mainly options from Markdown Editor
Core, including some adjustments. To update options at runtime, merge the old options object with the new options before applying the changes: this.options = { ...this.options, optionToUpdate: updateValue } .
|
{} |
toolbar: ToolbarItemDef[] |
Toolbar configuration. Can contain names of predefined items or objects of custom items. | See toolbar section. |
statusItems: StatusbarItemDef[] |
Statusbar configuration. Can contain names of predefined items or objects of custom items. | See statusbar section. |
showTooltips: boolean |
Specifies whether tooltips are shown for toolbar items. | true |
shortcutsInTooltips: boolean |
Specifies whether the applied keyboard shortcuts are included in the tooltips of toolbar items. | true |
materialStyle: boolean | 'standard' | 'fill' | 'legacy' |
Specifies whether or which Angular Material style is applied. | false ; for true , the default style is standard |
label: string | undefined |
The label text for the editor component. The label area is hidden, if no label is specified. | undefined |
disabled: boolean |
Specifies whether the editor is disabled. In the disabled mode, the preview with rendered markdown is shown instead of the editor area. | false |
showToolbar: boolean |
Specifies whether the toolbar is rendered. | true |
showStatusbar: boolean |
Specifies whether the statusbar is rendered. | true |
required: boolean |
Specifies whether the editor component is a required field. If true , an asterisk (*) is added to the label. (Apart from that, this has no other effect.) |
false |
language: LanguageTag |
Specifies the current language applied to all internationalizable properties. | en |
The most important Codemirror events are transformed to Angular outputs to facilitate event binding.
Output | Description |
---|---|
contentChange: ObservableEmitter<{ instance: Editor; changes: EditorChangeLinkedList[] }> |
Emits when the editor's content changes. |
curserActivity: ObservableEmitter<{ instance: Editor }> |
Emits when the cursor is moved. |
editorFocus: ObservableEmitter<{ instance: Editor; event: FocusEvent }> |
Emits when the editor receives focus. |
editorBlur: ObservableEmitter<{ instance: Editor; event: FocusEvent }> |
Emits when the editor loses focus. |
The toolbar is highly configurable and comes with many built-in items. You can simply
If you do not specify anything at all for the toolbar
input property, the default toolbar setup is applied.
The toolbar
input property in an array of type
type ToolbarItemDef = ToolbarItemName | ToolbarItem;
ToolbarItemName
is a union type of all built-in item names. The interface ToolbarItem
represents a full toolbar item.
interface ToolbarItem {
name: string;
action?: (...args: any[]) => void;
shortcut?: string;
isActive?: (...args: any[]) => boolean | number;
tooltip?: OptionalI18n<string>;
icon?: OptionalI18n<Icon>;
disableOnPreview?: boolean;
}
For details about the
OptionalI18n<T>
type, see Internationalization section.
In the following, we always apply a JavaScript variable to the toolbar
input property:
<ngx-markdown-editor [toolbar]="toolbar"></ngx-markdown-editor>
To build a toolbar from existing items, simply create an array of type ToolbarItemName[]
(or ToolbarItemDef[]
) and specify the items by name.
Additionally, there is a separator element, which you can insert at any position with '|'
.
public toolbar: ToolbarItemName[] = ['toggleBold', 'toggleItalic', '|', 'insertLink', '|', 'openMarkdownGuide'];
The naming convention for items is to use the name of the function that is triggered by the item.
You can adjust built-in items for you needs by defining an item with a name included in ToolbarItemName
. You do not have to specify
all item properties, but can simply adjust only a subset of them, the rest will keep their default values.
For example, if you want to give the toggleBold
item a new shortcut (default: Ctrl-B
) as well as change the tooltip, then proceed as following:
const newToggleBoldItem: ToolbarItem = {
name: 'toggleBold',
shortcut: 'Alt-B',
tooltip: 'Bold you shall be',
};
Then include this object into the toolbar item array (may be alongside ToolbarItemName
s):
public toolbar: ToolbarItemDef[] = [newToggleBoldItem, 'toggleItalic', ...];
This is very similar to configuring an existing item. Example:
const myItem: ToolbarItem = {
name: 'myCustomAction',
action: () => myCustomAction()
shortcut: 'Alt-B',
tooltip: 'Hint: No need to hard code the shortcut here, see input `shortcutsInTooltips`',
icon: {
format: 'material'
iconName: 'star'
}
};
Again you only have to include the properties you want to explicitly specify (except the obligatory name
property), all other item properties will be "as empty as possible" per default:
const defaultItem: ToolbarItem = {
name: '',
action: () => {},
shortcut: undefined,
isActive: undefined,
tooltip: '',
icon: { format: 'material', iconName: '' },
disableOnPreview: false,
};
Note, that although there is the built-in
setHeadingLevel
dropdown item, so far only custom button items can be constructed in the described way (setHeadingLevel
is implemented as a special case). This should satisfy most cases. If you require other items as well, you are welcome to fork this repo and/or make a pull request.
The default keymap is as follows (on Mac "Ctrl" is replaced with "Cmd"):
Action | Shortcut |
---|---|
setHeadingLevel |
Shift-Ctrl-Alt-H |
toggleHeadingLevel |
Alt-H |
increaseHeadingLevel |
Alt-H |
decreaseHeadingLevel |
Shift-Alt-H |
toggleBold |
Ctrl-B |
toggleItalic |
Ctrl-I |
toggleStrikethrough |
Ctrl-K |
toggleUnorderedList |
Ctrl-L |
toggleOrderedList |
Shift-Ctrl-L |
toggleCheckList |
Shift-Ctrl-Alt-L |
toggleQuote |
Ctrl-Q |
toggleInlineCode |
Ctrl-7 |
insertCodeBlock |
Shift-Ctrl-7 |
insertLink |
Ctrl-M |
insertImageLink |
Shift-Ctrl-M |
insertTable |
Ctrl-Alt-T |
insertHorizontalRule |
Shift-Ctrl-- |
toggleRichTextMode |
Alt-R |
formatContent |
Alt-F |
downloadAsFile |
Shift-Ctrl-S |
importFromFile |
Ctrl-Alt-I |
togglePreview |
Alt-p |
toggleSideBySidePreview |
Shift-Alt-P |
undo |
Ctrl-Z |
redo |
Ctrl-Y, Shift-Ctrl-Z |
openMarkdownGuide |
F1 |
For shortcuts that come built-in with CodeMirror, see CodeMirror documentation.
The primary to configure single shortcuts alongside with other item properties is to use the toolbar
configuration as described in the toolbar section.
However, if you want to customize keyboard shortcuts of a lot of (built-in) items you may also do this inside the options: Options
input property with options.shortcuts = {...}
. This is a decent alternative as you can specify many keybindings in a single object. Attention: Shortcuts defined in options.shortcuts
will override shortcuts specified in toolbar
.
When specifying custom shortcuts, mind the correct order of special keys: Shift-Cmd-Ctrl-Alt (see here).
As per default, keyboard shortcuts are always functioning for all built-in toolbar items, even when they are not included into the visible toolbar, in order to enable users to efficiently write Markdown.
However, you can configure this behavior inside the options: Options
input property with options.shortcutsEnabled
.
You can either disable shortcuts completely (shortcutsEnabled: 'none'
) or only enable them for items included in the toolbar or specified in options.shortcuts
(shortcutsEnabled: 'customOnly'
).
Icons can be specified in multiple ways. The easiest is to use an icon included in the Material icon font.
const item: Icon = {
format: 'material',
iconName: 'thumb_up',
};
But you can also use your own SVG icons by either specifying the icon's file path (location at runtime) or by including an SVG string into your TypeScript file:
const item: Icon = {
format: 'svgFile',
iconName: 'my_icon',
runTimePath: './path/to/icon.svg',
};
or
const item: Icon = {
format: 'svgFile',
iconName: 'my_icon',
svgHtmlString: '<svg viewBox="0 0 20 20"> <circle r="10" /> </svg>',
};
Depending on the format of your icon you might need to adjust the icon via CSS, e.g. similar to
.mat-button .mat-icon[data-mat-icon-name='my_icon'] {
height: 16px;
...;
}
Configuring the statusbar is very similar to configuring the toolbar, only simpler as there are only two properties for an item.
The statusbar
input property is an array of type
type StatusbarItemDef = StatusbarItemName | StatusbarItem;
StatusbarItemName
is a union type of all built-in item names. The interface StatusbarItem
represents a full statusbar item.
interface StatusbarItem {
name: string;
value: OptionalI18n<Observable<string>>;
}
The value
of an item is a observable (or an internationalized version of it), which will be observed by the statusbar.
For details about the
OptionalI18n<T>
type, see Internationalization section.
In the following, we always apply a JavaScript variable to the toolbar
input property:
<ngx-markdown-editor [toolbar]="toolbar"></ngx-markdown-editor>
To build a statusbar from existing items, simply create an array of type StatusbarItemName[]
(or StatusbarItemDef[]
) and specify the items by name.
Additionally, there is a separator element, which you can insert at any position with '|'
.
public statusbar: ToolbarItemName[] = ['wordCount', 'characterCount', '|', 'cursorPosition'];
The naming convention for items is to use the name of the subject / value that is displayed.
You can adjust built-in items for you needs by defining an item with a name included in ToolbarItemName
. However, this might only make sense if you want to keep an existing item name and implement internationalization for it, as this is almost the same as creating a new item. For this reason, we omit an example here and refer to the section below.
To create a custom statusbar item, simply define a new object of type StatusbarItem
:
const myItem: ToolbarItem = {
name: 'myValue',
value: of('static string'),
};
Then include this object into the statusbar item array (maybe alongside StatusbarItemName
s):
public toolbar: ToolbarItemDef[] = [newToggleBoldItem, 'toggleItalic', ...];
Ngx Markdown Editor provides opt-in internationalization for many objects like tooltips and even icons.
To realize this, a generic type named OptionalI18n<T>
was implemented:
type OptionalI18n<T> = T | ({ default: T } & { [lang in LanguageTag]?: T });
This enables you to specify either the same object for all languages (not using internationalization for this specific object)
or apply an i18n object for only those language you need. If you use the internationalized version, you are required
to define a default
value, that is applied for all languages you do not specify explicitly.
To summarize: you can go exactly as far with internationalization as you want to with this component.
Theming is an important issue when using third party components to integrate them smoothly into your application. Therefore, Ngx Markdown Editor provides default themes as well as an easy way to apply your own theme.
You can style every element inside <ngx-markdown-editor>
with CSS as the component does not use
view encapsulation (ViewEncapsulation.None
). You can also predefine different themes and apply them dynamically.
The default theme is named default
. Alternatively you can use the predefined material
theme and choose from different styles as described in the Getting started section.
To customize the editor's appearance for your needs, use options.editorThemes
.
The theme name specified here, will be applied as is to <ngx-markdown-editor>
as well as be applied with a cm-s-
prefix to the <div class="CodeMirror">
element.
For further details on CodeMirror's theming, visit the dedicated section on CodeMirror.
To apply a customized theme with the name "example"
{ editorThemes: ['example']}
in the options
input property,ngx-markdown-editor.example
in a CSS file,.cm-s-example
to style the CodeMirror element, andIf you only want to extend the default theme, you can either define new stylings for the classes ngx-markdown-editor.default
and .cm-s-default
and make sure that the "default" theme is applied or you can create your own additional theme and specify two themes in the options: { editorThemes: ['default', 'additional-theme'] }
.
Again, the default theme is named default
here, which applies the default markup styling from the gfm
CodeMirror mode.
Additionally there is a predefined theme preview-like-markup
which imitates the styling of Ngx Markdown's default styling.
This makes the markup look as similar to the preview as possible.
To customize the markup styling, use options.markupThemes
.
The theme specified here is only applied to the CodeMirror element <div class="CodeMirror"></div>
.
For detailed instructions how to define your own markup styling, visit the section on Markdown Editor Core.
You can either set the content using the input property data
(one-way binding, not like ngModel
):
<ngx-markdown-editor [data]="'Content changes whenever this input changes'"></ngx-markdown-editor>
Or you can do it using the MarkdownEditorComponent
instance:
@ViewChild(MarkdownEditorComponent) ngxMde: MarkdownEditorComponent;
this.ngxMde.mde.setContent('Any _Markdown_ **string**, where lines are separated with a new line character "\n".');
The CodeMirror instance is accessible through the Markdown Editor Core instance, which is in turn publicly accessible in MarkdownEditorComponent
.
@ViewChild(MarkdownEditorComponent) ngxMde: MarkdownEditorComponent;
const cm: CodeMirror.Editor = this.ngxMde.mde.cm;
MarkdownEditorComponent
With the utility function fromCmEvent
, Ngx Markdown Editor provides a convenient way to convert a CodeMirror event to an RxJS Observable
.
@ViewChild(MarkdownEditorComponent) ngxMde: MarkdownEditorComponent;
const eventObs: Observable<{...}> = fromCmEvent(this.ngxMde.mde.cm, 'mousedown');
eventObs.subscribe((instance, ...) => myEventHandler());
First of all, contributions in any way are very welcome! And a big thank you to all who decide to so!! :)
The code is neither perfect nor complete. If you have any suggestions, requirements or even just comments, please let me know and I will do my best do incorporate them! The even better (and probably faster) way for requesting code modifications, however, are pull requests. I am very happy about all code contributions as time is often rare around here... :)
Before you open an issue, please have one closer look if this is really an issue of Ngx Markdown Editor or if it rather belongs to Markdown Editor Core.
When writing issues, please give a clear description of the current state and what you are unhappy about. Then, if possible, propose your solution or at least leave a short statement of your thoughts about it.
Recipe for making a pull request:
npm i yarn -g
.yarn
to install all dependencies.ng build ngx-markdown-editor --watch
to build the library in watch mode.ng serve
to test your changes in the demo app.This project uses Yarn as package manager. So you must use this one to install dependencies when contributing code. The scripts in package.json still work with npm
, although it is recommended to always use yarn
throughout the project.
We use Commitlint to guarantee structured commit messages. This means
you must write commit messages that meet the rules of Commitlint. If you are not familiar with
Commitlint, you can use the CLI tool Commitizen by running yarn run commit
, which assists you to
write conventional messages. You can also install Commitizen globally on your system, if you want to use the shorter cli commands cz
or git cz
.
There are not many strict guidelines to keep in mind, but please adapt to the project's code style when contributing. Only one more thing shall be mentioned here:
We use Prettier to ensure consistent formatting. Therefore, you should install a Prettier plugin for your IDE. Further it is highly recommended to enable "Format on save", which is also set as the project's default for VSCode.
There is a pre-commit git hook for Prettier, which checks the formatting of all files. Occasionally
it might happen that this hook fails although you have "Format on save" enabled. This is usually
due to wrong line endings, e.g. caused by yarn add ...
or some other file-writing script or tool.
In this case, run yarn run format:write
to let Prettier correct the wrong formatting and then try to commit again. Unfortunately,
the format:write
command cannot be set as a pre-commit hook as it is not known in general, which
files need to be staged afterwards.