Hi,
I'm facing an issue when I try to test a component including the quill-editor
I got the same results as in https://github.com/KillerCodeMonkey/ngx-quill/issues/153
I tried 2 approach :
quill-editor component is a child of my component I did tried like in the source test file to import QuillModule in my component, pass it to the imports, provide the QuillModule.forRoot().providers but I always got a TypeError: Cannot read property 'classList' of nullngClass or anything related to class manipulation in my component.QuillModule I got issues with the FormModule Error: No value accessor for form control with unspecified name attributeDo you have any idea ?
Or could you add some tests in your exemple repo ?
It would help a lot. I can try to help if you want too.
Have a nice day, thanks !
could you share some example tests?
The 'classList' of null stuff is part of a PR, where you can now pass classes as a string, e.g.
<quill-editor classes="testi test"></quill-editor>
So maybe you pass something invalid there?
Ok, I tried to add classes="aze" on my quill-editor component but didn't change.
Here is what I got :
text-layer-editor.component.html
<div
class="rm-text-layer--editor--wrapper"
[ngStyle]=getStyle()>
<quill-editor
class="rm-text-layer--editor"
[placeholder]="defaultPlaceholder"
format="html"
[(ngModel)]="rmTextLayer.content"
[modules]="modules"
(onBlur)="blur()"
(onEditorCreated)="onCreated($event)"
(onEditorChanged)="onChange($event)"
[preserveWhitespace]="true">
</quill-editor>
</div>
text-layer-editor.component.spec.ts
import { async, ComponentFixture, TestBed } from '@angular/core/testing'
import { FormsModule } from '@angular/forms'
import { Subject } from 'rxjs'
import { QuillModule } from 'ngx-quill'
import { TextLayerEditorComponent } from './text-layer-editor.component'
import { TextLayerEditorService } from './text-layer-editor.service'
describe('TextLayerEditorComponent', () => {
let component: TextLayerEditorComponent
let fixture: ComponentFixture<TextLayerEditorComponent>
let fakeTextLayerEditorService: any
beforeEach(async(() => {
fakeTextLayerEditorService = {}
TestBed.configureTestingModule({
declarations: [ TextLayerEditorComponent ],
imports: [FormsModule, QuillModule],
providers: [
{ provide: TextLayerEditorService, useValue: fakeTextLayerEditorService },
QuillModule.forRoot().providers,
]
}).compileComponents()
fixture = TestBed.createComponent(TextLayerEditorComponent) as ComponentFixture<TextLayerEditorComponent>
}))
beforeEach(() => {
component = fixture.componentInstance
component.setEditor = new Subject()
fixture.detectChanges()
})
it('should create', () => {
expect(component).toBeTruthy()
})
})
Bonus : not really useful here :
text-layer-editor.component.ts
import { Component, OnInit, Input, OnDestroy } from '@angular/core'
import { Observable } from 'rxjs'
import { TextLayer } from '../config/text-layer.model'
import { getPosition } from '../../config/position-helper'
import { getSelectedNode } from '../config/text-layer-helper'
import { TextLayerEditorService } from './text-layer-editor.service'
import { quillModules } from './quill-editor-config'
@Component({
selector: 'rm-editor-text-layer-editor',
templateUrl: './text-layer-editor.component.html',
styleUrls: ['./text-layer-editor.component.sass']
})
export class TextLayerEditorComponent implements OnInit, OnDestroy {
@Input() setEditor: Observable<any>
setEditorSubscription: any
editor: any
rmTextLayer: TextLayer = { x: 0, y: 0, w: 0, h: 0 }
defaultPlaceholder = 'Enter text here'
modules = quillModules
constructor(private _editorService: TextLayerEditorService) {}
get isDisplayed(): boolean {
return this._editorService.isDisplayed
}
set isDisplayed(newVal: boolean) {
this._editorService.isDisplayed = newVal
}
ngOnInit() {
this.setEditorSubscription = this.setEditor.subscribe(
(params) => this.handleSetEditor(params)
)
}
onCreated(event: any) {
this.editor = event
}
handleSetEditor(params: any) {
this.isDisplayed = true
this.rmTextLayer = params.layer
setTimeout(() => {
this.editor.focus()
const range = document.createRange()
const sel = window.getSelection()
const { selectedNodeIndex, carretOffset } = params.carretPosition
const selectedNode = getSelectedNode(this.editor.root, selectedNodeIndex)
// range.setStart(selectedNode, carretOffset)
range.selectNodeContents(selectedNode)
// range.collapse(true)
sel.removeAllRanges()
sel.addRange(range)
}, 20)
}
getStyle() {
return {
display: (this.isDisplayed) ? 'block' : 'none',
...getPosition(this.rmTextLayer)
}
}
blur() {
this.isDisplayed = false
}
onChange(ev: any) {
this._editorService.onEditorChange(ev.editor)
}
ngOnDestroy() {
this.setEditorSubscription.unsubscribe()
}
}
Thats strange. i do not know if i have the time this week to inspect this deeper.
Maybe you can just try to add a simple test to the example repo?
Yeah I can't test my component's including quill-editor, quill-view or quill-toolbar yet :(
I'm still new to Karma but it's just a expect(component).toBeTruthy() so that's weird.
I'll see what I can do on the example repo.
strange, i added a simple test to the example repo and it is working quite fine for me:
Sweet !
I spotted a difference here.
In your beforeEach you didn't add fixture.detectChanges()
If I remove it, it works. If I let it it won't
but even with.
describe('TextLayerEditorComponent', () => {
let fixture: ComponentFixture<DefaultComponent>
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ DefaultComponent ],
imports: [FormsModule, QuillModule],
providers: QuillModule.forRoot().providers
}).compileComponents()
fixture = TestBed.createComponent(DefaultComponent) as ComponentFixture<DefaultComponent>
fixture.detectChanges()
}))
it('should create', () => {
expect(fixture.componentInstance).toBeTruthy()
})
})
it is working
True, that's interesting but I'm a bit clueless :/
ah i think your providers are wrong, just try the following:
TestBed.configureTestingModule({
declarations: [ TextLayerEditorComponent ],
imports: [FormsModule, QuillModule],
providers: [
{ provide: TextLayerEditorService, useValue: fakeTextLayerEditorService },
...QuillModule.forRoot().providers,
]
}).compileComponents()
fixture = TestBed.createComponent(TextLayerEditorComponent) as ComponentFixture<TextLayerEditorComponent>
}))
Yeah I tried it too, I even removed the fakeTextLayerEditorService like this
describe('TextLayerEditorComponent', () => {
let component: TextLayerEditorComponent
let fixture: ComponentFixture<TextLayerEditorComponent>
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ TextLayerEditorComponent ],
imports: [FormsModule, QuillModule],
providers: QuillModule.forRoot().providers
}).compileComponents()
fixture = TestBed.createComponent(TextLayerEditorComponent) as ComponentFixture<TextLayerEditorComponent>
}))
beforeEach(() => {
component = fixture.componentInstance
component.setEditor = new Subject()
fixture.detectChanges()
})
it('should create', () => {
expect(component).toBeTruthy()
})
})
And still got the same issue 馃槺
okay and can you try to remove "class="rm-text-layer--editor" from the editor component?.
Yes, it's the same.
In your example, the one you use with format="html" is wrapped in a formGroup and has a formControlName
Is that required ?
i do not think so.
the example testcase is also not using formControls :)
could you simplify your component a little bit?
keep in mind that detectChange, triggers the changeDetection, so ngOnInit gets called, so could you comment out your handleSetEditor and ngOnInit function?
Yes I tried but didn't change neither
New hint, it work when I comment my modules = quillModules
It's coming from this config for info
import Quill from 'quill'
import { FontSizeConfig } from '../config/available-font-sizes'
import { AvailableFonts } from '../config/available-fonts'
import { LineHeightConfig } from '../config/available-line-height'
import { LetterSpacingConfig } from '../config/available-letter-spacings'
import { AvailableColors } from '../config/available-colors'
import { BottomSpaceConfig } from './../config/available-bottom-space'
import {
setBottomSpace,
updateFontSize,
updateLineHeight,
updateLetterSpacing,
updateBottomSpace,
setColor,
setTextAlign
} from '../config/format-helper'
const size = Quill.import('attributors/style/size')
const font = Quill.import('formats/font')
const Parchment = Quill.import('parchment')
const getConf = (whitelistValue: string[], scopeName: string): any => {
return {scope: Parchment.Scope[scopeName], whitelist: whitelistValue}
}
const newStyle = (
customName: string, cssProperty: string,
whitelistValue: string[], scopeName: string = 'INLINE') => {
return new Parchment.Attributor.Style(customName, cssProperty, getConf(whitelistValue, scopeName))
}
const lineHeightStyle = newStyle('lineHeight', 'line-height', LineHeightConfig.availableString)
const letterSpacingStyle = newStyle('letterSpacing', 'letter-spacing', LetterSpacingConfig.availablePx)
const colorStyle = newStyle('customColor', 'color', AvailableColors)
const bottomSpaceStyle = newStyle('bottomSpace', 'margin-bottom', BottomSpaceConfig.availablePx, 'BLOCK')
const textAlignStyle = newStyle('textAlign', 'text-align', ['left', 'right', 'center', 'justify'], 'BLOCK')
size.whitelist = FontSizeConfig.availablePx
font.whitelist = AvailableFonts
Quill.register(lineHeightStyle, true)
Quill.register(letterSpacingStyle, true)
Quill.register(colorStyle, true)
Quill.register(bottomSpaceStyle, true)
Quill.register(size, true)
Quill.register(font, true)
Quill.register(textAlignStyle, true)
export const quillModules = {
toolbar: {
container: '#rm-text-layer--toolbar',
handlers: {
increaseFontSize() { updateFontSize(this.quill, 1) },
decreaseFontSize() { updateFontSize(this.quill, -1) },
increaseLineHeight() { updateLineHeight(this.quill, 0.5) },
decreaseLineHeight() { updateLineHeight(this.quill, -0.5) },
increaseLetterSpacing() { updateLetterSpacing(this.quill, 1) },
decreaseLetterSpacing() { updateLetterSpacing(this.quill, -1) },
setColor(value) { setColor(this.quill, value) },
setBottomSpace(value) { setBottomSpace(this.quill, value) },
increaseBottomSpace() { updateBottomSpace(this.quill, 1) },
decreaseBottomSpace() { updateBottomSpace(this.quill, -1) },
chooseTextAlign(value) { setTextAlign(this.quill, value) },
}
},
}
For info here is my format helper
import { getCurrentSelectionParentElement } from './text-layer-helper'
export const pxToNumber = (px: string): number => parseInt(px.replace('px', ''), 10)
/*
* Getters
*********/
export const getCurrentFormat = (quill: any) => {
return quill.getFormat(quill.selection.savedRange)
}
export const getPropertyFormat = (quill: any, format: any, formatProp: string, defaultVal) => {
return (format || getCurrentFormat(quill))[formatProp] || defaultVal
}
export const getCurrentFontSize = (quill: any, format?: any): number => {
const currentSize = getPropertyFormat(quill, format, 'size', '16px')
return pxToNumber(currentSize)
}
export const getCurrentLineHeight = (quill: any, format?: any): number => {
const currentValue = getPropertyFormat(quill, format, 'lineHeight', '1.5')
return parseFloat(currentValue)
}
export const getCurrentLetterSpacing = (quill: any, format?: any): number => {
const currentValue = getPropertyFormat(quill, format, 'letterSpacing', '0px')
return pxToNumber(currentValue)
}
export const getCurrentBottomSpace = (quill: any, format?: any): number => {
const currentValue = getPropertyFormat(quill, format, 'bottomSpace', '0px')
return pxToNumber(currentValue)
}
export const getTextAlign = (quill: any, format?: any): string => {
return getPropertyFormat(quill, format, 'textAlign', 'left')
}
export const isCurrentlyBold = (quill: any, format?: any): boolean => {
return getPropertyFormat(quill, format, 'bold', false)
}
export const isCurrentlyItalic = (quill: any, format?: any): boolean => {
return getPropertyFormat(quill, format, 'italic', false)
}
/*
* Setters
*********/
const setCustom = (customProp: string, quill: any, value: string) => { quill.format(customProp, value, 'user') }
export const setColor = (quill: any, value: string) => { setCustom('customColor', quill, value) }
export const setFontSize = (quill: any, value: string) => { setCustom('font-size', quill, value) }
export const setLineHeight = (quill: any, value: string) => { setCustom('lineHeight', quill, value) }
export const setLetterSpacing = (quill: any, value: string) => { setCustom('letterSpacing', quill, value) }
export const setBottomSpace = (quill: any, value: string) => { setCustom('bottomSpace', quill, value) }
export const setTextAlign = (quill: any, value: string) => { setCustom('text-align', quill, value) }
/*
* Updates
*********/
export const updateFontSize = (quill: any, variation: number) => {
setFontSize(quill, `${ getCurrentFontSize(quill) + variation }px`)
}
export const updateLineHeight = (quill: any, variation: number) => {
setLineHeight(quill, `${ ((getCurrentLineHeight(quill) * 10) + variation) / 10 }`)
}
export const updateLetterSpacing = (quill: any, variation: number) => {
setLetterSpacing(quill, `${ getCurrentLetterSpacing(quill) + variation }px`)
}
export const updateBottomSpace = (quill: any, variation: number) => {
const node = getCurrentSelectionParentElement('P')
const sel = window.getSelection()
const range = document.createRange()
range.selectNode(node)
const userRange = document.createRange()
userRange.setStart(sel.anchorNode, sel.anchorOffset)
userRange.setEnd(sel.focusNode, sel.focusOffset)
sel.removeAllRanges()
sel.addRange(range)
setBottomSpace(quill, `${getCurrentBottomSpace(quill) + variation}px`)
sel.removeAllRanges()
sel.addRange(userRange)
}
woah but thats a thing of your project.
I will totaly not check your whole quill configuration 馃槈
And the error message with the classList of null stuff makes no sense then?!
what i see is: #rm-text-layer--toolbar is the selector for your toolbar container, but there is no toolbar with that id?
Yes sure, I won't ask for that :)
Yeah the error make no sense :/
The toolbar is defined in an other component.
I use multiple quill-view on my page and one quill-editor bind to a quill-editor-toolbar.
When I click on a quill view I set the editor at the same place and edit the content by binding the content of the view / editor. It works like a charm but not the tests
Thanks for your help, it led me on the toolbar and it was the issue.
In the beforeEach I tried to redefine my component modules and I saw that some document query where called.
So I fixed it like that
beforeEach(() => {
component = fixture.componentInstance
component.setEditor = new Subject()
component.modules = {
toolbar: [['bold', 'italic']]
} as any
fixture.detectChanges()
})
I had to cast it as any because my modules structure is different by default.
Thanks @KillerCodeMonkey always a pleasure to find some solutions with you ! :)
(this way it uses the default quill toolbar instead of trying to fetch a custom one from the DOM, which is defined in an other component)
Ah nice you solved it.
Yeah that still feel a bit like it's a trick but at least it's not a commented test ...
Have a nice day, thanks for your help !