From 709b49cb75b35fdd8dbb8c4a95d29fbbe2ea6622 Mon Sep 17 00:00:00 2001 From: yavorsk Date: Mon, 2 Sep 2024 10:18:16 +0300 Subject: [PATCH 01/32] empty editing placeholder for rich text field --- .../src/components/base-field.directive.ts | 19 ++++++++++++++ .../default-empty-field-editing.component.ts | 10 ++++++++ .../src/components/rendering-field.ts | 7 +++--- .../src/components/rich-text.directive.ts | 25 +++++++++++++------ 4 files changed, 51 insertions(+), 10 deletions(-) create mode 100644 packages/sitecore-jss-angular/src/components/base-field.directive.ts create mode 100644 packages/sitecore-jss-angular/src/components/default-empty-field-editing.component.ts diff --git a/packages/sitecore-jss-angular/src/components/base-field.directive.ts b/packages/sitecore-jss-angular/src/components/base-field.directive.ts new file mode 100644 index 0000000000..78ec6af067 --- /dev/null +++ b/packages/sitecore-jss-angular/src/components/base-field.directive.ts @@ -0,0 +1,19 @@ +import { Directive, Type, ViewContainerRef, EmbeddedViewRef, TemplateRef } from '@angular/core'; + +@Directive() +export abstract class BaseFieldDirective { + protected viewRef: EmbeddedViewRef; + + constructor(protected viewContainer: ViewContainerRef) {} + + protected renderEmptyFieldEditingComponent( + emptyFieldEditingComponent: Type | TemplateRef + ) { + if (typeof emptyFieldEditingComponent === 'object') { + this.viewContainer.createEmbeddedView(emptyFieldEditingComponent as TemplateRef); + } else { + this.viewContainer.clear(); + this.viewContainer.createComponent(emptyFieldEditingComponent as Type); + } + } +} diff --git a/packages/sitecore-jss-angular/src/components/default-empty-field-editing.component.ts b/packages/sitecore-jss-angular/src/components/default-empty-field-editing.component.ts new file mode 100644 index 0000000000..5f6310196e --- /dev/null +++ b/packages/sitecore-jss-angular/src/components/default-empty-field-editing.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +/** + * Demonstrates usage of a Rich Text (HTML) content field within JSS. + */ +@Component({ + selector: 'app-default-empty-field-editing', + template: '[No text in field]', +}) +export class DefaultEmptyFieldEditingComponent {} diff --git a/packages/sitecore-jss-angular/src/components/rendering-field.ts b/packages/sitecore-jss-angular/src/components/rendering-field.ts index f9f7d783f0..608e666bcf 100644 --- a/packages/sitecore-jss-angular/src/components/rendering-field.ts +++ b/packages/sitecore-jss-angular/src/components/rendering-field.ts @@ -1,12 +1,13 @@ /* eslint-disable @typescript-eslint/no-empty-interface */ -export interface RenderingField { +import { FieldMetadata } from '@sitecore-jss/sitecore-jss/layout'; + +export interface RenderingField extends FieldMetadata { value?: V; editable?: string; } -export interface DateField { +export interface DateField extends RenderingField { value?: string | number | Date; - editable?: string; } export interface FileFieldValue { diff --git a/packages/sitecore-jss-angular/src/components/rich-text.directive.ts b/packages/sitecore-jss-angular/src/components/rich-text.directive.ts index 61fb87a26b..e35ac78927 100644 --- a/packages/sitecore-jss-angular/src/components/rich-text.directive.ts +++ b/packages/sitecore-jss-angular/src/components/rich-text.directive.ts @@ -1,6 +1,5 @@ import { Directive, - EmbeddedViewRef, Input, OnChanges, SimpleChanges, @@ -11,23 +10,29 @@ import { import { Router } from '@angular/router'; import { isAbsoluteUrl } from '@sitecore-jss/sitecore-jss/utils'; import { RichTextField } from './rendering-field'; +import { BaseFieldDirective } from './base-field.directive'; +import { DefaultEmptyFieldEditingComponent } from './default-empty-field-editing.component'; +import { isFieldValueEmpty } from '@sitecore-jss/sitecore-jss/layout'; @Directive({ selector: '[scRichText]', }) -export class RichTextDirective implements OnChanges { +export class RichTextDirective extends BaseFieldDirective implements OnChanges { @Input('scRichTextEditable') editable = true; + @Input('scRichTextEmptyFieldEditingTemplate') emptyFieldEditingTemplate: TemplateRef; @Input('scRichText') field: RichTextField; - private viewRef: EmbeddedViewRef; - constructor( - private viewContainer: ViewContainerRef, + viewContainer: ViewContainerRef, private templateRef: TemplateRef, private renderer: Renderer2, private router: Router - ) {} + ) { + super(viewContainer); + } + + ngOnInit() {} ngOnChanges(changes: SimpleChanges) { if (changes.field || changes.editable) { @@ -42,7 +47,13 @@ export class RichTextDirective implements OnChanges { private updateView() { const field = this.field; - if (!field || (!field.editable && !field.value)) { + if (!field?.editable && isFieldValueEmpty(this.field)) { + if (this.field?.metadata && this.editable) { + super.renderEmptyFieldEditingComponent( + this.emptyFieldEditingTemplate ?? DefaultEmptyFieldEditingComponent + ); + } + return; } From 8b816d764a3fe834968208877723d5e6b23863fb Mon Sep 17 00:00:00 2001 From: yavorsk Date: Mon, 2 Sep 2024 10:22:10 +0300 Subject: [PATCH 02/32] minor updates --- .../src/components/rich-text.directive.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/sitecore-jss-angular/src/components/rich-text.directive.ts b/packages/sitecore-jss-angular/src/components/rich-text.directive.ts index e35ac78927..6d4a4bf2ae 100644 --- a/packages/sitecore-jss-angular/src/components/rich-text.directive.ts +++ b/packages/sitecore-jss-angular/src/components/rich-text.directive.ts @@ -19,10 +19,11 @@ import { isFieldValueEmpty } from '@sitecore-jss/sitecore-jss/layout'; }) export class RichTextDirective extends BaseFieldDirective implements OnChanges { @Input('scRichTextEditable') editable = true; - @Input('scRichTextEmptyFieldEditingTemplate') emptyFieldEditingTemplate: TemplateRef; @Input('scRichText') field: RichTextField; + @Input('scRichTextEmptyFieldEditingTemplate') emptyFieldEditingTemplate: TemplateRef; + constructor( viewContainer: ViewContainerRef, private templateRef: TemplateRef, @@ -32,8 +33,6 @@ export class RichTextDirective extends BaseFieldDirective implements OnChanges { super(viewContainer); } - ngOnInit() {} - ngOnChanges(changes: SimpleChanges) { if (changes.field || changes.editable) { if (!this.viewRef) { From 3926a04dc061d46b7f2930d6b7878dd34a809951 Mon Sep 17 00:00:00 2001 From: yavorsk Date: Mon, 2 Sep 2024 10:49:17 +0300 Subject: [PATCH 03/32] empty editing placeholder for date and text fields --- .../src/components/date.directive.ts | 22 +++++++++++++------ .../src/components/rendering-field.ts | 4 +--- .../src/components/text.directive.ts | 20 ++++++++++++----- 3 files changed, 30 insertions(+), 16 deletions(-) diff --git a/packages/sitecore-jss-angular/src/components/date.directive.ts b/packages/sitecore-jss-angular/src/components/date.directive.ts index 9d1981e302..0960866cf8 100644 --- a/packages/sitecore-jss-angular/src/components/date.directive.ts +++ b/packages/sitecore-jss-angular/src/components/date.directive.ts @@ -1,7 +1,6 @@ import { DatePipe } from '@angular/common'; import { Directive, - EmbeddedViewRef, Input, OnChanges, SimpleChanges, @@ -9,11 +8,14 @@ import { ViewContainerRef, } from '@angular/core'; import { DateField } from './rendering-field'; +import { BaseFieldDirective } from './base-field.directive'; +import { isFieldValueEmpty } from '@sitecore-jss/sitecore-jss/layout'; +import { DefaultEmptyFieldEditingComponent } from './default-empty-field-editing.component'; @Directive({ selector: '[scDate]', }) -export class DateDirective implements OnChanges { +export class DateDirective extends BaseFieldDirective implements OnChanges { @Input('scDateFormat') format?: string; @Input('scDateTimezone') timezone?: string; @@ -24,13 +26,15 @@ export class DateDirective implements OnChanges { @Input('scDate') field: DateField; - private viewRef: EmbeddedViewRef; + @Input('scDateEmptyFieldEditingTemplate') emptyFieldEditingTemplate: TemplateRef; constructor( - private viewContainer: ViewContainerRef, + viewContainer: ViewContainerRef, private templateRef: TemplateRef, private datePipe: DatePipe - ) {} + ) { + super(viewContainer); + } ngOnChanges(changes: SimpleChanges) { if (changes.field || changes.format) { @@ -46,8 +50,12 @@ export class DateDirective implements OnChanges { private updateView() { const field = this.field; - if (!field || (!field.editable && !field.value)) { - return; + if (!field?.editable && isFieldValueEmpty(this.field)) { + if (this.field?.metadata && this.editable) { + super.renderEmptyFieldEditingComponent( + this.emptyFieldEditingTemplate ?? DefaultEmptyFieldEditingComponent + ); + } } const html = field.editable && this.editable ? field.editable : field.value; diff --git a/packages/sitecore-jss-angular/src/components/rendering-field.ts b/packages/sitecore-jss-angular/src/components/rendering-field.ts index 608e666bcf..6f8ad3016e 100644 --- a/packages/sitecore-jss-angular/src/components/rendering-field.ts +++ b/packages/sitecore-jss-angular/src/components/rendering-field.ts @@ -6,9 +6,7 @@ export interface RenderingField extends FieldMetadata { editable?: string; } -export interface DateField extends RenderingField { - value?: string | number | Date; -} +export interface DateField extends RenderingField {} export interface FileFieldValue { src?: string; diff --git a/packages/sitecore-jss-angular/src/components/text.directive.ts b/packages/sitecore-jss-angular/src/components/text.directive.ts index 6e823e9f74..89d116e6cd 100644 --- a/packages/sitecore-jss-angular/src/components/text.directive.ts +++ b/packages/sitecore-jss-angular/src/components/text.directive.ts @@ -1,6 +1,5 @@ import { Directive, - EmbeddedViewRef, Input, OnChanges, SimpleChanges, @@ -8,20 +7,25 @@ import { ViewContainerRef, } from '@angular/core'; import { TextField } from './rendering-field'; +import { BaseFieldDirective } from './base-field.directive'; +import { isFieldValueEmpty } from '@sitecore-jss/sitecore-jss/layout'; +import { DefaultEmptyFieldEditingComponent } from './default-empty-field-editing.component'; @Directive({ selector: '[scText]', }) -export class TextDirective implements OnChanges { +export class TextDirective extends BaseFieldDirective implements OnChanges { @Input('scTextEditable') editable = true; @Input('scTextEncode') encode = true; @Input('scText') field: TextField; - private viewRef: EmbeddedViewRef; + @Input('scTextEmptyFieldEditingTemplate') emptyFieldEditingTemplate: TemplateRef; - constructor(private viewContainer: ViewContainerRef, private templateRef: TemplateRef) {} + constructor(viewContainer: ViewContainerRef, private templateRef: TemplateRef) { + super(viewContainer); + } ngOnChanges(changes: SimpleChanges) { if (changes.field || changes.editable || changes.encode) { @@ -38,8 +42,12 @@ export class TextDirective implements OnChanges { const field = this.field; let editable = this.editable; - if (!field || (!field.editable && (field.value === undefined || field.value === ''))) { - return; + if (!field?.editable && isFieldValueEmpty(this.field)) { + if (this.field?.metadata && this.editable) { + super.renderEmptyFieldEditingComponent( + this.emptyFieldEditingTemplate ?? DefaultEmptyFieldEditingComponent + ); + } } // can't use editable value if we want to output unencoded From 4688d9edeefadab7459ea484d6b76299a6983d4a Mon Sep 17 00:00:00 2001 From: yavorsk Date: Mon, 2 Sep 2024 11:08:40 +0300 Subject: [PATCH 04/32] date, text directive fixes --- packages/sitecore-jss-angular/src/components/date.directive.ts | 2 ++ packages/sitecore-jss-angular/src/components/text.directive.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/packages/sitecore-jss-angular/src/components/date.directive.ts b/packages/sitecore-jss-angular/src/components/date.directive.ts index 0960866cf8..0ac8176392 100644 --- a/packages/sitecore-jss-angular/src/components/date.directive.ts +++ b/packages/sitecore-jss-angular/src/components/date.directive.ts @@ -56,6 +56,8 @@ export class DateDirective extends BaseFieldDirective implements OnChanges { this.emptyFieldEditingTemplate ?? DefaultEmptyFieldEditingComponent ); } + + return; } const html = field.editable && this.editable ? field.editable : field.value; diff --git a/packages/sitecore-jss-angular/src/components/text.directive.ts b/packages/sitecore-jss-angular/src/components/text.directive.ts index 89d116e6cd..e5ba43e067 100644 --- a/packages/sitecore-jss-angular/src/components/text.directive.ts +++ b/packages/sitecore-jss-angular/src/components/text.directive.ts @@ -48,6 +48,8 @@ export class TextDirective extends BaseFieldDirective implements OnChanges { this.emptyFieldEditingTemplate ?? DefaultEmptyFieldEditingComponent ); } + + return; } // can't use editable value if we want to output unencoded From 3f2d8d01e911f145ca5a59eb23177a12b2819021 Mon Sep 17 00:00:00 2001 From: yavorsk Date: Mon, 2 Sep 2024 12:58:26 +0300 Subject: [PATCH 05/32] refactoring --- .../src/components/base-field.directive.ts | 29 ++++++++++++++----- .../src/components/date.directive.ts | 14 +++------ .../src/components/rendering-field.ts | 8 ++--- .../src/components/rich-text.directive.ts | 12 ++------ .../src/components/text.directive.ts | 17 ++++------- 5 files changed, 37 insertions(+), 43 deletions(-) diff --git a/packages/sitecore-jss-angular/src/components/base-field.directive.ts b/packages/sitecore-jss-angular/src/components/base-field.directive.ts index 78ec6af067..6f824d650d 100644 --- a/packages/sitecore-jss-angular/src/components/base-field.directive.ts +++ b/packages/sitecore-jss-angular/src/components/base-field.directive.ts @@ -1,19 +1,32 @@ import { Directive, Type, ViewContainerRef, EmbeddedViewRef, TemplateRef } from '@angular/core'; +import { RenderingField } from './rendering-field'; +import { GenericFieldValue, isFieldValueEmpty } from '@sitecore-jss/sitecore-jss/layout'; @Directive() export abstract class BaseFieldDirective { protected viewRef: EmbeddedViewRef; + protected abstract field: RenderingField; + protected abstract emptyFieldEditingTemplate: TemplateRef; + protected abstract editable: boolean; constructor(protected viewContainer: ViewContainerRef) {} - protected renderEmptyFieldEditingComponent( - emptyFieldEditingComponent: Type | TemplateRef - ) { - if (typeof emptyFieldEditingComponent === 'object') { - this.viewContainer.createEmbeddedView(emptyFieldEditingComponent as TemplateRef); - } else { - this.viewContainer.clear(); - this.viewContainer.createComponent(emptyFieldEditingComponent as Type); + protected shouldRender() { + return this.field?.editable || !isFieldValueEmpty(this.field); + } + + protected shouldRenderEmptyEditingComponent() { + return this.field?.metadata && this.editable; + } + + protected renderEmpty(emptyFieldEditingComponent: Type) { + if (this.shouldRenderEmptyEditingComponent()) { + if (this.emptyFieldEditingTemplate) { + this.viewContainer.createEmbeddedView(this.emptyFieldEditingTemplate); + } else { + this.viewContainer.clear(); + this.viewContainer.createComponent(emptyFieldEditingComponent); + } } } } diff --git a/packages/sitecore-jss-angular/src/components/date.directive.ts b/packages/sitecore-jss-angular/src/components/date.directive.ts index 0ac8176392..13a4d3a2da 100644 --- a/packages/sitecore-jss-angular/src/components/date.directive.ts +++ b/packages/sitecore-jss-angular/src/components/date.directive.ts @@ -9,7 +9,6 @@ import { } from '@angular/core'; import { DateField } from './rendering-field'; import { BaseFieldDirective } from './base-field.directive'; -import { isFieldValueEmpty } from '@sitecore-jss/sitecore-jss/layout'; import { DefaultEmptyFieldEditingComponent } from './default-empty-field-editing.component'; @Directive({ @@ -48,18 +47,13 @@ export class DateDirective extends BaseFieldDirective implements OnChanges { } private updateView() { - const field = this.field; - - if (!field?.editable && isFieldValueEmpty(this.field)) { - if (this.field?.metadata && this.editable) { - super.renderEmptyFieldEditingComponent( - this.emptyFieldEditingTemplate ?? DefaultEmptyFieldEditingComponent - ); - } - + if (!this.shouldRender()) { + super.renderEmpty(DefaultEmptyFieldEditingComponent); return; } + const field = this.field; + const html = field.editable && this.editable ? field.editable : field.value; const setDangerously = field.editable && this.editable; diff --git a/packages/sitecore-jss-angular/src/components/rendering-field.ts b/packages/sitecore-jss-angular/src/components/rendering-field.ts index 6f8ad3016e..b6155ff67f 100644 --- a/packages/sitecore-jss-angular/src/components/rendering-field.ts +++ b/packages/sitecore-jss-angular/src/components/rendering-field.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-empty-interface */ -import { FieldMetadata } from '@sitecore-jss/sitecore-jss/layout'; +import { FieldMetadata, GenericFieldValue } from '@sitecore-jss/sitecore-jss/layout'; -export interface RenderingField extends FieldMetadata { +export interface RenderingField extends FieldMetadata { value?: V; editable?: string; } @@ -14,9 +14,7 @@ export interface FileFieldValue { displayName?: string; } -export interface FileField extends FileFieldValue, RenderingField { - value?: FileFieldValue; -} +export interface FileField extends FileFieldValue, RenderingField {} export interface ImageFieldValue { [key: string]: unknown; diff --git a/packages/sitecore-jss-angular/src/components/rich-text.directive.ts b/packages/sitecore-jss-angular/src/components/rich-text.directive.ts index 6d4a4bf2ae..20e0faa758 100644 --- a/packages/sitecore-jss-angular/src/components/rich-text.directive.ts +++ b/packages/sitecore-jss-angular/src/components/rich-text.directive.ts @@ -12,7 +12,6 @@ import { isAbsoluteUrl } from '@sitecore-jss/sitecore-jss/utils'; import { RichTextField } from './rendering-field'; import { BaseFieldDirective } from './base-field.directive'; import { DefaultEmptyFieldEditingComponent } from './default-empty-field-editing.component'; -import { isFieldValueEmpty } from '@sitecore-jss/sitecore-jss/layout'; @Directive({ selector: '[scRichText]', @@ -45,17 +44,12 @@ export class RichTextDirective extends BaseFieldDirective implements OnChanges { } private updateView() { - const field = this.field; - if (!field?.editable && isFieldValueEmpty(this.field)) { - if (this.field?.metadata && this.editable) { - super.renderEmptyFieldEditingComponent( - this.emptyFieldEditingTemplate ?? DefaultEmptyFieldEditingComponent - ); - } - + if (!this.shouldRender()) { + super.renderEmpty(DefaultEmptyFieldEditingComponent); return; } + const field = this.field; const html = field.editable && this.editable ? field.editable : field.value; this.viewRef.rootNodes.forEach((node) => { node.innerHTML = html; diff --git a/packages/sitecore-jss-angular/src/components/text.directive.ts b/packages/sitecore-jss-angular/src/components/text.directive.ts index e5ba43e067..a9cfe2d0e7 100644 --- a/packages/sitecore-jss-angular/src/components/text.directive.ts +++ b/packages/sitecore-jss-angular/src/components/text.directive.ts @@ -8,7 +8,6 @@ import { } from '@angular/core'; import { TextField } from './rendering-field'; import { BaseFieldDirective } from './base-field.directive'; -import { isFieldValueEmpty } from '@sitecore-jss/sitecore-jss/layout'; import { DefaultEmptyFieldEditingComponent } from './default-empty-field-editing.component'; @Directive({ @@ -39,19 +38,15 @@ export class TextDirective extends BaseFieldDirective implements OnChanges { } private updateView() { - const field = this.field; - let editable = this.editable; - - if (!field?.editable && isFieldValueEmpty(this.field)) { - if (this.field?.metadata && this.editable) { - super.renderEmptyFieldEditingComponent( - this.emptyFieldEditingTemplate ?? DefaultEmptyFieldEditingComponent - ); - } - + if (!this.shouldRender()) { + console.log('should not render'); + super.renderEmpty(DefaultEmptyFieldEditingComponent); return; } + const field = this.field; + let editable = this.editable; + // can't use editable value if we want to output unencoded if (!this.encode) { editable = false; From fa029aaa79006e28d1f44878d10d37b41353ec1d Mon Sep 17 00:00:00 2001 From: yavorsk Date: Mon, 2 Sep 2024 14:00:19 +0300 Subject: [PATCH 06/32] remove consolelog --- packages/sitecore-jss-angular/src/components/text.directive.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/sitecore-jss-angular/src/components/text.directive.ts b/packages/sitecore-jss-angular/src/components/text.directive.ts index a9cfe2d0e7..c2fb3195e0 100644 --- a/packages/sitecore-jss-angular/src/components/text.directive.ts +++ b/packages/sitecore-jss-angular/src/components/text.directive.ts @@ -39,7 +39,6 @@ export class TextDirective extends BaseFieldDirective implements OnChanges { private updateView() { if (!this.shouldRender()) { - console.log('should not render'); super.renderEmpty(DefaultEmptyFieldEditingComponent); return; } From 5a34c4aa604818dbd7d05500b63ce8dc6dbe98a0 Mon Sep 17 00:00:00 2001 From: yavorsk Date: Mon, 2 Sep 2024 14:45:17 +0300 Subject: [PATCH 07/32] empty field placeholder for link fields --- .../src/components/base-field.directive.ts | 4 ++-- .../src/components/generic-link.directive.ts | 4 ++++ .../src/components/link.directive.ts | 23 ++++++++++++------- .../src/components/router-link.directive.ts | 4 ++++ 4 files changed, 25 insertions(+), 10 deletions(-) diff --git a/packages/sitecore-jss-angular/src/components/base-field.directive.ts b/packages/sitecore-jss-angular/src/components/base-field.directive.ts index 6f824d650d..f8257caa4b 100644 --- a/packages/sitecore-jss-angular/src/components/base-field.directive.ts +++ b/packages/sitecore-jss-angular/src/components/base-field.directive.ts @@ -12,10 +12,10 @@ export abstract class BaseFieldDirective { constructor(protected viewContainer: ViewContainerRef) {} protected shouldRender() { - return this.field?.editable || !isFieldValueEmpty(this.field); + return !!this.field?.editable || !isFieldValueEmpty(this.field); } - protected shouldRenderEmptyEditingComponent() { + private shouldRenderEmptyEditingComponent() { return this.field?.metadata && this.editable; } diff --git a/packages/sitecore-jss-angular/src/components/generic-link.directive.ts b/packages/sitecore-jss-angular/src/components/generic-link.directive.ts index 61dafc4993..5bee78751c 100644 --- a/packages/sitecore-jss-angular/src/components/generic-link.directive.ts +++ b/packages/sitecore-jss-angular/src/components/generic-link.directive.ts @@ -21,6 +21,10 @@ export class GenericLinkDirective extends LinkDirective { @Input('scGenericLinkExtras') extras?: NavigationExtras; + @Input('scGenericLinkEmptyFieldEditingTemplate') declare emptyFieldEditingTemplate: TemplateRef< + unknown + >; + constructor( viewContainer: ViewContainerRef, templateRef: TemplateRef, diff --git a/packages/sitecore-jss-angular/src/components/link.directive.ts b/packages/sitecore-jss-angular/src/components/link.directive.ts index 528cf39214..fcb5bd8bf1 100644 --- a/packages/sitecore-jss-angular/src/components/link.directive.ts +++ b/packages/sitecore-jss-angular/src/components/link.directive.ts @@ -9,23 +9,29 @@ import { ViewContainerRef, } from '@angular/core'; import { LinkField } from './rendering-field'; +import { BaseFieldDirective } from './base-field.directive'; +import { DefaultEmptyFieldEditingComponent } from './default-empty-field-editing.component'; @Directive({ selector: '[scLink]' }) -export class LinkDirective implements OnChanges { +export class LinkDirective extends BaseFieldDirective implements OnChanges { @Input('scLinkEditable') editable = true; @Input('scLinkAttrs') attrs: { [attr: string]: string } = {}; @Input('scLink') field: LinkField; + @Input('scLinkEmptyFieldEditingTemplate') emptyFieldEditingTemplate: TemplateRef; + private inlineRef: HTMLSpanElement | null = null; constructor( - protected viewContainer: ViewContainerRef, + viewContainer: ViewContainerRef, protected templateRef: TemplateRef, protected renderer: Renderer2, private elementRef: ElementRef - ) {} + ) { + super(viewContainer); + } ngOnChanges(changes: SimpleChanges) { if (changes.field || changes.editable || changes.attrs) { @@ -76,15 +82,16 @@ export class LinkDirective implements OnChanges { } } - private shouldRender() { - return this.field && (this.field.href || this.field.value?.href || this.field.text); - } - private updateView() { const field = this.field; if (this.editable && field && field.editableFirstPart && field.editableLastPart) { this.renderInlineWrapper(field.editableFirstPart, field.editableLastPart); - } else if (this.shouldRender()) { + } else { + if (!this.shouldRender()) { + super.renderEmpty(DefaultEmptyFieldEditingComponent); + return; + } + const props = field.href ? field : field.value; const linkText = field.text || field.value?.text || field.href || field.value?.href; diff --git a/packages/sitecore-jss-angular/src/components/router-link.directive.ts b/packages/sitecore-jss-angular/src/components/router-link.directive.ts index 7ef49dca12..63c0cc7617 100644 --- a/packages/sitecore-jss-angular/src/components/router-link.directive.ts +++ b/packages/sitecore-jss-angular/src/components/router-link.directive.ts @@ -18,6 +18,10 @@ export class RouterLinkDirective extends LinkDirective { @Input('scRouterLink') declare field: LinkField; + @Input('scRouterLinkEmptyFieldEditingTemplate') declare emptyFieldEditingTemplate: TemplateRef< + unknown + >; + constructor( viewContainer: ViewContainerRef, templateRef: TemplateRef, From c8de1035bc6d3a18b71fcb17f00285cc05bf7182 Mon Sep 17 00:00:00 2001 From: yavorsk Date: Mon, 2 Sep 2024 15:11:52 +0300 Subject: [PATCH 08/32] empty field editing placeholder for image field --- ...ult-empty-field-editing-image.component.ts | 17 ++++++++++++++ .../default-empty-field-editing.component.ts | 2 +- .../src/components/image.directive.ts | 23 ++++++++++++------- 3 files changed, 33 insertions(+), 9 deletions(-) create mode 100644 packages/sitecore-jss-angular/src/components/default-empty-field-editing-image.component.ts diff --git a/packages/sitecore-jss-angular/src/components/default-empty-field-editing-image.component.ts b/packages/sitecore-jss-angular/src/components/default-empty-field-editing-image.component.ts new file mode 100644 index 0000000000..2328363266 --- /dev/null +++ b/packages/sitecore-jss-angular/src/components/default-empty-field-editing-image.component.ts @@ -0,0 +1,17 @@ +import { Component } from '@angular/core'; + +/** + * Default component that will be rendered in pages when image field is empty. + */ +@Component({ + selector: 'app-default-empty-field-editing-image', + template: ` + + `, +}) +export class DefaultEmptyFieldEditingImageComponent {} diff --git a/packages/sitecore-jss-angular/src/components/default-empty-field-editing.component.ts b/packages/sitecore-jss-angular/src/components/default-empty-field-editing.component.ts index 5f6310196e..b97e07ef69 100644 --- a/packages/sitecore-jss-angular/src/components/default-empty-field-editing.component.ts +++ b/packages/sitecore-jss-angular/src/components/default-empty-field-editing.component.ts @@ -1,7 +1,7 @@ import { Component } from '@angular/core'; /** - * Demonstrates usage of a Rich Text (HTML) content field within JSS. + * Default component that will be rendered in pages when field is empty; applies for text, richtext, date and link fields. */ @Component({ selector: 'app-default-empty-field-editing', diff --git a/packages/sitecore-jss-angular/src/components/image.directive.ts b/packages/sitecore-jss-angular/src/components/image.directive.ts index ea54de1d52..38d3c1b0ca 100644 --- a/packages/sitecore-jss-angular/src/components/image.directive.ts +++ b/packages/sitecore-jss-angular/src/components/image.directive.ts @@ -10,13 +10,17 @@ import { } from '@angular/core'; import { mediaApi } from '@sitecore-jss/sitecore-jss/media'; import { ImageField, ImageFieldValue } from './rendering-field'; +import { BaseFieldDirective } from './base-field.directive'; +import { DefaultEmptyFieldEditingImageComponent } from './default-empty-field-editing-image.component'; @Directive({ selector: '[scImage]' }) -export class ImageDirective implements OnChanges { - @Input('scImage') field: ImageField | ''; +export class ImageDirective extends BaseFieldDirective implements OnChanges { + @Input('scImage') field: ImageField; @Input('scImageEditable') editable = true; + @Input('scImageEmptyFieldEditingTemplate') emptyFieldEditingTemplate: TemplateRef; + /** * Custom regexp that finds media URL prefix that will be replaced by `/-/jssmedia` or `/~/jssmedia`. * @example @@ -33,11 +37,13 @@ export class ImageDirective implements OnChanges { private inlineRef: HTMLSpanElement | null = null; constructor( - private viewContainer: ViewContainerRef, + viewContainer: ViewContainerRef, private templateRef: TemplateRef, private renderer: Renderer2, private elementRef: ElementRef - ) {} + ) { + super(viewContainer); + } ngOnChanges(changes: SimpleChanges) { if (changes.field || changes.editable || changes.urlParams || changes.attrs) { @@ -52,16 +58,17 @@ export class ImageDirective implements OnChanges { } private updateView() { + if (!this.shouldRender()) { + super.renderEmpty(DefaultEmptyFieldEditingImageComponent); + return; + } + const overrideAttrs = { ...this.getElementAttrs(), ...this.attrs, }; const media = this.field; - if (!media || (!media.editable && !media.value && !media.src)) { - return; - } - let attrs: { [attr: string]: string } | null = {}; // we likely have an experience editor value, should be a string From 195dc5b721464860ef2106b3f4b715b6938da66a Mon Sep 17 00:00:00 2001 From: yavorsk Date: Tue, 3 Sep 2024 12:10:42 +0300 Subject: [PATCH 09/32] unit tests for image, text, richtext, date --- .../src/components/date.directive.spec.ts | 136 ++++++++++++++++-- .../src/components/image.directive.spec.ts | 135 ++++++++++++++++- .../components/rich-text.directive.spec.ts | 106 ++++++++++++-- .../src/components/text.directive.spec.ts | 115 +++++++++++++-- 4 files changed, 456 insertions(+), 36 deletions(-) diff --git a/packages/sitecore-jss-angular/src/components/date.directive.spec.ts b/packages/sitecore-jss-angular/src/components/date.directive.spec.ts index 3cbbf41d1b..0fe5ebf17e 100644 --- a/packages/sitecore-jss-angular/src/components/date.directive.spec.ts +++ b/packages/sitecore-jss-angular/src/components/date.directive.spec.ts @@ -1,10 +1,11 @@ import { DatePipe, formatDate } from '@angular/common'; -import { Component, DebugElement, Input } from '@angular/core'; +import { Component, DebugElement, Input, TemplateRef } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { textField as eeTextData } from '../test-data/ee-data'; import { DateDirective } from './date.directive'; import { TextField } from './rendering-field'; +import { EMPTY_DATE_FIELD_VALUE } from '@sitecore-jss/sitecore-jss/layout'; const testDate = new Date('2010-12-12T12:00Z'); const testIsoDateValue = testDate.toISOString(); @@ -29,26 +30,56 @@ class TestComponent { @Input() timezone = testTimezone; } +const emptyDateFieldEditingTemplateId = 'emptyDateFieldEditingTemplate'; +const emptyDateFieldEditingTemplate = + '[This is a *custom* empty field component for date]'; + +@Component({ + selector: 'test-empty-template-date', + template: ` + + + ${emptyDateFieldEditingTemplate} + + `, +}) +class TestEmptyTemplateComponent { + @Input() field: TextField; + @Input() editable = true; + @Input() format = testFormat; + @Input() locale = testLocale; + @Input() timezone = testTimezone; + @Input() emptyFieldEditingTemplate: TemplateRef; +} + describe('', () => { let fixture: ComponentFixture; let de: DebugElement; + let deSpan: DebugElement; let comp: TestComponent; beforeEach(() => { TestBed.configureTestingModule({ - declarations: [DateDirective, TestComponent], + declarations: [DateDirective, TestComponent, TestEmptyTemplateComponent], providers: [DatePipe], }); fixture = TestBed.createComponent(TestComponent); fixture.detectChanges(); - de = fixture.debugElement.query(By.css('span')); + de = fixture.debugElement; + deSpan = de.query(By.css('span')); comp = fixture.componentInstance; }); it('should render nothing with missing field', () => { - const rendered = de.nativeElement.innerHTML; + const rendered = deSpan.nativeElement.innerHTML; expect(rendered).toBe(''); }); @@ -57,7 +88,7 @@ describe('', () => { comp.field = field; fixture.detectChanges(); - const rendered = de.nativeElement.innerHTML; + const rendered = deSpan.nativeElement.innerHTML; expect(rendered).toBe(''); }); @@ -69,7 +100,7 @@ describe('', () => { comp.field = field; fixture.detectChanges(); - const rendered = de.nativeElement.innerHTML; + const rendered = deSpan.nativeElement.innerHTML; expect(rendered).toBe('editable'); }); @@ -82,7 +113,7 @@ describe('', () => { comp.editable = false; fixture.detectChanges(); - const rendered = de.nativeElement.innerHTML; + const rendered = deSpan.nativeElement.innerHTML; expect(rendered).toBe(defaultFormattedDate); }); @@ -93,7 +124,7 @@ describe('', () => { comp.field = field; fixture.detectChanges(); - const rendered = de.nativeElement.innerHTML; + const rendered = deSpan.nativeElement.innerHTML; expect(rendered).toBe(defaultFormattedDate); }); @@ -104,8 +135,95 @@ describe('', () => { comp.field = field; fixture.detectChanges(); - const rendered = de.nativeElement.innerHTML; + const rendered = deSpan.nativeElement.innerHTML; expect(rendered).toContain(''); }); + + describe('editMode metadata', () => { + const testMetadata = { + contextItem: { + id: '{09A07660-6834-476C-B93B-584248D3003B}', + language: 'en', + revision: 'a0b36ce0a7db49418edf90eb9621e145', + version: 1, + }, + fieldId: '{414061F4-FBB1-4591-BC37-BFFA67F745EB}', + fieldType: 'date', + rawValue: 'Test1', + }; + + it('should render default empty field component when field value is empty', () => { + const field = { + value: '', + metadata: testMetadata, + }; + comp.field = field; + fixture.detectChanges(); + + const rendered = de.nativeElement.innerHTML; + expect(rendered).toContain('[No text in field]'); + }); + + it('should render default empty field component when field value is the default empty date value', () => { + const field = { + value: EMPTY_DATE_FIELD_VALUE, + metadata: testMetadata, + }; + comp.field = field; + fixture.detectChanges(); + + const rendered = de.nativeElement.innerHTML; + expect(rendered).toContain('[No text in field]'); + }); + + it('should render custom empty field component when provided, when field value is empty', () => { + fixture = TestBed.createComponent(TestEmptyTemplateComponent); + fixture.detectChanges(); + + de = fixture.debugElement; + comp = fixture.componentInstance; + + const field = { + value: '', + metadata: testMetadata, + }; + comp.field = field; + fixture.detectChanges(); + + const rendered = de.nativeElement.innerHTML; + expect(rendered).toContain(emptyDateFieldEditingTemplate); + }); + + it('should render custom empty field component when provided, when field value the default empty date value', () => { + fixture = TestBed.createComponent(TestEmptyTemplateComponent); + fixture.detectChanges(); + + de = fixture.debugElement; + comp = fixture.componentInstance; + + const field = { + value: EMPTY_DATE_FIELD_VALUE, + metadata: testMetadata, + }; + comp.field = field; + fixture.detectChanges(); + + const rendered = de.nativeElement.innerHTML; + expect(rendered).toContain(emptyDateFieldEditingTemplate); + }); + + it('should render nothing when field value is empty, when editing is explicitly disabled', () => { + const field = { + value: '', + metadata: testMetadata, + }; + comp.field = field; + comp.editable = false; + fixture.detectChanges(); + + const rendered = deSpan.nativeElement.innerHTML; + expect(rendered).toBe(''); + }); + }); }); diff --git a/packages/sitecore-jss-angular/src/components/image.directive.spec.ts b/packages/sitecore-jss-angular/src/components/image.directive.spec.ts index f9f4d95443..fdac77892a 100644 --- a/packages/sitecore-jss-angular/src/components/image.directive.spec.ts +++ b/packages/sitecore-jss-angular/src/components/image.directive.spec.ts @@ -1,4 +1,4 @@ -import { Component, DebugElement, Input } from '@angular/core'; +import { Component, DebugElement, Input, TemplateRef } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; @@ -41,6 +41,34 @@ class AnotherTestComponent { @Input() mediaUrlPrefix?: RegExp; } +const emptyImageFieldEditingTemplateId = 'emptyImageFieldEditingTemplate'; +const emptyImageFieldEditingTemplate = 'Empty image'; +const emptyImageFieldEditingTemplateDefaultTestString = + 'M174,54c7.17,0,13,5.83,13,13s-5.83,13-13,13s-13-5.83-13-13S166.83,54,174,54 M174,52 c-8.28,0-15,6.72-15,15s6.72,15,15,15s15-6.72,15-15S182.28,52,174,52L174,52z'; + +@Component({ + selector: 'test-empty-template-image', + template: ` + + + ${emptyImageFieldEditingTemplate} + + `, +}) +class TestEmptyTemplateComponent { + @Input() field: ImageField; + @Input() emptyFieldEditingTemplate: TemplateRef; + @Input() editable = true; +} + describe('', () => { let fixture: ComponentFixture; let de: DebugElement; @@ -48,7 +76,12 @@ describe('', () => { beforeEach(() => { TestBed.configureTestingModule({ - declarations: [ImageDirective, TestComponent, AnotherTestComponent], + declarations: [ + ImageDirective, + TestComponent, + AnotherTestComponent, + TestEmptyTemplateComponent, + ], }); fixture = TestBed.createComponent(TestComponent); @@ -346,6 +379,104 @@ describe('', () => { }); }); + describe('editMode metadata', () => { + const testMetadata = { + contextItem: { + id: '{09A07660-6834-476C-B93B-584248D3003B}', + language: 'en', + revision: 'a0b36ce0a7db49418edf90eb9621e145', + version: 1, + }, + fieldId: '{414061F4-FBB1-4591-BC37-BFFA67F745EB}', + fieldType: 'image', + rawValue: 'Test1', + }; + + it('should render default empty field component for Image when field value src is not present', () => { + const field = { + value: { src: undefined }, + metadata: testMetadata, + }; + comp.field = field; + fixture.detectChanges(); + + const rendered = de.nativeElement.innerHTML; + expect(rendered).toContain(emptyImageFieldEditingTemplateDefaultTestString); + }); + + it('should render default empty field component for Image when field src is not present', () => { + const field = { + src: undefined, + metadata: testMetadata, + }; + comp.field = field; + fixture.detectChanges(); + + const rendered = de.nativeElement.innerHTML; + expect(rendered).toContain(emptyImageFieldEditingTemplateDefaultTestString); + }); + + it('should render custom empty field component when provided, when field value src is not present', () => { + fixture = TestBed.createComponent(TestEmptyTemplateComponent); + fixture.detectChanges(); + + de = fixture.debugElement; + comp = fixture.componentInstance; + + const field = { + value: { src: undefined }, + metadata: testMetadata, + }; + comp.field = field; + fixture.detectChanges(); + + const rendered = de.nativeElement.innerHTML; + expect(rendered).toContain(emptyImageFieldEditingTemplate); + }); + + it('should render custom empty field component when provided, when field src is not present', () => { + fixture = TestBed.createComponent(TestEmptyTemplateComponent); + fixture.detectChanges(); + + de = fixture.debugElement; + comp = fixture.componentInstance; + + const field = { + src: undefined, + metadata: testMetadata, + }; + comp.field = field; + fixture.detectChanges(); + + const rendered = de.nativeElement.innerHTML; + expect(rendered).toContain(emptyImageFieldEditingTemplate); + }); + + it('should render nothing when field value src is not present, when editing is explicitly disabled', () => { + const field = { + value: { src: undefined }, + metadata: testMetadata, + }; + comp.field = field; + comp.editable = false; + fixture.detectChanges(); + + expect(de.children.length).toBe(0); + }); + + it('should render nothing when field src is not present, when editing is explicitly disabled', () => { + const field = { + src: undefined, + metadata: testMetadata, + }; + comp.field = field; + comp.editable = false; + fixture.detectChanges(); + + expect(de.children.length).toBe(0); + }); + }); + it('should render no when media prop is empty', () => { const img = ''; comp.field = img; diff --git a/packages/sitecore-jss-angular/src/components/rich-text.directive.spec.ts b/packages/sitecore-jss-angular/src/components/rich-text.directive.spec.ts index 3a6993d8d0..0721e1aed7 100644 --- a/packages/sitecore-jss-angular/src/components/rich-text.directive.spec.ts +++ b/packages/sitecore-jss-angular/src/components/rich-text.directive.spec.ts @@ -1,4 +1,4 @@ -import { Component, DebugElement, Input } from '@angular/core'; +import { Component, DebugElement, Input, TemplateRef } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { Router } from '@angular/router'; @@ -19,26 +19,53 @@ class TestComponent { @Input() editable = true; } +const emptyTextFieldEditingTemplateId = 'emptyTextFieldEditingTemplate'; +const emptyTextFieldEditingTemplate = + '[This is a *custom* empty field component for text]'; + +@Component({ + selector: 'test-empty-template-rich-text', + template: ` +

+ + ${emptyTextFieldEditingTemplate} + + `, +}) +class TestEmptyTemplateComponent { + @Input() field: RichTextField; + @Input() emptyFieldEditingTemplate: TemplateRef; + @Input() editable = true; +} + describe('
', () => { let fixture: ComponentFixture; let de: DebugElement; + let deH: DebugElement; let comp: TestComponent; beforeEach(() => { TestBed.configureTestingModule({ - declarations: [RichTextDirective, TestComponent], + declarations: [RichTextDirective, TestComponent, TestEmptyTemplateComponent], imports: [RouterTestingModule.withRoutes([{ path: 'lorem', component: TestComponent }])], }); fixture = TestBed.createComponent(TestComponent); fixture.detectChanges(); - de = fixture.debugElement.query(By.css('h1')); + de = fixture.debugElement; + deH = de.query(By.css('h1')); comp = fixture.componentInstance; }); it('should render nothing with missing field', () => { - const rendered = de.nativeElement.innerHTML; + const rendered = deH.nativeElement.innerHTML; expect(rendered).toBe(''); }); @@ -46,7 +73,7 @@ describe('
', () => { comp.field = {}; fixture.detectChanges(); - const rendered = de.nativeElement.innerHTML; + const rendered = deH.nativeElement.innerHTML; expect(rendered).toBe(''); }); @@ -58,7 +85,7 @@ describe('
', () => { comp.field = field; fixture.detectChanges(); - const rendered = de.nativeElement.innerHTML; + const rendered = deH.nativeElement.innerHTML; expect(rendered).toBe('editable'); }); @@ -71,7 +98,7 @@ describe('
', () => { comp.editable = false; fixture.detectChanges(); - const rendered = de.nativeElement.innerHTML; + const rendered = deH.nativeElement.innerHTML; expect(rendered).toBe('Hello World'); }); @@ -82,7 +109,7 @@ describe('
', () => { comp.field = field; fixture.detectChanges(); - const rendered = de.nativeElement.innerHTML; + const rendered = deH.nativeElement.innerHTML; expect(rendered).toBe('value'); }); @@ -93,7 +120,7 @@ describe('
', () => { comp.field = field; fixture.detectChanges(); - const rendered = de.nativeElement.innerHTML; + const rendered = deH.nativeElement.innerHTML; expect(rendered).toBe(field.value); }); @@ -106,7 +133,7 @@ describe('
', () => { const router: Router = TestBed.inject(Router); spyOn(router, 'navigateByUrl'); - const renderedLink = de.nativeElement.querySelector('a[href]'); + const renderedLink = deH.nativeElement.querySelector('a[href]'); expect(renderedLink.getAttribute('href')).toBe('/lorem'); renderedLink.click(); fixture.detectChanges(); @@ -120,8 +147,65 @@ describe('
', () => { comp.field = field; fixture.detectChanges(); - const rendered = de.nativeElement.innerHTML; + const rendered = deH.nativeElement.innerHTML; expect(rendered).toContain(''); }); + + describe('editMode metadata', () => { + const testMetadata = { + contextItem: { + id: '{09A07660-6834-476C-B93B-584248D3003B}', + language: 'en', + revision: 'a0b36ce0a7db49418edf90eb9621e145', + version: 1, + }, + fieldId: '{414061F4-FBB1-4591-BC37-BFFA67F745EB}', + fieldType: 'single-line', + rawValue: 'Test1', + }; + + it('should render default empty field component when field value is empty', () => { + const field = { + value: '', + metadata: testMetadata, + }; + comp.field = field; + fixture.detectChanges(); + + const rendered = de.nativeElement.innerHTML; + expect(rendered).toContain('[No text in field]'); + }); + + it('should render custom empty field component when provided, when field value is empty', () => { + fixture = TestBed.createComponent(TestEmptyTemplateComponent); + fixture.detectChanges(); + + de = fixture.debugElement; + comp = fixture.componentInstance; + + const field = { + value: '', + metadata: testMetadata, + }; + comp.field = field; + fixture.detectChanges(); + + const rendered = de.nativeElement.innerHTML; + expect(rendered).toContain(emptyTextFieldEditingTemplate); + }); + + it('should render nothing when field value is empty, when editing is explicitly disabled', () => { + const field = { + value: '', + metadata: testMetadata, + }; + comp.field = field; + comp.editable = false; + fixture.detectChanges(); + + const rendered = deH.nativeElement.innerHTML; + expect(rendered).toBe(''); + }); + }); }); diff --git a/packages/sitecore-jss-angular/src/components/text.directive.spec.ts b/packages/sitecore-jss-angular/src/components/text.directive.spec.ts index fbe32d28f8..50958fee9c 100644 --- a/packages/sitecore-jss-angular/src/components/text.directive.spec.ts +++ b/packages/sitecore-jss-angular/src/components/text.directive.spec.ts @@ -1,4 +1,4 @@ -import { Component, DebugElement, Input } from '@angular/core'; +import { Component, DebugElement, Input, TemplateRef } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; @@ -18,25 +18,55 @@ class TestComponent { @Input() encode = true; } +const emptyTextFieldEditingTemplateId = 'emptyTextFieldEditingTemplate'; +const emptyTextFieldEditingTemplate = + '[This is a *custom* empty field component for text]'; + +@Component({ + selector: 'test-empty-template-text', + template: ` + + + + ${emptyTextFieldEditingTemplate} + + `, +}) +class TestEmptyTemplateComponent { + @Input() field: TextField; + @Input() emptyFieldEditingTemplate: TemplateRef; + @Input() editable = true; + @Input() encode = true; +} + describe('', () => { let fixture: ComponentFixture; let de: DebugElement; + let deSpan: DebugElement; let comp: TestComponent; beforeEach(() => { TestBed.configureTestingModule({ - declarations: [TextDirective, TestComponent], + declarations: [TextDirective, TestComponent, TestEmptyTemplateComponent], }); fixture = TestBed.createComponent(TestComponent); fixture.detectChanges(); - de = fixture.debugElement.query(By.css('span')); + de = fixture.debugElement; + deSpan = de.query(By.css('span')); comp = fixture.componentInstance; }); it('should render nothing with missing field', () => { - const rendered = de.nativeElement.innerHTML; + const rendered = deSpan.nativeElement.innerHTML; expect(rendered).toBe(''); }); @@ -45,7 +75,7 @@ describe('', () => { comp.field = field; fixture.detectChanges(); - const rendered = de.nativeElement.innerHTML; + const rendered = deSpan.nativeElement.innerHTML; expect(rendered).toBe(''); }); @@ -56,7 +86,7 @@ describe('', () => { comp.field = field; fixture.detectChanges(); - const rendered = de.nativeElement.innerHTML; + const rendered = deSpan.nativeElement.innerHTML; expect(rendered).toBe(''); }); @@ -68,7 +98,7 @@ describe('', () => { comp.field = field; fixture.detectChanges(); - const rendered = de.nativeElement.innerHTML; + const rendered = deSpan.nativeElement.innerHTML; expect(rendered).toBe('editable'); }); @@ -81,7 +111,7 @@ describe('', () => { comp.editable = false; fixture.detectChanges(); - const rendered = de.nativeElement.innerHTML; + const rendered = deSpan.nativeElement.innerHTML; expect(rendered).toBe('value'); }); @@ -94,7 +124,7 @@ describe('', () => { comp.editable = false; fixture.detectChanges(); - const rendered = de.nativeElement.innerHTML; + const rendered = deSpan.nativeElement.innerHTML; expect(rendered).toContain('< >'); }); @@ -105,7 +135,7 @@ describe('', () => { comp.field = field; fixture.detectChanges(); - const rendered = de.nativeElement.innerHTML; + const rendered = deSpan.nativeElement.innerHTML; expect(rendered).toBe('value'); }); @@ -116,7 +146,7 @@ describe('', () => { comp.field = field; fixture.detectChanges(); - const rendered = de.nativeElement.innerHTML; + const rendered = deSpan.nativeElement.innerHTML; expect(rendered).toBe('1.23'); }); @@ -127,7 +157,7 @@ describe('', () => { comp.field = field; fixture.detectChanges(); - const rendered = de.nativeElement.innerHTML; + const rendered = deSpan.nativeElement.innerHTML; expect(rendered).toBe('0'); }); @@ -139,7 +169,7 @@ describe('', () => { comp.encode = false; fixture.detectChanges(); - const rendered = de.nativeElement.innerHTML; + const rendered = deSpan.nativeElement.innerHTML; expect(rendered).toBe(field.value); }); @@ -150,7 +180,7 @@ describe('', () => { comp.field = field; fixture.detectChanges(); - const rendered = de.nativeElement.innerHTML; + const rendered = deSpan.nativeElement.innerHTML; expect(rendered).toContain(''); }); @@ -164,4 +194,61 @@ describe('', () => { // expect(rendered.html()).to.contain('

'); // expect(rendered.html()).to.contain('value'); // }); + + describe('editMode metadata', () => { + const testMetadata = { + contextItem: { + id: '{09A07660-6834-476C-B93B-584248D3003B}', + language: 'en', + revision: 'a0b36ce0a7db49418edf90eb9621e145', + version: 1, + }, + fieldId: '{414061F4-FBB1-4591-BC37-BFFA67F745EB}', + fieldType: 'single-line', + rawValue: 'Test1', + }; + + it('should render default empty field component when field value is empty', () => { + const field = { + value: '', + metadata: testMetadata, + }; + comp.field = field; + fixture.detectChanges(); + + const rendered = de.nativeElement.innerHTML; + expect(rendered).toContain('[No text in field]'); + }); + + it('should render custom empty field component when provided, when field value is empty', () => { + fixture = TestBed.createComponent(TestEmptyTemplateComponent); + fixture.detectChanges(); + + de = fixture.debugElement; + comp = fixture.componentInstance; + + const field = { + value: '', + metadata: testMetadata, + }; + comp.field = field; + fixture.detectChanges(); + + const rendered = de.nativeElement.innerHTML; + expect(rendered).toContain(emptyTextFieldEditingTemplate); + }); + + it('should render nothing when field value is empty, when editing is explicitly disabled', () => { + const field = { + value: '', + metadata: testMetadata, + }; + comp.field = field; + comp.editable = false; + fixture.detectChanges(); + + const rendered = deSpan.nativeElement.innerHTML; + expect(rendered).toBe(''); + }); + }); }); From e5c106d78ee1c34af2d1d7410a1f23b14e71b2fb Mon Sep 17 00:00:00 2001 From: yavorsk Date: Tue, 3 Sep 2024 20:25:31 +0300 Subject: [PATCH 10/32] link directives unit tests --- .../components/generic-link.directive.spec.ts | 260 +++++++++++++++++- .../src/components/link.directive.spec.ts | 255 ++++++++++++++++- .../src/components/link.directive.ts | 5 + .../components/router-link.directive.spec.ts | 256 ++++++++++++++++- 4 files changed, 766 insertions(+), 10 deletions(-) diff --git a/packages/sitecore-jss-angular/src/components/generic-link.directive.spec.ts b/packages/sitecore-jss-angular/src/components/generic-link.directive.spec.ts index 6e7c2a0fc5..b7f84459b9 100644 --- a/packages/sitecore-jss-angular/src/components/generic-link.directive.spec.ts +++ b/packages/sitecore-jss-angular/src/components/generic-link.directive.spec.ts @@ -1,4 +1,4 @@ -import { Component, DebugElement, Input } from '@angular/core'; +import { Component, DebugElement, Input, TemplateRef } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; @@ -25,6 +25,37 @@ class TestComponent { @Input() extras = {}; } +const emptyLinkFieldEditingTemplateId = 'emptyImageFieldEditingTemplate'; +const emptyLinkFieldEditingTemplate = '[This is a *custom* empty field template]'; +const emptyLinkFieldEditingTemplateDefaultTestString = '[No text in field]'; + +@Component({ + selector: 'test-empty-template-generic-link', + template: ` + + + ${emptyLinkFieldEditingTemplate} + + `, +}) +class TestEmptyTemplateComponent { + @Input() field: LinkField; + @Input() editable = true; + @Input() attrs = {}; + @Input() extras = {}; + @Input() emptyFieldEditingTemplate: TemplateRef; +} + describe('', () => { let fixture: ComponentFixture; let de: DebugElement; @@ -32,7 +63,7 @@ describe('', () => { beforeEach(() => { TestBed.configureTestingModule({ - declarations: [GenericLinkDirective, TestComponent], + declarations: [GenericLinkDirective, TestComponent, TestEmptyTemplateComponent], imports: [RouterTestingModule], }); @@ -173,6 +204,102 @@ describe('', () => { expect(rendered.nativeElement.title).toBe('footip'); expect(rendered.nativeElement.className).toBe('external-css-class my-link'); }); + + describe('editMode metadata', () => { + const testMetadata = { + contextItem: { + id: '{09A07660-6834-476C-B93B-584248D3003B}', + language: 'en', + revision: 'a0b36ce0a7db49418edf90eb9621e145', + version: 1, + }, + fieldId: '{414061F4-FBB1-4591-BC37-BFFA67F745EB}', + fieldType: 'link', + rawValue: 'Test1', + }; + + it('should render default empty field component when field value href is not present', () => { + const field = { + value: { src: undefined }, + metadata: testMetadata, + }; + comp.field = field; + fixture.detectChanges(); + + const rendered = de.nativeElement.innerHTML; + expect(rendered).toContain(emptyLinkFieldEditingTemplateDefaultTestString); + }); + + it('should render default empty field component when field href is not present', () => { + const field = { + hred: undefined, + metadata: testMetadata, + }; + comp.field = field; + fixture.detectChanges(); + + const rendered = de.nativeElement.innerHTML; + expect(rendered).toContain(emptyLinkFieldEditingTemplateDefaultTestString); + }); + + it('should render custom empty field component when provided, when field value href is not present', () => { + fixture = TestBed.createComponent(TestEmptyTemplateComponent); + fixture.detectChanges(); + + de = fixture.debugElement; + comp = fixture.componentInstance; + + const field = { + value: { href: undefined }, + metadata: testMetadata, + }; + comp.field = field; + fixture.detectChanges(); + + const rendered = de.nativeElement.innerHTML; + expect(rendered).toContain(emptyLinkFieldEditingTemplate); + }); + + it('should render custom empty field component when provided, when field href is not present', () => { + fixture = TestBed.createComponent(TestEmptyTemplateComponent); + fixture.detectChanges(); + + de = fixture.debugElement; + comp = fixture.componentInstance; + + const field = { + href: undefined, + metadata: testMetadata, + }; + comp.field = field; + fixture.detectChanges(); + + const rendered = de.nativeElement.innerHTML; + expect(rendered).toContain(emptyLinkFieldEditingTemplate); + }); + + it('should render nothing when field value href is not present, when editing is explicitly disabled', () => { + const field = { + value: { href: undefined }, + metadata: testMetadata, + }; + comp.field = field; + comp.editable = false; + fixture.detectChanges(); + expect(de.children.length).toBe(0); + }); + + it('should render nothing when field href is empty, when editing is explicitly disabled', () => { + const field = { + href: undefined, + metadata: testMetadata, + }; + comp.field = field; + comp.editable = false; + fixture.detectChanges(); + expect(de.children.length).toBe(0); + }); + }); }); @Component({ @@ -190,6 +317,33 @@ class TestWithChildrenComponent { @Input() extras = {}; } +@Component({ + selector: 'test-empty-template-generic-link', + template: ` + hello world + + ${emptyLinkFieldEditingTemplate} + + `, +}) +class TestEmptyTemplateWithChildrenComponent { + @Input() field: LinkField; + @Input() editable = true; + @Input() attrs = {}; + @Input() extras = {}; + @Input() emptyFieldEditingTemplate: TemplateRef; +} + describe('children', () => { let fixture: ComponentFixture; let de: DebugElement; @@ -197,7 +351,11 @@ describe('children', () => { beforeEach(() => { TestBed.configureTestingModule({ - declarations: [GenericLinkDirective, TestWithChildrenComponent], + declarations: [ + GenericLinkDirective, + TestWithChildrenComponent, + TestEmptyTemplateWithChildrenComponent, + ], imports: [RouterTestingModule], }); @@ -230,6 +388,102 @@ describe('children', () => { const rendered = de.query(By.css('a')); expect(rendered.nativeElement.innerHTML).toContain('hello world'); }); + + describe('editMode metadata', () => { + const testMetadata = { + contextItem: { + id: '{09A07660-6834-476C-B93B-584248D3003B}', + language: 'en', + revision: 'a0b36ce0a7db49418edf90eb9621e145', + version: 1, + }, + fieldId: '{414061F4-FBB1-4591-BC37-BFFA67F745EB}', + fieldType: 'link', + rawValue: 'Test1', + }; + + it('should render default empty field component when field value href is not present', () => { + const field = { + value: { src: undefined }, + metadata: testMetadata, + }; + comp.field = field; + fixture.detectChanges(); + + const rendered = de.nativeElement.innerHTML; + expect(rendered).toContain(emptyLinkFieldEditingTemplateDefaultTestString); + }); + + it('should render default empty field component when field href is not present', () => { + const field = { + hred: undefined, + metadata: testMetadata, + }; + comp.field = field; + fixture.detectChanges(); + + const rendered = de.nativeElement.innerHTML; + expect(rendered).toContain(emptyLinkFieldEditingTemplateDefaultTestString); + }); + + it('should render custom empty field component when provided, when field value href is not present', () => { + fixture = TestBed.createComponent(TestEmptyTemplateWithChildrenComponent); + fixture.detectChanges(); + + de = fixture.debugElement; + comp = fixture.componentInstance; + + const field = { + value: { href: undefined }, + metadata: testMetadata, + }; + comp.field = field; + fixture.detectChanges(); + + const rendered = de.nativeElement.innerHTML; + expect(rendered).toContain(emptyLinkFieldEditingTemplate); + }); + + it('should render custom empty field component when provided, when field href is not present', () => { + fixture = TestBed.createComponent(TestEmptyTemplateWithChildrenComponent); + fixture.detectChanges(); + + de = fixture.debugElement; + comp = fixture.componentInstance; + + const field = { + href: undefined, + metadata: testMetadata, + }; + comp.field = field; + fixture.detectChanges(); + + const rendered = de.nativeElement.innerHTML; + expect(rendered).toContain(emptyLinkFieldEditingTemplate); + }); + + it('should render nothing when field value href is not present, when editing is explicitly disabled', () => { + const field = { + value: { href: undefined }, + metadata: testMetadata, + }; + comp.field = field; + comp.editable = false; + fixture.detectChanges(); + expect(de.children.length).toBe(0); + }); + + it('should render nothing when field href is empty, when editing is explicitly disabled', () => { + const field = { + href: undefined, + metadata: testMetadata, + }; + comp.field = field; + comp.editable = false; + fixture.detectChanges(); + expect(de.children.length).toBe(0); + }); + }); }); describe('', () => { diff --git a/packages/sitecore-jss-angular/src/components/link.directive.spec.ts b/packages/sitecore-jss-angular/src/components/link.directive.spec.ts index bea5191eb4..b2bfce7fc7 100644 --- a/packages/sitecore-jss-angular/src/components/link.directive.spec.ts +++ b/packages/sitecore-jss-angular/src/components/link.directive.spec.ts @@ -1,4 +1,4 @@ -import { Component, DebugElement, Input } from '@angular/core'; +import { Component, DebugElement, Input, TemplateRef } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; @@ -18,6 +18,32 @@ class TestComponent { @Input() attrs = {}; } +const emptyLinkFieldEditingTemplateId = 'emptyImageFieldEditingTemplate'; +const emptyLinkFieldEditingTemplate = '[This is a *custom* empty field template]'; +const emptyLinkFieldEditingTemplateDefaultTestString = '[No text in field]'; + +@Component({ + selector: 'test-empty-template-link', + template: ` + + + ${emptyLinkFieldEditingTemplate} + + `, +}) +class TestEmptyTemplateComponent { + @Input() field: LinkField; + @Input() editable = true; + @Input() attrs = {}; + @Input() emptyFieldEditingTemplate: TemplateRef; +} + describe('', () => { let fixture: ComponentFixture; let de: DebugElement; @@ -25,7 +51,7 @@ describe('', () => { beforeEach(() => { TestBed.configureTestingModule({ - declarations: [LinkDirective, TestComponent], + declarations: [LinkDirective, TestComponent, TestEmptyTemplateComponent], }); fixture = TestBed.createComponent(TestComponent); @@ -187,7 +213,7 @@ describe('', () => { comp.editable = false; comp.field = field; fixture.detectChanges(); - + console.log(de.nativeElement); const rendered = de.query(By.css('a')); expect(rendered.nativeElement.href).toBe(''); @@ -228,6 +254,102 @@ describe('', () => { expect(rendered.nativeElement.innerHTML).toBe(field.text); }); }); + + describe('editMode metadata', () => { + const testMetadata = { + contextItem: { + id: '{09A07660-6834-476C-B93B-584248D3003B}', + language: 'en', + revision: 'a0b36ce0a7db49418edf90eb9621e145', + version: 1, + }, + fieldId: '{414061F4-FBB1-4591-BC37-BFFA67F745EB}', + fieldType: 'link', + rawValue: 'Test1', + }; + + it('should render default empty field component when field value href is not present', () => { + const field = { + value: { src: undefined }, + metadata: testMetadata, + }; + comp.field = field; + fixture.detectChanges(); + + const rendered = de.nativeElement.innerHTML; + expect(rendered).toContain(emptyLinkFieldEditingTemplateDefaultTestString); + }); + + it('should render default empty field component when field href is not present', () => { + const field = { + hred: undefined, + metadata: testMetadata, + }; + comp.field = field; + fixture.detectChanges(); + + const rendered = de.nativeElement.innerHTML; + expect(rendered).toContain(emptyLinkFieldEditingTemplateDefaultTestString); + }); + + it('should render custom empty field component when provided, when field value href is not present', () => { + fixture = TestBed.createComponent(TestEmptyTemplateComponent); + fixture.detectChanges(); + + de = fixture.debugElement; + comp = fixture.componentInstance; + + const field = { + value: { href: undefined }, + metadata: testMetadata, + }; + comp.field = field; + fixture.detectChanges(); + + const rendered = de.nativeElement.innerHTML; + expect(rendered).toContain(emptyLinkFieldEditingTemplate); + }); + + it('should render custom empty field component when provided, when field href is not present', () => { + fixture = TestBed.createComponent(TestEmptyTemplateComponent); + fixture.detectChanges(); + + de = fixture.debugElement; + comp = fixture.componentInstance; + + const field = { + href: undefined, + metadata: testMetadata, + }; + comp.field = field; + fixture.detectChanges(); + + const rendered = de.nativeElement.innerHTML; + expect(rendered).toContain(emptyLinkFieldEditingTemplate); + }); + + it('should render nothing when field value href is not present, when editing is explicitly disabled', () => { + const field = { + value: { href: undefined }, + metadata: testMetadata, + }; + comp.field = field; + comp.editable = false; + fixture.detectChanges(); + expect(de.children.length).toBe(0); + }); + + it('should render nothing when field href is empty, when editing is explicitly disabled', () => { + const field = { + href: undefined, + metadata: testMetadata, + }; + comp.field = field; + comp.editable = false; + fixture.detectChanges(); + expect(de.children.length).toBe(0); + }); + }); }); @Component({ @@ -244,6 +366,31 @@ class TestWithChildrenComponent { @Input() attrs = {}; } +@Component({ + selector: 'test-empty-template-link', + template: ` + hello world + + ${emptyLinkFieldEditingTemplate} + + `, +}) +class TestEmptyTemplateWithChildrenComponent { + @Input() field: LinkField; + @Input() editable = true; + @Input() attrs = {}; + @Input() emptyFieldEditingTemplate: TemplateRef; +} + describe('children', () => { let fixture: ComponentFixture; let de: DebugElement; @@ -251,7 +398,11 @@ describe('children', () => { beforeEach(() => { TestBed.configureTestingModule({ - declarations: [LinkDirective, TestWithChildrenComponent], + declarations: [ + LinkDirective, + TestWithChildrenComponent, + TestEmptyTemplateWithChildrenComponent, + ], }); fixture = TestBed.createComponent(TestWithChildrenComponent); @@ -350,6 +501,102 @@ describe('children', () => { expect(rendered.nativeElement.innerHTML).toContain('hello world'); }); }); + + describe('editMode metadata', () => { + const testMetadata = { + contextItem: { + id: '{09A07660-6834-476C-B93B-584248D3003B}', + language: 'en', + revision: 'a0b36ce0a7db49418edf90eb9621e145', + version: 1, + }, + fieldId: '{414061F4-FBB1-4591-BC37-BFFA67F745EB}', + fieldType: 'link', + rawValue: 'Test1', + }; + + it('should render default empty field component when field value href is not present', () => { + const field = { + value: { src: undefined }, + metadata: testMetadata, + }; + comp.field = field; + fixture.detectChanges(); + + const rendered = de.nativeElement.innerHTML; + expect(rendered).toContain(emptyLinkFieldEditingTemplateDefaultTestString); + }); + + it('should render default empty field component when field href is not present', () => { + const field = { + hred: undefined, + metadata: testMetadata, + }; + comp.field = field; + fixture.detectChanges(); + + const rendered = de.nativeElement.innerHTML; + expect(rendered).toContain(emptyLinkFieldEditingTemplateDefaultTestString); + }); + + it('should render custom empty field component when provided, when field value href is not present', () => { + fixture = TestBed.createComponent(TestEmptyTemplateWithChildrenComponent); + fixture.detectChanges(); + + de = fixture.debugElement; + comp = fixture.componentInstance; + + const field = { + value: { href: undefined }, + metadata: testMetadata, + }; + comp.field = field; + fixture.detectChanges(); + + const rendered = de.nativeElement.innerHTML; + expect(rendered).toContain(emptyLinkFieldEditingTemplate); + }); + + it('should render custom empty field component when provided, when field href is not present', () => { + fixture = TestBed.createComponent(TestEmptyTemplateWithChildrenComponent); + fixture.detectChanges(); + + de = fixture.debugElement; + comp = fixture.componentInstance; + + const field = { + href: undefined, + metadata: testMetadata, + }; + comp.field = field; + fixture.detectChanges(); + + const rendered = de.nativeElement.innerHTML; + expect(rendered).toContain(emptyLinkFieldEditingTemplate); + }); + + it('should render nothing when field value href is not present, when editing is explicitly disabled', () => { + const field = { + value: { href: undefined }, + metadata: testMetadata, + }; + comp.field = field; + comp.editable = false; + fixture.detectChanges(); + expect(de.children.length).toBe(0); + }); + + it('should render nothing when field href is empty, when editing is explicitly disabled', () => { + const field = { + href: undefined, + metadata: testMetadata, + }; + comp.field = field; + comp.editable = false; + fixture.detectChanges(); + expect(de.children.length).toBe(0); + }); + }); }); @Component({ diff --git a/packages/sitecore-jss-angular/src/components/link.directive.ts b/packages/sitecore-jss-angular/src/components/link.directive.ts index fcb5bd8bf1..3c5521da06 100644 --- a/packages/sitecore-jss-angular/src/components/link.directive.ts +++ b/packages/sitecore-jss-angular/src/components/link.directive.ts @@ -143,4 +143,9 @@ export class LinkDirective extends BaseFieldDirective implements OnChanges { view.destroy(); return attrs; } + + protected shouldRender() { + // render the field if field is empty but we have 'text' and we are not in edditing mode, to preserve existing functionality + return super.shouldRender() || (this.field?.text && !this.field?.metadata); + } } diff --git a/packages/sitecore-jss-angular/src/components/router-link.directive.spec.ts b/packages/sitecore-jss-angular/src/components/router-link.directive.spec.ts index 8e64198b7f..0712361c43 100644 --- a/packages/sitecore-jss-angular/src/components/router-link.directive.spec.ts +++ b/packages/sitecore-jss-angular/src/components/router-link.directive.spec.ts @@ -1,4 +1,4 @@ -import { Component, DebugElement, Input } from '@angular/core'; +import { Component, DebugElement, Input, TemplateRef } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; @@ -23,6 +23,35 @@ class TestComponent { @Input() attrs = {}; } +const emptyLinkFieldEditingTemplateId = 'emptyImageFieldEditingTemplate'; +const emptyLinkFieldEditingTemplate = '[This is a *custom* empty field template]'; +const emptyLinkFieldEditingTemplateDefaultTestString = '[No text in field]'; + +@Component({ + selector: 'test-empty-template-router-link', + template: ` + + + ${emptyLinkFieldEditingTemplate} + + `, +}) +class TestEmptyTemplateComponent { + @Input() field: LinkField; + @Input() editable = true; + @Input() attrs = {}; + @Input() emptyFieldEditingTemplate: TemplateRef; +} + describe('', () => { let fixture: ComponentFixture; let de: DebugElement; @@ -30,7 +59,7 @@ describe('', () => { beforeEach(() => { TestBed.configureTestingModule({ - declarations: [RouterLinkDirective, TestComponent], + declarations: [RouterLinkDirective, TestComponent, TestEmptyTemplateComponent], imports: [RouterTestingModule], }); @@ -173,6 +202,102 @@ describe('', () => { expect(rendered.nativeElement.title).toBe('footip'); expect(rendered.nativeElement.className).toBe('external-css-class my-link'); }); + + describe('editMode metadata', () => { + const testMetadata = { + contextItem: { + id: '{09A07660-6834-476C-B93B-584248D3003B}', + language: 'en', + revision: 'a0b36ce0a7db49418edf90eb9621e145', + version: 1, + }, + fieldId: '{414061F4-FBB1-4591-BC37-BFFA67F745EB}', + fieldType: 'link', + rawValue: 'Test1', + }; + + it('should render default empty field component when field value href is not present', () => { + const field = { + value: { src: undefined }, + metadata: testMetadata, + }; + comp.field = field; + fixture.detectChanges(); + + const rendered = de.nativeElement.innerHTML; + expect(rendered).toContain(emptyLinkFieldEditingTemplateDefaultTestString); + }); + + it('should render default empty field component when field href is not present', () => { + const field = { + hred: undefined, + metadata: testMetadata, + }; + comp.field = field; + fixture.detectChanges(); + + const rendered = de.nativeElement.innerHTML; + expect(rendered).toContain(emptyLinkFieldEditingTemplateDefaultTestString); + }); + + it('should render custom empty field component when provided, when field value href is not present', () => { + fixture = TestBed.createComponent(TestEmptyTemplateComponent); + fixture.detectChanges(); + + de = fixture.debugElement; + comp = fixture.componentInstance; + + const field = { + value: { href: undefined }, + metadata: testMetadata, + }; + comp.field = field; + fixture.detectChanges(); + + const rendered = de.nativeElement.innerHTML; + expect(rendered).toContain(emptyLinkFieldEditingTemplate); + }); + + it('should render custom empty field component when provided, when field href is not present', () => { + fixture = TestBed.createComponent(TestEmptyTemplateComponent); + fixture.detectChanges(); + + de = fixture.debugElement; + comp = fixture.componentInstance; + + const field = { + href: undefined, + metadata: testMetadata, + }; + comp.field = field; + fixture.detectChanges(); + + const rendered = de.nativeElement.innerHTML; + expect(rendered).toContain(emptyLinkFieldEditingTemplate); + }); + + it('should render nothing when field value href is not present, when editing is explicitly disabled', () => { + const field = { + value: { href: undefined }, + metadata: testMetadata, + }; + comp.field = field; + comp.editable = false; + fixture.detectChanges(); + expect(de.children.length).toBe(0); + }); + + it('should render nothing when field href is empty, when editing is explicitly disabled', () => { + const field = { + href: undefined, + metadata: testMetadata, + }; + comp.field = field; + comp.editable = false; + fixture.detectChanges(); + expect(de.children.length).toBe(0); + }); + }); }); @Component({ @@ -189,6 +314,31 @@ class TestWithChildrenComponent { @Input() attrs = {}; } +@Component({ + selector: 'test-empty-template-link', + template: ` + hello world + + ${emptyLinkFieldEditingTemplate} + + `, +}) +class TestEmptyTemplateWithChildrenComponent { + @Input() field: LinkField; + @Input() editable = true; + @Input() attrs = {}; + @Input() emptyFieldEditingTemplate: TemplateRef; +} + describe('children', () => { let fixture: ComponentFixture; let de: DebugElement; @@ -196,7 +346,11 @@ describe('children', () => { beforeEach(() => { TestBed.configureTestingModule({ - declarations: [RouterLinkDirective, TestWithChildrenComponent], + declarations: [ + RouterLinkDirective, + TestWithChildrenComponent, + TestEmptyTemplateWithChildrenComponent, + ], imports: [RouterTestingModule], }); @@ -229,4 +383,100 @@ describe('children', () => { const rendered = de.query(By.css('a')); expect(rendered.nativeElement.innerHTML).toContain('hello world'); }); + + describe('editMode metadata', () => { + const testMetadata = { + contextItem: { + id: '{09A07660-6834-476C-B93B-584248D3003B}', + language: 'en', + revision: 'a0b36ce0a7db49418edf90eb9621e145', + version: 1, + }, + fieldId: '{414061F4-FBB1-4591-BC37-BFFA67F745EB}', + fieldType: 'link', + rawValue: 'Test1', + }; + + it('should render default empty field component when field value href is not present', () => { + const field = { + value: { src: undefined }, + metadata: testMetadata, + }; + comp.field = field; + fixture.detectChanges(); + + const rendered = de.nativeElement.innerHTML; + expect(rendered).toContain(emptyLinkFieldEditingTemplateDefaultTestString); + }); + + it('should render default empty field component when field href is not present', () => { + const field = { + hred: undefined, + metadata: testMetadata, + }; + comp.field = field; + fixture.detectChanges(); + + const rendered = de.nativeElement.innerHTML; + expect(rendered).toContain(emptyLinkFieldEditingTemplateDefaultTestString); + }); + + it('should render custom empty field component when provided, when field value href is not present', () => { + fixture = TestBed.createComponent(TestEmptyTemplateWithChildrenComponent); + fixture.detectChanges(); + + de = fixture.debugElement; + comp = fixture.componentInstance; + + const field = { + value: { href: undefined }, + metadata: testMetadata, + }; + comp.field = field; + fixture.detectChanges(); + + const rendered = de.nativeElement.innerHTML; + expect(rendered).toContain(emptyLinkFieldEditingTemplate); + }); + + it('should render custom empty field component when provided, when field href is not present', () => { + fixture = TestBed.createComponent(TestEmptyTemplateWithChildrenComponent); + fixture.detectChanges(); + + de = fixture.debugElement; + comp = fixture.componentInstance; + + const field = { + href: undefined, + metadata: testMetadata, + }; + comp.field = field; + fixture.detectChanges(); + + const rendered = de.nativeElement.innerHTML; + expect(rendered).toContain(emptyLinkFieldEditingTemplate); + }); + + it('should render nothing when field value href is not present, when editing is explicitly disabled', () => { + const field = { + value: { href: undefined }, + metadata: testMetadata, + }; + comp.field = field; + comp.editable = false; + fixture.detectChanges(); + expect(de.children.length).toBe(0); + }); + + it('should render nothing when field href is empty, when editing is explicitly disabled', () => { + const field = { + href: undefined, + metadata: testMetadata, + }; + comp.field = field; + comp.editable = false; + fixture.detectChanges(); + expect(de.children.length).toBe(0); + }); + }); }); From 090b0048499f6f1d1a1a50cee0b9d51feffd49b2 Mon Sep 17 00:00:00 2001 From: yavorsk Date: Tue, 3 Sep 2024 20:37:07 +0300 Subject: [PATCH 11/32] remove console.log --- .../sitecore-jss-angular/src/components/link.directive.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sitecore-jss-angular/src/components/link.directive.spec.ts b/packages/sitecore-jss-angular/src/components/link.directive.spec.ts index b2bfce7fc7..b6d81cdea6 100644 --- a/packages/sitecore-jss-angular/src/components/link.directive.spec.ts +++ b/packages/sitecore-jss-angular/src/components/link.directive.spec.ts @@ -213,7 +213,7 @@ describe('', () => { comp.editable = false; comp.field = field; fixture.detectChanges(); - console.log(de.nativeElement); + const rendered = de.query(By.css('a')); expect(rendered.nativeElement.href).toBe(''); From cd22f8939d8b7f2e5a430a2bb2e953a2ef9e00b1 Mon Sep 17 00:00:00 2001 From: yavorsk Date: Wed, 4 Sep 2024 09:46:49 +0300 Subject: [PATCH 12/32] fix lint errors --- .../src/components/base-field.directive.ts | 8 ++++---- .../src/components/link.directive.ts | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/sitecore-jss-angular/src/components/base-field.directive.ts b/packages/sitecore-jss-angular/src/components/base-field.directive.ts index f8257caa4b..92b4708266 100644 --- a/packages/sitecore-jss-angular/src/components/base-field.directive.ts +++ b/packages/sitecore-jss-angular/src/components/base-field.directive.ts @@ -15,10 +15,6 @@ export abstract class BaseFieldDirective { return !!this.field?.editable || !isFieldValueEmpty(this.field); } - private shouldRenderEmptyEditingComponent() { - return this.field?.metadata && this.editable; - } - protected renderEmpty(emptyFieldEditingComponent: Type) { if (this.shouldRenderEmptyEditingComponent()) { if (this.emptyFieldEditingTemplate) { @@ -29,4 +25,8 @@ export abstract class BaseFieldDirective { } } } + + private shouldRenderEmptyEditingComponent() { + return this.field?.metadata && this.editable; + } } diff --git a/packages/sitecore-jss-angular/src/components/link.directive.ts b/packages/sitecore-jss-angular/src/components/link.directive.ts index 3c5521da06..d22d850aa6 100644 --- a/packages/sitecore-jss-angular/src/components/link.directive.ts +++ b/packages/sitecore-jss-angular/src/components/link.directive.ts @@ -82,6 +82,11 @@ export class LinkDirective extends BaseFieldDirective implements OnChanges { } } + protected shouldRender() { + // render the field if field is empty but we have 'text' and we are not in edditing mode, to preserve existing functionality + return super.shouldRender() || (this.field?.text && !this.field?.metadata); + } + private updateView() { const field = this.field; if (this.editable && field && field.editableFirstPart && field.editableLastPart) { @@ -143,9 +148,4 @@ export class LinkDirective extends BaseFieldDirective implements OnChanges { view.destroy(); return attrs; } - - protected shouldRender() { - // render the field if field is empty but we have 'text' and we are not in edditing mode, to preserve existing functionality - return super.shouldRender() || (this.field?.text && !this.field?.metadata); - } } From 48e48feb1f5054d697f1ea64d4f13df8e20a21a5 Mon Sep 17 00:00:00 2001 From: yavorsk Date: Wed, 4 Sep 2024 09:50:15 +0300 Subject: [PATCH 13/32] minor naming fixes --- .../src/components/generic-link.directive.spec.ts | 2 +- .../sitecore-jss-angular/src/components/link.directive.spec.ts | 2 +- .../src/components/router-link.directive.spec.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/sitecore-jss-angular/src/components/generic-link.directive.spec.ts b/packages/sitecore-jss-angular/src/components/generic-link.directive.spec.ts index b7f84459b9..ba972213e7 100644 --- a/packages/sitecore-jss-angular/src/components/generic-link.directive.spec.ts +++ b/packages/sitecore-jss-angular/src/components/generic-link.directive.spec.ts @@ -25,7 +25,7 @@ class TestComponent { @Input() extras = {}; } -const emptyLinkFieldEditingTemplateId = 'emptyImageFieldEditingTemplate'; +const emptyLinkFieldEditingTemplateId = 'emptyLinkFieldEditingTemplate'; const emptyLinkFieldEditingTemplate = '[This is a *custom* empty field template]'; const emptyLinkFieldEditingTemplateDefaultTestString = '[No text in field]'; diff --git a/packages/sitecore-jss-angular/src/components/link.directive.spec.ts b/packages/sitecore-jss-angular/src/components/link.directive.spec.ts index b6d81cdea6..8bf4476e88 100644 --- a/packages/sitecore-jss-angular/src/components/link.directive.spec.ts +++ b/packages/sitecore-jss-angular/src/components/link.directive.spec.ts @@ -18,7 +18,7 @@ class TestComponent { @Input() attrs = {}; } -const emptyLinkFieldEditingTemplateId = 'emptyImageFieldEditingTemplate'; +const emptyLinkFieldEditingTemplateId = 'emptyLinkFieldEditingTemplate'; const emptyLinkFieldEditingTemplate = '[This is a *custom* empty field template]'; const emptyLinkFieldEditingTemplateDefaultTestString = '[No text in field]'; diff --git a/packages/sitecore-jss-angular/src/components/router-link.directive.spec.ts b/packages/sitecore-jss-angular/src/components/router-link.directive.spec.ts index 0712361c43..cfb3d7e803 100644 --- a/packages/sitecore-jss-angular/src/components/router-link.directive.spec.ts +++ b/packages/sitecore-jss-angular/src/components/router-link.directive.spec.ts @@ -23,7 +23,7 @@ class TestComponent { @Input() attrs = {}; } -const emptyLinkFieldEditingTemplateId = 'emptyImageFieldEditingTemplate'; +const emptyLinkFieldEditingTemplateId = 'emptyLinkFieldEditingTemplate'; const emptyLinkFieldEditingTemplate = '[This is a *custom* empty field template]'; const emptyLinkFieldEditingTemplateDefaultTestString = '[No text in field]'; From 1b3e562ee9ded1518614fae23a40542355447ca0 Mon Sep 17 00:00:00 2001 From: yavorsk Date: Wed, 4 Sep 2024 10:53:18 +0300 Subject: [PATCH 14/32] minor fix --- packages/sitecore-jss-angular/src/components/link.directive.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sitecore-jss-angular/src/components/link.directive.ts b/packages/sitecore-jss-angular/src/components/link.directive.ts index d22d850aa6..fbbda551d0 100644 --- a/packages/sitecore-jss-angular/src/components/link.directive.ts +++ b/packages/sitecore-jss-angular/src/components/link.directive.ts @@ -84,7 +84,7 @@ export class LinkDirective extends BaseFieldDirective implements OnChanges { protected shouldRender() { // render the field if field is empty but we have 'text' and we are not in edditing mode, to preserve existing functionality - return super.shouldRender() || (this.field?.text && !this.field?.metadata); + return super.shouldRender() || !!(this.field?.text && !this.field?.metadata); } private updateView() { From 5396fd0069db92eb562da4b38cc97b468bbc5c7e Mon Sep 17 00:00:00 2001 From: yavorsk Date: Wed, 4 Sep 2024 11:50:41 +0300 Subject: [PATCH 15/32] add Date type to possible types for GenericFieldValue base package type (in order to make it compatible with angular package types); updated isFieldValueEmpty function and unit tests for that change --- .../src/components/rendering-field.ts | 2 +- packages/sitecore-jss/src/layout/models.ts | 1 + packages/sitecore-jss/src/layout/utils.test.ts | 16 ++++++++++++++++ packages/sitecore-jss/src/layout/utils.ts | 11 +++++++++-- 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/packages/sitecore-jss-angular/src/components/rendering-field.ts b/packages/sitecore-jss-angular/src/components/rendering-field.ts index b6155ff67f..a965ac6681 100644 --- a/packages/sitecore-jss-angular/src/components/rendering-field.ts +++ b/packages/sitecore-jss-angular/src/components/rendering-field.ts @@ -6,7 +6,7 @@ export interface RenderingField extends FieldMetadata { editable?: string; } -export interface DateField extends RenderingField {} +export interface DateField extends RenderingField {} export interface FileFieldValue { src?: string; diff --git a/packages/sitecore-jss/src/layout/models.ts b/packages/sitecore-jss/src/layout/models.ts index 97adc1e5a2..89c882dd57 100644 --- a/packages/sitecore-jss/src/layout/models.ts +++ b/packages/sitecore-jss/src/layout/models.ts @@ -121,6 +121,7 @@ export type GenericFieldValue = | string | boolean | number + | Date | { [key: string]: unknown } | Array<{ [key: string]: unknown }>; diff --git a/packages/sitecore-jss/src/layout/utils.test.ts b/packages/sitecore-jss/src/layout/utils.test.ts index a6174cf027..f9da8952d7 100644 --- a/packages/sitecore-jss/src/layout/utils.test.ts +++ b/packages/sitecore-jss/src/layout/utils.test.ts @@ -181,6 +181,22 @@ describe('sitecore-jss layout utils', () => { const result = isFieldValueEmpty(field); expect(result).to.be.false; }); + + it('should return true if Date field is empty for Date type', () => { + expect( + isFieldValueEmpty({ + value: undefined, + }) + ).to.be.true; + }); + + it('should return false if Date field is not empty for Date type', () => { + const field = { + value: new Date('2024-01-01T00:00:00Z'), + }; + const result = isFieldValueEmpty(field); + expect(result).to.be.false; + }); }); describe('boolean', () => { diff --git a/packages/sitecore-jss/src/layout/utils.ts b/packages/sitecore-jss/src/layout/utils.ts index bd991e5953..400c810758 100644 --- a/packages/sitecore-jss/src/layout/utils.ts +++ b/packages/sitecore-jss/src/layout/utils.ts @@ -113,7 +113,13 @@ export function isFieldValueEmpty(field: GenericFieldValue | Partial): bo !(fieldValue as { [key: string]: unknown }).src; const isLinkFieldEmpty = (fieldValue: GenericFieldValue) => !(fieldValue as { [key: string]: unknown }).href; - const isDateFieldEmpty = (fieldValue: GenericFieldValue) => fieldValue === EMPTY_DATE_FIELD_VALUE; + const isDateFieldEmpty = (fieldValue: GenericFieldValue) => { + if (typeof fieldValue === 'string') { + return fieldValue === EMPTY_DATE_FIELD_VALUE; + } else { + return !(typeof (fieldValue as Date)?.getMonth === 'function'); + } + }; const isEmpty = (fieldValue: GenericFieldValue) => { if (fieldValue === null || fieldValue === undefined) { @@ -124,7 +130,8 @@ export function isFieldValueEmpty(field: GenericFieldValue | Partial): bo return ( isImageFieldEmpty(fieldValue) && isFileFieldEmpty(fieldValue) && - isLinkFieldEmpty(fieldValue) + isLinkFieldEmpty(fieldValue) && + isDateFieldEmpty(fieldValue) ); } else if (typeof fieldValue === 'number' || typeof fieldValue === 'boolean') { // Avoid returning true for 0 and false values From aa4d3d313d4ce684458b9229ce27ffbac83816c5 Mon Sep 17 00:00:00 2001 From: yavorsk Date: Wed, 4 Sep 2024 12:00:20 +0300 Subject: [PATCH 16/32] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12d046b872..4b8016bce8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,7 @@ Our versioning strategy is as follows: * Promo component ([#1897](https://github.com/Sitecore/jss/pull/1897)) * PageContent component ([#1905](https://github.com/Sitecore/jss/pull/1905)) * Navigation component ([#1894](https://github.com/Sitecore/jss/pull/1894)) +* `[sitecore-jss]``[sitecore-jss-angular]` Default Placeholder Content for empty fields in editMode metadata - in edit mode metadata in Pages, angular package field directives will render default or custom placeholder content if the provided field is empty; base package's GenericFieldValue model is updated to accept Date type ([#1916](https://github.com/Sitecore/jss/pull/1916)) ### 🛠 Breaking Change From 0558e8dad2adcfec090ec4339cd162fad0e1296c Mon Sep 17 00:00:00 2001 From: yavorsk Date: Thu, 5 Sep 2024 11:57:37 +0300 Subject: [PATCH 17/32] add unit test for invalid Date object --- packages/sitecore-jss/src/layout/utils.test.ts | 8 ++++++++ packages/sitecore-jss/src/layout/utils.ts | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/sitecore-jss/src/layout/utils.test.ts b/packages/sitecore-jss/src/layout/utils.test.ts index f9da8952d7..67bdb7e7ec 100644 --- a/packages/sitecore-jss/src/layout/utils.test.ts +++ b/packages/sitecore-jss/src/layout/utils.test.ts @@ -197,6 +197,14 @@ describe('sitecore-jss layout utils', () => { const result = isFieldValueEmpty(field); expect(result).to.be.false; }); + + it('should return true if Date field is invalid for Date type', () => { + const field = { + value: new Date('invalid-date-string'), + }; + const result = isFieldValueEmpty(field); + expect(result).to.be.true; + }) }); describe('boolean', () => { diff --git a/packages/sitecore-jss/src/layout/utils.ts b/packages/sitecore-jss/src/layout/utils.ts index 400c810758..1c001fe4c1 100644 --- a/packages/sitecore-jss/src/layout/utils.ts +++ b/packages/sitecore-jss/src/layout/utils.ts @@ -117,7 +117,7 @@ export function isFieldValueEmpty(field: GenericFieldValue | Partial): bo if (typeof fieldValue === 'string') { return fieldValue === EMPTY_DATE_FIELD_VALUE; } else { - return !(typeof (fieldValue as Date)?.getMonth === 'function'); + return !(typeof (fieldValue as Date)?.getMonth === 'function' && !isNaN((fieldValue as Date)?.getMonth())); } }; From 1a10142bc9047f20ebcefc6e75678b2165117f95 Mon Sep 17 00:00:00 2001 From: yavorsk Date: Thu, 5 Sep 2024 13:10:01 +0300 Subject: [PATCH 18/32] fix lint error --- packages/sitecore-jss/src/layout/utils.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/sitecore-jss/src/layout/utils.ts b/packages/sitecore-jss/src/layout/utils.ts index 1c001fe4c1..b5656dc13b 100644 --- a/packages/sitecore-jss/src/layout/utils.ts +++ b/packages/sitecore-jss/src/layout/utils.ts @@ -117,7 +117,10 @@ export function isFieldValueEmpty(field: GenericFieldValue | Partial): bo if (typeof fieldValue === 'string') { return fieldValue === EMPTY_DATE_FIELD_VALUE; } else { - return !(typeof (fieldValue as Date)?.getMonth === 'function' && !isNaN((fieldValue as Date)?.getMonth())); + return !( + typeof (fieldValue as Date)?.getMonth === 'function' && + !isNaN((fieldValue as Date)?.getMonth()) + ); } }; From 194abcd7be456ea33a6ecc039d4397d6bc2523b1 Mon Sep 17 00:00:00 2001 From: yavorsk Date: Thu, 5 Sep 2024 13:17:35 +0300 Subject: [PATCH 19/32] rename default placeholder components --- packages/sitecore-jss-angular/src/components/date.directive.ts | 2 +- ... default-empty-image-field-editing-placeholder.component.ts} | 2 +- ...> default-empty-text-field-editing-placeholder.component.ts} | 2 +- packages/sitecore-jss-angular/src/components/link.directive.ts | 2 +- .../sitecore-jss-angular/src/components/rich-text.directive.ts | 2 +- packages/sitecore-jss-angular/src/components/text.directive.ts | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) rename packages/sitecore-jss-angular/src/components/{default-empty-field-editing-image.component.ts => default-empty-image-field-editing-placeholder.component.ts} (95%) rename packages/sitecore-jss-angular/src/components/{default-empty-field-editing.component.ts => default-empty-text-field-editing-placeholder.component.ts} (81%) diff --git a/packages/sitecore-jss-angular/src/components/date.directive.ts b/packages/sitecore-jss-angular/src/components/date.directive.ts index 13a4d3a2da..5e21973d99 100644 --- a/packages/sitecore-jss-angular/src/components/date.directive.ts +++ b/packages/sitecore-jss-angular/src/components/date.directive.ts @@ -9,7 +9,7 @@ import { } from '@angular/core'; import { DateField } from './rendering-field'; import { BaseFieldDirective } from './base-field.directive'; -import { DefaultEmptyFieldEditingComponent } from './default-empty-field-editing.component'; +import { DefaultEmptyFieldEditingComponent } from './default-empty-text-field-editing-placeholder.component'; @Directive({ selector: '[scDate]', diff --git a/packages/sitecore-jss-angular/src/components/default-empty-field-editing-image.component.ts b/packages/sitecore-jss-angular/src/components/default-empty-image-field-editing-placeholder.component.ts similarity index 95% rename from packages/sitecore-jss-angular/src/components/default-empty-field-editing-image.component.ts rename to packages/sitecore-jss-angular/src/components/default-empty-image-field-editing-placeholder.component.ts index 2328363266..3c435c8229 100644 --- a/packages/sitecore-jss-angular/src/components/default-empty-field-editing-image.component.ts +++ b/packages/sitecore-jss-angular/src/components/default-empty-image-field-editing-placeholder.component.ts @@ -4,7 +4,7 @@ import { Component } from '@angular/core'; * Default component that will be rendered in pages when image field is empty. */ @Component({ - selector: 'app-default-empty-field-editing-image', + selector: 'app-default-empty-image-field-editing-placeholder', template: ` [No text in field]', }) export class DefaultEmptyFieldEditingComponent {} diff --git a/packages/sitecore-jss-angular/src/components/link.directive.ts b/packages/sitecore-jss-angular/src/components/link.directive.ts index fbbda551d0..d36f5523a5 100644 --- a/packages/sitecore-jss-angular/src/components/link.directive.ts +++ b/packages/sitecore-jss-angular/src/components/link.directive.ts @@ -10,7 +10,7 @@ import { } from '@angular/core'; import { LinkField } from './rendering-field'; import { BaseFieldDirective } from './base-field.directive'; -import { DefaultEmptyFieldEditingComponent } from './default-empty-field-editing.component'; +import { DefaultEmptyFieldEditingComponent } from './default-empty-text-field-editing-placeholder.component'; @Directive({ selector: '[scLink]' }) export class LinkDirective extends BaseFieldDirective implements OnChanges { diff --git a/packages/sitecore-jss-angular/src/components/rich-text.directive.ts b/packages/sitecore-jss-angular/src/components/rich-text.directive.ts index 20e0faa758..043298211d 100644 --- a/packages/sitecore-jss-angular/src/components/rich-text.directive.ts +++ b/packages/sitecore-jss-angular/src/components/rich-text.directive.ts @@ -11,7 +11,7 @@ import { Router } from '@angular/router'; import { isAbsoluteUrl } from '@sitecore-jss/sitecore-jss/utils'; import { RichTextField } from './rendering-field'; import { BaseFieldDirective } from './base-field.directive'; -import { DefaultEmptyFieldEditingComponent } from './default-empty-field-editing.component'; +import { DefaultEmptyFieldEditingComponent } from './default-empty-text-field-editing-placeholder.component'; @Directive({ selector: '[scRichText]', diff --git a/packages/sitecore-jss-angular/src/components/text.directive.ts b/packages/sitecore-jss-angular/src/components/text.directive.ts index c2fb3195e0..c2760a0cba 100644 --- a/packages/sitecore-jss-angular/src/components/text.directive.ts +++ b/packages/sitecore-jss-angular/src/components/text.directive.ts @@ -8,7 +8,7 @@ import { } from '@angular/core'; import { TextField } from './rendering-field'; import { BaseFieldDirective } from './base-field.directive'; -import { DefaultEmptyFieldEditingComponent } from './default-empty-field-editing.component'; +import { DefaultEmptyFieldEditingComponent } from './default-empty-text-field-editing-placeholder.component'; @Directive({ selector: '[scText]', From 640071545ed2e7bf06e6e9ae6c6468f5d7d2bfe1 Mon Sep 17 00:00:00 2001 From: yavorsk Date: Thu, 5 Sep 2024 13:25:03 +0300 Subject: [PATCH 20/32] additional renaming --- ...fault-empty-image-field-editing-placeholder.component.ts | 6 +++--- .../sitecore-jss-angular/src/components/image.directive.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/sitecore-jss-angular/src/components/default-empty-image-field-editing-placeholder.component.ts b/packages/sitecore-jss-angular/src/components/default-empty-image-field-editing-placeholder.component.ts index 3c435c8229..76690323cb 100644 --- a/packages/sitecore-jss-angular/src/components/default-empty-image-field-editing-placeholder.component.ts +++ b/packages/sitecore-jss-angular/src/components/default-empty-image-field-editing-placeholder.component.ts @@ -9,9 +9,9 @@ import { Component } from '@angular/core'; `, + styles: `img { min-width:48px; min-height:48px; max-width:400px; max-height:400px; cursor:pointer }`, }) -export class DefaultEmptyFieldEditingImageComponent {} +export class DefaultEmptyImageFieldEditingComponent {} diff --git a/packages/sitecore-jss-angular/src/components/image.directive.ts b/packages/sitecore-jss-angular/src/components/image.directive.ts index 38d3c1b0ca..ad0d204e9a 100644 --- a/packages/sitecore-jss-angular/src/components/image.directive.ts +++ b/packages/sitecore-jss-angular/src/components/image.directive.ts @@ -11,7 +11,7 @@ import { import { mediaApi } from '@sitecore-jss/sitecore-jss/media'; import { ImageField, ImageFieldValue } from './rendering-field'; import { BaseFieldDirective } from './base-field.directive'; -import { DefaultEmptyFieldEditingImageComponent } from './default-empty-field-editing-image.component'; +import { DefaultEmptyImageFieldEditingComponent } from './default-empty-image-field-editing-placeholder.component'; @Directive({ selector: '[scImage]' }) export class ImageDirective extends BaseFieldDirective implements OnChanges { @@ -59,7 +59,7 @@ export class ImageDirective extends BaseFieldDirective implements OnChanges { private updateView() { if (!this.shouldRender()) { - super.renderEmpty(DefaultEmptyFieldEditingImageComponent); + super.renderEmpty(DefaultEmptyImageFieldEditingComponent); return; } From c82be95636370cf4fabd44bd2df87db4ecf39258 Mon Sep 17 00:00:00 2001 From: yavorsk Date: Thu, 5 Sep 2024 13:44:59 +0300 Subject: [PATCH 21/32] fix lint issue --- packages/sitecore-jss/src/layout/utils.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sitecore-jss/src/layout/utils.test.ts b/packages/sitecore-jss/src/layout/utils.test.ts index 67bdb7e7ec..44c7422049 100644 --- a/packages/sitecore-jss/src/layout/utils.test.ts +++ b/packages/sitecore-jss/src/layout/utils.test.ts @@ -204,7 +204,7 @@ describe('sitecore-jss layout utils', () => { }; const result = isFieldValueEmpty(field); expect(result).to.be.true; - }) + }); }); describe('boolean', () => { From 1c2a3cd6011a69b81e2d50787e31bc0bebf776aa Mon Sep 17 00:00:00 2001 From: yavorsk Date: Thu, 5 Sep 2024 14:04:29 +0300 Subject: [PATCH 22/32] fix lint issue --- .../default-empty-image-field-editing-placeholder.component.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/sitecore-jss-angular/src/components/default-empty-image-field-editing-placeholder.component.ts b/packages/sitecore-jss-angular/src/components/default-empty-image-field-editing-placeholder.component.ts index 76690323cb..ab8487b814 100644 --- a/packages/sitecore-jss-angular/src/components/default-empty-image-field-editing-placeholder.component.ts +++ b/packages/sitecore-jss-angular/src/components/default-empty-image-field-editing-placeholder.component.ts @@ -12,6 +12,7 @@ import { Component } from '@angular/core'; class="scEmptyImage" /> `, - styles: `img { min-width:48px; min-height:48px; max-width:400px; max-height:400px; cursor:pointer }`, + styles: + 'img { min-width:48px; min-height:48px; max-width:400px; max-height:400px; cursor:pointer }', }) export class DefaultEmptyImageFieldEditingComponent {} From e50b5201efb3633908c98551b55dda691d77c776 Mon Sep 17 00:00:00 2001 From: yavorsk Date: Thu, 5 Sep 2024 14:34:29 +0300 Subject: [PATCH 23/32] add jsdoc comments --- .../src/components/base-field.directive.ts | 13 +++++++++++++ .../src/components/link.directive.ts | 12 ++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/packages/sitecore-jss-angular/src/components/base-field.directive.ts b/packages/sitecore-jss-angular/src/components/base-field.directive.ts index 92b4708266..3431343e26 100644 --- a/packages/sitecore-jss-angular/src/components/base-field.directive.ts +++ b/packages/sitecore-jss-angular/src/components/base-field.directive.ts @@ -2,6 +2,9 @@ import { Directive, Type, ViewContainerRef, EmbeddedViewRef, TemplateRef } from import { RenderingField } from './rendering-field'; import { GenericFieldValue, isFieldValueEmpty } from '@sitecore-jss/sitecore-jss/layout'; +/** + * Base class that contains common functionality for the field directives. + */ @Directive() export abstract class BaseFieldDirective { protected viewRef: EmbeddedViewRef; @@ -11,10 +14,17 @@ export abstract class BaseFieldDirective { constructor(protected viewContainer: ViewContainerRef) {} + /** + * Determines if directive should render the field as is + * Returns true if we are in edit mode 'chromes' (field.editable is present) or field is not empty + */ protected shouldRender() { return !!this.field?.editable || !isFieldValueEmpty(this.field); } + /** + * Renders the empty field markup which is required by Pages in editMode 'metadata' in case field is empty. + */ protected renderEmpty(emptyFieldEditingComponent: Type) { if (this.shouldRenderEmptyEditingComponent()) { if (this.emptyFieldEditingTemplate) { @@ -26,6 +36,9 @@ export abstract class BaseFieldDirective { } } + /** + * Determines if empty editing markup should be rendered for edit mode 'metadata' + */ private shouldRenderEmptyEditingComponent() { return this.field?.metadata && this.editable; } diff --git a/packages/sitecore-jss-angular/src/components/link.directive.ts b/packages/sitecore-jss-angular/src/components/link.directive.ts index d36f5523a5..de7851a95d 100644 --- a/packages/sitecore-jss-angular/src/components/link.directive.ts +++ b/packages/sitecore-jss-angular/src/components/link.directive.ts @@ -82,9 +82,17 @@ export class LinkDirective extends BaseFieldDirective implements OnChanges { } } + /** + * Determines if directive should render the field as is + * Returns true if we are in edit mode 'chromes' (field.editable is present) or field is not empty + * or link field text is present and we are not in edit mode 'metadata' + * The right side of the expression was added to preserve existing functionality + */ protected shouldRender() { - // render the field if field is empty but we have 'text' and we are not in edditing mode, to preserve existing functionality - return super.shouldRender() || !!(this.field?.text && !this.field?.metadata); + return ( + super.shouldRender() || + !!((this.field?.text || this.field?.value?.text) && !this.field?.metadata) + ); } private updateView() { From afa5f29b38d9248fc51133b8bc7399eebb0fa5f1 Mon Sep 17 00:00:00 2001 From: yavorsk Date: Thu, 5 Sep 2024 14:52:11 +0300 Subject: [PATCH 24/32] updates to changelog --- CHANGELOG.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b8016bce8..a18659cc76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,7 +44,16 @@ Our versioning strategy is as follows: * Promo component ([#1897](https://github.com/Sitecore/jss/pull/1897)) * PageContent component ([#1905](https://github.com/Sitecore/jss/pull/1905)) * Navigation component ([#1894](https://github.com/Sitecore/jss/pull/1894)) -* `[sitecore-jss]``[sitecore-jss-angular]` Default Placeholder Content for empty fields in editMode metadata - in edit mode metadata in Pages, angular package field directives will render default or custom placeholder content if the provided field is empty; base package's GenericFieldValue model is updated to accept Date type ([#1916](https://github.com/Sitecore/jss/pull/1916)) +* `[sitecore-jss]``[sitecore-jss-angular]` Default Placeholder Content for empty fields in editMode metadata - in edit mode metadata in Pages, angular package field directives will render default or custom placeholder content if the provided field is empty; ([#1916](https://github.com/Sitecore/jss/pull/1916)) + * custom placeholder content can be provided to field directives by passing the corresponding input: + - `scDateEmptyFieldEditingTemplate` for _scDate_ + - `scGenericLinkEmptyFieldEditingTemplate` for _scGenericLink_ + - `scImageEmptyFieldEditingTemplate` for _scImage_ + - `scLinkEmptyFieldEditingTemplate` for _scLink_ + - `scRichTextEmptyFieldEditingTemplate` for _scRichText_ + - `scRouterLinkEmptyFieldEditingTemplate` for _scRouterLink_ + - `scTextEmptyFieldEditingTemplate` for _scText_ +* `[sitecore-jss]` GenericFieldValue model is updated to accept Date type ([#1916](https://github.com/Sitecore/jss/pull/1916)) ### 🛠 Breaking Change From 29a593ed0741ffd97a8eeb0820648ecdaded580c Mon Sep 17 00:00:00 2001 From: yavorsk Date: Thu, 5 Sep 2024 15:13:27 +0300 Subject: [PATCH 25/32] reafactoring: add property defaultFieldEditingComponent to field directives and instantiate it in the constructor instead of passing it as an argument --- .../src/components/base-field.directive.ts | 7 ++++--- .../sitecore-jss-angular/src/components/date.directive.ts | 6 +++++- .../sitecore-jss-angular/src/components/image.directive.ts | 6 +++++- .../sitecore-jss-angular/src/components/link.directive.ts | 6 +++++- .../src/components/rich-text.directive.ts | 6 +++++- .../sitecore-jss-angular/src/components/text.directive.ts | 6 +++++- 6 files changed, 29 insertions(+), 8 deletions(-) diff --git a/packages/sitecore-jss-angular/src/components/base-field.directive.ts b/packages/sitecore-jss-angular/src/components/base-field.directive.ts index 3431343e26..5db6aa7971 100644 --- a/packages/sitecore-jss-angular/src/components/base-field.directive.ts +++ b/packages/sitecore-jss-angular/src/components/base-field.directive.ts @@ -9,8 +9,9 @@ import { GenericFieldValue, isFieldValueEmpty } from '@sitecore-jss/sitecore-jss export abstract class BaseFieldDirective { protected viewRef: EmbeddedViewRef; protected abstract field: RenderingField; - protected abstract emptyFieldEditingTemplate: TemplateRef; protected abstract editable: boolean; + protected abstract emptyFieldEditingTemplate: TemplateRef; + protected abstract defaultFieldEditingComponent: Type; constructor(protected viewContainer: ViewContainerRef) {} @@ -25,13 +26,13 @@ export abstract class BaseFieldDirective { /** * Renders the empty field markup which is required by Pages in editMode 'metadata' in case field is empty. */ - protected renderEmpty(emptyFieldEditingComponent: Type) { + protected renderEmpty() { if (this.shouldRenderEmptyEditingComponent()) { if (this.emptyFieldEditingTemplate) { this.viewContainer.createEmbeddedView(this.emptyFieldEditingTemplate); } else { this.viewContainer.clear(); - this.viewContainer.createComponent(emptyFieldEditingComponent); + this.viewContainer.createComponent(this.defaultFieldEditingComponent); } } } diff --git a/packages/sitecore-jss-angular/src/components/date.directive.ts b/packages/sitecore-jss-angular/src/components/date.directive.ts index 5e21973d99..a86839eeec 100644 --- a/packages/sitecore-jss-angular/src/components/date.directive.ts +++ b/packages/sitecore-jss-angular/src/components/date.directive.ts @@ -5,6 +5,7 @@ import { OnChanges, SimpleChanges, TemplateRef, + Type, ViewContainerRef, } from '@angular/core'; import { DateField } from './rendering-field'; @@ -27,12 +28,15 @@ export class DateDirective extends BaseFieldDirective implements OnChanges { @Input('scDateEmptyFieldEditingTemplate') emptyFieldEditingTemplate: TemplateRef; + protected defaultFieldEditingComponent: Type; + constructor( viewContainer: ViewContainerRef, private templateRef: TemplateRef, private datePipe: DatePipe ) { super(viewContainer); + this.defaultFieldEditingComponent = DefaultEmptyFieldEditingComponent; } ngOnChanges(changes: SimpleChanges) { @@ -48,7 +52,7 @@ export class DateDirective extends BaseFieldDirective implements OnChanges { private updateView() { if (!this.shouldRender()) { - super.renderEmpty(DefaultEmptyFieldEditingComponent); + super.renderEmpty(); return; } diff --git a/packages/sitecore-jss-angular/src/components/image.directive.ts b/packages/sitecore-jss-angular/src/components/image.directive.ts index ad0d204e9a..9a7fa717ab 100644 --- a/packages/sitecore-jss-angular/src/components/image.directive.ts +++ b/packages/sitecore-jss-angular/src/components/image.directive.ts @@ -6,6 +6,7 @@ import { Renderer2, SimpleChanges, TemplateRef, + Type, ViewContainerRef, } from '@angular/core'; import { mediaApi } from '@sitecore-jss/sitecore-jss/media'; @@ -21,6 +22,8 @@ export class ImageDirective extends BaseFieldDirective implements OnChanges { @Input('scImageEmptyFieldEditingTemplate') emptyFieldEditingTemplate: TemplateRef; + protected defaultFieldEditingComponent: Type; + /** * Custom regexp that finds media URL prefix that will be replaced by `/-/jssmedia` or `/~/jssmedia`. * @example @@ -43,6 +46,7 @@ export class ImageDirective extends BaseFieldDirective implements OnChanges { private elementRef: ElementRef ) { super(viewContainer); + this.defaultFieldEditingComponent = DefaultEmptyImageFieldEditingComponent; } ngOnChanges(changes: SimpleChanges) { @@ -59,7 +63,7 @@ export class ImageDirective extends BaseFieldDirective implements OnChanges { private updateView() { if (!this.shouldRender()) { - super.renderEmpty(DefaultEmptyImageFieldEditingComponent); + super.renderEmpty(); return; } diff --git a/packages/sitecore-jss-angular/src/components/link.directive.ts b/packages/sitecore-jss-angular/src/components/link.directive.ts index de7851a95d..b38c9f35ec 100644 --- a/packages/sitecore-jss-angular/src/components/link.directive.ts +++ b/packages/sitecore-jss-angular/src/components/link.directive.ts @@ -6,6 +6,7 @@ import { Renderer2, SimpleChanges, TemplateRef, + Type, ViewContainerRef, } from '@angular/core'; import { LinkField } from './rendering-field'; @@ -22,6 +23,8 @@ export class LinkDirective extends BaseFieldDirective implements OnChanges { @Input('scLinkEmptyFieldEditingTemplate') emptyFieldEditingTemplate: TemplateRef; + protected defaultFieldEditingComponent: Type; + private inlineRef: HTMLSpanElement | null = null; constructor( @@ -31,6 +34,7 @@ export class LinkDirective extends BaseFieldDirective implements OnChanges { private elementRef: ElementRef ) { super(viewContainer); + this.defaultFieldEditingComponent = DefaultEmptyFieldEditingComponent; } ngOnChanges(changes: SimpleChanges) { @@ -101,7 +105,7 @@ export class LinkDirective extends BaseFieldDirective implements OnChanges { this.renderInlineWrapper(field.editableFirstPart, field.editableLastPart); } else { if (!this.shouldRender()) { - super.renderEmpty(DefaultEmptyFieldEditingComponent); + super.renderEmpty(); return; } diff --git a/packages/sitecore-jss-angular/src/components/rich-text.directive.ts b/packages/sitecore-jss-angular/src/components/rich-text.directive.ts index 043298211d..f8ae5205b8 100644 --- a/packages/sitecore-jss-angular/src/components/rich-text.directive.ts +++ b/packages/sitecore-jss-angular/src/components/rich-text.directive.ts @@ -6,6 +6,7 @@ import { TemplateRef, ViewContainerRef, Renderer2, + Type, } from '@angular/core'; import { Router } from '@angular/router'; import { isAbsoluteUrl } from '@sitecore-jss/sitecore-jss/utils'; @@ -23,6 +24,8 @@ export class RichTextDirective extends BaseFieldDirective implements OnChanges { @Input('scRichTextEmptyFieldEditingTemplate') emptyFieldEditingTemplate: TemplateRef; + protected defaultFieldEditingComponent: Type; + constructor( viewContainer: ViewContainerRef, private templateRef: TemplateRef, @@ -30,6 +33,7 @@ export class RichTextDirective extends BaseFieldDirective implements OnChanges { private router: Router ) { super(viewContainer); + this.defaultFieldEditingComponent = DefaultEmptyFieldEditingComponent; } ngOnChanges(changes: SimpleChanges) { @@ -45,7 +49,7 @@ export class RichTextDirective extends BaseFieldDirective implements OnChanges { private updateView() { if (!this.shouldRender()) { - super.renderEmpty(DefaultEmptyFieldEditingComponent); + super.renderEmpty(); return; } diff --git a/packages/sitecore-jss-angular/src/components/text.directive.ts b/packages/sitecore-jss-angular/src/components/text.directive.ts index c2760a0cba..4d0d26d83b 100644 --- a/packages/sitecore-jss-angular/src/components/text.directive.ts +++ b/packages/sitecore-jss-angular/src/components/text.directive.ts @@ -4,6 +4,7 @@ import { OnChanges, SimpleChanges, TemplateRef, + Type, ViewContainerRef, } from '@angular/core'; import { TextField } from './rendering-field'; @@ -22,8 +23,11 @@ export class TextDirective extends BaseFieldDirective implements OnChanges { @Input('scTextEmptyFieldEditingTemplate') emptyFieldEditingTemplate: TemplateRef; + protected defaultFieldEditingComponent: Type; + constructor(viewContainer: ViewContainerRef, private templateRef: TemplateRef) { super(viewContainer); + this.defaultFieldEditingComponent = DefaultEmptyFieldEditingComponent; } ngOnChanges(changes: SimpleChanges) { @@ -39,7 +43,7 @@ export class TextDirective extends BaseFieldDirective implements OnChanges { private updateView() { if (!this.shouldRender()) { - super.renderEmpty(DefaultEmptyFieldEditingComponent); + super.renderEmpty(); return; } From 42c071d29fdc35ce2db223b5afe368fab986b0ae Mon Sep 17 00:00:00 2001 From: yavorsk Date: Thu, 5 Sep 2024 15:19:13 +0300 Subject: [PATCH 26/32] additional tsdoc comments --- .../src/components/base-field.directive.ts | 6 ++++++ .../sitecore-jss-angular/src/components/date.directive.ts | 6 ++++++ .../src/components/generic-link.directive.ts | 3 +++ .../sitecore-jss-angular/src/components/image.directive.ts | 6 ++++++ .../sitecore-jss-angular/src/components/link.directive.ts | 6 ++++++ .../src/components/rich-text.directive.ts | 6 ++++++ .../src/components/router-link.directive.ts | 3 +++ .../sitecore-jss-angular/src/components/text.directive.ts | 6 ++++++ 8 files changed, 42 insertions(+) diff --git a/packages/sitecore-jss-angular/src/components/base-field.directive.ts b/packages/sitecore-jss-angular/src/components/base-field.directive.ts index 5db6aa7971..61b81f6bc7 100644 --- a/packages/sitecore-jss-angular/src/components/base-field.directive.ts +++ b/packages/sitecore-jss-angular/src/components/base-field.directive.ts @@ -10,7 +10,13 @@ export abstract class BaseFieldDirective { protected viewRef: EmbeddedViewRef; protected abstract field: RenderingField; protected abstract editable: boolean; + /** + * Custom template to render in Pages in Metadata edit mode if field value is empty + */ protected abstract emptyFieldEditingTemplate: TemplateRef; + /** + * Default component to render in Pages in Metadata edit mode if field value is empty and emptyFieldEditingTemplate is not provided + */ protected abstract defaultFieldEditingComponent: Type; constructor(protected viewContainer: ViewContainerRef) {} diff --git a/packages/sitecore-jss-angular/src/components/date.directive.ts b/packages/sitecore-jss-angular/src/components/date.directive.ts index a86839eeec..21e81edd60 100644 --- a/packages/sitecore-jss-angular/src/components/date.directive.ts +++ b/packages/sitecore-jss-angular/src/components/date.directive.ts @@ -26,8 +26,14 @@ export class DateDirective extends BaseFieldDirective implements OnChanges { @Input('scDate') field: DateField; + /** + * Custom template to render in Pages in Metadata edit mode if field value is empty + */ @Input('scDateEmptyFieldEditingTemplate') emptyFieldEditingTemplate: TemplateRef; + /** + * Default component to render in Pages in Metadata edit mode if field value is empty and emptyFieldEditingTemplate is not provided + */ protected defaultFieldEditingComponent: Type; constructor( diff --git a/packages/sitecore-jss-angular/src/components/generic-link.directive.ts b/packages/sitecore-jss-angular/src/components/generic-link.directive.ts index 5bee78751c..428c898688 100644 --- a/packages/sitecore-jss-angular/src/components/generic-link.directive.ts +++ b/packages/sitecore-jss-angular/src/components/generic-link.directive.ts @@ -21,6 +21,9 @@ export class GenericLinkDirective extends LinkDirective { @Input('scGenericLinkExtras') extras?: NavigationExtras; + /** + * Custom template to render in Pages in Metadata edit mode if field value is empty + */ @Input('scGenericLinkEmptyFieldEditingTemplate') declare emptyFieldEditingTemplate: TemplateRef< unknown >; diff --git a/packages/sitecore-jss-angular/src/components/image.directive.ts b/packages/sitecore-jss-angular/src/components/image.directive.ts index 9a7fa717ab..e3e99ea538 100644 --- a/packages/sitecore-jss-angular/src/components/image.directive.ts +++ b/packages/sitecore-jss-angular/src/components/image.directive.ts @@ -20,8 +20,14 @@ export class ImageDirective extends BaseFieldDirective implements OnChanges { @Input('scImageEditable') editable = true; + /** + * Custom template to render in Pages in Metadata edit mode if field value is empty + */ @Input('scImageEmptyFieldEditingTemplate') emptyFieldEditingTemplate: TemplateRef; + /** + * Default component to render in Pages in Metadata edit mode if field value is empty and emptyFieldEditingTemplate is not provided + */ protected defaultFieldEditingComponent: Type; /** diff --git a/packages/sitecore-jss-angular/src/components/link.directive.ts b/packages/sitecore-jss-angular/src/components/link.directive.ts index b38c9f35ec..27780eced7 100644 --- a/packages/sitecore-jss-angular/src/components/link.directive.ts +++ b/packages/sitecore-jss-angular/src/components/link.directive.ts @@ -21,8 +21,14 @@ export class LinkDirective extends BaseFieldDirective implements OnChanges { @Input('scLink') field: LinkField; + /** + * Custom template to render in Pages in Metadata edit mode if field value is empty + */ @Input('scLinkEmptyFieldEditingTemplate') emptyFieldEditingTemplate: TemplateRef; + /** + * Default component to render in Pages in Metadata edit mode if field value is empty and emptyFieldEditingTemplate is not provided + */ protected defaultFieldEditingComponent: Type; private inlineRef: HTMLSpanElement | null = null; diff --git a/packages/sitecore-jss-angular/src/components/rich-text.directive.ts b/packages/sitecore-jss-angular/src/components/rich-text.directive.ts index f8ae5205b8..ab6390efda 100644 --- a/packages/sitecore-jss-angular/src/components/rich-text.directive.ts +++ b/packages/sitecore-jss-angular/src/components/rich-text.directive.ts @@ -22,8 +22,14 @@ export class RichTextDirective extends BaseFieldDirective implements OnChanges { @Input('scRichText') field: RichTextField; + /** + * Custom template to render in Pages in Metadata edit mode if field value is empty + */ @Input('scRichTextEmptyFieldEditingTemplate') emptyFieldEditingTemplate: TemplateRef; + /** + * Default component to render in Pages in Metadata edit mode if field value is empty and emptyFieldEditingTemplate is not provided + */ protected defaultFieldEditingComponent: Type; constructor( diff --git a/packages/sitecore-jss-angular/src/components/router-link.directive.ts b/packages/sitecore-jss-angular/src/components/router-link.directive.ts index 63c0cc7617..c422a0b02b 100644 --- a/packages/sitecore-jss-angular/src/components/router-link.directive.ts +++ b/packages/sitecore-jss-angular/src/components/router-link.directive.ts @@ -18,6 +18,9 @@ export class RouterLinkDirective extends LinkDirective { @Input('scRouterLink') declare field: LinkField; + /** + * Custom template to render in Pages in Metadata edit mode if field value is empty + */ @Input('scRouterLinkEmptyFieldEditingTemplate') declare emptyFieldEditingTemplate: TemplateRef< unknown >; diff --git a/packages/sitecore-jss-angular/src/components/text.directive.ts b/packages/sitecore-jss-angular/src/components/text.directive.ts index 4d0d26d83b..2fce6603bc 100644 --- a/packages/sitecore-jss-angular/src/components/text.directive.ts +++ b/packages/sitecore-jss-angular/src/components/text.directive.ts @@ -21,8 +21,14 @@ export class TextDirective extends BaseFieldDirective implements OnChanges { @Input('scText') field: TextField; + /** + * Custom template to render in Pages in Metadata edit mode if field value is empty + */ @Input('scTextEmptyFieldEditingTemplate') emptyFieldEditingTemplate: TemplateRef; + /** + * Default component to render in Pages in Metadata edit mode if field value is empty and emptyFieldEditingTemplate is not provided + */ protected defaultFieldEditingComponent: Type; constructor(viewContainer: ViewContainerRef, private templateRef: TemplateRef) { From 6c1c7c61535fc843c200dfe004c4297c7ad47dfa Mon Sep 17 00:00:00 2001 From: yavorsk Date: Thu, 5 Sep 2024 15:38:04 +0300 Subject: [PATCH 27/32] fix lint issue --- .../src/components/image.directive.ts | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/sitecore-jss-angular/src/components/image.directive.ts b/packages/sitecore-jss-angular/src/components/image.directive.ts index e3e99ea538..b70cf497c4 100644 --- a/packages/sitecore-jss-angular/src/components/image.directive.ts +++ b/packages/sitecore-jss-angular/src/components/image.directive.ts @@ -20,16 +20,6 @@ export class ImageDirective extends BaseFieldDirective implements OnChanges { @Input('scImageEditable') editable = true; - /** - * Custom template to render in Pages in Metadata edit mode if field value is empty - */ - @Input('scImageEmptyFieldEditingTemplate') emptyFieldEditingTemplate: TemplateRef; - - /** - * Default component to render in Pages in Metadata edit mode if field value is empty and emptyFieldEditingTemplate is not provided - */ - protected defaultFieldEditingComponent: Type; - /** * Custom regexp that finds media URL prefix that will be replaced by `/-/jssmedia` or `/~/jssmedia`. * @example @@ -43,6 +33,16 @@ export class ImageDirective extends BaseFieldDirective implements OnChanges { @Input('scImageAttrs') attrs: { [param: string]: unknown } = {}; + /** + * Custom template to render in Pages in Metadata edit mode if field value is empty + */ + @Input('scImageEmptyFieldEditingTemplate') emptyFieldEditingTemplate: TemplateRef; + + /** + * Default component to render in Pages in Metadata edit mode if field value is empty and emptyFieldEditingTemplate is not provided + */ + protected defaultFieldEditingComponent: Type; + private inlineRef: HTMLSpanElement | null = null; constructor( From c8edacfcab3f0a2f95479728eea0fddba1a68cd5 Mon Sep 17 00:00:00 2001 From: yavorsk Date: Thu, 5 Sep 2024 17:40:43 +0300 Subject: [PATCH 28/32] remove unnecessary function --- .../src/components/base-field.directive.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/packages/sitecore-jss-angular/src/components/base-field.directive.ts b/packages/sitecore-jss-angular/src/components/base-field.directive.ts index 61b81f6bc7..6e3a682e4e 100644 --- a/packages/sitecore-jss-angular/src/components/base-field.directive.ts +++ b/packages/sitecore-jss-angular/src/components/base-field.directive.ts @@ -33,7 +33,7 @@ export abstract class BaseFieldDirective { * Renders the empty field markup which is required by Pages in editMode 'metadata' in case field is empty. */ protected renderEmpty() { - if (this.shouldRenderEmptyEditingComponent()) { + if (this.field?.metadata && this.editable) { if (this.emptyFieldEditingTemplate) { this.viewContainer.createEmbeddedView(this.emptyFieldEditingTemplate); } else { @@ -42,11 +42,4 @@ export abstract class BaseFieldDirective { } } } - - /** - * Determines if empty editing markup should be rendered for edit mode 'metadata' - */ - private shouldRenderEmptyEditingComponent() { - return this.field?.metadata && this.editable; - } } From 18f6a112d99f35be1086cc34ff18f8d8eedd6d70 Mon Sep 17 00:00:00 2001 From: yavorsk Date: Thu, 5 Sep 2024 19:27:55 +0300 Subject: [PATCH 29/32] base directive unit tests --- .../components/base-field.directive.spec.ts | 170 ++++++++++++++++++ .../src/test-data/test-base.directive.ts | 54 ++++++ 2 files changed, 224 insertions(+) create mode 100644 packages/sitecore-jss-angular/src/components/base-field.directive.spec.ts create mode 100644 packages/sitecore-jss-angular/src/test-data/test-base.directive.ts diff --git a/packages/sitecore-jss-angular/src/components/base-field.directive.spec.ts b/packages/sitecore-jss-angular/src/components/base-field.directive.spec.ts new file mode 100644 index 0000000000..eace34072e --- /dev/null +++ b/packages/sitecore-jss-angular/src/components/base-field.directive.spec.ts @@ -0,0 +1,170 @@ +import { Component, DebugElement, Input, TemplateRef } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; + +import { TextField } from './rendering-field'; +import { TestBaseDirective } from '../test-data/test-base.directive'; + +@Component({ + selector: 'test-base', + template: ` + + `, +}) +class TestComponent { + @Input() field: TextField; + @Input() editable = true; +} + +const emptyTextFieldEditingTemplateId = 'emptyTextFieldEditingTemplate'; +const emptyTextFieldEditingTemplate = + '[This is a *custom* empty field component for text]'; + +@Component({ + selector: 'test-base-template', + template: ` + + + + ${emptyTextFieldEditingTemplate} + + `, +}) +class TestEmptyTemplateComponent { + @Input() field: TextField; + @Input() emptyFieldEditingTemplate: TemplateRef; + @Input() editable = true; +} + +describe('', () => { + let fixture: ComponentFixture; + let de: DebugElement; + let deSpan: DebugElement; + let comp: TestComponent; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [TestBaseDirective, TestComponent, TestEmptyTemplateComponent], + }); + + fixture = TestBed.createComponent(TestComponent); + fixture.detectChanges(); + + de = fixture.debugElement; + deSpan = de.query(By.css('span')); + comp = fixture.componentInstance; + }); + + describe('edit mode cromes', () => { + it('should render field value if it is present', () => { + const field: { [prop: string]: unknown } = { + value: 'value', + }; + comp.field = field; + fixture.detectChanges(); + + const rendered = deSpan.nativeElement.innerHTML; + expect(rendered).toBe('value'); + }); + + it('should render field editable if it is present', () => { + const field: { [prop: string]: unknown } = { + value: 'value', + editable: 'editable', + }; + comp.field = field; + fixture.detectChanges(); + + const rendered = deSpan.nativeElement.innerHTML; + expect(rendered).toBe(field.editable); + }); + + it('should render nothing if field.editable and value are missing', () => { + const field: { [prop: string]: unknown } = { + value: '', + }; + comp.field = field; + fixture.detectChanges(); + + const rendered = deSpan.nativeElement.innerHTML; + expect(rendered).toBe(''); + }); + }); + + describe('edit mode metadata', () => { + const testMetadata = { + contextItem: { + id: '{09A07660-6834-476C-B93B-584248D3003B}', + language: 'en', + revision: 'a0b36ce0a7db49418edf90eb9621e145', + version: 1, + }, + fieldId: '{414061F4-FBB1-4591-BC37-BFFA67F745EB}', + fieldType: 'single-line', + rawValue: 'Test1', + }; + + it('should render field value if field value is not empty', () => { + const field: { [prop: string]: unknown } = { + value: 'value', + metadata: testMetadata, + }; + comp.field = field; + fixture.detectChanges(); + + const rendered = deSpan.nativeElement.innerHTML; + expect(rendered).toBe('value'); + }); + + describe('field value is empty', () => { + it('should render default empty editing component if custom is not provided', () => { + const field = { + value: '', + metadata: testMetadata, + }; + comp.field = field; + fixture.detectChanges(); + + const rendered = de.nativeElement.innerHTML; + expect(rendered).toContain('[No text in field]'); + }); + + it('should render custom empty editing template if provided', () => { + fixture = TestBed.createComponent(TestEmptyTemplateComponent); + fixture.detectChanges(); + + de = fixture.debugElement; + comp = fixture.componentInstance; + + const field = { + value: '', + metadata: testMetadata, + }; + comp.field = field; + fixture.detectChanges(); + + const rendered = de.nativeElement.innerHTML; + expect(rendered).toContain(emptyTextFieldEditingTemplate); + }); + + it('should render nothing when field value is empty, when editing is explicitly disabled', () => { + const field = { + value: '', + metadata: testMetadata, + }; + comp.field = field; + comp.editable = false; + fixture.detectChanges(); + + const rendered = deSpan.nativeElement.innerHTML; + expect(rendered).toBe(''); + }); + }); + }); +}); diff --git a/packages/sitecore-jss-angular/src/test-data/test-base.directive.ts b/packages/sitecore-jss-angular/src/test-data/test-base.directive.ts new file mode 100644 index 0000000000..e502492d81 --- /dev/null +++ b/packages/sitecore-jss-angular/src/test-data/test-base.directive.ts @@ -0,0 +1,54 @@ +import { + Directive, + Input, + OnChanges, + SimpleChanges, + TemplateRef, + Type, + ViewContainerRef, +} from '@angular/core'; +import { TextField } from '../components/rendering-field'; +import { BaseFieldDirective } from '../components/base-field.directive'; +import { DefaultEmptyFieldEditingComponent } from '../components/default-empty-text-field-editing-placeholder.component'; + +@Directive({ + selector: '[scTestBase]', +}) +export class TestBaseDirective extends BaseFieldDirective implements OnChanges { + @Input('scTestBaseEditable') editable = true; + @Input('scTestBase') field: TextField; + @Input('scTestBaseEmptyFieldEditingTemplate') emptyFieldEditingTemplate: TemplateRef; + protected defaultFieldEditingComponent: Type; + + constructor(viewContainer: ViewContainerRef, private templateRef: TemplateRef) { + super(viewContainer); + this.defaultFieldEditingComponent = DefaultEmptyFieldEditingComponent; + } + + ngOnChanges(changes: SimpleChanges) { + if (changes.field || changes.editable || changes.encode) { + if (!this.viewRef) { + this.viewContainer.clear(); + this.viewRef = this.viewContainer.createEmbeddedView(this.templateRef); + } + + this.updateView(); + } + } + + private updateView() { + if (!this.shouldRender()) { + super.renderEmpty(); + return; + } + + const field = this.field; + let editable = this.editable; + + const html = field.editable && editable ? field.editable : field.value; + + this.viewRef.rootNodes.forEach((node) => { + node.textContent = html; + }); + } +} From 1dc55b21dad7155dba7dcc429e3136f5abd523fd Mon Sep 17 00:00:00 2001 From: yavorsk Date: Thu, 5 Sep 2024 19:37:08 +0300 Subject: [PATCH 30/32] fix lint issue --- .../sitecore-jss-angular/src/test-data/test-base.directive.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sitecore-jss-angular/src/test-data/test-base.directive.ts b/packages/sitecore-jss-angular/src/test-data/test-base.directive.ts index e502492d81..9d00017a92 100644 --- a/packages/sitecore-jss-angular/src/test-data/test-base.directive.ts +++ b/packages/sitecore-jss-angular/src/test-data/test-base.directive.ts @@ -43,7 +43,7 @@ export class TestBaseDirective extends BaseFieldDirective implements OnChanges { } const field = this.field; - let editable = this.editable; + const editable = this.editable; const html = field.editable && editable ? field.editable : field.value; From a670a60be1a84d38d0c17bceee858315f9605fe9 Mon Sep 17 00:00:00 2001 From: Illia Kovalenko <23364749+illiakovalenko@users.noreply.github.com> Date: Fri, 6 Sep 2024 13:27:55 +0300 Subject: [PATCH 31/32] Update packages/sitecore-jss-angular/src/components/base-field.directive.spec.ts --- .../src/components/base-field.directive.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sitecore-jss-angular/src/components/base-field.directive.spec.ts b/packages/sitecore-jss-angular/src/components/base-field.directive.spec.ts index eace34072e..65b4518497 100644 --- a/packages/sitecore-jss-angular/src/components/base-field.directive.spec.ts +++ b/packages/sitecore-jss-angular/src/components/base-field.directive.spec.ts @@ -61,7 +61,7 @@ describe('', () => { comp = fixture.componentInstance; }); - describe('edit mode cromes', () => { + describe('edit mode chromes', () => { it('should render field value if it is present', () => { const field: { [prop: string]: unknown } = { value: 'value', From f505d90d2f7d2f3091def1eb673369cfbe244bac Mon Sep 17 00:00:00 2001 From: illiakovalenko Date: Fri, 6 Sep 2024 13:44:53 +0300 Subject: [PATCH 32/32] Minor renaming --- .../default-empty-image-field-editing-placeholder.component.ts | 2 +- .../default-empty-text-field-editing-placeholder.component.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/sitecore-jss-angular/src/components/default-empty-image-field-editing-placeholder.component.ts b/packages/sitecore-jss-angular/src/components/default-empty-image-field-editing-placeholder.component.ts index ab8487b814..660d7ef8aa 100644 --- a/packages/sitecore-jss-angular/src/components/default-empty-image-field-editing-placeholder.component.ts +++ b/packages/sitecore-jss-angular/src/components/default-empty-image-field-editing-placeholder.component.ts @@ -4,7 +4,7 @@ import { Component } from '@angular/core'; * Default component that will be rendered in pages when image field is empty. */ @Component({ - selector: 'app-default-empty-image-field-editing-placeholder', + selector: 'sc-default-empty-image-field-editing-placeholder', template: ` [No text in field]', }) export class DefaultEmptyFieldEditingComponent {}