In this part of our series, we want to show order_source in the order grid in Admin. We’re close, the value of order_source has now been stored in the sales_order_grid table, and now we want to see it.
The solution itself is relatively straight forward, just a few lines of well chosen xml:
DavidMann/OrderSource/view/adminhtml/ui_component/sales_order_grid.xml – make the new attribute available to the grid column UI component
1<listing xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance” xsi:noNamespaceSchemaLocation=”urn:magento:module:Magento_Ui:etc/ui_configuration.xsd”>
2<columns name=”sales_order_columns”>
3<column name=”order_source”>
4<argument name=”data” xsi:type=”array”>
5<item name=”config” xsi:type=”array”>
6<item name=”filter” xsi:type=”string”>text</item>
7<item name=”label” xsi:type=”string” translate=”true”>Order Source</item>
8</item>
9</argument>
10</column>
11</columns>
12</listing>
This along with other core sales order grid columns declared in vendor/magento/module-sales/view/adminhtml/ui_component/sales_order_grid.xml is then available ultimately to be displayed in the sales order grid with the UI ‘Listing’ Component and its child UI Component ‘Column’.
We’ll see a little later how the javascript including knockoutjs and the templates of these UI components gets the required values for the grid from a large JSON object partly derived from the xml above.
But before we look at the UI components, I want to make a couple of stops along the way.
As part of rendering the sales order grid page, Magento reads layout xml converting it to layout element objects. You can see that here:-
vendor/magento/framework//Model/Layout/Merge.php
773protected function _loadFileLayoutUpdatesXml()
774{
780foreach ($updateFiles as $file) {
786$fileXml = $this->_loadXmlString($fileStr);
821return $layoutXml;
822}
Where ‘$fileXml = $this->_loadXmlString($fileStr)’ calls this basic PHP function:-
vendor/magento/framework/View/Model/Layout/Merge.php
562protected function _loadXmlString($xmlString)
563{
564return simplexml_load_string($xmlString, \Magento\Framework\View\Layout\Element::class);
565}
In the case where $file = “/var/www/html/vendor/magento/module-sales/view/adminhtml/layout/sales_order_index.xml” in the foreach loop on line 780 of vendor/magento/framework/View/Model/Layout/Merge.php, then the xml content is this:-
vendor/magento/module-sales/view/adminhtml/layout/sales_order_index.xml
1<?xml version=”1.0″?>
2<!–
3/**
4* Copyright © Magento, Inc. All rights reserved.
5* See COPYING.txt for license details.
6*/
7–>
8<page xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance” xsi:noNamespaceSchemaLocation=”urn:magento:framework:View/Layout/etc/page_configuration.xsd”>
9<update handle=”styles”/>
10<body>
11<referenceContainer name=”content”>
12<uiComponent name=”sales_order_grid”/>
13</referenceContainer>
14</body>
15</page>
This layout positions the sales_order_grid UI component on the admin/sales/order/index page. After simplexml_load_string on line 564 of vendor/magento/framework/View/Model/Layout/Merge.php, the Layout Element looks like this:-
If you want to hit breakpoints in your IDE to examine this yourself, then remember to invoke ‘bin/magento cache:flush layout’ otherwise Magento will grab values from layout cache rather than files. And conditional breakpoints are useful here if you don’t want to cycle through every layout file!
Next comes the generation of layout blocks, and then Magento reads UI Component xml including our custom file DavidMann/OrderSource/view/adminhtml/ui_component/sales_order_grid.xml.
vendor/magento/framework/View/Layout/Reader/UiComponent.php
83public function interpret(Context $readerContext, Element $currentElement)
84{
100$config = $this->uiConfigFactory->create([‘componentName’ => $referenceName])->get($referenceName);
111}
This cycles through UI Component xml for the page and when $referenceName = “sales_order_grid” we get an array $config that looks like this:-
As well the core columns from vendor/magento/module-sales/view/adminhtml/ui_component/sales_order_grid.xml, you can see the created array contains the custom column order_source from DavidMann/OrderSource/view/adminhtml/ui_component/sales_order_grid.xml.
Now it gets interesting – the process of converting this array to JSON and making it accessible to UI Components in the sales order grid.
Look at this code:
vendor/magento/module-ui/TemplateEngine/Xhtml/Result.php
112public function __toString()
113{
124$this->appendLayoutConfiguration();
125$result = $this->compiler->postprocessing($this->template->__toString());
134}
Line 124 calls the method in Result.php – ‘appendLayoutConfiguration()’ which has two jobs. Firstly on lines 101 & 102, it JSON encodes the $config array created by vendor/magento/framework/View/Layout/Reader/UiComponent.php already. Recall that $config contains the combined sale order grid columns. Secondly, on line 104 it appends the resultant JSON and other text to the parent html template that displays the sales grid.
vendor/magento/module-ui/TemplateEngine/Xhtml/Result.php
99public function appendLayoutConfiguration()
100{
101$layoutConfiguration = $this->wrapContent(
102$this->jsonSerializer->serialize($this->structure->generate($this->component))
103);
104$this->template->append($layoutConfiguration);
105}
The $data input into the serialize method in vendor/magento/framework/Serialize/Serializer/JsonHexTag.php is the $config array.
vendor/magento/framework/Serialize/Serializer/JsonHexTag.php
27public function serialize($data): string
28{
29$result = json_encode($data, JSON_HEX_TAG);
30if (false === $result) {
31throw new \InvalidArgumentException(‘Unable to serialize value.’);
32}
33return $result;
34}
The output $result is $config as JSON. Here’s a snippet:
vendor/magento/framework/Serialize/Serializer/JsonHexTag.php – return $result
1{
2“types”:{
3“dataSource”:[
4
5],
6“bookmark”:{
7“extends”:”sales_order_grid”,
8“current”:{
9“columns”:{
10“increment_id”:{
11“visible”:true,
12“sorting”:false
13},
14“store_id”:{
15“visible”:true,
16“sorting”:false
17},
18“billing_name”:{
19“visible”:true,
20“sorting”:false
21},
22“shipping_name”:{
23“visible”:true,
24“sorting”:false
25},
26“base_grand_total”:{
27“visible”:false,
28“sorting”:false
29},
30“grand_total”:{
31“visible”:true,
32“sorting”:false
33},
34
35
36“created_at”:{
37“sorting”:”desc”,
38“visible”:true
39},
40“order_source”:{
41“sorting”:false,
42“visible”:true
43}
44},
45“displayMode”:”grid”,
All columns including order_source are now in JSON format. Looking back at line 101 in vendor/magento/module-ui/TemplateEngine/Xhtml/Result.php appendLayoutConfiguration(), we see a call to wrapContent after the JSON has been assembled.
vendor/magento/module-ui/TemplateEngine/Xhtml/Result.php
142protected function wrapContent($content)
143{
144return ‘<script type=”text/x-magento-init”><![CDATA[‘
145. ‘{“*”: {“Magento_Ui/js/core/app”: ‘ . str_replace([‘<![CDATA[‘, ‘]]>’], ”, $content) . ‘}}’
146. ‘]]></script>’;
147}
This method adds a string containing the script tag, ‘text/x-magento-init’ and ‘{“*”: {“Magento_Ui/js/core/app”:’ to the JSON string Magento has just built. When Magento encounters a ‘text/x-magento-init’ script tag with an ‘*’ attribute, it will
1) Initialize the specified RequireJS module
2) Call the function returned by that module, passing in the data object – which is the JSON object that Magento converted from the array.
But currently this string doesn’t exist in any HTML template so we want to see what happens next and where this string is placed. Line 104 in the appendLayoutConfiguration method of vendor/magento/module-ui/TemplateEngine/Xhtml/Result.php calls the append method in vendor/magento/framework/View/TemplateEngine/Xhtml/Template.php
vendor/magento/framework/View/TemplateEngine/Xhtml/Template.php
57public function append($content)
58{
59$ownerDocument= $this->templateNode->ownerDocument;
60$document = new \DOMDocument();
61$document->loadXml($content, LIBXML_PARSEHUGE);
62$this->templateNode->appendChild(
63$ownerDocument->importNode($document->documentElement, true)
64);
65}
The append method selects the template associated with this UI Component and appends the newly created string to the end of it.
This is the template with the appended script tag shown in orange. As we’ve seen the appended script string is created on the fly and is not part of the existing template as you’ll see if you go and take a look at the file in our core Magento code. Only the first part of the script tag contents are shown here due to its size!
vendor/magento/module-ui/view/base/ui_component/templates/listing/default.xhtml
8<div
9class=”admin__data-grid-outer-wrap”
10data-bind=”scope: ‘{{getName()}}.{{getName()}}'”
11xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance”
12xsi:noNamespaceSchemaLocation=”urn:magento:module:Magento_Ui:etc/ui_template.xsd”>
13<div data-role=”spinner” data-component=”{{getName()}}.{{getName()}}.{{spinner}}” class=”admin__data-grid-loading-mask”>
14<div class=”spinner”>
15<span/><span/><span/><span/><span/><span/><span/><span/>
16</div>
17</div>
18<!– ko template: getTemplate() –><!– /ko –>
19<script type=”text/x-magento-init”>{“*”: {“Magento_Ui/js/core/app”: {“types”:{“dataSource”:[],”text”:{“component”:…
20</div>
This template presents the entire sales order grid including header information and that includes search, filter and actions. With the JSON object made available by ‘text/x-magento-init’ this and any other HTML templates can access the data needed to populate the sales order grid. There are two templates underneath default.xhtml which pull back data into the grid. The first of these that we want to investigate is vendor/magento/module-ui/view/base/web/templates/grid/listing.html which the knockout binding on line 18 retrieves with:- ‘<!– ko template: getTemplate() –><!– /ko –>’.
vendor/magento/module-ui/view/base/web/templates/grid/listing.html
7<div class=”admin__data-grid-wrap” data-role=”grid-wrapper”>
8<table class=”data-grid” data-role=”grid”>
9<thead>
10<tr each=”data: getVisible(), as: ‘$col'” render=”getHeader()”/>
11</thead>
12<tbody>
13<tr class=”data-row” repeat=”foreach: rows, item: ‘$row'” css=”‘_odd-row’: $index % 2″>
14<td outerfasteach=”data: getVisible(), as: ‘$col'”
15css=”getFieldClass($row())” click=”getFieldHandler($row())” template=”getBody()”/>
16</tr>
17<tr ifnot=”hasData()” class=”data-grid-tr-no-data”>
18<td attr=”colspan: countVisible()” translate=”‘We couldn\’t find any records.'”/>
19</tr>
20</tbody>
21</table>
22</div>
This template is responsible for pulling back each row of the grid with the knockoutjs ‘foreach’ loop on line 13 and within that loop, on lines 14 & 15 pulling back each column of the row with the ‘outerfasteach’ loop. Within the inner ‘outerfasteachloop’ loop on line 18, you can see ‘template=”getBody”‘. This is a reference to the knockoutjs binding in listing.js – the javascript component of this Listing UI Component. On line 234 of listing.js , you can see the knockout binding, the binding name ‘getTemplate’ and the binding value which is a function.
vendor/magento/module-ui/view/base/web/js/grid/listing.js
234getTemplate: function () {
235var mode = this.displayModes[this.displayMode];
236return mode.template;
237},
This binding pulls back the template name which is vendor/magento/module-ui/view/base/web/templates/grid/cells/text.html. Within the template text.html you can see just one line of HTML comprising the knockoutjs getLabel.
vendor/magento/module-ui/view/base/web/templates/grid/cells/text.html
7<div class=”data-grid-cell-content” text=”$col.getLabel($row())”/>
And in the javascript for the column UI Component you can see this binding:
vendor/magento/module-ui/view/base/web/js/grid/columns/column.js
300getLabel: function (record) {
301return record[this.index];
302},
That takes us to the end of our journey. We’ve skirted over a couple of topics including knockoutjs, but we’ll end it there and provide more detail in a subsequent post.
In the next post, we’ll expose order_source in the API.