WYSIWYG markdown Editor 🍼 Milkdown for Angular out of box, only supports Angular 17+. Allow you to use native Angular components to create nodeView/pluginView/widgetView, and provide corresponding examples.
You can run this example by:
git clone https://github.com/ousc/ng-milkdown.git
cd ng-milkdown
npm install
npm run start
https://ousc.github.io/ng-milkdown
Angular adapter for ProseMirror, only supports Angular 17+.
https://github.com/ousc/ng-prosemirror-adapter
theme-nord
(preset)preset-commonmark
(preset)plugin-listener
(preset)preset-gfm
(supported)plugin-history
(supported)plugin-prism
(supported)plugin-clipboard
(supported)plugin-cursor
(supported)plugin-math
(supported)plugin-block
(supported)plugin-indent
(supported)plugin-tooltip
(supported)plugin-slash
(supported)plugin-diagram
(supported)plugin-emoji
(supported)plugin-cursor
(supported)plugin-trailing
(supported)plugin-upload
(supported)plugin-collab
(supported)plugin-copilot
(supported)usage of plugins can be found in example;
npm install ng-milkdown ng-prosemirror-adapter @milkdown/core @milkdown/ctx @milkdown/plugin-listener @milkdown/preset-commonmark @milkdown/theme-nord
<ng-milkdown-provider>
<ng-milkdown
[config]="config"
[plugins]="plugins"
[(ngModel)]="value"
[(loading)]="loading"
[spinner]="spinner"
(ngModelChange)="onChange($event)"
(onReady)="editor = $event"
/>
</ng-milkdown-provider>
import {NgMilkdownProvider} from "./ng-milkdown-provider.component";
const tooltip = tooltipFactory('my-tooltip')
const slash = slashFactory('my-slash')
@Component({...})
export class WorkGroundComponent {
@ViewChild(NgMilkdownProvider, {static: true}) provider: NgMilkdownProvider;
config = (ctx: any) => {
ctx.set(editorViewOptionsCtx, {
attributes: {
class: "prose dark:prose-invert outline-none mx-auto px-2 py-4 box-border milkdown-theme-nord editor",
spellcheck: "false",
},
});
}
plugins: NgMilkdownPlugin[] = [
gfm,
history,
prism,
clipboard,
cursor,
math,
emoji,
[
diagram, // diagram plugin
$view(diagramSchema.node, () =>
this.provider.createNodeView({ // create node view for diagram node
component: Diagram,
stopEvent: () => true,
})
)
],
$view(listItemSchema.node, () =>
this.provider.createNodeView({component: ListItem}) // create node view for list item node
),
{
plugin: block,
config: ctx => {
ctx.set(block.key, {
view: this.provider.createPluginView({ // create plugin view for block plugin
component: BlockComponent,
inputs: {ctx}
})
});
}
},
$provide(linkPlugin), // $provide is an alias of `provider => MilkdownPlugin`, allow you create your own plugin without waiting for `provider` initialization
{
plugin: indent,
config: ctx => {
ctx.set(indentConfig.key as any, { // set indent config
type: 'space',
size: 4,
});
}
},
{
plugin: tooltip,
config: ctx => {
ctx.set(tooltip.key, {
view: this.provider.createPluginView({component: ImageTooltipComponent}) // create plugin view for tooltip plugin
})
}
},
{
plugin: slash,
config: ctx => {
ctx.set(slash.key, {
view: this.provider.createPluginView({component: SlashComponent}) // create plugin view for slash plugin
})
}
}
];
value = 'Hello, World!';
editor: Editor;
onChange(markdownText: string) {
console.log({markdownText});
}
}
Property | Description | Type | Default |
---|---|---|---|
[classList] |
editor element class names | string[] |
[] |
[config] |
config before Editor.create() | NgMilkdownEditorConfig |
(ctx: Ctx) => void 0 |
[plugins] |
milkdown plugin to use | NgMilkdownPlugin[] |
[] |
[editor] |
pass in a fully controlled editor object | (HTMLElement) => Editor |
- |
[loading] |
set the loading status of editor | boolean |
true |
[spinner] |
custom spinner | TemplateRef<any> |
- |
[ngModel] |
current value , double binding | DefaultValue |
- |
(ngModelChange) |
callback when markdown change | EventEmitter<string> |
- |
(onReady) |
A callback function, can be executed when editor has bean created | Editor |
- |
@Component({
template: `
<button (click)="setBold($event)">
Bold
</button>
`,
...
})
export class ImageTooltipComponent extends NgMilkdownTooltip {
setBold(e: MouseEvent) {
e.preventDefault();
this.action(callCommand(toggleStrongCommand.key));
}
}
@Component({
template: `
@for (item of list;track item) {
<button
[class]="selected === $index ? ['selected'] : []"
(mousemove)="selected = $index"
(mousedown)="action(onPick)"
>
{{item.label}}
</button>
}
`,
...
})
export class SlashComponent extends NgMilkdownSlash {
override get onPick(): (ctx: Ctx) => void {
return (ctx: Ctx) => {
this.removeSlash(ctx);
ctx.get(commandsCtx).call(createCodeBlockCommand.key);
ctx.get(editorViewCtx).focus();
}
}
}
@Component({
selector: 'block',
template: `
<div class="w-6 bg-slate-200 rounded hover:bg-slate-300 cursor-grab">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width={1.5} stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6.75a.75.75 0 110-1.5.75.75 0 010 1.5zM12 12.75a.75.75 0 110-1.5.75.75 0 010 1.5zM12 18.75a.75.75 0 110-1.5.75.75 0 010 1.5z" />
</svg>
</div>
`,
styles:[],
standalone: true
})
export class BlockComponent extends NgMilkdownBlock {}
It's very easy to create a nodeView in ng-milkdown, you can use the following example
@Component({
selector: 'list-item',
template: `
<li [class]="['flex-column', 'flex', 'items-start', 'gap-2', selected ? 'ProseMirror-selectednode' : '']">
<span class="flex h-6 items-center">
@if (isBullet && checked != null) {
<input class="form-checkbox rounded" (change)="checked = !checked" type="checkbox"
checked="checked"/>
} @else if (isBullet) {
<span class="h-2 w-2 rounded-full bg-nord-10 dark:bg-nord9"></span>
} @else {
<span class="text-nord-10">{{ label }}</span>
}
</span>
<div class="min-w-0" #contentRef></div>
</li>
`,
styles: [`
:host {
display: contents;
}
`],
standalone: true
})
export class ListItem extends NgMilkdownNodeComp {
get checked() {
return this.node.attrs?.checked;
}
set checked(checked){
this.setAttrs({checked})
}
get isBullet() {
return this.node.attrs?.listType === "bullet";
}
get label() {
return this.node.attrs?.label;
}
}
Firstly, you should make your nodeView class inherit from NgMilkdownNodeComp
.
To additionally, you should add #contentRef
ElementRef in your nodeView, which will be used to render the content of the node.
More detailed examples and more plugins can be found in example;