Michael Hladky
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
      • Invitee
    • Publish Note

      Publish Note

      Everyone on the web can find and read all notes of this public team.
      Once published, notes can be searched and viewed by anyone online.
      See published notes
      Please check the box to agree to the Community Guidelines.
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Engagement control
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Versions and GitHub Sync Engagement control Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
Invitee
Publish Note

Publish Note

Everyone on the web can find and read all notes of this public team.
Once published, notes can be searched and viewed by anyone online.
See published notes
Please check the box to agree to the Community Guidelines.
Engagement control
Commenting
Permission
Disabled Forbidden Owners Signed-in users Everyone
Enable
Permission
  • Forbidden
  • Owners
  • Signed-in users
  • Everyone
Suggest edit
Permission
Disabled Forbidden Owners Signed-in users Everyone
Enable
Permission
  • Forbidden
  • Owners
  • Signed-in users
Emoji Reply
Enable
Import from Dropbox Google Drive Gist Clipboard
   owned this note    owned this note      
Published Linked with GitHub
Subscribed
  • Any changes
    Be notified of any changes
  • Mention me
    Be notified of mention me
  • Unsubscribe
Subscribe
# Design Doc - Let Directive author: [Michael Hladky] created: 2020-02-02 status: [draft | **in review** | approved | implemented | obsolete] doc url: https://hackmd.io/8_3rp0A7RweSYJiulsifbQ?both feedback url: https://github.com/ngrx/platform/issues/2441 design doc template version: 1.0.0 ## Objective The subject of this design doc is to create a structural directive, similar to `*ngIf` directive but without the display/show functionality. It should just bind observable values to the view. It takes [asynchronous primitives](https://gist.github.com/BioPhoton/e8e650dc3b8a7798d09d3a66916bbe10#asynchronousobservable-primitive) and binds and renders their value to the template. The main goal is to have such a directive also work zone-less. In this document use cases and scenarios are defined to add a structural directive to the new ngrx/component package. This directive is one of many [reactive primitives](https://gist.github.com/BioPhoton/e8e650dc3b8a7798d09d3a66916bbe10#reactive-primitives) to make Angular more reactive. We can look at it as a derivative of the [`ngrxPush` pipe](https://hackmd.io/Uus0vFu3RmWRVGgmtzuUWQ?both=). The directive solves following [primitive reactive problems](https://gist.github.com/BioPhoton/e8e650dc3b8a7798d09d3a66916bbe10#primitive-recative-problems): - Retrieve asynchronous primitives - Subscription handling - Change detection and rendering As this document uses specific terminology find detailed explanations in the gist here: [Reactive Angular Terminology](https://gist.github.com/BioPhoton/e8e650dc3b8a7798d09d3a66916bbe10) Especially rendering should be done only over the `ngrxPush` or `*ngrxLet` as the render process is different from template expressions to template bindings. Handling of change detection in the component would make it impossible to interact with either only the EnbeddedView or the ComponentView and would lead to unwanted performance drawbacks. This topic is explained in depth in another document listed in the caveats section. ### Approvals - [ ] Tim Deschryver - [ ] Wes Grimes - [ ] Alex Okrushko - [ ] Brandon Roberts - [ ] Mike Ryan - [ ] Rob Wormald ### Expected User/Developer Experience With the `*ngrxLet` directive, the user will be able to [bind](https://gist.github.com/BioPhoton/e8e650dc3b8a7798d09d3a66916bbe10#bind) the emitted notifications of an observable to an `EmbeddedView`. This is done by using the structural directive in the template, in the same way, he would use the `*ngIf` to [bind](https://gist.github.com/BioPhoton/e8e650dc3b8a7798d09d3a66916bbe10#bind) values but without the display/hide logic of the `*ngIf`. The user can use any observable that can be referenced over the component class. File: **any.component.ts** ```typescript import { Component, Input } from '@angular/core'; import { Subject, interval } from 'rxjs'; @Component({ selector: 'any', templateUrl: 'any.component.html' // Implementation works independent of the ChangeDetectionStrategy }) export class AnyComponent { inputDecoratorSubject$ = new Subject<any>(); @Input() set value(value: any) { this.inputDecorator$.next(value); }; classInternalObservable$ = interval(1000); routerParams$ = this.activatedRoute.params; constructor(public activatedRoute: ActivatedRoute, public store$: Store<any>) { } } ``` and [bind](https://gist.github.com/BioPhoton/e8e650dc3b8a7798d09d3a66916bbe10#bind) it's valued to the part of the template the pipe is used. File: **any.component.html** ```htmlmixed <ng-container *ngrxLet="inputDecorator$ as o"> {{o}} </ng-container> <div *ngrxLet="classInternalObservable$ as o"> {{o}} </div> <ng-container *ngrxLet="routerParams$ as o"> {{o}} </ng-container> <div *ngrxLet="store$ as o"> {{o}} </div> ``` If there is already existing code that uses the `*ngIf` directive used only to bind observable values it the template, and not relying on the hide/show functionality of it, the `*ngrxLet` pipe can be used as a drop-in replacement and also drastically reduce the complexity of that code as the if approach sometimes requires dirty hacks to work with boolean values. ```htmlmixed <!-- before: --> <ng-container *ngIf="observable$ | async as o"> {{o}} </ng-container> <!-- after: --> <ng-container *ngrxLet="observable$ as o"> {{o}} </ng-container> ``` ### Background <!-- Stuff one needs to know to understand this doc: motivating examples, previous versions, and problems, links to related projects/design docs, etc. You should mention related work if applicable. This is background; do not write about ideas to solve problems here. --> The current approach to [bind](https://gist.github.com/BioPhoton/e8e650dc3b8a7798d09d3a66916bbe10#bind) observable values to an EmbededView looks like that: ```htmlmixed <ng-container *ngIf="observableNumber$ | async as n"> <app-number [number]="n"> </app-number> <app-number-special [number]="n"> </app-number-special> </ng-container> ``` The problem is `*ngIf` is also interfering with rendering and in case of a `0` the container would be hidden. ### Prior Art At the moment it is not possible to bind observable values to an EmbeddedView without additional behavior based on the emitted values- - `*ngIf` ```htmlmixed <ng-container *ngIf="observableNumber$ | async as n"> {{n}} </ng-container> ``` - `*ngFor` ```htmlmixed <ng-container *ngFor="let n of [observableNumber$ | async]> {{n}} </ng-container> ``` ## Prototype ### Minimal Design **ngrxLet.directive.ts** ```typescript // LetContext defined ths context structure export class LetContext { constructor( // to enable let we have to use $implicit public $implicit?: any, // to enable as we have to assign this public ngrxLet?: any, // value of error of undefined public $error?: Error | undefined, // true or undefined public $complete?: true | undefined ) {} } @Directive({selector: '[ngrxLet]'}) export class LetDirective implements OnInit, OnDestroy { // Initates the viewContext with an empty LetContext instance private viewContext = new LetContext(); // subscription to the renderer process private subscription = new Subscription(); // Enables to receive input binding changes push based private observablesSubject = new Subject<Observable<any>>(); // Input binding for the observable to bind to the EnbeddedView @Input() set ngrxLet(obs: Observable<any>) { this.observablesSubject.next(obs); } constructor( private cdRef: ChangeDetectorRef, private readonly templateRef: TemplateRef<LetContext>, private readonly viewContainerRef: ViewContainerRef ) { // Retreive values from passed argument this.subscription = this.observablesSubject.pipe( tap({ // Assign value that will get returned from the transform function // on the next change detection next: renderedValue => { // to enable `let` syntax we have to use $implicit (var; let v = var) this.viewContext.$implicit = renderedValue; // to enable `as` syntax we have to assign the directives selector (var as v) this.viewContext.ngrxLet = renderedValue; this.cdRef.detectChanges(); } }) ) // Start to render passed values .subscribe(); } ngOnInit() { // Create and embadded view with the created viewContext and bind it to the templateRef. this.viewContainerRef.createEmbeddedView( this.templateRef, this.viewContext ); } ngOnDestroy() { // Stop to render values this.subscription.unsubscribe(); // Clear the viewContainerRef this.viewContainerRef.clear(); } } ``` ### Detailed Design As these are a lot of equal code blocks also used in the push pipe to ensure the type of passed values or the flattening and update behavior of emitted notifications. Therefore we pull out some parts here to have these snippets available across exposed parts of the package. The first repetitive code is the type checking of passed values **toObservableValue.ts** ```typescript export function toObservableValue<T>( potentialObservableValue$: potentialObservableValue<T> ): Observable<T | undefined | null> { if (isUndefinedOrNullGuard(potentialObservableValue$)) { return of(potentialObservableValue$); } if ( isPromiseGuard(potentialObservableValue$) || isObservableGuard(potentialObservableValue$) ) { return from(potentialObservableValue$); } throw new ArgumentNotObservableError(); } ``` **processCdAwareObservables.ts** ```typescript export function processCdAwareObservables<T>( resetContextBehaviour: ( o$$: Observable<Observable<T>> ) => Observable<Observable<T>>, updateContextBehaviour: ( o$$: Observable<Observable<T>> ) => Observable<Observable<T>> ) { return (o$: Observable<potentialObservableValue<T>>): Observable<T> => { return o$.pipe( toObservableValue(), // Ignore observables of the same instances distinctUntilChanged(), resetContextBehaviour, // Add apply changes to context behaviour updateContextBehaviour, // @NOTICE Configure observable here with config // Add cd optimization behaviour // ---- // unsubscribe from previous observables // then flatten the latest internal observables into the output switchAll(), // reduce number of emissions to distinct values compared to teh previous one distinctUntilChanged() ); }; } ``` **ngrxLet.directive.ts** ```typescript @Directive({selector: '[ngrxLet]'}) export class LetDirective implements OnInit, OnDestroy { // Initates the viewContext with an empty LetContext instance private viewContext = new LetContext(); // subscription to the renderer process private subscription = new Subscription(); // Enables to receive input binding changes push based protected observablesSubject = new Subject<Observable<unknown> | Promise<unknown> | null | undefined>(); protected observables$ = this.observablesSubject.pipe( processCdAwareObservables( // In case we dont have a value set yet we will receive undefined // In some cases people try to stop rendering by appliing null // Also null is a legitiment value for and value not assigned yet tap({ next: (obs: Observable<unknown>) => { // Apply values that should get rendered this.renderedValue = undefined; // Render new values to the template this.cdRef.detectChanges(); } }), // Update renderedValue and render it to the template tap({ // Assign value that will get returned from the transform function // on the next change detection next: renderedValue => { // to enable `let` syntax we have to use $implicit (var; let v = var) this.viewContext.$implicit = renderedValue; // to enable `as` syntax we have to assign the directives selector (var as v) this.viewContext.ngrxLet = renderedValue; // Render new values to the template this.cdRef.detectChanges(); }, // Get error object here and apply needed error logic error: (e: unknown) => { // Logic to deal with error object // e. g. this.renderedValue = e.message; this.cdRef.detectCahnges(); // @Notice: // This is not catching the error } }) ) ); // Input binding for the observable to bind to the EnbeddedView @Input() set ngrxLet(obs: Observable<any>) { this.observablesSubject.next(obs); } constructor( private cdRef: ChangeDetectorRef, private readonly templateRef: TemplateRef<LetContext>, private readonly viewContainerRef: ViewContainerRef ) { // Retreive values from passed argument this.subscription = this.observables$. // Start to render passed values .subscribe(); } ngOnInit() { // Create and embadded view with the created viewContext and bind it to the templateRef. this.viewContainerRef.createEmbeddedView( this.templateRef, this.viewContext ); } ngOnDestroy() { // Stop to render values this.subscription.unsubscribe(); // Clear the viewContainerRef this.viewContainerRef.clear(); } } ``` ### Caveats <!-- You may need to describe what you did not do or why simpler approaches don't work. Mention other things to watch out for (if any). Security Considerations How you’ll be secure. Considerations should include sanitization, escaping, existing security protocols and web standards, permission, user privacy, etc. None --> **ViewEngine / Ivy interoperability** With the requirement to support both, we increase the complexity of the implementation. We have to do this because in ViewEngine applications [it's recommended to keep supporting View Engine in Angular versions 9 and 10](https://indepth.dev/the-angular-ivy-guide-for-library-authors/) **Usage of Angulars internal ɵ API:** With the current situation, we rely on Angulars internal `ɵmarkDirty` and `ɵdetectChanges` function. This may have critical effects as these APIs can change in any release. We need to ensure we can use `ChangeDetectorRef` instead. It would make a lot of sense if they expose it [Expose the `isPromise` utility function to the public API](https://github.com/ReactiveX/rxjs/pull/5291). **Immutability:** The second `distinctUntilChanged` operator for emitted values of the passed observables forces the user to work immutable. Even if this is in most cases given as people that work more reactively use normally also `ChangeDetectionStrategy.OnPush`. Still, we have to consider clear communication. **Coalescing and Scoped Coalescing:** This problem is explained in the document [Design Doc - Coalescing of Change Detection](https://hackmd.io/42oAMrzYReizA65AwQ7ZlQ) **Security Considerations:** Errors happening in the observables are caught and swallowed without any thrown error. The Observable completes instead. The error object is handled in another part of the code. **Sanitization:** Sanitization of the emitted values to get rendered in the template is done by default by Angular. ### Performance Considerations <!-- Try to describe under which conditions the suggested solution is performant and which factors play the key role when starting to get bad performance. Describe a specific situation in which we can run into performance problems If possible provide a POC or a theoretical explanation of a possible solution. --> It could be possible we run into similar problems as with the [Design Doc - Push Pipe](https://hackmd.io/Uus0vFu3RmWRVGgmtzuUWQ?view#Performance-Considerations) If so, details on the change detection coalescing feature can be found in [Design Doc - Coalescing of Change Detection](https://hackmd.io/42oAMrzYReizA65AwQ7ZlQ). ## Documentation Plan <!-- Try to describe the important parts of the implementation and how to documented it e.g. importance, a priority by relevance for user, level of detail, example needed. --> As the scope of this package is quite small I consider the documentation in text and some examples in the demo app and as code snippets is everything we need. Here a first draft of the documentation: [let.md](https://github.com/ngrx/platform/pull/2046/files#diff-a992500ae4e65e03046abc21cf705c86) ### Style Guide Impact <!-- Does the documentation influence the way the current style guide is structured? Also, does the new documentation introduces any technical implementations of the docs? If so please name them and give a detailed description of the impact and if possible some POCs. --> The needed documentation has no impact on the current way the documentation is maintained. ## Developer Impact ### Public API Surface Impact <!-- Are you adding new public APIs? If so, what's the change? (method signature changes, new classes/interfaces, new npm packages, config file properties or new config files altogether) Are the new APIs consistent with the existing APIs? Do they follow naming conventions and use correct terminology? --> ### Developer Experience Impact <!-- How will this change impact developer experience? Are we adding new tools developers need to learn? Are we asking developers to change their workflows in any way? Are we placing any new constraints on how the developer should write the code to be compatible with this change? --> The new directive does not affect the existing API interface. It will get introduced under a new package `@ngrx/component`. It exposes only the pipes class and its usage in the template as a pipe reference. Following dependencies are needed: - @angular/core@>=8 - @rxjs@>=6.5.4 The new directive is prefixed with `ngrx` and named `let`. It is used the same way as the `*ngIf`, `*ngSwitch` or `*ngFor` structural directives are used `<ng-container *ngrxLet="interval$ as n">{{n}}</ng-container>`. The term 'let' refers to ECMAScript's let statement, which declares a block scope local variable whose initial value is optional. This tries to give the user an intuitive understanding of its behavior out of the pipe's name itself. ### Breaking Changes The new directive will not introduce any breaking changes. ### Rollout Plan <!-- Are the implementation delays about to negatively affect or delay the release of other features or increase the size. --> The directive and especially its features can be rolled out in the following way: **Alpha releases of the @ngrx/component package** and the directive include all features the async pipe has regarding the processing of passed values, to have the directive ready for a drop-in replacement of the situations where ```htmlmixed= <ng-container *ngIf="observableNumber$ | async as n"> {{n}} </ng-container> ``` is used. Also, the detection depending if zone is present or not. Shipped Features: - Take promises or observables, retrieve their values and render the value to the template - Handling null and undefined values in a clean unified/structured way - Triggers change-detection differently if `zone.js` is present or not (`detectChanges` or `markForCheck`) - **Beta releases of the @ngrx/component package** and the directive include a configuration argument to opt-in the coalescing of change detection calls. Shipped Features: - Expose context variables `$error` and `$completed` - Add config options to the directive as arguments over input bindings to opt-in new features - Coalescing of change detection calls to boost performance **Rc and official releases of the @ngrx/component package** and the directive include all features and the change detection coalescing is opt-out by default. Alos internal operators and utils related to zone checks and event coalescing can be exposed in this releases. Shipped Features: - Coalescing of change detection calls to boost performance is on by default and can get opted-out ### Rollback Plan <!-- How do you plan to roll back the change if major issues are found? --> In the late alpha versions, we introduce the coalescing of change detection calls. If any critical problems occur in the alpha version we can just don't ship further versions. Until the first beta, we should have to clarify this. This is the moment we introduce the configuration option for the pipe. The coalescing can be rolled back by removing the option from the config object. If a major issue is found in RC or even official release we can remove the push pipe from the package as it is no other part relies on the pipe. There are no implementation details that could delay the release of other packages. Also, the bundle size of other features will not increase by introducing this feature. ### Maintenance Plan <!-- Include alternate design ideas you tried out, but didn't continue with them. List their disadvantages or at least why you did not invest more time in researching them. --> If we can get rid of the usage of Angulars internal API we have pretty low maintenance costs. Depending on how long we want to support interrupt ViewEngine and Ivy we have a year to 1,5 to remove the ViewEngine related code. The rest of the code relies on some pretty common Angular parts from the core package and some RxJS operators creation functions and utils naming: `Observable, pipe, isObservable, from, of, filter, tap, switchAll, distinctUntilChanged, shareReplay` `shareReplay` will have a breaking change for our current usage as the configuration object will get removed. A minor refactoring. From all the other used operator we only use one where a breaking change can happen which is `tap`. If Observabls make it in the standard the most probably will have callbacks as arguments instead of an observer object. But this is also a minor refactoring. ## Alternatives considered <!-- Include alternate design ideas you tried out, but didn't continue with them. List their disadvantages or at least why you did not invest more time in researching them. --> **Imperative Code style** An alternative implementation could be done without RxJS as Angulars `async` pipe is built. I went the Reactive way because - A) not a lot of operators -B) Easier to maintain and extend (configuration) Drawbacks: The use of RxJS could force the user to include operators in their bundle which they don't want. An increased bundle size is expected through the following operators (rare used operators at the top): - ?.?kB switchAll - 275B pipe - 5.0kB ReplaySubject // Low risc as frequently used - ?.?kB distinctUntilChanged - ?.?kB filter - 3.3kB from - 2.8kB of - ?.?kB tap - 2.6kB Observable **Change Detection over ApplicationRef.tick()** An alternative way of triggering the changeDetection would be to call `ApplicationRef.tick()`. I did not go that way or even tried it out as It would result in a full application render for every change detection. **Change Detection in the Component** In the component, not all Observables values need to get rendered. Examples can be any background process lite polling or refresh clicks that dispatch an action or so. **Mention multi `async` example?** The example looks like that: ```htmlmixed= *ngIf = { a: a$ | async, b: b$ | async } as vm <ng-container *ngIf="{ o1: observable1$ | async, o2: observable2$ | async } as viewContext"> {{viewContext.o1}} {{viewContext.o2}} </ng-container> ``` I did not include it on purpose as composition should be placed in the Typescript section. More flexible, less noisy template. **Providing template slots for error and complete** Here a solution that provides template slots. The document is not focusing on this feature as it trys to start with the very minimum and add features after a solid base. In the planed release it is easily possible to implement it manually. [Let with themplate](https://dev.to/angular/handling-observables-with-structural-directives-in-angular-112j) **Already existing Packages** The suggested version is the most primitive implementation. In the [RFC](https://github.com/ngrx/platform/issues/2052) for this package is listed alternative implementations. Those implementations either didn't respect the zone-less mode or had additional logic implemented to add templates for the context or other things that would go over the features of a [reactive primitives](https://gist.github.com/BioPhoton/e8e650dc3b8a7798d09d3a66916bbe10#reactive-primitives). ## Work Breakdown <!-- Explain how multiple people would actively working on the suggested code base. If needed include branching suggestions or the way code interacts --> As the pipes scope is small enough to get maintained by one person at a time we don't need to describe the breakdown of work. ## Resources **Research Paper/Design Docs/StackBlitz/Video/Podcast/Blog/Tweet/Graphic:** - [Reactive Angular Terminology](https://gist.github.com/BioPhoton/e8e650dc3b8a7798d09d3a66916bbe10) In: GitHub Gist, Michael Hladky. - [RFC: Component: Proposal for a new package component](https://github.com/ngrx/platform/issues/2052) In: GitHub, Michael Hladky. - [Render3/VIEW_DATA.md](https://github.com/angular/angular/blob/master/packages/core/src/render3/VIEW_DATA.md) In: GitHub, Angular. - [Design Doc - Coalescing of Change Detection](https://hackmd.io/42oAMrzYReizA65AwQ7ZlQ) In: HackMd, Michael Hladky. - [Your Top Three Angular 2 Questions Answered](https://www.youtube.com/watch?v=bH73HCxjDH0&feature=youtu.be&t=38m39s) In: YouTube, Rob Wormald, 2017-04-11. **Github Pull Request/Gists/Issue/Doc/Source Link:** - [angular-ivy-detection.ts](https://gist.github.com/LayZeeDK/2cd1ff1fc605af6f578a39311b3aba99) - [RFC: Component: Proposal for a new package component](https://github.com/ngrx/platform/issues/2052) - [RFC: Component: `ngrxLet` and `embeddedView`](https://github.com/ngrx/platform/issues/2051) - [RFC: Component: Evaluation of using angular's internal `ɵdetectChanges` method](https://github.com/ngrx/platform/issues/2050) - [isPromise check](https://github.com/ReactiveX/rxjs/pull/5291)

Import from clipboard

Paste your webpage below. It will be converted to Markdown.

Advanced permission required

Your current role can only read. Ask the system administrator to acquire write and comment permission.

This team is disabled

Sorry, this team is disabled. You can't edit this note.

This note is locked

Sorry, only owner can edit this note.

Reach the limit

Sorry, you've reached the max length this note can be.
Please reduce the content or divide it to more notes, thank you!

Import from Gist

Import from Snippet

or

Export to Snippet

Are you sure?

Do you really want to delete this note?
All users will lose their connection.

Create a note from template

Create a note from template

Oops...
This template is not available.
Upgrade
All
  • All
  • Team
No template found.

Create custom template

Upgrade

Delete template

Do you really want to delete this template?
Turn this template into a regular note and keep its content, versions, and comments.

This page need refresh

You have an incompatible client version.
Refresh to update.
New version available!
See releases notes here
Refresh to enjoy new features.
Your user state has changed.
Refresh to load new user state.

Sign in

Forgot password

or

By clicking below, you agree to our terms of service.

Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
Wallet ( )
Connect another wallet

New to HackMD? Sign up

Help

  • English
  • 中文
  • Français
  • Deutsch
  • 日本語
  • Español
  • Català
  • Ελληνικά
  • Português
  • italiano
  • Türkçe
  • Русский
  • Nederlands
  • hrvatski jezik
  • język polski
  • Українська
  • हिन्दी
  • svenska
  • Esperanto
  • dansk

Documents

Help & Tutorial

How to use Book mode

How to use Slide mode

API Docs

Edit in VSCode

Install browser extension

Get in Touch

Feedback

Discord

Send us email

Resources

Releases

Pricing

Blog

Policy

Terms

Privacy

Cheatsheet

Syntax Example Reference
# Header Header 基本排版
- Unordered List
  • Unordered List
1. Ordered List
  1. Ordered List
- [ ] Todo List
  • Todo List
> Blockquote
Blockquote
**Bold font** Bold font
*Italics font* Italics font
~~Strikethrough~~ Strikethrough
19^th^ 19th
H~2~O H2O
++Inserted text++ Inserted text
==Marked text== Marked text
[link text](https:// "title") Link
![image alt](https:// "title") Image
`Code` Code 在筆記中貼入程式碼
```javascript
var i = 0;
```
var i = 0;
:smile: :smile: Emoji list
{%youtube youtube_id %} Externals
$L^aT_eX$ LaTeX
:::info
This is a alert area.
:::

This is a alert area.

Versions and GitHub Sync
Get Full History Access

  • Edit version name
  • Delete

revision author avatar     named on  

More Less

No updates to save
Compare
    Choose a version
    No search result
    Version not found
Sign in to link this note to GitHub
Learn more
This note is not linked with GitHub
 

Feedback

Submission failed, please try again

Thanks for your support.

On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

Please give us some advice and help us improve HackMD.

 

Thanks for your feedback

Remove version name

Do you want to remove this version name and description?

Transfer ownership

Transfer to
    Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

      Link with GitHub

      Please authorize HackMD on GitHub
      • Please sign in to GitHub and install the HackMD app on your GitHub repo.
      • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
      Learn more  Sign in to GitHub

      Push the note to GitHub Push to GitHub Pull a file from GitHub

        Authorize again
       

      Choose which file to push to

      Select repo
      Refresh Authorize more repos
      Select branch
      Select file
      Select branch
      Choose version(s) to push
      • Save a new version and push
      • Choose from existing versions
      Include title and tags
      Available push count

      Pull from GitHub

       
      File from GitHub
      File from HackMD

      GitHub Link Settings

      File linked

      Linked by
      File path
      Last synced branch
      Available push count

      Danger Zone

      Unlink
      You will no longer receive notification when GitHub file changes after unlink.

      Syncing

      Push failed

      Push successfully