Ngx-quill: Material Design

Created on 7 Jan 2019  Â·  15Comments  Â·  Source: KillerCodeMonkey/ngx-quill

I use this in a Material Form Field but it dosen't work.

Please let me know, how I can bring the editor to a material design component.



Most helpful comment

If anyone is looking for solution, here is mine

Base class for material validation error:

import {  ErrorStateMatcher, CanUpdateErrorStateCtor, mixinErrorState } from "@angular/material/core";
import { NgForm, FormGroupDirective, NgControl } from '@angular/forms';


/* class is used in custom material form field component/wrapper 
    provide material functionality to work with erorsState & ErrorStateMatcher  */
export class CustomErrorStateBase {
    constructor(public _defaultErrorStateMatcher: ErrorStateMatcher,
                public _parentForm: NgForm,
                public _parentFormGroup: FormGroupDirective,
                public ngControl: NgControl) {}
  }


  /* mixinErrorState - material function that appy logic related with material forms errors  */
export const CustomErrorStateMixin: CanUpdateErrorStateCtor & typeof CustomErrorStateBase = mixinErrorState(CustomErrorStateBase);  

Wrapper directive:

import { FocusMonitor } from '@angular/cdk/a11y';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { Directive, DoCheck, ElementRef, HostBinding, Input, OnDestroy, OnInit, Optional, Self } from '@angular/core';
import { FormGroupDirective, NgControl, NgForm } from '@angular/forms';
import { ErrorStateMatcher } from '@angular/material/core';
import { MatFormFieldControl } from '@angular/material/form-field';
import { QuillEditorComponent } from 'ngx-quill';
import { Subscription } from 'rxjs';
import { CustomErrorStateMixin } from '../../../custom-form-fields/Custom-error-state.base';


/* Class is use to insert quill-editor(ngx-quill) inside of the material form field */
@Directive({
  selector: '[CustomRichEditor]',
  providers: [{ provide: MatFormFieldControl, useExisting: QuillFormFieldDirective }],
})
export class QuillFormFieldDirective extends CustomErrorStateMixin implements OnInit, OnDestroy, DoCheck, MatFormFieldControl<QuillFormFieldDirective>{

  _quillInstance: any;
  _quillSubscription: Subscription;

  constructor(
    public _defaultErrorStateMatcher: ErrorStateMatcher,
    @Optional() public _parentForm: NgForm,
    @Optional() public _parentFormGroup: FormGroupDirective,
    @Optional() @Self() public ngControl: NgControl,
    private focusMonitor: FocusMonitor,
    private elementRef: ElementRef<HTMLElement>,
    quillEditor: QuillEditorComponent) {

    super(_defaultErrorStateMatcher, _parentForm, _parentFormGroup, ngControl);

    this._quillSubscription = quillEditor.onEditorCreated.subscribe((event) => this.quillEditorCreated(event));

    focusMonitor.monitor(elementRef.nativeElement, true).subscribe(origin => {
      this.focused = !!origin;
      this.stateChanges.next();
    });
  }

  ngOnInit(): void {
  }

  ngOnDestroy() {
    this.stateChanges.complete();
    this._quillSubscription.unsubscribe();
    this.focusMonitor.stopMonitoring(this.elementRef.nativeElement);
  }

  ngDoCheck() {
    if (this.ngControl) {
      // We need to re-evaluate this on every change detection cycle, because there are some
      // error triggers that we can't subscribe to (e.g. parent form submissions). 
      this.updateErrorState();
    }
  }

  quillEditorCreated($event) {
    this._quillInstance = $event;
  }

  private _value;
  set value(newValue: QuillFormFieldDirective | null) {
    this._value = newValue;
    this.stateChanges.next();
  }


  controlType = 'quill-wrapper-directive';
  static nextId = 0;
  @HostBinding() id = `${this.controlType}-id-${QuillFormFieldDirective.nextId++}`;
  focused: boolean;

  @Input()
  get placeholder() {

    return this._placeholder;
  }
  set placeholder(plh) {
    this._placeholder = plh;
    this.stateChanges.next();
  }
  private _placeholder: string;

  get empty() {
    let isEmpty = true;;
    if (this._quillInstance) {
      // since quill always apply new line '/n' at the end of file, lenght === 1 means there is no text inside
      isEmpty = this._quillInstance.getLength() === 1;
    }
    return isEmpty;
  }

  @HostBinding('class.floating')
  get shouldLabelFloat() {
    return this.focused || !this.empty;
  }

  @Input()
  get required() {
    return this._required;
  }
  set required(req) {
    this._required = coerceBooleanProperty(req);
    this.stateChanges.next();
  }
  private _required = false;

  @Input()
  get disabled(): boolean { return this._disabled; }
  set disabled(value: boolean) {
    this._disabled = coerceBooleanProperty(value);
    this.stateChanges.next();
  }
  private _disabled = false;

  // this two properties are taken from base class: CustomErrorStateMixin - through material mixinErrorState
  // errorState = false;
  // stateChanges = new Subject<void>();

  @HostBinding('attr.aria-describedby') describedBy = '';
  setDescribedByIds(ids: string[]) {
    this.describedBy = ids.join(' ');
  }

  onContainerClick(event: MouseEvent) {
  }
}

Example of how to use directive + ngx-quill

<mat-form-field >
    <mat-label>
        Label here
    </mat-label>
    <quill-editor customRichEditor required>
    </quill-editor>

    <mat-hint> this is hint</mat-hint>
    <!-- custom validation error works!!  change ngIf to work with your reactiveFormField -->
    <mat-error *ngIf=" formControl.hasError('required')">
        <span class="mat-caption">
            <strong>REQUIRED</strong>
        </span>
    </mat-error>
</mat-form-field>

All 15 comments

please provide an example as stackblitz or codepen or a demo repo.

Hello

Here is my test repo

https://github.com/tomort/material-quill

you can see the different toolbar and the floating label is on the wrong position.

=================================================

M.J.L. Technische Software GmbH
Puchstrasse 17
A 8020 Graz

E-Mail: t.[email protected]t.orthaber@mjl.at
Web: www.mjl.athttp://www.mjl.at/
Tel. Nr: 0316 / 24 25 61 14
Fax Nr: 0316 / 24 25 61 90

Von: Bengt Weiße notifications@github.com
Gesendet: Montag, 7. Januar 2019 10:48
An: KillerCodeMonkey/ngx-quill ngx-quill@noreply.github.com
Cc: Thomas Orthaber t.orthaber@mjl.at; Author author@noreply.github.com
Betreff: Re: [KillerCodeMonkey/ngx-quill] Material Design (#312)

please provide an example as stackblitz or codepen or a demo repo.

—
You are receiving this because you authored the thread.
Reply to this email directly, view it on GitHubhttps://github.com/KillerCodeMonkey/ngx-quill/issues/312#issuecomment-451879123, or mute the threadhttps://github.com/notifications/unsubscribe-auth/AotE5jwnvy2ShNGLYUfpqoU-XAHnlFrYks5vAxfdgaJpZM4ZzFra.

but that is not a problem with ngx-quill?

this is a general problem in your custom component???
So your question fits more on stack overflow?

If you want to change the toolbar position just checkout the quilljs toolbar documentation.
https://quilljs.com/docs/modules/toolbar/#container

There is an explanation how to build a custom toolbar.
So just put the toolbar underneath your editor element.

The toolbar container can be a css-selector, a whole html element and so on.
Just checkout my component. I added a optional custom slot for the toolbar. It is added above the editor-element. If you would add the editor element first the toolbar would be at the bottom.

First time I try this:

I got the error:
AppComponent.html:20 ERROR Error: mat-form-field must contain a MatFormFieldControl.
at getMatFormFieldMissingControlError (form-field.es5.js:119)
at MatFormField.push../node_modules/@angular/material/esm5/form-field.es5.js.MatFormField._validateControlChild (form-field.es5.js:764)
at MatFormField.push../node_modules/@angular/material/esm5/form-field.es5.js.MatFormField.ngAfterContentInit (form-field.es5.js:453)
at callProviderLifecycles (core.js:20975)
at callElementProvidersLifecycles (core.js:20956)
at callLifecycleHooksChildrenFirst (core.js:20946)
at checkAndUpdateView (core.js:21877)
at callViewAction (core.js:22114)
at execComponentViewsAction (core.js:22056)
at checkAndUpdateView (core.js:21879)

now i build my own control an hope you see quickly whats wrong.

Try it on stackoverflow, hope they coud help me.

Von: Bengt Weiße notifications@github.com
Gesendet: Montag, 7. Januar 2019 15:51
An: KillerCodeMonkey/ngx-quill ngx-quill@noreply.github.com
Cc: Thomas Orthaber t.orthaber@mjl.at; Author author@noreply.github.com
Betreff: Re: [KillerCodeMonkey/ngx-quill] Material Design (#312)

but that is not a problem with ngx-quill?

this is a general problem in your custom component???
So your question fits more on stack overflow?

If you want to change the toolbar position just checkout the quilljs toolbar documentation.
https://quilljs.com/docs/modules/toolbar/#container

There is an explanation how to build a custom toolbar.
So just put the toolbar underneath your editor element.

—
You are receiving this because you authored the thread.
Reply to this email directly, view it on GitHubhttps://github.com/KillerCodeMonkey/ngx-quill/issues/312#issuecomment-451959216, or mute the threadhttps://github.com/notifications/unsubscribe-auth/AotE5mKFnbEMx9JagGsE8d3uLNqk68-Qks5vA17ZgaJpZM4ZzFra.

ah okay now i get it.

yeah this will not work out of the box, because it is more or less a material design thing.
But in your i case i would not create a whole new quill component.

Try building a custom MatFormFieldControl, but using my component in there.

Or try to use my component for inherintence. So you do not have to implement anything on your own and build simply a wrapper that material is happy.

Thx for help

=================================================

M.J.L. Technische Software GmbH
Puchstrasse 17
A 8020 Graz

E-Mail: t.[email protected]t.orthaber@mjl.at
Web: www.mjl.athttp://www.mjl.at/
Tel. Nr: 0316 / 24 25 61 14
Fax Nr: 0316 / 24 25 61 90

Von: Bengt Weiße notifications@github.com
Gesendet: Montag, 7. Januar 2019 16:21
An: KillerCodeMonkey/ngx-quill ngx-quill@noreply.github.com
Cc: Thomas Orthaber t.orthaber@mjl.at; Author author@noreply.github.com
Betreff: Re: [KillerCodeMonkey/ngx-quill] Material Design (#312)

ah okay now i get it.

yeah this will not work out of the box, because it is more or less a material design thing.
But in your i case i would not create a whole new quill component.

Try building a custom MatFormFieldControl, but using my component in there.

—
You are receiving this because you authored the thread.
Reply to this email directly, view it on GitHubhttps://github.com/KillerCodeMonkey/ngx-quill/issues/312#issuecomment-451968969, or mute the threadhttps://github.com/notifications/unsubscribe-auth/AotE5rCUcdLuORsnOwgPzhgUYYTSeqmEks5vA2XBgaJpZM4ZzFra.

btw: i released a new version where you can positionate your custom toolbar at the bottom of the editor ;)

@tomort ,

Did you succeed? If so, could you present your solution?

Tks!

@tomort any update on this?

@tomort same here...any update ?

If anyone is looking for solution, here is mine

Base class for material validation error:

import {  ErrorStateMatcher, CanUpdateErrorStateCtor, mixinErrorState } from "@angular/material/core";
import { NgForm, FormGroupDirective, NgControl } from '@angular/forms';


/* class is used in custom material form field component/wrapper 
    provide material functionality to work with erorsState & ErrorStateMatcher  */
export class CustomErrorStateBase {
    constructor(public _defaultErrorStateMatcher: ErrorStateMatcher,
                public _parentForm: NgForm,
                public _parentFormGroup: FormGroupDirective,
                public ngControl: NgControl) {}
  }


  /* mixinErrorState - material function that appy logic related with material forms errors  */
export const CustomErrorStateMixin: CanUpdateErrorStateCtor & typeof CustomErrorStateBase = mixinErrorState(CustomErrorStateBase);  

Wrapper directive:

import { FocusMonitor } from '@angular/cdk/a11y';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { Directive, DoCheck, ElementRef, HostBinding, Input, OnDestroy, OnInit, Optional, Self } from '@angular/core';
import { FormGroupDirective, NgControl, NgForm } from '@angular/forms';
import { ErrorStateMatcher } from '@angular/material/core';
import { MatFormFieldControl } from '@angular/material/form-field';
import { QuillEditorComponent } from 'ngx-quill';
import { Subscription } from 'rxjs';
import { CustomErrorStateMixin } from '../../../custom-form-fields/Custom-error-state.base';


/* Class is use to insert quill-editor(ngx-quill) inside of the material form field */
@Directive({
  selector: '[CustomRichEditor]',
  providers: [{ provide: MatFormFieldControl, useExisting: QuillFormFieldDirective }],
})
export class QuillFormFieldDirective extends CustomErrorStateMixin implements OnInit, OnDestroy, DoCheck, MatFormFieldControl<QuillFormFieldDirective>{

  _quillInstance: any;
  _quillSubscription: Subscription;

  constructor(
    public _defaultErrorStateMatcher: ErrorStateMatcher,
    @Optional() public _parentForm: NgForm,
    @Optional() public _parentFormGroup: FormGroupDirective,
    @Optional() @Self() public ngControl: NgControl,
    private focusMonitor: FocusMonitor,
    private elementRef: ElementRef<HTMLElement>,
    quillEditor: QuillEditorComponent) {

    super(_defaultErrorStateMatcher, _parentForm, _parentFormGroup, ngControl);

    this._quillSubscription = quillEditor.onEditorCreated.subscribe((event) => this.quillEditorCreated(event));

    focusMonitor.monitor(elementRef.nativeElement, true).subscribe(origin => {
      this.focused = !!origin;
      this.stateChanges.next();
    });
  }

  ngOnInit(): void {
  }

  ngOnDestroy() {
    this.stateChanges.complete();
    this._quillSubscription.unsubscribe();
    this.focusMonitor.stopMonitoring(this.elementRef.nativeElement);
  }

  ngDoCheck() {
    if (this.ngControl) {
      // We need to re-evaluate this on every change detection cycle, because there are some
      // error triggers that we can't subscribe to (e.g. parent form submissions). 
      this.updateErrorState();
    }
  }

  quillEditorCreated($event) {
    this._quillInstance = $event;
  }

  private _value;
  set value(newValue: QuillFormFieldDirective | null) {
    this._value = newValue;
    this.stateChanges.next();
  }


  controlType = 'quill-wrapper-directive';
  static nextId = 0;
  @HostBinding() id = `${this.controlType}-id-${QuillFormFieldDirective.nextId++}`;
  focused: boolean;

  @Input()
  get placeholder() {

    return this._placeholder;
  }
  set placeholder(plh) {
    this._placeholder = plh;
    this.stateChanges.next();
  }
  private _placeholder: string;

  get empty() {
    let isEmpty = true;;
    if (this._quillInstance) {
      // since quill always apply new line '/n' at the end of file, lenght === 1 means there is no text inside
      isEmpty = this._quillInstance.getLength() === 1;
    }
    return isEmpty;
  }

  @HostBinding('class.floating')
  get shouldLabelFloat() {
    return this.focused || !this.empty;
  }

  @Input()
  get required() {
    return this._required;
  }
  set required(req) {
    this._required = coerceBooleanProperty(req);
    this.stateChanges.next();
  }
  private _required = false;

  @Input()
  get disabled(): boolean { return this._disabled; }
  set disabled(value: boolean) {
    this._disabled = coerceBooleanProperty(value);
    this.stateChanges.next();
  }
  private _disabled = false;

  // this two properties are taken from base class: CustomErrorStateMixin - through material mixinErrorState
  // errorState = false;
  // stateChanges = new Subject<void>();

  @HostBinding('attr.aria-describedby') describedBy = '';
  setDescribedByIds(ids: string[]) {
    this.describedBy = ids.join(' ');
  }

  onContainerClick(event: MouseEvent) {
  }
}

Example of how to use directive + ngx-quill

<mat-form-field >
    <mat-label>
        Label here
    </mat-label>
    <quill-editor customRichEditor required>
    </quill-editor>

    <mat-hint> this is hint</mat-hint>
    <!-- custom validation error works!!  change ngIf to work with your reactiveFormField -->
    <mat-error *ngIf=" formControl.hasError('required')">
        <span class="mat-caption">
            <strong>REQUIRED</strong>
        </span>
    </mat-error>
</mat-form-field>

@KillerCodeMonkey: Could the proposed solution (wrapper) be part of this awesome library?

nope, but this would be a new angular module, because it would have a peer dependency to material-ui, and i do not want that in this native angular module.

Maybe, if i have time i could create a new repo for that.

I think it makes sense to have such peer project that you maintain parallel to ngx-quills.

@m-malczyk Thanks a lot ! It works just awesome with also awesome @KillerCodeMonkey's lib!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

EventectiveNButler picture EventectiveNButler  Â·  21Comments

tokaika picture tokaika  Â·  23Comments

manandkumaar picture manandkumaar  Â·  26Comments

zrilo picture zrilo  Â·  21Comments

zpydee picture zpydee  Â·  80Comments