Skip to content

Commit f02dbb8

Browse files
author
Tom German
committed
Implemented Custom Formatting for Dynamic Form body
1 parent d9281fa commit f02dbb8

File tree

4 files changed

+161
-46
lines changed

4 files changed

+161
-46
lines changed

src/controls/dynamicForm/DynamicForm.module.scss

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,3 +147,46 @@
147147
}
148148
}
149149
}
150+
151+
h2.sectionTitle {
152+
color: #000000;
153+
font-weight: 600;
154+
font-size: 16px;
155+
margin-top: 6px;
156+
margin-bottom: 12px;
157+
clear: both;
158+
}
159+
.sectionFormFields {
160+
display: flex;
161+
flex-wrap: wrap;
162+
}
163+
.sectionFormField {
164+
@media (max-width: 1920px) {
165+
min-width: 244px;
166+
max-width: 244px;
167+
}
168+
@media (max-width: 1366px) {
169+
min-width: 248px;
170+
max-width: 248px;
171+
margin-right: 44px;
172+
}
173+
@media (max-width: 1024px) {
174+
min-width: 272px;
175+
max-width: 272px;
176+
}
177+
@media (max-width: 640px) {
178+
min-width: 432px;
179+
max-width: 432px;
180+
}
181+
@media (max-width: 480px) {
182+
width: 90%;
183+
}
184+
}
185+
.sectionLine {
186+
width: 100%;
187+
border-top: 1px solid #edebe9;
188+
border-bottom-width: 0;
189+
border-left-width: 0;
190+
border-right-width: 0;
191+
clear: both;
192+
}

src/controls/dynamicForm/DynamicForm.tsx

Lines changed: 94 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import "@pnp/sp/content-types";
3333
import "@pnp/sp/folders";
3434
import "@pnp/sp/items";
3535
import { sp } from "@pnp/sp";
36+
import { ICustomFormatting, ICustomFormattingBodySection } from "./ICustomFormatting";
3637

3738
const stackTokens: IStackTokens = { childrenGap: 20 };
3839

@@ -102,9 +103,17 @@ export class DynamicForm extends React.Component<
102103
* Default React component render method
103104
*/
104105
public render(): JSX.Element {
105-
const { fieldCollection, hiddenByFormula, isSaving, validationErrors } = this.state;
106+
const { customFormatting, fieldCollection, isSaving } = this.state;
106107

107-
const fieldOverrides = this.props.fieldOverrides;
108+
const bodySections = customFormatting?.body || [];
109+
if (bodySections.length > 0) {
110+
const specifiedFields: string[] = bodySections.reduce((prev, cur) => {
111+
prev.push(...cur.fields);
112+
return prev;
113+
}, []);
114+
const omittedFields = fieldCollection.filter(f => !specifiedFields.includes(f.columnInternalName)).map(f => f.columnInternalName);
115+
bodySections[bodySections.length - 1].fields.push(...omittedFields);
116+
}
108117

109118
return (
110119
<div>
@@ -117,33 +126,20 @@ export class DynamicForm extends React.Component<
117126
</div>
118127
) : (
119128
<div>
120-
{fieldCollection.map((v, i) => {
121-
if (hiddenByFormula.find(h => h === v.columnInternalName)) {
122-
return null;
123-
}
124-
let validationErrorMessage: string = "";
125-
if (validationErrors[v.columnInternalName]) {
126-
validationErrorMessage = validationErrors[v.columnInternalName];
127-
}
128-
if (
129-
fieldOverrides &&
130-
Object.prototype.hasOwnProperty.call(
131-
fieldOverrides,
132-
v.columnInternalName
133-
)
134-
) {
135-
v.disabled = v.disabled || isSaving;
136-
return fieldOverrides[v.columnInternalName](v);
137-
}
138-
return (
139-
<DynamicField
140-
key={v.columnInternalName}
141-
{...v}
142-
disabled={v.disabled || isSaving}
143-
validationErrorMessage={validationErrorMessage}
144-
/>
145-
);
146-
})}
129+
{bodySections.length > 0 && bodySections.map((section, i) => (
130+
<>
131+
<h2 className={styles.sectionTitle}>{section.displayname}</h2>
132+
<div className={styles.sectionFormFields}>
133+
{section.fields.map((f, i) => (
134+
<div key={f} className={styles.sectionFormField}>
135+
{ this.renderField(fieldCollection.find(fc => fc.columnInternalName === f) as IDynamicFieldProps)}
136+
</div>
137+
))}
138+
</div>
139+
{ i < bodySections.length - 1 && <hr className={styles.sectionLine} aria-hidden={true} />}
140+
</>
141+
))}
142+
{bodySections.length === 0 && fieldCollection.map((f, i) => this.renderField(f))}
147143
{!this.props.disabled && (
148144
<Stack className={styles.buttons} horizontal tokens={stackTokens}>
149145
<PrimaryButton
@@ -189,6 +185,36 @@ export class DynamicForm extends React.Component<
189185
);
190186
}
191187

188+
private renderField = (field: IDynamicFieldProps): JSX.Element => {
189+
const { fieldOverrides } = this.props;
190+
const { hiddenByFormula, isSaving, validationErrors } = this.state;
191+
if (hiddenByFormula.find(h => h === field.columnInternalName)) {
192+
return null;
193+
}
194+
let validationErrorMessage: string = "";
195+
if (validationErrors[field.columnInternalName]) {
196+
validationErrorMessage = validationErrors[field.columnInternalName];
197+
}
198+
if (
199+
fieldOverrides &&
200+
Object.prototype.hasOwnProperty.call(
201+
fieldOverrides,
202+
field.columnInternalName
203+
)
204+
) {
205+
field.disabled = field.disabled || isSaving;
206+
return fieldOverrides[field.columnInternalName](field);
207+
}
208+
return (
209+
<DynamicField
210+
key={field.columnInternalName}
211+
{...field}
212+
disabled={field.disabled || isSaving}
213+
validationErrorMessage={validationErrorMessage}
214+
/>
215+
);
216+
}
217+
192218
//trigger when the user submits the form.
193219
private onSubmitClick = async (): Promise<void> => {
194220
const {
@@ -570,10 +596,16 @@ export class DynamicForm extends React.Component<
570596
let contentTypeId = this.props.contentTypeId;
571597
let contentTypeName: string;
572598
try {
599+
600+
// Fetch form rendering information from SharePoint
573601
const listInfo = await this._spService.getListFormRenderInfo(listId);
602+
603+
// Fetch additional information about fields from SharePoint
604+
// (Number fields for min and max values, and fields with validation)
574605
const additionalInfo = await this._spService.getAdditionalListFormFieldInfo(listId);
575-
576606
const numberFields = additionalInfo.filter((f) => f.TypeAsString === "Number" || f.TypeAsString === "Currency");
607+
608+
// Build a dictionary of validation formulas and messages
577609
const validationFormulas: Record<string, Pick<ISPField,"ValidationFormula"|"ValidationMessage">> = additionalInfo.reduce((prev, cur) => {
578610
if (!prev[cur.InternalName] && cur.ValidationFormula) {
579611
prev[cur.InternalName] = {
@@ -584,26 +616,15 @@ export class DynamicForm extends React.Component<
584616
return prev;
585617
}, {});
586618

587-
const spList = sp.web.lists.getById(listId);
588-
let item = null;
589-
let etag: string | undefined = undefined;
590-
if (listItemId !== undefined && listItemId !== null && listItemId !== 0) {
591-
item = await spList.items.getById(listItemId).get();
592-
593-
if (onListItemLoaded) {
594-
await onListItemLoaded(item);
595-
}
596-
597-
if (respectETag !== false) {
598-
etag = item["odata.etag"];
599-
}
600-
}
601-
619+
// If no content type ID is provided, use the default (first one in the list)
602620
if (contentTypeId === undefined || contentTypeId === "") {
603621
contentTypeId = Object.keys(listInfo.ContentTypeIdToNameMap)[0];
604622
}
605623
contentTypeName = listInfo.ContentTypeIdToNameMap[contentTypeId];
606624

625+
// Build a dictionary of client validation formulas and messages
626+
// These are formulas that are added in Edit Form > Edit Columns > Edit Conditional Formula
627+
// They are evaluated on the client side, and determine whether a field should be hidden or shown
607628
const clientValidationFormulas = listInfo.ClientForms.Edit[contentTypeName].reduce((prev, cur) => {
608629
if (cur.ClientValidationFormula) {
609630
prev[cur.InternalName] = {
@@ -614,6 +635,30 @@ export class DynamicForm extends React.Component<
614635
return prev;
615636
}, {} as Record<string, Pick<ISPField, "ValidationFormula" | "ValidationMessage">>);
616637

638+
// Custom Formatting
639+
let bodySections: ICustomFormattingBodySection[];
640+
if (listInfo.ClientFormCustomFormatter && listInfo.ClientFormCustomFormatter[contentTypeId]) {
641+
const customFormatInfo = JSON.parse(listInfo.ClientFormCustomFormatter[contentTypeId]) as ICustomFormatting;
642+
bodySections = customFormatInfo.bodyJSONFormatter.sections;
643+
}
644+
645+
// Load SharePoint list item
646+
const spList = sp.web.lists.getById(listId);
647+
let item = null;
648+
let etag: string | undefined = undefined;
649+
if (listItemId !== undefined && listItemId !== null && listItemId !== 0) {
650+
item = await spList.items.getById(listItemId).get();
651+
652+
if (onListItemLoaded) {
653+
await onListItemLoaded(item);
654+
}
655+
656+
if (respectETag !== false) {
657+
etag = item["odata.etag"];
658+
}
659+
}
660+
661+
// Build the field collection
617662
const tempFields: IDynamicFieldProps[] = [];
618663
let order: number = 0;
619664
const hiddenFields =
@@ -817,7 +862,10 @@ export class DynamicForm extends React.Component<
817862
clientValidationFormulas,
818863
fieldCollection: tempFields,
819864
validationFormulas,
820-
etag
865+
etag,
866+
customFormatting: {
867+
body: bodySections,
868+
}
821869
}, () => this.performValidation(true));
822870

823871
} catch (error) {
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { CSSProperties } from "react";
2+
3+
interface ICustomFormattingNode {
4+
elmType: keyof HTMLElementTagNameMap;
5+
style: CSSProperties;
6+
children?: ICustomFormattingNode[];
7+
}
8+
9+
export interface ICustomFormattingBodySection {
10+
displayname: string;
11+
fields: string[];
12+
}
13+
14+
export interface ICustomFormatting {
15+
headerJSONFormatter: ICustomFormattingNode;
16+
bodyJSONFormatter: {
17+
sections: ICustomFormattingBodySection[];
18+
};
19+
footerJSONFormatter: ICustomFormattingNode;
20+
}

src/controls/dynamicForm/IDynamicFormState.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11

22
import { ISPField } from '../../common/SPEntities';
3+
import { ICustomFormattingBodySection } from './ICustomFormatting';
34
import { IDynamicFieldProps } from './dynamicField/IDynamicFieldProps';
45
export interface IDynamicFormState {
56
fieldCollection: IDynamicFieldProps[];
@@ -11,6 +12,9 @@ export interface IDynamicFormState {
1112
hiddenByFormula: string[];
1213
/** Populated by evaluation of List Column Setting validation. Key is internal field name, value is the configured error message. */
1314
validationErrors: Record<string, string>;
15+
customFormatting?: {
16+
body: ICustomFormattingBodySection[];
17+
}
1418
isSaving?: boolean;
1519
etag?: string;
1620
isValidationErrorDialogOpen: boolean;

0 commit comments

Comments
 (0)