angularangular-materialmonaco-editor

Angular material dialog slow with broken rendition and binding


I have an Angular 17 app using Angular Material and Angular Monaco editor v2. Please find the full repro on Stackblitz.

The repro app has a Monaco editor bound to CTRL+P. When you press this hotkey, a dialog should popup, wrapping the HelloComponent in the same project. This is a dummy component with a single text box input, where you can enter a name.

The issue is that when the dialog is opened, its rendition is somehow delayed, up to the point that material styles do not appear, and binding is so slow that it breaks.

I tried to use detach change detection and re-attach it after the dialog is closed, as suggested here, but nothing changes. Additionally, the component inside the dialog is really simple, so there are no intrinsic performance issues in it.

Repro Project Description

To make the name input component usable both as a "normal" component and as a component wrapped in a material dialog, it gets optional injections in its constructor for MatDialogRef (so it can close the dialog passing data back) and data optionally received by injection via token MAT_DIALOG_DATA.

Data is of type HelloData, which just contains a string name property. The popup should show the received name if any, and let you edit it. When you click it, it will return the new name.

On the container component side, the dialog is opened on key binding CTRL+P on the underlying Monaco instance, by calling insertText. This gets the currently selected text from Monaco, and opens the dialog passing to it this text as the editable name.

Once the dialog is closed with OK, the container component will take care of replacing the selection with the new name if any.

This is the full component template:

<div>
  <form [formGroup]="form" (submit)="save()">
    <mat-form-field>
      <input
        type="text"
        matInput
        [formControl]="inputName"
        placeholder="name"
      />
    </mat-form-field>
    <p>Hello, {{ inputName.value }}!</p>
    <button mat-flat-button type="submit">OK</button>
  </form>
</div>

And its corresponding code:

export interface HelloData {
  name?: string;
}

@Component({
  selector: 'app-hello',
  standalone: true,
  imports: [
    CommonModule,
    FormsModule,
    ReactiveFormsModule,
    MatButtonModule,
    MatFormFieldModule,
    MatDialogModule,
    MatInputModule,
  ],
  templateUrl: './hello.component.html',
  styleUrl: './hello.component.css',
})
export class HelloComponent {
  public inputName: FormControl<string | null>;
  public form: FormGroup;

  constructor(
    formBuilder: FormBuilder,
    @Optional()
    public dialogRef?: MatDialogRef<HelloComponent>,
    @Optional()
    @Inject(MAT_DIALOG_DATA)
    public data?: HelloData
  ) {
    this.inputName = formBuilder.control(data?.name || null);
    this.form = formBuilder.group({
      name: this.inputName,
    });
  }

  public save(): void {
    this.dialogRef?.close(this.inputName.value);
  }
}

Finally, this is how the dialog is opened and closed:

private async promptName(name?: string): Promise<string | undefined> {
  this._cd.detach();
  const dialogRef = this._dialog.open(HelloComponent, {
    data: {
      name,
    },
  });
  const result: HelloData | undefined = await firstValueFrom(
    dialogRef.afterClosed()
  );
  this._cd.reattach();
  this._cd.detectChanges();
  return result?.name;
}

Solution

  • It looks like the commands executed by the monaco are handled outside of the NgZone, so it doesn't trigger change detection at all. And since it's outside of the zone, manually checking for changes on the ChangeDetectorRef will not yield any results either. This results in the pure html being pushed to DOM without the other stuff that is triggered by change detection.

    You should wrap the command handle in ngZone.run. Inject the zone:

    constructor(private _dialog: MatDialog, private ngZone: NgZone) {}
    

    And then wrap the handler in ngZone.run:

    this._editor.addCommand(
      monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyP,
      async () => {
        this.ngZone.run(async () => await this.insertText());
      }
    );
    

    Here's a working fork of your stackblitz.

    All in all, I would assume that if you're using an Angular wrapper for monaco, it should handle this kind of stuff, so you should probably create an issue in the relevant git repo.