diff --git a/CHANGELOG.md b/CHANGELOG.md
index d7f2271b04..e8e12783b8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -25,6 +25,7 @@ Our versioning strategy is as follows:
* `[create-sitecore-jss]` Rework Angular initializer to support XMCloud and SXP journeys ([#1845](https://github.com/Sitecore/jss/pull/1845))([#1858](https://github.com/Sitecore/jss/pull/1858))
* `[create-sitecore-jss]` Allows proxy apps to be installed alongside main apps ([#1858](https://github.com/Sitecore/jss/pull/1858))
* `nodeAppDestination` arg can be passed into `create-sitecore-jss` command to define path for proxy to be installed in
+* `[sitecore-jss-angular]` Angular placeholder now supports SXA components ([#1870](https://github.com/Sitecore/jss/pull/1870))
### 🛠 Breaking Change
diff --git a/packages/sitecore-jss-angular/src/components/placeholder.component.spec.ts b/packages/sitecore-jss-angular/src/components/placeholder.component.spec.ts
index cb397d021e..e2b39d1e08 100644
--- a/packages/sitecore-jss-angular/src/components/placeholder.component.spec.ts
+++ b/packages/sitecore-jss-angular/src/components/placeholder.component.spec.ts
@@ -1,4 +1,13 @@
-import { Component, DebugElement, EventEmitter, Injectable, Input, Output } from '@angular/core';
+import {
+ Component,
+ DebugElement,
+ EventEmitter,
+ Injectable,
+ Input,
+ Output,
+ TemplateRef,
+ ViewChild,
+} from '@angular/core';
import { Router } from '@angular/router';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { Location } from '@angular/common';
@@ -10,6 +19,9 @@ import { convertedData as eeData } from '../test-data/ee-data';
import {
convertedDevData as nonEeDevData,
convertedLayoutServiceData as nonEeLsData,
+ sxaRenderingData,
+ sxaRenderingDynamicPlaceholderData,
+ sxaRenderingDoubleDigitDynamicPlaceholderData,
} from '../test-data/non-ee-data';
import { LazyComponent } from '../test-data/lazy-loading/lazy-component.component';
import { JssCanActivate, JssCanActivateFn, JssResolve } from '../services/placeholder.token';
@@ -675,3 +687,160 @@ describe(' with lazy loaded modules', () => {
});
});
});
+
+@Component({
+ selector: 'test-rich-text',
+ template: `
+
+ Rich text
+
+
+
+
+
+
+
+
+
+
+ `,
+})
+class TestRichTextComponent {
+ @Input() rendering: ComponentRendering;
+ @ViewChild('default', { static: true }) defaultVariant: TemplateRef;
+ @ViewChild('withTitle', { static: true }) withTitleVariant: TemplateRef;
+ public get variant(): TemplateRef {
+ return this.rendering.params?.FieldNames === 'WithTitle'
+ ? this.withTitleVariant
+ : this.defaultVariant;
+ }
+}
+
+describe('SXA components', () => {
+ let fixture: ComponentFixture;
+ let de: DebugElement;
+ let comp: TestPlaceholderComponent;
+
+ beforeEach(
+ waitForAsync(() => {
+ TestBed.configureTestingModule({
+ declarations: [TestPlaceholderComponent, TestRichTextComponent],
+ imports: [
+ RouterTestingModule,
+ JssModule.withComponents([{ name: 'RichText', type: TestRichTextComponent }]),
+ ],
+ providers: [],
+ });
+
+ fixture = TestBed.createComponent(TestPlaceholderComponent);
+ de = fixture.debugElement;
+
+ comp = fixture.componentInstance;
+ fixture.detectChanges();
+ })
+ );
+
+ it(
+ 'should render',
+ waitForAsync(async () => {
+ const component = sxaRenderingData.sitecore.route;
+ const phKey = 'main';
+ comp.name = phKey;
+ comp.rendering = (component as unknown) as ComponentRendering;
+ fixture.detectChanges();
+
+ await fixture.whenStable();
+ fixture.detectChanges();
+
+ expect(de.children.length).toBe(1);
+
+ const richText = de.query(By.directive(TestRichTextComponent));
+ expect(richText).not.toBeNull();
+ expect(richText.nativeElement.innerHTML).toContain('rendering-variant');
+
+ const container = de.query(By.css('.rendering-variant'));
+ expect(container).not.toBeNull();
+ expect(container.attributes.class).toEqual(
+ 'col-9|col-sm-10|col-md-12|col-lg-6|col-xl-7|col-xxl-8 rendering-variant test-css-class-x'
+ );
+
+ const title = de.query(By.css('.title'));
+ expect(title).not.toBeNull();
+ expect(title.nativeElement.innerHTML).toEqual('Rich Text Rendering Variant');
+
+ const text = de.query(By.css('.text'));
+ expect(text).not.toBeNull();
+ expect(text.nativeElement.innerHTML).toEqual('Test RichText');
+ })
+ );
+
+ it(
+ 'should render another rendering variant',
+ waitForAsync(async () => {
+ const component = sxaRenderingData.sitecore.route;
+ const phKey = 'main-second';
+ comp.name = phKey;
+ comp.rendering = (component as unknown) as ComponentRendering;
+ fixture.detectChanges();
+
+ await fixture.whenStable();
+ fixture.detectChanges();
+
+ expect(de.children.length).toBe(1);
+
+ const richText = de.query(By.directive(TestRichTextComponent));
+ expect(richText).not.toBeNull();
+ expect(richText.nativeElement.innerHTML).toContain('rendering-variant');
+
+ const container = de.query(By.css('.rendering-variant'));
+ expect(container).not.toBeNull();
+ expect(container.attributes.class).toEqual(
+ 'col-9|col-sm-10|col-md-12|col-lg-6|col-xl-7|col-xxl-8 rendering-variant test-css-class-y'
+ );
+
+ const span = de.query(By.css('.default'));
+ expect(span).not.toBeNull();
+ expect(span.nativeElement.innerHTML).toEqual('Rich text');
+ })
+ );
+
+ it(
+ 'should render with container-{*} type dynamic placeholder',
+ waitForAsync(async () => {
+ const component = sxaRenderingDynamicPlaceholderData.sitecore.route;
+ const phKey = 'container-1';
+ comp.name = phKey;
+ comp.rendering = (component as unknown) as ComponentRendering;
+ fixture.detectChanges();
+
+ await fixture.whenStable();
+ fixture.detectChanges();
+
+ expect(de.children.length).toBe(1);
+
+ const richText = de.query(By.directive(TestRichTextComponent));
+ expect(richText).not.toBeNull();
+ expect(richText.nativeElement.innerHTML).toContain('rendering-variant');
+ })
+ );
+
+ it(
+ 'should render with dynamic-1-{*} type dynamic placeholder',
+ waitForAsync(async () => {
+ const component = sxaRenderingDoubleDigitDynamicPlaceholderData.sitecore.route;
+ const phKey = 'dynamic-1-{*}';
+ comp.name = phKey;
+ comp.rendering = (component as unknown) as ComponentRendering;
+ fixture.detectChanges();
+
+ await fixture.whenStable();
+ fixture.detectChanges();
+
+ expect(de.children.length).toBe(1);
+
+ const richText = de.query(By.directive(TestRichTextComponent));
+ expect(richText).not.toBeNull();
+ expect(richText.nativeElement.innerHTML).toContain('rendering-variant');
+ })
+ );
+});
diff --git a/packages/sitecore-jss-angular/src/components/placeholder.component.ts b/packages/sitecore-jss-angular/src/components/placeholder.component.ts
index b34c614164..41378f5ee9 100644
--- a/packages/sitecore-jss-angular/src/components/placeholder.component.ts
+++ b/packages/sitecore-jss-angular/src/components/placeholder.component.ts
@@ -23,7 +23,11 @@ import {
ViewContainerRef,
} from '@angular/core';
import { Data, Router, UrlTree } from '@angular/router';
-import { ComponentRendering, HtmlElementRendering } from '@sitecore-jss/sitecore-jss/layout';
+import {
+ ComponentRendering,
+ HtmlElementRendering,
+ EditMode,
+} from '@sitecore-jss/sitecore-jss/layout';
import { Observable } from 'rxjs';
import { takeWhile } from 'rxjs/operators';
import { JssCanActivateRedirectError } from '../services/jss-can-activate-error';
@@ -40,6 +44,10 @@ import {
PLACEHOLDER_MISSING_COMPONENT_COMPONENT,
} from '../services/placeholder.token';
import { constants } from '@sitecore-jss/sitecore-jss';
+import {
+ isDynamicPlaceholder,
+ getDynamicPlaceholderPattern,
+} from '@sitecore-jss/sitecore-jss/layout';
import { PlaceholderLoadingDirective } from './placeholder-loading.directive';
import { RenderEachDirective } from './render-each.directive';
import { RenderEmptyDirective } from './render-empty.directive';
@@ -48,10 +56,34 @@ import { isRawRendering } from './rendering';
/**
* @param {ComponentRendering} rendering
* @param {string} name
+ * @param {EditMode} [editMode]
*/
-function getPlaceholder(rendering: ComponentRendering, name: string) {
+function getPlaceholder(rendering: ComponentRendering, name: string, editMode?: EditMode) {
+ let phName = name.slice();
+
+ /**
+ * Process (SXA) dynamic placeholders
+ * Find and replace the matching dynamic placeholder e.g 'nameOfContainer-{*}' with the requested e.g. 'nameOfContainer-1'.
+ * For Metadata EditMode, we need to keep the raw placeholder name in place.
+ */
+ rendering?.placeholders &&
+ Object.keys(rendering.placeholders).forEach((placeholder) => {
+ const patternPlaceholder = isDynamicPlaceholder(placeholder)
+ ? getDynamicPlaceholderPattern(placeholder)
+ : null;
+
+ if (patternPlaceholder && patternPlaceholder.test(phName)) {
+ if (editMode === EditMode.Metadata) {
+ phName = placeholder;
+ } else {
+ rendering.placeholders![phName] = rendering.placeholders![placeholder];
+ delete rendering.placeholders![placeholder];
+ }
+ }
+ });
+
if (rendering && rendering.placeholders && Object.keys(rendering.placeholders).length > 0) {
- return rendering.placeholders[name];
+ return rendering.placeholders[phName];
}
return null;
}
@@ -95,6 +127,7 @@ export class PlaceholderComponent implements OnInit, OnChanges, DoCheck, OnDestr
private _componentInstances: { [prop: string]: unknown }[] = [];
private destroyed = false;
private parentStyleAttribute = '';
+ private editMode?: EditMode = undefined;
constructor(
private differs: KeyValueDiffers,
@@ -208,7 +241,8 @@ export class PlaceholderComponent implements OnInit, OnChanges, DoCheck, OnDestr
return;
}
- const placeholder = this.renderings || getPlaceholder(this.rendering, this.name || '');
+ const placeholder =
+ this.renderings || getPlaceholder(this.rendering, this.name || '', this.editMode);
if (!placeholder) {
console.warn(
diff --git a/packages/sitecore-jss-angular/src/services/jss-component-factory.service.ts b/packages/sitecore-jss-angular/src/services/jss-component-factory.service.ts
index 943b9e2762..fbf89439fa 100644
--- a/packages/sitecore-jss-angular/src/services/jss-component-factory.service.ts
+++ b/packages/sitecore-jss-angular/src/services/jss-component-factory.service.ts
@@ -51,7 +51,7 @@ export class JssComponentFactoryService {
if (loadedComponent) {
return Promise.resolve({
- componentDefinition: component,
+ componentDefinition: this.applySXAParams(component),
componentImplementation: loadedComponent.type,
canActivate: loadedComponent.canActivate,
resolve: loadedComponent.resolve,
@@ -85,7 +85,7 @@ export class JssComponentFactoryService {
}
return {
- componentDefinition: component,
+ componentDefinition: this.applySXAParams(component),
componentImplementation: componentType,
componentModuleRef: moduleRef,
canActivate: lazyComponent.canActivate,
@@ -116,4 +116,21 @@ export class JssComponentFactoryService {
componentDefinition: component,
});
}
+
+ private applySXAParams(rendering: ComponentRendering) {
+ if (!rendering.params?.FieldNames) {
+ // Not SXA component
+ return rendering;
+ }
+ // Provide aggregated SXA styles on params 'styles'
+ const styles = [];
+ if (rendering.params.GridParameters) {
+ styles.push(rendering.params.GridParameters);
+ }
+ if (rendering.params.Styles) {
+ styles.push(rendering.params.Styles);
+ }
+ rendering.params.styles = styles.join(' ');
+ return rendering;
+ }
}
diff --git a/packages/sitecore-jss-angular/src/test-data/non-ee-data.ts b/packages/sitecore-jss-angular/src/test-data/non-ee-data.ts
index 863289f818..4b337c693a 100644
--- a/packages/sitecore-jss-angular/src/test-data/non-ee-data.ts
+++ b/packages/sitecore-jss-angular/src/test-data/non-ee-data.ts
@@ -157,3 +157,140 @@ export const convertedLayoutServiceData = {
},
},
};
+
+export const sxaRenderingData = {
+ sitecore: {
+ context: {
+ pageEditing: false,
+ },
+ route: {
+ name: 'Home',
+ displayName: 'Home',
+ fields: {
+ key: {
+ value: 'This is a some sample <p>field data</p> o'boy! "wow"',
+ },
+ },
+ placeholders: {
+ main: [
+ {
+ uid: 'c4d5d43b-5aa8-4e03-8f16-9428f3e02d5c',
+ componentName: 'RichText',
+ dataSource: '/sitecore/content/SxaSample/SxaSampleSite/Home/Data/RichText',
+ params: {
+ GridParameters: 'col-9|col-sm-10|col-md-12|col-lg-6|col-xl-7|col-xxl-8',
+ FieldNames: 'WithTitle',
+ Styles: 'test-css-class-x',
+ },
+ fields: {
+ Text: {
+ value: 'Test RichText',
+ },
+ Title: {
+ value: 'Rich Text Rendering Variant',
+ },
+ },
+ },
+ ],
+ 'main-second': [
+ {
+ uid: 'c4d5d43b-5aa8-4e03-8f16-9428f3e02d5c',
+ componentName: 'RichText',
+ dataSource: '/sitecore/content/SxaSample/SxaSampleSite/Home/Data/RichText',
+ params: {
+ GridParameters: 'col-9|col-sm-10|col-md-12|col-lg-6|col-xl-7|col-xxl-8',
+ FieldNames: 'Default',
+ Styles: 'test-css-class-y',
+ },
+ fields: {
+ Text: {
+ value: 'Test RichText',
+ },
+ Title: {
+ value: 'Rich Text Rendering Variant',
+ },
+ },
+ },
+ ],
+ },
+ },
+ },
+};
+
+export const sxaRenderingDynamicPlaceholderData = {
+ sitecore: {
+ context: {
+ pageEditing: false,
+ },
+ route: {
+ name: 'Home',
+ displayName: 'Home',
+ fields: {
+ key: {
+ value: 'This is a some sample <p>field data</p> o'boy! "wow"',
+ },
+ },
+ placeholders: {
+ 'container-{*}': [
+ {
+ uid: 'c4d5d43b-5aa8-4e03-8f16-9428f3e02d5c',
+ componentName: 'RichText',
+ dataSource: '/sitecore/content/SxaSample/SxaSampleSite/Home/Data/RichText',
+ params: {
+ GridParameters: 'col-9|col-sm-10|col-md-12|col-lg-6|col-xl-7|col-xxl-8',
+ FieldNames: 'WithTitle',
+ Styles: 'test-css-class-x',
+ },
+ fields: {
+ Text: {
+ value: 'Test RichText',
+ },
+ Title: {
+ value: 'Rich Text Rendering Variant',
+ },
+ },
+ },
+ ],
+ },
+ },
+ },
+};
+
+export const sxaRenderingDoubleDigitDynamicPlaceholderData = {
+ sitecore: {
+ context: {
+ pageEditing: false,
+ },
+ route: {
+ name: 'Home',
+ displayName: 'Home',
+ fields: {
+ key: {
+ value: 'This is a some sample <p>field data</p> o'boy! "wow"',
+ },
+ },
+ placeholders: {
+ 'dynamic-1-{*}': [
+ {
+ uid: 'c4d5d43b-5aa8-4e03-8f16-9428f3e02d5c',
+ componentName: 'RichText',
+ dataSource: '/sitecore/content/SxaSample/SxaSampleSite/Home/Data/RichText',
+ params: {
+ GridParameters: 'col-9|col-sm-10|col-md-12|col-lg-6|col-xl-7|col-xxl-8',
+ FieldNames: 'WithTitle',
+ Styles: 'test-css-class-x',
+ },
+ fields: {
+ Text: {
+ value: 'Test RichText',
+ },
+ Title: {
+ value: 'Rich Text Rendering Variant',
+ },
+ },
+ },
+ ],
+ },
+ },
+ },
+};
diff --git a/packages/sitecore-jss-react/src/components/Placeholder.test.tsx b/packages/sitecore-jss-react/src/components/Placeholder.test.tsx
index 485e4fc384..87c43eb881 100644
--- a/packages/sitecore-jss-react/src/components/Placeholder.test.tsx
+++ b/packages/sitecore-jss-react/src/components/Placeholder.test.tsx
@@ -932,22 +932,6 @@ describe('PlaceholderMetadata', () => {
});
});
-it('isDynamicPlaceholder', () => {
- expect(isDynamicPlaceholder('container-{*}')).to.be.true;
- expect(isDynamicPlaceholder('container-1-{*}')).to.be.true;
- expect(isDynamicPlaceholder('container-1-2')).to.be.false;
- expect(isDynamicPlaceholder('container-1')).to.be.false;
- expect(isDynamicPlaceholder('container-1-2-3')).to.be.false;
- expect(isDynamicPlaceholder('container-1-{*}-3')).to.be.true;
-});
-
-it('getDynamicPlaceholderPattern', () => {
- expect(getDynamicPlaceholderPattern('container-{*}').test('container-1')).to.be.true;
- expect(getDynamicPlaceholderPattern('container-{*}').test('container-1-2')).to.be.false;
- expect(getDynamicPlaceholderPattern('container-1-{*}').test('container-1-2')).to.be.true;
- expect(getDynamicPlaceholderPattern('container-1-{*}').test('container-1-2-3')).to.be.false;
-});
-
after(() => {
(global as any).window.close();
});
diff --git a/packages/sitecore-jss-react/src/components/PlaceholderCommon.tsx b/packages/sitecore-jss-react/src/components/PlaceholderCommon.tsx
index cff5643817..ad1cc228a2 100644
--- a/packages/sitecore-jss-react/src/components/PlaceholderCommon.tsx
+++ b/packages/sitecore-jss-react/src/components/PlaceholderCommon.tsx
@@ -9,6 +9,8 @@ import {
Item,
HtmlElementRendering,
EditMode,
+ isDynamicPlaceholder,
+ getDynamicPlaceholderPattern,
} from '@sitecore-jss/sitecore-jss/layout';
import { constants } from '@sitecore-jss/sitecore-jss';
import { convertAttributesToReactProps } from '../utils';
@@ -31,22 +33,6 @@ export type ComponentProps = {
rendering: ComponentRendering;
};
-/**
- * Returns a regular expression pattern for a dynamic placeholder name.
- * @param {string} placeholder Placeholder name with a dynamic segment (e.g. 'main-{*}')
- * @returns Regular expression pattern for the dynamic segment
- */
-export const getDynamicPlaceholderPattern = (placeholder: string) => {
- return new RegExp(`^${placeholder.replace(/\{\*\}+/i, '\\d+')}$`);
-};
-
-/**
- * Checks if the placeholder name is dynamic.
- * @param {string} placeholder Placeholder name
- * @returns True if the placeholder name is dynamic
- */
-export const isDynamicPlaceholder = (placeholder: string) => placeholder.indexOf('{*}') !== -1;
-
export interface PlaceholderProps {
[key: string]: unknown;
/** Name of the placeholder to render. */
@@ -155,9 +141,9 @@ export class PlaceholderCommon extends React.Compone
let phName = name.slice();
/**
- * [Chromes Mode]: [SXA] it needs for deleting dynamics placeholder when we set him number(props.name) of container.
- * from backend side we get common name of placeholder is called 'nameOfContainer-{*}' where '{*}' marker for replacing.
- * [Metadata Mode]: We need to keep the raw placeholder name. e.g 'nameOfContainer-{*}' instead of 'nameOfContainer-1'
+ * Process (SXA) dynamic placeholders
+ * Find and replace the matching dynamic placeholder e.g 'nameOfContainer-{*}' with the requested e.g. 'nameOfContainer-1'.
+ * For Metadata EditMode, we need to keep the raw placeholder name in place.
*/
if (rendering?.placeholders) {
Object.keys(rendering.placeholders).forEach((placeholder) => {
diff --git a/packages/sitecore-jss-react/src/components/PlaceholderMetadata.tsx b/packages/sitecore-jss-react/src/components/PlaceholderMetadata.tsx
index c5969c211c..4f7932fd6c 100644
--- a/packages/sitecore-jss-react/src/components/PlaceholderMetadata.tsx
+++ b/packages/sitecore-jss-react/src/components/PlaceholderMetadata.tsx
@@ -1,6 +1,9 @@
import React, { ReactNode } from 'react';
-import { ComponentRendering } from '@sitecore-jss/sitecore-jss/layout';
-import { getDynamicPlaceholderPattern, isDynamicPlaceholder } from './PlaceholderCommon';
+import {
+ ComponentRendering,
+ getDynamicPlaceholderPattern,
+ isDynamicPlaceholder,
+} from '@sitecore-jss/sitecore-jss/layout';
/**
* Props containing the component data to render.
diff --git a/packages/sitecore-jss/src/layout/index.ts b/packages/sitecore-jss/src/layout/index.ts
index b601e6ed18..9b4eb5cdad 100644
--- a/packages/sitecore-jss/src/layout/index.ts
+++ b/packages/sitecore-jss/src/layout/index.ts
@@ -22,6 +22,8 @@ export {
getFieldValue,
getChildPlaceholder,
isFieldValueEmpty,
+ isDynamicPlaceholder,
+ getDynamicPlaceholderPattern,
EMPTY_DATE_FIELD_VALUE,
} from './utils';
diff --git a/packages/sitecore-jss/src/layout/utils.test.ts b/packages/sitecore-jss/src/layout/utils.test.ts
index 4085625777..a6174cf027 100644
--- a/packages/sitecore-jss/src/layout/utils.test.ts
+++ b/packages/sitecore-jss/src/layout/utils.test.ts
@@ -5,6 +5,8 @@ import {
getFieldValue,
getChildPlaceholder,
isFieldValueEmpty,
+ isDynamicPlaceholder,
+ getDynamicPlaceholderPattern,
EMPTY_DATE_FIELD_VALUE,
} from './utils';
@@ -249,4 +251,27 @@ describe('sitecore-jss layout utils', () => {
});
});
});
+
+ describe('isDynamicPlaceholder', () => {
+ it('should return true if placeholder is dynamic', () => {
+ expect(isDynamicPlaceholder('container-{*}')).to.be.true;
+ expect(isDynamicPlaceholder('container-1-{*}')).to.be.true;
+ });
+
+ it('should return false if placeholder is not dynamic', () => {
+ expect(isDynamicPlaceholder('container-1-2')).to.be.false;
+ expect(isDynamicPlaceholder('container-1')).to.be.false;
+ expect(isDynamicPlaceholder('container-1-2-3')).to.be.false;
+ expect(isDynamicPlaceholder('container-1-{*}-3')).to.be.true;
+ });
+ });
+
+ describe('getDynamicPlaceholderPattern', () => {
+ it('should return dynamic placeholder pattern', () => {
+ expect(getDynamicPlaceholderPattern('container-{*}').test('container-1')).to.be.true;
+ expect(getDynamicPlaceholderPattern('container-{*}').test('container-1-2')).to.be.false;
+ expect(getDynamicPlaceholderPattern('container-1-{*}').test('container-1-2')).to.be.true;
+ expect(getDynamicPlaceholderPattern('container-1-{*}').test('container-1-2-3')).to.be.false;
+ });
+ });
});
diff --git a/packages/sitecore-jss/src/layout/utils.ts b/packages/sitecore-jss/src/layout/utils.ts
index bb09356476..bd991e5953 100644
--- a/packages/sitecore-jss/src/layout/utils.ts
+++ b/packages/sitecore-jss/src/layout/utils.ts
@@ -79,6 +79,22 @@ export function getChildPlaceholder(
return rendering.placeholders[placeholderName];
}
+/**
+ * Returns a regular expression pattern for a dynamic placeholder name.
+ * @param {string} placeholder Placeholder name with a dynamic segment (e.g. 'main-{*}')
+ * @returns Regular expression pattern for the dynamic segment
+ */
+export const getDynamicPlaceholderPattern = (placeholder: string) => {
+ return new RegExp(`^${placeholder.replace(/\{\*\}+/i, '\\d+')}$`);
+};
+
+/**
+ * Checks if the placeholder name is dynamic.
+ * @param {string} placeholder Placeholder name
+ * @returns True if the placeholder name is dynamic
+ */
+export const isDynamicPlaceholder = (placeholder: string) => placeholder.indexOf('{*}') !== -1;
+
/**
* The default value for an empty Date field.
* This value is defined as a default one by .NET