Dữ liệu & Biến thể
Docs/Dữ liệu & biến thể

Cú pháp Handlebars

Đưa dữ liệu từ params và props vào template bằng Handlebars, từ hiển thị văn bản đến tạo bảng và nội dung có điều kiện.

8 phút đọc15 phần
Trong bài này

Khi thiết kế template, bạn thường cần đưa dữ liệu vào nội dung: tên người nhận lấy từ params, tiêu đề lấy từ props, hoặc danh sách sản phẩm được trình bày thành từng dòng trong bảng. Để kết nối những giá trị này với HTML/CSS, Tembuilder sử dụng Handlebars — một cú pháp cho phép bạn đánh dấu nơi dữ liệu sẽ xuất hiện và cách nội dung được hiển thị.

Chẳng hạn, bạn viết {{ params.recipient }} tại vị trí cần hiển thị tên người nhận. Khi xem trước hoặc tạo PDF, Tembuilder sẽ đọc giá trị tương ứng và thay biểu thức đó bằng nội dung thực tế. Nhờ vậy, cùng một thiết kế có thể dùng lại cho nhiều bộ dữ liệu.

Bài này sẽ hướng dẫn bạn từng bước: đọc giá trị từ Parameters và Props, hiển thị nội dung theo điều kiện, lặp danh sách, rồi sử dụng những cú pháp nâng cao khi template cần xử lý cấu trúc dữ liệu phức tạp hơn.

1. Từ template đến tài liệu

Bắt đầu với một ví dụ đơn giản: bạn đã tạo prop heading có giá trị Hóa đơn cho section và khai báo parameter recipient có giá trị Tembuilder. Trong HTML của section, đặt mỗi giá trị vào vị trí muốn hiển thị bằng cú pháp {{ ... }}:

<h1>{{ props.heading }}</h1>
<p>{{ params.recipient }}</p>

Kết quả HTML:

<h1>Hóa đơn</h1>
<p>Tembuilder</p>
{{ props.heading }} → Hóa đơn{{ params.recipient }} → Tembuilder
→
TembuilderHóa đơn

Tembuilder

Biểu thức được thay bằng dữ liệu trước bước phân trang PDF.

2. Chọn đúng đường dẫn dữ liệu

Đường dẫn Giá trị được đọc
{{ params.recipient }} Dữ liệu của lần chạy, sau khi áp dụng giá trị mặc định.
{{ props.heading }} Props của phần đang biên dịch. Trong section, prop của section ghi đè Global prop trùng tên.
{{ global.props.company }} Global prop, kể cả khi section có prop cùng tên.
{{ @root.params.recipient }} Dữ liệu lần chạy khi đang ở trong vòng lặp hoặc khối with.

Nên viết rõ params. thay vì tên parameter đứng riêng. Tên parameter cũng được đưa ra scope gốc để tương thích, nhưng props, params, global là các tên dành cho context của platform.

Mỗi section, Header và Footer có context biên dịch riêng. @root là gốc của lần biên dịch đó, không phải section khác. Variant quyết định giá trị prop override trước khi biểu thức được chạy.

3. Text, Number, Boolean và giá trị thiếu

Đây là dữ liệu lần chạy: nhập trực tiếp các trường trong Run, không bọc thêm một lớp params. Khai báo kiểu tương ứng ở Parameters → Schema.

{
  "recipient": "Tembuilder & Tembuilder",
  "quantity": 2,
  "balance": 0,
  "paid": false,
  "totalLabel": "250.000 VND"
}
<p>{{ params.recipient }} · {{ params.quantity }}</p>
<p>{{ params.totalLabel }}</p>
{{#if params.paid}}Đã thanh toán{{else}}Chờ thanh toán{{/if}}

Text hiển thị thành văn bản, Number hiển thị giá trị số; in trực tiếp Boolean cho kết quả true hoặc false. Dùng điều kiện để đổi thành nhãn dễ đọc. Giữ 2 là số và false là Boolean; "2" và "false" là chuỗi.

Giá trị không tìm thấy thường được in thành rỗng. Điều này không thay thế việc khai báo trường bắt buộc hoặc kiểm tra payload. Sai cú pháp hay gọi helper không tồn tại sẽ dừng biên dịch.

Handlebars không tự tính quantity * price, gọi .toFixed(2) hay định dạng ngày, tiền tệ. Hãy tính tổng và chuẩn bị chuỗi hiển thị trước khi render. {{ params.quantity + 1 }} và {{ params.total.toFixed(2) }} không phải biểu thức JavaScript được hỗ trợ.

4. Object và tên thuộc tính

{
  "recipient": { "name": "Tembuilder", "address": { "city": "Hà Nội" } },
  "labels": { "invoice_title": "Hóa đơn", "postal_code": "10000" }
}
<p>{{ params.recipient.name }} · {{ params.recipient.address.city }}</p>
<h2>{{ params.labels.invoice_title }}</h2>
<p>{{ params.labels.postal_code }}</p>

Dùng tên trường như recipientName hoặc postal_code. Editor thay dấu cách, dấu gạch ngang và ký tự ngoài chữ cái, chữ số, dấu gạch dưới bằng _; ký tự đầu cần là chữ cái hoặc dấu gạch dưới. Với schema import có sẵn key như postal code, Handlebars đọc được bằng {{ params.labels.[postal code] }}. Cú pháp này không thay đổi quy tắc đặt tên trong editor. Muốn hiển thị Object, hãy đọc từng trường hoặc lặp các thuộc tính; in cả Object không phải cách chuyển nó thành JSON.

5. Điều kiện: if, else, unless

{{#if params.paid}}
  <p>Đã nhận thanh toán</p>
{{else if params.overdue}}
  <p>Quá hạn thanh toán</p>
{{else}}
  <p>Chờ thanh toán</p>
{{/if}}
{{#unless params.paid}}<p>Vui lòng thanh toán trước hạn.</p>{{/unless}}
{{#if params.balance includeZero=true}}<p>Số dư: {{ params.balance }}</p>{{/if}}

if chọn nhánh âm khi gặp false, null, giá trị thiếu, chuỗi rỗng, số không hoặc array rỗng. includeZero=true cho số không đi vào nhánh dương. Object rỗng và chuỗi "false" đi vào nhánh dương. unless đảo điều kiện. if không đổi context hiện tại.

Platform không có sẵn helper eq, gt, and, or. Hãy tính Boolean như overdue trong dữ liệu đầu vào thay vì viết {{#if params.balance > 0}}.

6. Array: each và danh sách rỗng

{
  "items": [{ "name": "Thiết kế", "quantity": 2 }, { "name": "Rà soát", "quantity": 1 }],
  "tags": ["Bản nháp", "Nội bộ"]
}
<table>
  <thead><tr><th>Mục</th><th>Số lượng</th></tr></thead>
  <tbody>
    {{#each params.items}}
      <tr><td>{{ this.name }}</td><td>{{ this.quantity }}</td></tr>
    {{else}}
      <tr><td colspan="2">Chưa có mục nào</td></tr>
    {{/each}}
  </tbody>
</table>
{{#each params.tags}}<span>{{ this }}</span>{{/each}}

this là phần tử hiện tại: Object đối với items, chuỗi đối với tags. Có thể đọc trực tiếp phần tử đầu bằng {{ params.items.[0].name }}. Với bảng dài, xem hướng dẫn phân trang.

{{#each params.items}} {{ this.name }} · {{ this.quantity }}{{else}} Chưa có mục nào {{/each}}
MụcSố lượng
Thiết kế2
Rà soát1
Một phần tử → một dòng. Danh sách rỗng → nhánh else.

7. Metadata của vòng lặp và lặp Object

Biểu thức Ý nghĩa bên trong each
@index Vị trí phần tử array, bắt đầu từ không.
@first, @last Cờ phần tử đầu/cuối array.
@key Tên thuộc tính khi lặp Object.
{{#each params.tags}}
  {{#if @first}}<strong>Nhãn:</strong>{{/if}}
  {{ this }}{{#unless @last}}, {{/unless}}
{{/each}}
{{#each params.recipient}}
  <p>{{ @key }}: {{ this }}</p>
{{/each}}

Không dùng @index + 1 để đánh số thứ tự. Nếu cần số bắt đầu từ một, truyền thêm trường rowNumber cho từng phần tử.

8. Context: with, đường dẫn cha và root

{{#with params.recipient}}
  <p>{{ name }}</p>
{{else}}
  <p>Chưa có người nhận</p>
{{/with}}
{{#each params.items}}
  <p>{{ this.name }} — {{ ../props.heading }}</p>
  <small>{{ @root.global.props.company }}</small>
{{/each}}

with và each đổi context. ../ quay về context cha; @root giúp đọc scope gốc ổn định qua nhiều vòng lặp. Thêm một khối if không đòi hỏi thêm một lớp ../.

Alias giúp đọc danh sách lồng nhau dễ hơn. Trong ví dụ sau, khai báo groups là array Object, mỗi phần tử có name và array items:

{{#each params.groups as |group groupIndex|}}
  <h2>{{ group.name }}</h2>
  {{#each group.items as |item itemIndex|}}
    <p>{{ groupIndex }}.{{ itemIndex }} — {{ item.name }}</p>
  {{/each}}
{{/each}}

9. Lookup động và biểu thức lồng nhau

Dùng lookup khi key được truyền qua dữ liệu. Khai báo labels là Object và selectedKey là Text; với { "labels": { "approved": "Đã duyệt" }, "selectedKey": "approved" }:

<p>{{ lookup params.labels params.selectedKey }}</p>
{{#with (lookup params.labels params.selectedKey)}}<strong>{{ this }}</strong>{{/with}}

Dấu ngoặc tròn truyền kết quả helper vào helper khác, không chạy JavaScript tùy ý. Đối số có dấu nháy là chuỗi cố định; đường dẫn không có nháy đọc dữ liệu. includeZero=true là tùy chọn có tên của helper, không phải phép gán vào dữ liệu đầu vào.

10. Escape HTML và giá trị CSS

<p>{{ params.recipient }}</p>

Khi recipient là Tembuilder & Tembuilder, mã HTML chứa Tembuilder &amp; Tembuilder và tài liệu hiển thị Tembuilder & Tembuilder. Giữ hai cặp ngoặc cho dữ liệu thông thường.

{{{ params.trustedHtml }}} và {{& params.trustedHtml }} chèn HTML không escape. Chỉ dùng cho markup đáng tin cậy, đã được làm sạch và chủ đích tạo thành phần tử tài liệu. Escape không thay thế kiểm tra URL hoặc CSS: cần kiểm tra giá trị đưa vào href, src hay style.

Trong trình sửa CSS, có thể đọc một Color prop được kiểm soát:

h1 { color: {{ props.accent }}; }

Đặt accent là màu hợp lệ như #2563eb. Handlebars thay giá trị; kết quả vẫn phải là CSS hợp lệ.

11. Chú thích, khoảng trắng và ngoặc nguyên văn

{{! Ghi chú nội bộ, không có trong kết quả }}
{{!-- Ghi chú dài có thể chứa {{ biểu thức }} --}}
<span>{{ params.recipient ~}}</span>

~ cạnh dấu phân cách xóa khoảng trắng liền kề ở phía đó. Dùng cẩn thận khi các từ cần khoảng cách. Handlebars loại chú thích của nó khi biên dịch. Tembuilder tiếp tục loại chú thích HTML ở bước làm sạch, nên cả hai loại đều không còn trong HTML cuối cùng của tài liệu.

Để hiển thị nguyên một biểu thức thay vì tính nó:

<p>\{{ params.recipient }}</p>

Tài liệu hiển thị {{ params.recipient }}. Raw block ({{{{raw}}}}…{{{{/raw}}}}) cần block helper đã đăng ký; platform không cung cấp helper raw.

12. Partial và các tính năng nâng cao khác

Inline partial tái sử dụng markup bên trong cùng một nguồn được biên dịch:

{{#*inline "recipientCard"}}
  <p>{{ name }}</p>
{{/inline}}
{{> recipientCard params.recipient}}

Định nghĩa này không tự chia sẻ qua các file section, Header hay Footer. {{> sharedCard}} cần có định nghĩa tương ứng; Tembuilder không có nơi đăng ký global partial hoặc JavaScript helper tùy ý cho người dùng.

Handlebars còn hỗ trợ tham số partial, chọn partial động, partial block và custom block helper. Đây là khả năng của ngôn ngữ, không đồng nghĩa platform cung cấp giao diện đăng ký tương ứng. Không chèn registerHelper, registerPartial hoặc script vào HTML để cấu hình bước biên dịch.

Helper có sẵn log ghi ra log của môi trường thực thi, không in vào PDF. Nên kiểm tra kết quả bằng Preview và Run; tránh log dữ liệu nhạy cảm. Không cần decorator hay truy cập prototype/phương thức để viết tài liệu: dùng dữ liệu JSON thuần và các helper đã mô tả.

13. Ví dụ section hoàn chỉnh

Tạo prop heading = "Hóa đơn" cho section và Global prop company = "Tembuilder". Khai báo recipient (Object có name: Text), items (Array Object có name: Text và quantity: Number), paid (Boolean), totalLabel (Text). Chạy với dữ liệu:

{
  "recipient": { "name": "Tembuilder" },
  "items": [{ "name": "Thiết kế", "quantity": 2 }],
  "paid": false,
  "totalLabel": "250.000 VND"
}

Dán vào HTML của section:

<h1>{{ props.heading }}</h1>
<p>{{ global.props.company }} → {{ params.recipient.name }}</p>
<ul>
  {{#each params.items}}
    <li>{{ this.name }} × {{ this.quantity }}</li>
  {{else}}
    <li>Chưa có mục nào</li>
  {{/each}}
</ul>
<p>Tổng: {{ params.totalLabel }}</p>
<p>{{#if params.paid}}Đã thanh toán{{else}}Chờ thanh toán{{/if}}</p>

Nội dung mong đợi: Hóa đơn, Tembuilder → Tembuilder, Thiết kế × 2, Tổng: 250.000 VND, Chờ thanh toán. Thử items rỗng và paid: true để kiểm tra cả hai nhánh thay thế trong Preview/Run.

14. Xử lý lỗi và quy tắc cho AI

Hiện tượng Cần kiểm tra
Giá trị rỗng Tên trường, kiểu đã khai báo, payload, variant đang chọn và context.
Sai giá trị trong vòng lặp Dùng this, alias, ../ hoặc @root để chọn đúng scope.
Số không bị ẩn Dùng includeZero=true nếu không là giá trị hợp lệ cần hiển thị.
Điều kiện đi vào nhánh không mong muốn Kiểm tra chuỗi "false" và Boolean false.
Lỗi biên dịch Đóng đúng tên từng block; bỏ toán tử JavaScript và helper/partial chưa có.
[object Object] Đọc từng trường hoặc lặp thuộc tính thay vì in cả Object.

Khi AI viết template: giữ đúng tên và kiểu trường, dùng namespace rõ ràng, tính logic nghiệp vụ trước khi render, có nhánh cho danh sách rỗng, giữ escape cho giá trị thường và kiểm tra cả dữ liệu có phần tử lẫn dữ liệu rỗng. Một giá trị trống không chứng minh binding thành công. Xem thêm lỗi thường gặp.

15. Tài liệu Handlebars chính thức

Xem hướng dẫn Handlebars chính thức để tìm hiểu toàn bộ ngôn ngữ. Nếu ví dụ upstream yêu cầu đăng ký hoặc cấu hình runtime phía host, hãy áp dụng phạm vi hỗ trợ của Tembuilder đã nêu ở trên.

Tìm tài liệu