Skip to content
This repository was archived by the owner on May 7, 2021. It is now read-only.

Commit b378247

Browse files
sanbornsenjoshuawilson
authored andcommitted
fix(markdown): will allow or disallow saving empty field (#148)
* fix(bug): replaced nodeValue with childNodes - The value of nodeValue is coming null sometime - To reproduce the bug start editing, blur the field and focus again on the field and see th error on console. * feat(markdown): added option to allow or disallow saving empty field * fix(markdown): tests added to verify allowEmptyField flag
1 parent 7dcc8a1 commit b378247

File tree

5 files changed

+134
-78
lines changed

5 files changed

+134
-78
lines changed

src/app/editable/almeditable.directive.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,12 @@ export class AlmEditableDirective implements OnInit, OnChanges {
1919
@Output('onUpdate') onUpdate = new EventEmitter();
2020
@Input() editable = true;
2121

22-
constructor(private elementRef: ElementRef) {
23-
}
24-
2522
private content: any = '';
2623
private element: HTMLElement = this.elementRef.nativeElement;
2724

25+
constructor(private elementRef: ElementRef) {
26+
}
27+
2828
ngOnInit() {
2929
this.element.style.whiteSpace = 'pre-wrap';
3030
if (this.editable) {
@@ -42,7 +42,7 @@ export class AlmEditableDirective implements OnInit, OnChanges {
4242

4343
onEdit() {
4444
let newContent = this.element.innerText;
45-
if (this.content != newContent) {
45+
if (this.content !== newContent) {
4646
this.content = newContent;
4747
this.onUpdate.emit(this.content);
4848
}
@@ -73,7 +73,7 @@ export class AlmEditableDirective implements OnInit, OnChanges {
7373
let sel = window.getSelection();
7474
range.setStart(
7575
this.element.childNodes[this.element.childNodes.length - 1],
76-
this.element.childNodes[this.element.childNodes.length - 1].nodeValue.length
76+
this.element.childNodes[this.element.childNodes.length - 1].childNodes.length
7777
);
7878
range.collapse(true);
7979
sel.removeAllRanges();

src/app/markdown/examples/markdown-example.component.html

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,20 @@
1818
</div>
1919
</div>
2020

21+
<label> Field with restricted empty field save </label>
22+
<div class="row">
23+
<div class="col-xs-12">
24+
<f8-markdown
25+
[rawText]="''"
26+
[renderedText]="''"
27+
[placeholder]="'This is placeholder text'"
28+
[allowEmptySave]="false"
29+
(onSaveClick)="onSaveOrPreview($event)"
30+
(showPreview)="onSaveOrPreview($event)">
31+
</f8-markdown>
32+
</div>
33+
</div>
34+
2135
<label> Field with placeholder </label>
2236
<div class="row">
2337
<div class="col-xs-12">

src/app/markdown/markdown.component.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
*ngIf="viewType==='markdown'"
2222
[editable]="true"
2323
class="editor-box editor-markdown"
24+
(keyup)="editorKeyUp($event)"
2425
[innerText]="rawText">
2526
</p>
2627
<div #editorBox #previewArea
@@ -53,8 +54,7 @@
5354
<button
5455
(click)="saveClick()"
5556
class="fl btn btn-primary pull-right action-btn btn-save"
56-
[disabled]="saving"
57-
[class.disabled]="saving">
57+
[disabled]="saving || (!allowEmptySave && fieldEmpty)">
5858
<i class="fa fa-check"></i>
5959
</button>
6060
</div>

src/app/markdown/markdown.component.spec.ts

Lines changed: 86 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -30,58 +30,90 @@ describe('Markdown component - ', () => {
3030
});
3131
}));
3232

33-
it('Should handle Markdown checkboxes correctly.',
34-
inject([DomSanitizer], (domSanitizer: DomSanitizer) => {
35-
// tslint:disable-next-line:max-line-length
36-
comp.inpRawText = '# hello, markdown!\n* [ ] Item 1\n* [x] Item 2\n* [ ] Item 3';
37-
let originalHTML = '<h1>hello, markdown!\</h1><ul>' +
38-
// tslint:disable-next-line:max-line-length
39-
'<li><input class="markdown-checkbox" type="checkbox" data-checkbox-index="0"></input> Item 0</li>' +
40-
// tslint:disable-next-line:max-line-length
41-
'<li><input class="markdown-checkbox" type="checkbox" checked="" data-checkbox-index="1"></input> Item 1</li>' +
42-
// tslint:disable-next-line:max-line-length
43-
'<li><input class="markdown-checkbox" type="checkbox" data-checkbox-index="2"></input> Item 2</li></ul>';
44-
// in this test, we provide a SaveValue to the component.
45-
comp.inpRenderedText = domSanitizer.bypassSecurityTrustHtml(originalHTML);
46-
// this first detectChanges() updates the component that one of the @Inputs has changed.
47-
fixture.detectChanges();
48-
// because of https://github.com/angular/angular/issues/9866, detectChanges() does not
49-
// call ngOnChanges() on the component (yeah, it it as broken as it sounds). So
50-
// we need to call the component manually to update.
51-
comp.ngOnChanges({
52-
inpRawText: {} as SimpleChange,
53-
inpRenderedText: {} as SimpleChange
54-
} as SimpleChanges);
55-
// and because the test framework is not even able to detect inner changes to a component,
56-
// we need to call detectChanges() again.
57-
fixture.detectChanges();
58-
// also, using query() is also not working. Maybe due to the dynamic update of innerHTML.
59-
// So we need to use the nativeElement to get a selector working.
60-
// tslint:disable-next-line:max-line-length
61-
let markdownPreview: Element = fixture.debugElement.nativeElement.querySelector('.markdown-rendered');
62-
expect(markdownPreview).not.toBeNull();
63-
// preview render of the template default
64-
let markdownCheckboxElementList = markdownPreview.querySelectorAll('.markdown-checkbox');
65-
expect(markdownCheckboxElementList).not.toBeNull();
66-
expect(markdownCheckboxElementList.length).toBe(3);
67-
expect(markdownCheckboxElementList[0].hasAttribute('checked')).toBeFalsy();
68-
expect(markdownCheckboxElementList[1].hasAttribute('checked')).toBeTruthy();
69-
expect(markdownCheckboxElementList[2].hasAttribute('checked')).toBeFalsy();
70-
// tick a checkbox
71-
let checkboxElem = markdownCheckboxElementList[0] as HTMLElement;
72-
checkboxElem.click();
73-
// see if it ends up in the Markdown
74-
expect(comp.rawText.indexOf('[x] Item 1')).toBeGreaterThan(-1);
75-
// tick another checkbox
76-
checkboxElem = markdownCheckboxElementList[2] as HTMLElement;
77-
checkboxElem.click();
78-
// see if it ends up in the Markdown
79-
expect(comp.rawText.indexOf('[x] Item 3')).toBeGreaterThan(-1);
80-
// untick a checkbox
81-
checkboxElem = markdownCheckboxElementList[1] as HTMLElement;
82-
checkboxElem.click();
83-
// see if it ends up in the Markdown
84-
expect(comp.rawText.indexOf('[ ] Item 2')).toBeGreaterThan(-1);
85-
})
86-
);
33+
it('Should handle Markdown checkboxes correctly.',
34+
inject([DomSanitizer], (domSanitizer: DomSanitizer) => {
35+
// tslint:disable-next-line:max-line-length
36+
comp.inpRawText = '# hello, markdown!\n* [ ] Item 1\n* [x] Item 2\n* [ ] Item 3';
37+
let originalHTML = '<h1>hello, markdown!\</h1><ul>' +
38+
// tslint:disable-next-line:max-line-length
39+
'<li><input class="markdown-checkbox" type="checkbox" data-checkbox-index="0"></input> Item 0</li>' +
40+
// tslint:disable-next-line:max-line-length
41+
'<li><input class="markdown-checkbox" type="checkbox" checked="" data-checkbox-index="1"></input> Item 1</li>' +
42+
// tslint:disable-next-line:max-line-length
43+
'<li><input class="markdown-checkbox" type="checkbox" data-checkbox-index="2"></input> Item 2</li></ul>';
44+
// in this test, we provide a SaveValue to the component.
45+
comp.inpRenderedText = domSanitizer.bypassSecurityTrustHtml(originalHTML);
46+
// this first detectChanges() updates the component that one of the @Inputs has changed.
47+
fixture.detectChanges();
48+
// because of https://github.com/angular/angular/issues/9866, detectChanges() does not
49+
// call ngOnChanges() on the component (yeah, it it as broken as it sounds). So
50+
// we need to call the component manually to update.
51+
comp.ngOnChanges({
52+
inpRawText: {} as SimpleChange,
53+
inpRenderedText: {} as SimpleChange
54+
} as SimpleChanges);
55+
// and because the test framework is not even able to detect inner changes to a component,
56+
// we need to call detectChanges() again.
57+
fixture.detectChanges();
58+
// also, using query() is also not working. Maybe due to the dynamic update of innerHTML.
59+
// So we need to use the nativeElement to get a selector working.
60+
// tslint:disable-next-line:max-line-length
61+
let markdownPreview: Element = fixture.debugElement.nativeElement.querySelector('.markdown-rendered');
62+
expect(markdownPreview).not.toBeNull();
63+
// preview render of the template default
64+
let markdownCheckboxElementList = markdownPreview.querySelectorAll('.markdown-checkbox');
65+
expect(markdownCheckboxElementList).not.toBeNull();
66+
expect(markdownCheckboxElementList.length).toBe(3);
67+
expect(markdownCheckboxElementList[0].hasAttribute('checked')).toBeFalsy();
68+
expect(markdownCheckboxElementList[1].hasAttribute('checked')).toBeTruthy();
69+
expect(markdownCheckboxElementList[2].hasAttribute('checked')).toBeFalsy();
70+
// tick a checkbox
71+
let checkboxElem = markdownCheckboxElementList[0] as HTMLElement;
72+
checkboxElem.click();
73+
// see if it ends up in the Markdown
74+
expect(comp.rawText.indexOf('[x] Item 1')).toBeGreaterThan(-1);
75+
// tick another checkbox
76+
checkboxElem = markdownCheckboxElementList[2] as HTMLElement;
77+
checkboxElem.click();
78+
// see if it ends up in the Markdown
79+
expect(comp.rawText.indexOf('[x] Item 3')).toBeGreaterThan(-1);
80+
// untick a checkbox
81+
checkboxElem = markdownCheckboxElementList[1] as HTMLElement;
82+
checkboxElem.click();
83+
// see if it ends up in the Markdown
84+
expect(comp.rawText.indexOf('[ ] Item 2')).toBeGreaterThan(-1);
85+
})
86+
);
87+
88+
fit('should emit output onsave empty field when ' +
89+
'`allowEmptySave` is false and the field is empty', () => {
90+
spyOn(comp.onSaveClick, 'emit');
91+
comp.allowEmptySave = false;
92+
comp.fieldEmpty = true;
93+
comp.previousRawText = 'abc';
94+
comp.rawText = 'xyz';
95+
comp.saveClick();
96+
expect(comp.onSaveClick.emit).not.toHaveBeenCalled();
97+
});
98+
99+
fit('should emit output onsave empty field when `allowEmptySave` is true', () => {
100+
spyOn(comp.onSaveClick, 'emit');
101+
comp.allowEmptySave = true;
102+
comp.fieldEmpty = true;
103+
comp.previousRawText = 'abc';
104+
comp.rawText = 'xyz';
105+
comp.saveClick();
106+
expect(comp.onSaveClick.emit).toHaveBeenCalled();
107+
});
108+
109+
fit('should emit output onsave empty field when ' +
110+
'`allowEmptySave` is false and the field is not empty', () => {
111+
spyOn(comp.onSaveClick, 'emit');
112+
comp.allowEmptySave = false;
113+
comp.fieldEmpty = false;
114+
comp.previousRawText = 'abc';
115+
comp.rawText = 'xyz';
116+
comp.saveClick();
117+
expect(comp.onSaveClick.emit).toHaveBeenCalled();
118+
});
87119
});

src/app/markdown/markdown.component.ts

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export class MarkdownComponent implements OnChanges, OnInit, AfterViewChecked {
3939
@Input() placeholder: string = 'This is place holder';
4040
@Input() editAllow: boolean = true;
4141
@Input() renderedHeight: number = 300;
42+
@Input() allowEmptySave: boolean = true;
4243

4344
@Output() onActiveEditor = new EventEmitter();
4445
@Output() onSaveClick = new EventEmitter();
@@ -54,14 +55,16 @@ export class MarkdownComponent implements OnChanges, OnInit, AfterViewChecked {
5455
// these need to be public for the tests accessing them.
5556
renderedText: any = '';
5657
rawText = '';
58+
fieldEmpty: boolean = true;
59+
previousRawText = '';
60+
5761
private markdownViewExpanded: boolean = false;
5862
private tabBarVisible: boolean = true;
5963
private viewType: string = 'preview'; // markdown
6064
private editorActive: boolean = false;
6165
private showMore = false;
6266
private inputsDisabled = false;
6367

64-
private previousRawText = '';
6568
private previousRenderedText = '';
6669

6770
constructor(private cdr: ChangeDetectorRef, private elementRef: ElementRef) {}
@@ -244,25 +247,32 @@ export class MarkdownComponent implements OnChanges, OnInit, AfterViewChecked {
244247
}
245248

246249
saveClick() {
247-
if (this.viewType === 'markdown' &&
248-
this.previousRawText !== this.editorInput.nativeElement.innerText.trim()) {
249-
this.saving = true;
250-
this.onSaveClick.emit({
251-
rawText: this.editorInput.nativeElement.innerText.trim(),
252-
callBack: (t: string, m: string) => this.saveUpdate(t, m)
253-
});
254-
} else if (this.viewType === 'preview' &&
255-
this.previousRawText !== this.rawText) {
256-
this.saving = true;
257-
this.onSaveClick.emit({
258-
rawText: this.rawText,
259-
callBack: (t: string, m: string) => this.saveUpdate(t, m)
260-
});
261-
} else {
262-
this.deactivateEditor();
250+
if (this.allowEmptySave || (!this.fieldEmpty && !this.allowEmptySave)) {
251+
if (this.viewType === 'markdown' &&
252+
this.previousRawText !== this.editorInput.nativeElement.innerText.trim()) {
253+
this.saving = true;
254+
this.onSaveClick.emit({
255+
rawText: this.editorInput.nativeElement.innerText.trim(),
256+
callBack: (t: string, m: string) => this.saveUpdate(t, m)
257+
});
258+
} else if (this.viewType === 'preview' &&
259+
this.previousRawText !== this.rawText) {
260+
this.saving = true;
261+
this.onSaveClick.emit({
262+
rawText: this.rawText,
263+
callBack: (t: string, m: string) => this.saveUpdate(t, m)
264+
});
265+
} else {
266+
this.deactivateEditor();
267+
}
263268
}
264269
}
265270

271+
editorKeyUp(event: Event) {
272+
this.fieldEmpty =
273+
event.srcElement.textContent.trim() === '';
274+
}
275+
266276
closeClick() {
267277
// Restore saved previous data
268278
this.rawText = this.previousRawText;

0 commit comments

Comments
 (0)