1
1
import * as React from "react" ;
2
2
import { Icon } from "office-ui-fabric-react" ;
3
3
import { FormulaEvaluation } from "./FormulaEvaluation" ;
4
- import { ASTNode } from "./FormulaEvaluation.types" ;
4
+ import { ASTNode , Context } from "./FormulaEvaluation.types" ;
5
5
import { ICustomFormattingExpressionNode , ICustomFormattingNode } from "./ICustomFormatting" ;
6
6
7
7
/**
@@ -13,13 +13,18 @@ export default class CustomFormattingHelper {
13
13
private _formulaEvaluator : FormulaEvaluation ;
14
14
15
15
/**
16
- *
16
+ * Custom Formatting Helper / Renderer
17
17
* @param formulaEvaluator An instance of FormulaEvaluation used for evaluating expressions in custom formatting
18
18
*/
19
19
constructor ( formulaEvaluator : FormulaEvaluation ) {
20
20
this . _formulaEvaluator = formulaEvaluator ;
21
21
}
22
22
23
+ /**
24
+ * The Formula Evaluator expects an ASTNode to be passed to it for evaluation. This method converts expressions
25
+ * described by the interface ICustomFormattingExpressionNode to ASTNodes.
26
+ * @param node An ICustomFormattingExpressionNode to be converted to an ASTNode
27
+ */
23
28
private convertCustomFormatExpressionNodes = ( node : ICustomFormattingExpressionNode | string | number | boolean ) : ASTNode => {
24
29
if ( typeof node !== "object" ) {
25
30
switch ( typeof node ) {
@@ -36,69 +41,116 @@ export default class CustomFormattingHelper {
36
41
return { type : "operator" , value : operator , operands } ;
37
42
}
38
43
39
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
40
- private evaluateCustomFormatContent = ( content : ICustomFormattingExpressionNode | ICustomFormattingNode | string | number | boolean , context : any ) : string | number | boolean => {
44
+ /**
45
+ * Given a single custom formatting expression, node or element, this method evaluates the expression and returns the result
46
+ * @param content An object, expression or literal value to be evaluated
47
+ * @param context A context object containing values / variables to be used in the evaluation
48
+ * @returns
49
+ */
50
+ private evaluateCustomFormatContent = ( content : ICustomFormattingExpressionNode | ICustomFormattingNode | string | number | boolean , context : Context ) : JSX . Element | string | number | boolean => {
51
+
52
+ // If content is a string or number, it is a literal value and should be returned as-is
41
53
if ( ( typeof content === "string" && content . charAt ( 0 ) !== "=" ) || typeof content === "number" ) return content ;
54
+
55
+ // If content is a string beginning with '=' it is a formula/expression, and should be evaluated
42
56
if ( typeof content === "string" && content . charAt ( 0 ) === "=" ) {
43
57
const result = this . _formulaEvaluator . evaluate ( content . substring ( 1 ) , context ) ;
44
58
return result ;
45
59
}
46
- if ( typeof content === "object" && ( Object . prototype . hasOwnProperty . call ( content , "elmType" ) ) ) {
47
- return this . renderCustomFormatContent ( content as ICustomFormattingNode , context ) ;
48
- } else if ( typeof content === "object" && ( Object . prototype . hasOwnProperty . call ( content , "operator" ) ) ) {
49
- const astNode = this . convertCustomFormatExpressionNodes ( content as ICustomFormattingExpressionNode ) ;
50
- const result = this . _formulaEvaluator . evaluateASTNode ( astNode , context ) ;
51
- if ( typeof result === "object" && Object . prototype . hasOwnProperty . call ( result , "elmType" ) ) {
52
- return this . renderCustomFormatContent ( result , context ) ;
60
+
61
+ // If content is an object, it is either further custom formatting described by an ICustomFormattingNode,
62
+ // or an expression to be evaluated - as described by an ICustomFormattingExpressionNode
63
+
64
+ if ( typeof content === "object" ) {
65
+
66
+ if ( Object . prototype . hasOwnProperty . call ( content , "elmType" ) ) {
67
+
68
+ // Custom Formatting Content
69
+ return this . renderCustomFormatContent ( content as ICustomFormattingNode , context ) ;
70
+
71
+ } else if ( Object . prototype . hasOwnProperty . call ( content , "operator" ) ) {
72
+
73
+ // Expression to be evaluated
74
+ const astNode = this . convertCustomFormatExpressionNodes ( content as ICustomFormattingExpressionNode ) ;
75
+ const result = this . _formulaEvaluator . evaluateASTNode ( astNode , context ) ;
76
+ if ( typeof result === "object" && Object . prototype . hasOwnProperty . call ( result , "elmType" ) ) {
77
+ return this . renderCustomFormatContent ( result , context ) ;
78
+ }
79
+ return result ;
80
+
53
81
}
54
- return result ;
55
- }
82
+ }
56
83
}
57
84
58
85
// eslint-disable-next-line @typescript-eslint/no-explicit-any
59
- public renderCustomFormatContent = ( node : ICustomFormattingNode , context : any , rootEl : boolean = false ) : any => {
86
+ public renderCustomFormatContent = ( node : ICustomFormattingNode , context : Context , rootEl : boolean = false ) : JSX . Element | string | number => {
87
+
88
+ // We don't want attempts to render custom format content to kill the component or web part,
89
+ // so we wrap the entire method in a try/catch block, log errors and return null if an error occurs
60
90
try {
91
+
92
+ // If node is a string or number, it is a literal value and should be returned as-is
61
93
if ( typeof node === "string" || typeof node === "number" ) return node ;
62
- // txtContent
63
- let textContent : JSX . Element | string | undefined ;
94
+
95
+ // Custom formatting nodes / elements may have a txtContent property, which represents the inner
96
+ // content of a HTML element. This can be a string literal, or another expression to be evaluated:
97
+ let textContent : JSX . Element | string | number | boolean | undefined ;
64
98
if ( node . txtContent ) {
65
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
66
- textContent = this . evaluateCustomFormatContent ( node . txtContent , context ) as any ;
99
+ textContent = this . evaluateCustomFormatContent ( node . txtContent , context ) ;
67
100
}
68
- // style
69
- const styleProperties = { } as React . CSSProperties ;
101
+
102
+ // Custom formatting nodes / elements may have a style property, which contains the style rules
103
+ // to be applied to the resulting HTML element. Rule values can be string literals or another expression
104
+ // to be evaluated:
105
+ const styleProperties : React . CSSProperties = { } ;
70
106
if ( node . style ) {
71
107
for ( const styleAttribute in node . style ) {
72
108
if ( node . style [ styleAttribute ] ) {
73
109
styleProperties [ styleAttribute ] = this . evaluateCustomFormatContent ( node . style [ styleAttribute ] , context ) as string ;
74
110
}
75
111
}
76
112
}
77
- // attributes
78
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
79
- const attributes = { } as any ;
113
+
114
+ // Custom formatting nodes / elements may have an attributes property, which represents the HTML attributes
115
+ // to be applied to the resulting HTML element. Attribute values can be string literals or another expression
116
+ // to be evaluated:
117
+ const attributes = { } as Record < string , string > ;
80
118
if ( node . attributes ) {
81
119
for ( const attribute in node . attributes ) {
82
120
if ( node . attributes [ attribute ] ) {
83
121
let attributeName = attribute ;
122
+
123
+ // Because we're using React to render the HTML content, we need to rename the 'class' attribute
84
124
if ( attributeName === "class" ) attributeName = "className" ;
125
+
126
+ // Evaluation
85
127
attributes [ attributeName ] = this . evaluateCustomFormatContent ( node . attributes [ attribute ] , context ) as string ;
128
+
129
+ // Add the 'sp-field-customFormatter' class to the root element
86
130
if ( attributeName === "className" && rootEl ) {
87
131
attributes [ attributeName ] = `${ attributes [ attributeName ] } sp-field-customFormatter` ;
88
132
}
89
133
}
90
134
}
91
135
}
92
- // children
136
+
137
+ // Custom formatting nodes / elements may have children. These are likely to be further custom formatting
93
138
let children : ( JSX . Element | string | number | boolean | undefined ) [ ] = [ ] ;
139
+
140
+ // If the node has an iconName property, we'll render an Icon component as the first child.
141
+ // SharePoint uses CSS to apply the icon in a ::before rule, but we can't count on the global selector for iconName
142
+ // being present on the page, so we'll add it as a child instead:
94
143
if ( attributes . iconName ) {
95
144
const icon = React . createElement ( Icon , { iconName : attributes . iconName } ) ;
96
145
children . push ( icon ) ;
97
146
}
147
+
148
+ // Each child object is evaluated recursively and added to the children array
98
149
if ( node . children ) {
99
150
children = node . children . map ( c => this . evaluateCustomFormatContent ( c , context ) ) ;
100
151
}
101
- // render
152
+
153
+ // The resulting HTML element is returned to the callee using React.createElement
102
154
const el = React . createElement ( node . elmType , { style : styleProperties , ...attributes } , textContent , ...children ) ;
103
155
return el ;
104
156
} catch ( error ) {
0 commit comments