Skip to content

Commit e8e5ba1

Browse files
authored
Merge pull request #15 from ericleib/dev/standalone
Migration to standalone components
2 parents 3a2f2c1 + cfc24b8 commit e8e5ba1

15 files changed

+82
-102
lines changed

README.md

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,18 @@
33

44
# ngx-remark
55

6-
**ngx-remark** is a library that allows to render Markdown with custom Angular components and templates.
6+
**ngx-remark** is a lightweight library that renders Markdown using **native Angular components and templates**.
77

8-
Most libraries for rendering Markdown in Angular first transform the Markdown to HTML and then use the `innerHTML` attribute to render the HTML. The problem of this approach is that there is no way to use Angular components or directives in any part of the generated HTML.
8+
Most Markdown libraries for Angular convert the source to HTML and then bind it with `[innerHTML]`. The problem with this approach is that the resulting HTML lives outside Angular's component tree — you cannot use Angular components, directives, pipes, or dependency injection inside it.
99

10-
In contrast, this library uses [Remark](https://remark.js.org/) to parse the Markdown into an abstract syntax tree (AST) and then uses Angular to render the AST as HTML. The `<remark>` component renders all standard Markdown elements with default built-in templates, but it also allows to override the templates for any element.
10+
**ngx-remark** takes a different route. It uses [Remark](https://remark.js.org/) to parse Markdown into an AST, then renders that tree with real Angular templates. The `<remark>` component ships with clean default templates for every standard Markdown element, but **you can override any of them** with your own components.
1111

1212
Typical use cases include:
1313

14-
- Displaying code blocks with a custom code editor.
15-
- Displaying custom tooltips over certain elements.
16-
- Allowing custom actions with buttons or links.
17-
- Integration with Angular features, like the [`Router`](#router-integration).
14+
- Rendering code blocks with a full-featured editor (e.g. Monaco, CodeMirror)
15+
- Adding interactive tooltips, popovers, or badges on specific elements
16+
- Turning links or buttons into Angular-powered actions (routing, modals, etc.)
17+
- Deep integration with Angular features like the [`Router`](#router-integration), forms, or signals
1818

1919
## Demo
2020

@@ -32,7 +32,7 @@ npm install ngx-remark remark
3232

3333
## Importing the library
3434

35-
Import the `RemarkModule` in your standalone component or module:
35+
Import `RemarkModule` in your standalone component or module:
3636

3737
```typescript
3838
import { RemarkModule } from 'ngx-remark';
@@ -45,6 +45,8 @@ import { RemarkModule } from 'ngx-remark';
4545
export class App { }
4646
```
4747

48+
Note: `RemarkModule` is a bundle of `RemarkComponent`, `RemarkNodeComponent` and `RemarkTemplateDirective`. You may also import these components individually, but in most cases you will need the three of them together.
49+
4850
## Usage
4951

5052
Use the `<remark>` component to render Markdown:

projects/demo/src/app/app.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export class AppComponent {
3737
if(state.mathExpressions) {
3838
processor = processor.use(remarkMath);
3939
}
40-
return {processor, ...state}
40+
return {processor, ...state};
4141
})
4242
)
4343
}

projects/lib/karma.conf.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,15 @@ module.exports = function (config) {
1010
require('karma-chrome-launcher'),
1111
require('karma-jasmine-html-reporter'),
1212
require('karma-coverage'),
13-
13+
1414
],
1515
client: {
1616
jasmine: {
1717
// you can add configuration options for Jasmine here
1818
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
1919
// for example, you can disable the random execution with `random: false`
2020
// or set a specific seed with `seed: 4321`
21+
random: false
2122
},
2223
clearContext: false // leave Jasmine Spec Runner output visible in browser
2324
},

projects/lib/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
{
22
"name": "ngx-remark",
3-
"version": "0.2.1",
3+
"version": "0.2.2",
44
"author": {
55
"name": "Eric Leibenguth",
66
"email": "eric.leibenguth@gmail.com",
77
"url": "https://github.com/ericleib",
88
"github": "ericleib",
99
"twitter": "EricLeibenguth"
1010
},
11-
"description": "Angular component for rendering custom markdown using Remark",
11+
"description": "Lightweight Angular component for rendering Markdown using Remark",
1212
"keywords": [
1313
"angular",
1414
"remark",

projects/lib/src/plugins/heading.component.ts

Lines changed: 12 additions & 13 deletions
Large diffs are not rendered by default.

projects/lib/src/public-api.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,21 @@
22
* Public API Surface of lib
33
*/
44

5-
export * from './remark.module';
6-
export * from './remark.component';
7-
export * from './remark-template.directive';
8-
export * from './remark-node.component';
5+
import { RemarkComponent } from './remark.component';
6+
import { RemarkNodeComponent } from './remark-node.component';
7+
import { RemarkTemplateDirective } from './remark-template.directive';
8+
9+
export {
10+
RemarkComponent,
11+
RemarkNodeComponent,
12+
RemarkTemplateDirective
13+
};
14+
15+
export const RemarkModule = [
16+
RemarkComponent,
17+
RemarkNodeComponent,
18+
RemarkTemplateDirective,
19+
] as const;
20+
921
export * from './remark-templates.service';
1022
export * from './plugins';

projects/lib/src/remark-node.component.html

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11

2-
<!--
2+
<!--
33
For each child, display its custom template or default one.
44
Tracking by index means that DOM elements are reused even when the node type and content is different.
55
Changes should still be reflected because of the @Input() node binding.
66
-->
7+
@let templates = templateService.templates();
78
@for (child of children(); track $index) {
8-
<ng-container *ngTemplateOutlet="templates()[child.type] ?? defaultTpl; context: { $implicit: child , parent: node()}">
9+
<ng-container *ngTemplateOutlet="templates[child.type] ?? defaultTpl; context: { $implicit: child , parent: node()}">
910
</ng-container>
1011
}
1112

projects/lib/src/remark-node.component.spec.ts

Lines changed: 16 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
22
import { RemarkNodeComponent } from './remark-node.component';
33
import { RemarkTemplatesService } from './remark-templates.service';
44
import { RemarkComponent } from './remark.component';
5-
import { RemarkTemplateDirective } from './remark-template.directive';
65
import { Root } from 'mdast';
76

87
function getNode(text: string, heading = 0) {
@@ -18,46 +17,41 @@ function getNode(text: string, heading = 0) {
1817
} as Root;
1918
}
2019

21-
class RemarkNodeComponentInit extends RemarkNodeComponent {
22-
ngOnInit() {}
23-
ngOnChanges() {}
24-
}
25-
2620
describe('RemarkNodeComponent', () => {
27-
let component: RemarkNodeComponentInit;
28-
let fixture: ComponentFixture<RemarkNodeComponentInit>;
21+
let component: RemarkNodeComponent;
22+
let fixture: ComponentFixture<RemarkNodeComponent>;
2923

3024
beforeEach(() => {
3125
// Create testing module
3226
TestBed.configureTestingModule({
33-
declarations: [RemarkComponent, RemarkNodeComponentInit, RemarkTemplateDirective],
27+
imports: [RemarkComponent, RemarkNodeComponent],
3428
providers: [RemarkTemplatesService]
3529
});
3630

3731
// Create an instance of RemarkComponent, to access an instance of the TemplateService
3832
const parentFixture = TestBed.createComponent(RemarkComponent);
39-
parentFixture.componentRef.setInput('markdown', '# Hello')
33+
parentFixture.componentRef.setInput('markdown', '# Hello');
4034
parentFixture.detectChanges();
41-
42-
// Create an instance of RemarkNodeComponentInit for testing
43-
spyOn(RemarkNodeComponentInit.prototype, 'ngOnInit' as any);
44-
spyOn(RemarkNodeComponentInit.prototype, 'ngOnChanges' as any);
45-
fixture = TestBed.createComponent(RemarkNodeComponentInit);
35+
36+
// Create an instance of RemarkNodeComponent for testing
37+
spyOn(RemarkNodeComponent.prototype, 'ngOnInit');
38+
spyOn(RemarkNodeComponent.prototype, 'ngOnChanges');
39+
fixture = TestBed.createComponent(RemarkNodeComponent);
4640
component = fixture.componentInstance;
4741
component.templateService = parentFixture.componentInstance.remarkTemplatesService;
4842
});
4943

5044
describe('with paragraph', () => {
5145

52-
beforeEach(() => {
46+
beforeEach(() => {
5347
fixture.componentRef.setInput('remarkNode', getNode('Hello'));
5448
fixture.detectChanges();
5549
});
5650

5751
it('should create', () => {
5852
expect(component).toBeTruthy();
59-
expect(RemarkNodeComponentInit.prototype.ngOnInit).toHaveBeenCalledTimes(2); // one for root and one for paragraph
60-
expect(RemarkNodeComponentInit.prototype.ngOnChanges).toHaveBeenCalledTimes(2); // one for root and one for paragraph
53+
expect(RemarkNodeComponent.prototype.ngOnInit).toHaveBeenCalledTimes(2); // one for root and one for paragraph
54+
expect(RemarkNodeComponent.prototype.ngOnChanges).toHaveBeenCalledTimes(2); // one for root and one for paragraph
6155
});
6256

6357
it('should render paragraph', () => {
@@ -69,8 +63,8 @@ describe('RemarkNodeComponent', () => {
6963
const el = fixture.nativeElement.querySelector('p');
7064
fixture.componentRef.setInput('remarkNode', getNode('World'));
7165
fixture.detectChanges();
72-
expect(RemarkNodeComponentInit.prototype.ngOnInit).toHaveBeenCalledTimes(2); // one for root and one for paragraph initially
73-
expect(RemarkNodeComponentInit.prototype.ngOnChanges).toHaveBeenCalledTimes(4); // then new root and new paragraph
66+
expect(RemarkNodeComponent.prototype.ngOnInit).toHaveBeenCalledTimes(2); // one for root and one for paragraph initially
67+
expect(RemarkNodeComponent.prototype.ngOnChanges).toHaveBeenCalledTimes(4); // then new root and new paragraph
7468
expect(fixture.nativeElement.querySelector('p')?.textContent).toContain('World');
7569
expect(fixture.nativeElement.querySelector('p')).toBe(el); // The element should be reused
7670
});
@@ -79,8 +73,8 @@ describe('RemarkNodeComponent', () => {
7973
const el = fixture.nativeElement.querySelector('p');
8074
fixture.componentRef.setInput('remarkNode', getNode('World', 2));
8175
fixture.detectChanges();
82-
expect(RemarkNodeComponentInit.prototype.ngOnInit).toHaveBeenCalledTimes(3); // one for root and one for paragraph initially, then the new heading
83-
expect(RemarkNodeComponentInit.prototype.ngOnChanges).toHaveBeenCalledTimes(4); // then new root and new paragraph
76+
expect(RemarkNodeComponent.prototype.ngOnInit).toHaveBeenCalledTimes(3); // one for root and one for paragraph initially, then the new heading
77+
expect(RemarkNodeComponent.prototype.ngOnChanges).toHaveBeenCalledTimes(4); // then new root and new paragraph
8478
expect(fixture.nativeElement.querySelector('h2')?.textContent).toContain('World');
8579
expect(fixture.nativeElement.querySelector('p')).toBeFalsy();
8680
expect(fixture.nativeElement.querySelector('h2')).not.toBe(el);
Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
11
import { ChangeDetectionStrategy, Component, computed, inject, input } from "@angular/core";
2+
import { NgTemplateOutlet } from "@angular/common";
23
import { Node, Parent } from "mdast";
34
import { RemarkTemplatesService } from "./remark-templates.service";
45

56
@Component({
67
selector: "remark-node, [remarkNode]",
78
templateUrl: "./remark-node.component.html",
89
changeDetection: ChangeDetectionStrategy.OnPush,
9-
standalone: false
10+
imports: [NgTemplateOutlet]
1011
})
1112
export class RemarkNodeComponent {
1213
templateService = inject(RemarkTemplatesService);
1314

1415
readonly node = input.required<Node>({ alias: "remarkNode" });
1516

16-
readonly children = computed(() => (this.node() as Parent).children)
17-
18-
get templates() {
19-
return this.templateService.templates;
20-
}
17+
readonly children = computed(() => (this.node() as Parent).children);
2118

19+
// Note: implemented solely for the purpose of tests (to use spyOn)
20+
ngOnInit() {}
21+
ngOnChanges() {}
2222
}

projects/lib/src/remark-template.directive.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { Directive, TemplateRef, inject, input } from "@angular/core";
22

33
@Directive({
44
selector: "[remarkTemplate]",
5-
standalone: false
65
})
76
export class RemarkTemplateDirective {
87
readonly template = inject<TemplateRef<any>>(TemplateRef);

0 commit comments

Comments
 (0)