Talking to the select lots modal from a document template
When an order type has a document template set for lot selection, the Select lots modal shows that template in a column beside the lot table. The template is an ordinary HTML document template, rendered for the order and loaded in an iframe.
The template and the modal can send each other messages:
- The modal tells the template when lot selections have been saved, so the template can update what it shows.
- The template asks the modal to reload the lot lines of one SKU, several SKUs or all of them, for example after the template changed the order itself.
Saving in the modal does not reload the template. The template receives a message and decides how to react: ignore it, update part of the page, or reload itself.
Note: The template is never reloaded behind your back, so anything the user has scrolled to, expanded or typed into the template stays as it is.
Before you start
You need:
- The powerpack (the document templates extension) on the team.
- An active document template with model
Orderand output typeHTML. - That template chosen in the order type’s settings, under Lot lines, outbound → Document template shown beside the lot selection. The setting only appears with the powerpack, and only lists active HTML templates on the Order model.
Open an order of that type, go to the outbound lots and click Select lots. The template appears on the right.
How the messages work
The two sides use the browser’s standard
window.postMessage
API. The template is served from the same address as tracezilla itself, so
both sides can check that a message really came from the other.
1
2
3
4
5
6
7
8
┌──────────── Select lots modal ────────────┬──── Template (iframe) ────┐
│ │ │
│ user saves lot selections ──────────────┼─▶ tracezilla:select-lots: │
│ │ saved │
│ │ │
│ reloads lot lines of the ◀──────────────┼── tracezilla:select-lots: │
│ requested SKUs │ refresh │
└───────────────────────────────────────────┴───────────────────────────┘
Every message is a plain object with a type field. Messages of any other
type are ignored by both sides.
Message reference
tracezilla:select-lots:saved (modal → template)
Sent after the modal has saved, both when lot selections are saved and after a barcode scan.
| Field | Type | Description |
|---|---|---|
type |
string | Always tracezilla:select-lots:saved. |
order_id |
string | ID of the order. |
action |
string | select_lots when lot selections were saved, barcode after a barcode scan. |
post_save_action |
string or null |
What ran together with the save: create_picking_list, create_delivery_note, mark_picked, mark_dispatched, reopen, or null for a plain save. |
sku_ids |
array of strings, or null |
SKUs in the saved selection. null means the modal doesn’t know which SKUs changed. Treat it as “any SKU may have changed”. |
lot_ids |
array of strings | Lots in the saved selection. Empty after a barcode scan. |
Example:
1
2
3
4
5
6
7
8
{
"type": "tracezilla:select-lots:saved",
"order_id": "9b2c6f0e-4a1d-4c5e-9f7a-2d3e4f5a6b7c",
"action": "select_lots",
"post_save_action": null,
"sku_ids": ["be3233c4-c0a2-4f47-a62a-303492c20ba6"],
"lot_ids": ["0f1e2d3c-4b5a-6978-8796-a5b4c3d2e1f0"]
}
Warning: A barcode scan always sends
sku_ids: null, because the scan response doesn’t say which SKU was hit.
tracezilla:select-lots:refresh (template → modal)
Asks the modal to reload lot lines from the server.
| Field | Type | Description |
|---|---|---|
type |
string | Always tracezilla:select-lots:refresh. |
sku_id |
string | Optional. Reload this one SKU. |
sku_ids |
array of strings | Optional. Reload these SKUs. Takes precedence over sku_id. |
Leave out both sku_id and sku_ids to reload every SKU.
What the modal does with it:
- Reloads only the requested SKUs. Other SKUs, including their unsaved quantities, are left alone.
- Discards unsaved quantities on the SKUs it reloads. If the user has typed quantities for a SKU and not saved yet, a refresh of that SKU throws them away. Only ask for a refresh when the order really changed.
- Ignores SKUs outside its scope. When the modal was opened for a single SKU, requests for other SKUs are ignored.
- Needs a pickup location and date on the order to reload single SKUs. Without them, a request for specific SKUs does nothing. A request for all SKUs still works.
- Reloads the order’s line table when it closes, so the order view behind the modal shows the change too.
Receiving: react to a save
Add a script to the template’s body. Always check both where the message
came from and its type before acting on it.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script>
window.addEventListener("message", function (evt) {
// Only accept messages from tracezilla, sent by the window around us.
if (evt.origin !== window.location.origin || evt.source !== window.parent) {
return;
}
var message = evt.data;
if (!message || message.type !== "tracezilla:select-lots:saved") {
return;
}
// Simplest possible reaction: render the template again.
window.location.reload();
});
</script>
Reloading the whole template is fine to start with. To keep the user’s place in a long template, update only the SKUs that changed instead. See Updating only the changed SKUs.
Sending: ask the modal to reload
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<script>
function refreshLotSelection(skuIds) {
// Opened on its own (not inside the modal), there's no one to tell.
if (window.parent === window) {
return;
}
var message = { type: "tracezilla:select-lots:refresh" };
if (skuIds && skuIds.length > 0) {
message.sku_ids = skuIds;
}
window.parent.postMessage(message, window.location.origin);
}
// One SKU: refreshLotSelection(['be3233c4-c0a2-4f47-a62a-303492c20ba6'])
// All SKUs: refreshLotSelection()
</script>
Always pass window.location.origin as the second argument. It makes sure
the message only goes to tracezilla, never to another site.
Updating only the changed SKUs
The template is rendered on the server by Liquid, so the easiest way to get fresh data is to ask the server for the same template again and swap in the parts that changed. The template’s own address already works for that.
- Wrap each SKU’s section in an element with a
data-sku-idattribute. - When a save message arrives, fetch the template again in the background.
- Replace the sections of the SKUs in
sku_ids, or all of them whensku_idsisnull.
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
<div data-sku-id="{{ sku_id }}">
<!-- whatever the template shows for this SKU -->
</div>
<script>
function replaceSkuSections(skuIds) {
fetch(window.location.href, { credentials: "same-origin" })
.then(function (response) {
return response.text();
})
.then(function (html) {
var fresh = new DOMParser().parseFromString(html, "text/html");
document.querySelectorAll("[data-sku-id]").forEach(function (section) {
var skuId = section.getAttribute("data-sku-id");
if (skuIds !== null && skuIds.indexOf(skuId) === -1) {
return;
}
var replacement = fresh.querySelector(
'[data-sku-id="' + skuId + '"]',
);
if (replacement) {
section.replaceWith(replacement);
} else {
// The SKU is no longer on the order.
section.remove();
}
});
});
}
window.addEventListener("message", function (evt) {
if (evt.origin !== window.location.origin || evt.source !== window.parent) {
return;
}
if (!evt.data || evt.data.type !== "tracezilla:select-lots:saved") {
return;
}
replaceSkuSections(evt.data.sku_ids);
});
</script>
In the first line, `` stands for whichever Liquid variable holds the SKU ID where your template loops over its SKUs. Use the SKU’s ID, not its SKU code: the messages always carry IDs.
Note: SKUs newly added to the order have no section on the page yet, so they won’t appear with this approach. Fall back to
window.location.reload()whenfreshcontainsdata-sku-idvalues the page doesn’t have.
Complete example
A minimal template that lists the order’s SKUs, updates them after every save, and has a button that asks the modal to reload one SKU.
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
<h3>Order {{ order.prefixed_number }}</h3>
<!-- Loop over your SKU data here, one section per SKU. -->
<div data-sku-id="{{ sku_id }}">
<strong>{{ sku_code }}</strong>
<button type="button" onclick="refreshLotSelection(['{{ sku_id }}'])">
Reload in lot selection
</button>
</div>
<script>
var embedded = window.parent !== window;
function refreshLotSelection(skuIds) {
if (!embedded) {
return;
}
var message = { type: "tracezilla:select-lots:refresh" };
if (skuIds && skuIds.length > 0) {
message.sku_ids = skuIds;
}
window.parent.postMessage(message, window.location.origin);
}
function replaceSkuSections(skuIds) {
fetch(window.location.href, { credentials: "same-origin" })
.then(function (response) {
return response.text();
})
.then(function (html) {
var fresh = new DOMParser().parseFromString(html, "text/html");
document.querySelectorAll("[data-sku-id]").forEach(function (section) {
var skuId = section.getAttribute("data-sku-id");
if (skuIds !== null && skuIds.indexOf(skuId) === -1) {
return;
}
var replacement = fresh.querySelector(
'[data-sku-id="' + skuId + '"]',
);
if (replacement) {
section.replaceWith(replacement);
} else {
section.remove();
}
});
});
}
window.addEventListener("message", function (evt) {
if (evt.origin !== window.location.origin || evt.source !== window.parent) {
return;
}
var message = evt.data;
if (!message || message.type !== "tracezilla:select-lots:saved") {
return;
}
replaceSkuSections(message.sku_ids);
});
</script>
Testing
- Open it in the modal. Messages only flow when the template is shown
inside the Select lots modal. Opened in its own tab, the template has no
parent to talk to. The
window.parent === windowcheck makes sending a no-op there. -
Watch the messages. In the browser’s developer tools, switch the console to the iframe’s context and add a temporary listener:
1 2 3
window.addEventListener("message", (evt) => console.log(evt.origin, evt.data), );
Save a lot selection in the modal and the save message is logged.
-
Send a refresh by hand. In the iframe’s console context, run:
1 2 3 4
window.parent.postMessage( { type: "tracezilla:select-lots:refresh" }, window.location.origin, );
The modal reloads its lot lines.
Troubleshooting
| Problem | Likely cause |
|---|---|
| The template doesn’t show in the modal. | The setting is empty, the template isn’t active, it isn’t an HTML template on the Order model, or the team doesn’t have the powerpack. |
| The template never receives a save message. | The listener checks evt.source against something other than window.parent, or compares type with a typo. Log every message to check. |
| A refresh request does nothing. | type has a typo, the message was sent without window.location.origin, or the order has no pickup location or date (needed to reload specific SKUs). |
| A refresh request for one SKU does nothing. | The modal was opened for a different single SKU and ignores others. |
| The user’s typed quantities disappeared. | The template asked for a refresh of that SKU. A refresh discards unsaved quantities on the SKUs it reloads. |
sku_ids is null. |
The save came from a barcode scan. Treat it as “any SKU may have changed”. |
See also
The quick edit modal for outbound order lines works the same way, with its own message types:
tracezilla:quick-edit:saved(modal → template). Also hasfailed_sku_idsandlines, with each saved line’s quantity, price, free units, line number, line text and discount.tracezilla:quick-edit:refresh(template → modal), with the samesku_idandsku_idsfields. Rows with unsaved edits are never overwritten by a refresh.
Its template is chosen in the order type’s settings under Document template shown beside the quick edit table.