Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: migrate to standalone #476

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/no-unsafe-assignment": "off",
"@typescript-eslint/no-unsafe-member-access": "off",
"@typescript-eslint/no-floating-promises": "off",
"comma-dangle": [
"error",
"always-multiline"
Expand All @@ -68,6 +69,7 @@
"order": "asc",
"caseInsensitive": true
},
"newlines-between": "never",
"pathGroups": [
{
"pattern": "@*/**",
Expand Down
44 changes: 25 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,18 +54,7 @@ StackBlitz available @ [https://stackblitz.com/edit/ngx-markdown](https://stackb
To add ngx-markdown library to your `package.json` use the following commands.

```bash
npm install ngx-markdown marked@^4.3.0 --save
npm install @types/marked@^4.3.0 --save-dev
```

As the library is using [Marked](https://github.com/chjj/marked) parser you will need to add `node_modules/marked/marked.min.js` to your application.

If you are using [Angular CLI](https://cli.angular.io/) you can follow the `angular.json` example below...

```diff
"scripts": [
+ "node_modules/marked/marked.min.js"
]
npm install ngx-markdown marked@^9
```

### Syntax highlight
Expand Down Expand Up @@ -588,8 +577,6 @@ MarkdownModule.forRoot({
gfm: true,
breaks: false,
pedantic: false,
smartLists: true,
smartypants: false,
},
},
}),
Expand Down Expand Up @@ -617,8 +604,6 @@ export function markedOptionsFactory(): MarkedOptions {
gfm: true,
breaks: false,
pedantic: false,
smartLists: true,
smartypants: false,
};
}

Expand All @@ -632,6 +617,29 @@ MarkdownModule.forRoot({
}),
```


### provide marked extensions

there is an Angular token `MARKDOWN_EXTENSIONS` that you can provide with array of extensions to be used

usage example for `app.config.ts` (but it's the same for `NgModule` or `Component` providers)
```ts
import { ApplicationConfig } from '@angular/core';
import { gfmHeadingId } from 'marked-gfm-heading-id';
import { MARKDOWN_EXTENSIONS } from 'ngx-markdown';

export const appConfig: ApplicationConfig = {
providers: [
// other providers
{
provide: MARKDOWN_EXTENSIONS,
useValue: [gfmHeadingId()],
},
Comment on lines +634 to +637

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this injection token, one option that's slightly more aligned with the new provide* function pattern, is to use a with* function that you pass in as a second parameter to the provide function, which allows users to add additional options like this use case. So, for example, a user could do this in their app.config

providers: [
  provideMarkdown({ ...markdownConfig }, withExtensions([gfmHeadingId()]),
]

The withExtensions function would set up the appropriate injection token for the provideMarkdown to use. You can see an example of this in the article I linked (search withColor): https://www.angulararchitects.io/en/blog/patterns-for-custom-standalone-apis-in-angular/

Angular's new provideRouter function has several with* feature functions, if you want to use that for inspiration as well. E.g. withPreloading https://angular.io/api/router/withPreloading

You could potentially do the same with several of the optional features that are embedded in the Markdown config, like moving Clipboard options into a withClipboard feature function. But that may be a bit more of an overhaul than @jfcere may be comfortable with?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should be consistent
Either to add the extensions to the configuration interface
Or to use with for everything
What do you think @jfcere

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main difference with the injection token, though, is that it's not integrated into the config interface right now. So it's already sort of its own thing, and could be converted to a with function. But I agree, it would be nice to lean all the way into that pattern for optional features.

Copy link
Owner

@jfcere jfcere Nov 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should be consistent Either to add the extensions to the configuration interface Or to use with for everything What do you think @jfcere

I agree that it should be consistent, honestly I never had time to really dive into the standalone components architecture so I am not sure which approach is best.

I always favored having providers within the markdownModuleConfig so that it kinda regroup everything that is used by the ngx-markdown library instead of adding providers in the providers property of the module and ending up with a bunch of stuff you are not clearly sure it is used by what module.

That being said, I do think that the MARKDOWN_EXTENSIONS should be an option within the markdownModuleConfig but this should be a comment for the previous PR #474. So I would be confortable with not having a bunch of withSomething functions for now.

Does it make sense to you guys?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree
I will do it right now

Copy link

@michaelfaith michaelfaith Nov 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jfcere I totally agree on having the self-contained config vs having a bunch of separate providers. But I feel like, when using the new provide* function paradigm, the with* feature functions feel a bit more natural, than passing in a large config to the function. So, for example, rather than having this

function markedOptionsFactory(anchorService: AnchorService): MarkedOptions {
  const renderer = new MarkedRenderer();

  // fix `href` for absolute link with fragments so that _copy-paste_ urls are correct
  renderer.link = (href: string, title: string, text: string) => {
    return MarkedRenderer.prototype.link.call(renderer, anchorService.normalizeExternalUrl(href), title, text) as string;
  };

  return { renderer };
}

provideMarkdown({
    markedOptions: {
      provide: MarkedOptions,
      useFactory: markedOptionsFactory,
      deps: [AnchorService],
    },
    clipboardOptions: {
      provide: ClipboardOptions,
      useValue: {
        buttonComponent: ClipboardButtonComponent,
      },
    },
    sanitize: SecurityContext.NONE,
  }),

you could have something like this

function createMarkedOptions: MarkedOptions {
  const anchorService: AnchorService = inject(AnchorService);
  const renderer = new MarkedRenderer();

  // fix `href` for absolute link with fragments so that _copy-paste_ urls are correct
  renderer.link = (href: string, title: string, text: string) => {
    return MarkedRenderer.prototype.link.call(renderer, anchorService.normalizeExternalUrl(href), title, text) as string;
  };

  return { renderer };
}

provideMarkdown(
  createMarkedOptions(),
  withClipboard({
      buttonComponent: ClipboardButtonComponent,
   }),
 ),

Internally those functions could still create the same config object, but it would remove the burden from the user to have to construct it. I don't have a strong preference, but the latter feels a bit more ergonomic and in line with the new direction the framework is going with their APIs.

Copy link
Owner

@jfcere jfcere Nov 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So taking the exemple...

provideMarkdown({
  markedOptions: {
    provide: MarkedOptions,
    useFactory: markedOptionsFactory,
    deps: [AnchorService],
  },
  markedExtensions {
    provide: MarkedExtensions,
    useValue: [gfmHeadingId()],
  },
  clipboardOptions: {
    provide: ClipboardOptions,
    useValue: { buttonComponent: ClipboardButtonComponent },
  },
  sanitize: SecurityContext.NONE,
}),

You would end up with 4 with* functions as such (one for each property of the MarkdownConfig) ?

provideMarkdown(
  withMarkedOptions(getMarkedOptions()),
  withMarkedExtensions([gfmHeadingId()]),
  withClipboardOptions({ buttonComponent: ClipboardButtonComponent }),
  withSanitize(SecurityContext.NONE),
),

Or this could probably be in the middle such as...

provideMarkdown({
  markedOptions: getMarkdownOptions(),
  markedExtensions: [gfmHeadingId()],
  clipboardOptions: { buttonComponent: ClipboardButtonComponent },
  sanitize: SecurityContext.NONE,
}),

I am an OCD person where having a bunch of with* functions doesn't seem like an absolute necessity and I would be comfortable with the last one, I think it's pretty straightforward and it does remove the burden on the user to construct the whole provider object (as this could be done internally).

What you guys think?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, I was thinking the core MarkedOptions would be the first parameter of the provideMarkdown function, since those are core functionality. And then the other types of feature options would be encapsulated by with functions.

e.g.

provideMarkdown(
  getMarkedOptions(),
  withMarkedExtensions([gfmHeadingId()]),
  withClipboardOptions({ buttonComponent: ClipboardButtonComponent }),
  withSanitize(SecurityContext.NONE),
),

It's just making a slight delineation between core function and additional features. But the other way could work too.

],
};
```


### Other application modules

Use `forChild` when importing `MarkdownModule` into other application modules to allow you to use the same parser configuration across your application.
Expand Down Expand Up @@ -660,7 +668,7 @@ https://angular.io/api/core/Component#preserveWhitespaces

### Component

You can use `markdown` component to either parse static markdown directly from your HTML markup, load the content from a remote URL using `src` property or bind a variable to your component using `data` property. To enable relative oath for links/images when using `src` input property to load content remotely, set the `srcRelativeLink` input property to `true`. You can get a hook on load complete using `load` output event property, on loading error using `error` output event property or when parsing is completed using `ready` output event property.
You can use `markdown` component to either parse static markdown directly from your HTML markup, load the content from a remote URL using `src` property or bind a variable to your component using `data` property. You can get a hook on load complete using `load` output event property, on loading error using `error` output event property or when parsing is completed using `ready` output event property.

```html
<!-- static markdown -->
Expand All @@ -671,7 +679,6 @@ You can use `markdown` component to either parse static markdown directly from y
<!-- loaded from remote url -->
<markdown
[src]="'path/to/file.md'"
[srcRelativeLink]="true"
(load)="onLoad($event)"
(error)="onError($event)">
</markdown>
Expand Down Expand Up @@ -702,7 +709,6 @@ The same way the component works, you can use `markdown` directive to accomplish
<!-- loaded from remote url -->
<div markdown
[src]="'path/to/file.md'"
[srcRelativeLink]="true"
(load)="onLoad($event)"
(error)="onError($event)">
</div>
Expand Down
38 changes: 38 additions & 0 deletions demo/src/app/app-routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Routes } from '@angular/router';

export const appRoutes: Routes = [
{
path: 'get-started',
loadComponent: () => import('./get-started/get-started.component'),
data: { label: 'Get Started' },
},
{
path: 'cheat-sheet',
loadComponent: () => import('./cheat-sheet/cheat-sheet.component'),
data: { label: 'Cheat Sheet' },
},
{
path: 'syntax-highlight',
loadComponent: () => import('./syntax-highlight/syntax-highlight.component'),
data: { label: 'Syntax Highlight' },
},
{
path: 'bindings',
loadComponent: () => import('./bindings/bindings.component'),
data: { label: 'Bindings' },
},
{
path: 'plugins',
loadComponent: () => import('./plugins/plugins.component'),
data: { label: 'Plugins' },
},
{
path: 're-render',
loadComponent: () => import('./rerender/rerender.component'),
data: { label: 'Re-render' },
},
{
path: '**',
redirectTo: 'get-started',
},
];
51 changes: 0 additions & 51 deletions demo/src/app/app-routing.module.ts

This file was deleted.

22 changes: 19 additions & 3 deletions demo/src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { DOCUMENT } from '@angular/common';
import { DOCUMENT, NgFor } from '@angular/common';
import { ChangeDetectionStrategy, Component, ElementRef, HostListener, Inject, OnInit, ViewChild } from '@angular/core';
import { Route, Router, RouterOutlet } from '@angular/router';

import { FlexModule } from '@angular/flex-layout/flex';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatTabsModule } from '@angular/material/tabs';
import { MatToolbarModule } from '@angular/material/toolbar';
import { Route, Router, RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
import { AnchorService } from '@shared/anchor/anchor.service';
import { ROUTE_ANIMATION } from './app.animation';
import { DEFAULT_THEME, LOCAL_STORAGE_THEME_KEY } from './app.constant';
Expand All @@ -13,6 +17,18 @@ import { isTheme, Theme } from './app.models';
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [
MatToolbarModule,
FlexModule,
MatButtonModule,
MatIconModule,
MatTabsModule,
NgFor,
RouterLinkActive,
RouterLink,
RouterOutlet,
],
})
export class AppComponent implements OnInit {

Expand Down
37 changes: 37 additions & 0 deletions demo/src/app/app.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { ApplicationConfig, SecurityContext } from '@angular/core';
import { provideAnimations } from '@angular/platform-browser/animations';
import { provideRouter, withInMemoryScrolling } from '@angular/router';
import { gfmHeadingId } from 'marked-gfm-heading-id';
import { ClipboardOptions, MARKDOWN_EXTENSIONS, MarkedOptions } from 'ngx-markdown';
import { provideMarkdown } from 'ngx-markdown';
import { markedOptionsFactory } from '@app/marked-options-factory';
import { AnchorService } from '@shared/anchor/anchor.service';
import { ClipboardButtonComponent } from '@shared/clipboard-button';
import { appRoutes } from './app-routes';

export const appConfig: ApplicationConfig = {
providers: [
provideMarkdown({
markedOptions: {
provide: MarkedOptions,
useFactory: markedOptionsFactory,
deps: [AnchorService],
},
clipboardOptions: {
provide: ClipboardOptions,
useValue: {
buttonComponent: ClipboardButtonComponent,
},
},
sanitize: SecurityContext.NONE,
}),
provideRouter(appRoutes, withInMemoryScrolling({scrollPositionRestoration: 'enabled', anchorScrolling: 'enabled'})),
{
provide: MARKDOWN_EXTENSIONS,
useValue: [gfmHeadingId()],
},
provideAnimations(),
provideHttpClient(withInterceptorsFromDi()),
],
};
60 changes: 0 additions & 60 deletions demo/src/app/app.module.ts

This file was deleted.

17 changes: 0 additions & 17 deletions demo/src/app/bindings/bindings-routing.module.ts

This file was deleted.

29 changes: 24 additions & 5 deletions demo/src/app/bindings/bindings.component.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,31 @@
import { ChangeDetectionStrategy, Component, ElementRef, OnInit } from '@angular/core';
import { FlexModule } from '@angular/flex-layout/flex';
import { FormsModule } from '@angular/forms';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { LanguagePipe } from '../../../../lib/src/language.pipe';
import { MarkdownComponent } from '../../../../lib/src/markdown.component';
import { MarkdownPipe } from '../../../../lib/src/markdown.pipe';
import { ScrollspyNavLayoutComponent } from '../shared/scrollspy-nav-layout/scrollspy-nav-layout.component';

@Component({
selector: 'app-bindings',
templateUrl: './bindings.component.html',
styleUrls: ['./bindings.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'app-bindings',
templateUrl: './bindings.component.html',
styleUrls: ['./bindings.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [
ScrollspyNavLayoutComponent,
MarkdownComponent,
FlexModule,
MatFormFieldModule,
MatInputModule,
FormsModule,
LanguagePipe,
MarkdownPipe,
],
})
export class BindingsComponent implements OnInit {
export default class BindingsComponent implements OnInit {

// remote url
demoPython = require('raw-loader!./remote/demo.py').default;
Expand Down
Loading