-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathtemplate-103.html
430 lines (386 loc) · 20.5 KB
/
template-103.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
<h1 id="custom-formatter">Custom Formatter</h1>
<p>Since Grid utilizes a row virtualization technique, where the same cell is reused across different rows, you cannot simply just create any content and stop there. With row virtualization the custom content that you are trying to create or render will be reused in the different rows. So you have to create the content once and reuse it from the cache. Then, you need to bind data from Grid's data store to the content for each data update and scrolling.</p>
<p>The process for writing the formatter is usually something like the following code:</p>
<pre><code class="language-js">let formatter = function(e) {
let cell = e.cell;
// Cache checking part (Reuse the same element)
let element = cell.getContent();
if (!element) {
// Element creation part
element = document.createElement('tag-name');
// Apply common static settings (once per element creation) here
// This could be attributes, styles, event listeners, and etc.
element.style.color = "red";
}
// Element updating part (bind data to the UI element)
element.value = e.data; // IMPORTANT
// The below code is required. It does not post any significant performance impact
cell.setContent(element);
};
</code></pre>
<h2 id="defining-the-formatter">Defining the formatter</h2>
<p>The formatter can be specified as a function on the <code>binding</code> property of the column configuration object.</p>
<pre><code class="language-js">let configObj = {
...
columns: [
{
field: "Field 1",
binding: function(e) {
e.cell.setContent(e.data);
}
},
...
],
...
}
</code></pre>
<aside class="info"><p>For displaying simple text, you do not need to create a custom formatter, as Grid already has simple text as its default formatter.</p>
</aside><h3 id="common-form-and-structure">Common form and structure</h3>
<p><code>binding</code> method will be called multiple times during user scrolling. The key here is to create a DOM element only once, reuse the same element and update the data every time the method is called. </p>
<p>In general, the formatter will be constructed by the following steps:</p>
<ol>
<li>Retrieve existing content from the cell.</li>
<li>Create the content, if it does not exist. During the content creation, add static styling and event listeners as needed.</li>
<li>Modify the content according to any runtime changes. This includes data update and styling.</li>
<li>Set the content back to the cell.</li>
</ol>
<p>Below is the most common form of formatter. It is recommended to write most custom formatters in this form and structure. </p>
<pre><code class="language-js">let formatter = function(e){
let cell = e.cell;
// Step 1: Utilizing cache
let content = cell.getContent();
if (!content) { // Check if cache exists
// Step 2: Content creation
content = document.createElement("xxx-xxxx");
let subContent = document.createElement("yyyy");
subContent.style.color = "brown";
let subContent2 = document.createElement("zz-zzz");
subContent2.addEventListener("click", eventHandler);
content._myContent = subContent2;
content.appendChild(subContent);
content.appendChild(subContent2);
}
// Step 3: Data update
content._myContent.textContent = e.data;
// Step 4: Storing cache for later calls
cell.setContent(content);
};
</code></pre>
<h4 id="updating-content-example">Updating content example</h4>
<pre><code class="language-js">let goodFormatter = function(e){
let cell = e.cell;
let content = cell.getContent();
if (!content) {
content = document.createElement("div");
}
// This area will be executed for each data update and scrolling
content.textContent = e.data;
cell.setContent(content);
};
let badFormatter = function(e){
let cell = e.cell;
let content = cell.getContent();
if (!content) {
// This area will be executed once per content creation
content = document.createElement("div");
// Since content are reused on different rows,
// this causes incorrect display of content when a user start to scroll
content.textContent = e.data;
}
cell.setContent(content);
};
</code></pre>
<h4 id="adding-click-event-listener-example">Adding click event listener example</h4>
<pre><code class="language-js">let goodFormatter = function(e){
let cell = e.cell;
let content = cell.getContent();
if (!content) {
// This area will be executed once per content creation
content = document.createElement("button");
content.addEventListener("click", function(){ console.log("clicked") });
}
content.textContent = e.data;
cell.setContent(content);
};
let badFormatter = function(e){
let cell = e.cell;
let content = cell.getContent();
if (!content) {
content = document.createElement("button");
}
// This area will be executed multiple times in grid life cycle
// The event listeners will be accumulated over time,
// resulting in large memory consumption
content.addEventListener("click", function(){ console.log("clicked") });
content.textContent = e.data;
cell.setContent(content);
};
</code></pre>
<h3 id="setting-styles-in-the-formatter">Setting styles in the formatter</h3>
<p>When making any type of modification in the <code>binding</code> method, you must reset the value back because the same cell will be reused by different rows.</p>
<pre><code class="language-js">let formatter = function(e){
let cell = e.cell;
let data = e.data;
let content = cell.getContent();
if (!content) {
content = document.createElement("span");
content.style.color = "salmon"; // Static styling
}
// Dynamic styling
if(data > 0) {
content.style.backgroundColor = "green";
} else if(data < 0) {
content.style.backgroundColor = "red";
} else {
content.style.backgroundColor = ""; // Reset the background
}
cell.setStyle("fontSize", Math.abs(data) > 10 ? "1.5em" : ""); // Styles can be applied to either the cell content or the cell itself.
content.textContent = data;
cell.setContent(content);
};
</code></pre>
<h3 id="interactive-content">Interactive content</h3>
<p>States must be saved back to Grid after users make changes to the content. This is because Grid's row virtualization will reuse the same content during the scroll. Also note that user interaction does not happen during the data binding. User interaction is an asynchronous operation. So do not rely on closure variables or inline functions. </p>
<p>Contexts and values may be a different. You will need to resolve the position or context at runtime by using <a href="#/general-concept/event-listeners">getRelativePosition</a>.</p>
<pre><code class="language-js">let formatter = function(e){
let cell = e.cell;
let dropdown = cell.getContent();
if (!dropdown) {
dropdown = document.createElement("select");
dropdown.addEventListener("change", dropdownChangeHandler); // Handle user interaction
for(let i = 0; i < 3; ++i) {
let option = document.createElement("option");
option.value = i;
option.textContent = "Value " + i;
dropdown.appendChild(option);
}
}
dropdown.selectedIndex = e.data;
cell.setContent(dropdown);
};
let dropdownChangeHandler = function(e) {
let dropdown = e.currentTarget;
let selectedIndex = dropdown.selectedIndex;
let pos = grid.api.getRelativePosition(e);
let rowDef = grid.api.getRowDefinition(pos.rowIndex);
rowDef.setData("Field XXXX", selectedIndex); // It is important to set the new data
};
</code></pre>
<aside class="info"><p>Find out more information about event handling <a href="./rendering/formatter-event-handling">here</a>.</p>
</aside><h3 id="content-with-multiple-states">Content with multiple states</h3>
<p>Content with multiple states (for example, colors, disabled and invalid states) must have all states stored in the Grid. You can have as many columns' data or fields as we want in order to represent the content. </p>
<p>The example below shows input with multiple states depending on the data on other fields.</p>
<pre><code class="language-js">let formatter = function(e){
let cell = e.cell;
let rowData = e.rowData;
let inputElem = cell.getContent();
if (!inputElem) {
inputElem = document.createElement("input");
}
let state1 = rowData["someField"];
if(state1) {
inputElem.setAttribute("disabled", "");
} else {
inputElem.removeAttribute("disabled");
}
let state2 = rowData["anotherField"];
if(state2) {
inputElem.setAttribute("error", "");
} else {
inputElem.removeAttribute("error");
}
inputElem.value = e.data;
cell.setContent(inputElem);
};
</code></pre>
<aside class="info"><p>Fields and data are completely independent from column UIs. This means you do not have to create a column for a field or data to exist. You can have any number of fields and data regardless of number of the columns you have. Fields and data can also be added or removed separately from Grid's columns.</p>
</aside><h2 id="formatterbinding-method-parameters">Formatter/binding method parameters</h2>
<p>Event argument <code>binding</code> method has the following parameter list:</p>
<ul>
<li><em>data</em> : The data value corresponding to the field and row of the cell</li>
<li><em>cell</em> : The cell object that provides access to the DOM element of the cell</li>
<li><em>rowIndex</em> : Index of the current row of the cell being rendered</li>
<li><em>rowDef</em> : The row definition object</li>
<li><em>rowData</em> : The data object that stores data of the entire row</li>
<li><em>colIndex</em> : Index of the current column of the cell being rendered</li>
<li><em>secton</em> : The section object hosting the current column</li>
</ul>
<h2 id="understanding-row-virtualization">Understanding row virtualization</h2>
<p>The row virtualization technique is <strong>different from the lazy initialization</strong> technique, which creates more content as users scroll down through all the available rows. For example, suppose you have 10,000 rows and Grid's view port can show 20 rows. With row virtualization Grid will create content for around 24 rows (20 rows + buffer rows) throughout Grid's life cycle. With lazy initialization Grid will create content for 24 rows at first load. As users scroll down through the available rows, more and more content will be created. Eventually, all content will be created for all 10,000 rows when using the lazy initialization technique.</p>
<p>A web page will get slower as more content/elements are put into the DOM tree. So it's better to reuse the same element whenever possible.</p>
<h2 id="custom-formatter-example">Custom formatter example</h2>
<p>The below example shows some basic custom formatters in a virtualized grid:</p>
<code-sandbox hash="ba4d5fef"><pre><code class="language-css">efx-grid {
height: 200px;
}
</code></pre>
<pre><code class="language-html"><efx-grid id="grid"></efx-grid>
</code></pre>
<pre><code class="language-javascript">import { halo } from './theme-loader.js'; // This line is only required for demo purpose. It is not relevant for your application.
await halo(); // This line is only required for demo purpose. It is not relevant for your application.
/* ---------------------------------- Note ----------------------------------
DataGenerator, Formatters and extensions are exposed to global scope
in the bundle file to make it easier to create live examples.
Importing formatters and extensions is still required in your application.
Please see the document for further information.
---------------------------------------------------------------------------*/
let rangeBarFormatter = function(e) {
let cell = e.cell;
let bar = cell.getContent(); // Utilize caching
if(!bar) {
bar = document.createElement("div");
bar.style.height = "8px";
bar.style.position = "relative";
bar.style.backgroundColor = "#EA7D22";
let indi = bar.indi = document.createElement("div");
indi.style.position = "absolute";
indi.style.top = "0";
indi.style.left = "10px";
indi.style.width = "3px";
indi.style.height = "100%";
indi.style.backgroundColor = "white";
bar.appendChild(indi);
}
cell.setContent(bar);
bar.indi.style.left = (e.data * 100)+"%";
};
let capitalizer = function(e) {
e.cell.setContent(e.data.toString().toUpperCase());
};
let yearDisplay = function(e) {
let cell = e.cell;
cell.setStyle("color", "black");
cell.setStyle("background-color", "#EA7D22");
cell.setContent("Year " + e.data.getFullYear());
};
let fields = ["companyName", "market", "CF_LAST", "float_1", "ISODate"];
let records = DataGenerator.generateRecords(fields, { numRows: 40 });
let configObj = {
columns: [
{name: "Company", field: fields[0], binding: capitalizer},
{name: "Market", field: fields[1], width: 100},
{name: "Last", field: fields[2], width: 80},
{name: "Buy/Sell", field: fields[3], width: 100, binding: rangeBarFormatter},
{name: "IPO", field: fields[4], alienment: "center", binding: yearDisplay}
],
staticDataRows: records
};
let grid = document.getElementById("grid");
grid.config = configObj;
</code></pre>
</code-sandbox><h2 id="formatting-custom-content-with-text-formatting-extension">Formatting custom content with Text Formatting Extension</h2>
<p>When using Text Formatting Extension, by default, custom content will be overwritten due to the need for rendering a formatted text by the extension. To format the text inside the custom content and avoid the overriding, you will need to instruct the extension on how and where to do the formatting. See <a href="#/extensions/tr-grid-textformatting">this page</a> for more information.</p>
<h2 id="writing-a-good-formatter">Writing a good formatter</h2>
<p>Be mindful that the <code>binding</code> method will be executed repeatedly and multiple times during data updates and scrolling. Performance is crucial, so you should write the code with caution. See the following guidelines for writing a good formatter.</p>
<h3 id="use-predefined-formatters">Use predefined formatters</h3>
<p>The most common formatters are already prewritten for you. Predefined formatters are optimized and easy to use. See <a href="#/rendering/predefined-formatter">this page for more details</a>.</p>
<h3 id="avoid-using-innerhtml-and-innertext">Avoid using innerHTML and innerText</h3>
<p><strong>innerHTML</strong> and <strong>innerText</strong> from native elements involve text parsing and element creation, both of which are computationally expensive. You can improve performance by avoiding using them.</p>
<pre><code class="language-js">function binding1(e) {
let content = document.createElement("div"); // Unneccessary creation as it will be called and created multiple times
content.innerHTML = "<span>" + e.data + "</span><span>2</span>"; // Slow due to text parsing and element creations
e.cell.setContent(content);
}
// The above function can be re-writen as the following function
function binding2(e) {
let cell = e.cell;
let content = cell.getContent();
if(!content) {
content = document.createElement("div");
let span1 = content._span1 = document.createElement("span");
let span2 = document.createElement("span");
span2.textContent = 2;
content.appendChild(span1);
content.appendChild(span2);
}
content._span1.textContent = e.data;
cell.setContent(content);
}
</code></pre>
<h3 id="avoid-creating-an-element-for-simple-text">Avoid creating an element for simple text</h3>
<p><code>setContent</code> method is already optimized for simple text.</p>
<pre><code class="language-js">function binding1(e) {
let content = document.createElement("div"); // Unneccessary creation
content.textContent = e.data + " text";
e.cell.setContent(content);
}
// The above function can be re-writen as the following function
function binding2(e) {
e.cell.setContent(e.data + " text");
}
</code></pre>
<h3 id="reuse-the-same-content-to-avoid-element-creation">Reuse the same content to avoid element creation</h3>
<p>Bind method will be executed repeatedly and multiple times during data update and scrolling. Minimize element creation by using cache.</p>
<pre><code class="language-js">function binding1(e) {
let content = document.createElement("input"); // Slow
content.value = e.data;
e.cell.setContent(content);
}
// The above function can be re-writen as the following function
function binding2(e) {
let cell = e.cell;
let content = cell.getContent(); // Get previous content
if(!content) {
content = document.createElement("input");
}
content.value = e.data;
cell.setContent(content);
}
</code></pre>
<h3 id="store-element-in-a-variable-for-easy-access">Store element in a variable for easy access</h3>
<p>For complex structure content, it is faster and easier to use a variable for referencing rather than navigating through a DOM tree.</p>
<pre><code class="language-js">function binding1(e) {
let cell = e.cell;
let content = cell.getContent();
if(!content) {
content = document.createElement("div");
let span1 = document.createElement("span");
let span2 = document.createElement("span");
content.appendChild(span1);
content.appendChild(span2);
}
cell.setContent(content);
let spans = content.getElementsByTagName("span"); // Slow
spans[0].textContent = e.data % 5;
spans[1].textContent = e.data % 3;
}
// The above function can be re-writen as the following function
function binding2(e) {
let cell = e.cell;
let content = cell.getContent();
if(!content) {
content = document.createElement("div");
let span1 = content._span1 = document.createElement("span");
let span2 = content._span2 = document.createElement("span");
content.appendChild(span1);
content.appendChild(span2);
}
content._span1.textContent = e.data % 5;
content._span2.textContent = e.data % 3;
cell.setContent(content);
}
</code></pre>
<aside class="info"><p>Cell can contain only one top level node/element (such as, single content). However, the content element can have any number of elements or nested elements.</p>
</aside><h3 id="use-rowdefinition-object-for-asynchronous-operation">Use RowDefinition object for asynchronous operation</h3>
<p>When dealing with asynchronous operations such as server request and response, it is important to use the correct context and data. Row index, cell element, and page can all be changed during the waiting time. Sorting, filtering, grouping, and pagination can also affect row order. So the most reliable way to identify Grid's row is to use RowDefinition object. </p>
<pre><code class="language-js">function binding1(e) {
let cell = e.cell;
let content = cell.getContent();
if(!content) {
content = document.createElement("button");
content.addEventListener("click", onRequestingData);
}
cell.setContent(content);
}
function onRequestingData(e) {
let pos = grid.api.getRelativePosition(e);
let rowDef = grid.api.getRowDefinition(pos.rowIndex);
requestServerData({}, onServerResponse.bind(null, rowDef));
}
function onServerResponse(rowDef, resp) {
// do something
}
</code></pre>
<br>
<br>
<br>