前端

Odoo 动态占位符为什么不是“插个字段名”:HTML Editor、QWeb 表达式与默认值兜底链路讲透

在邮件模板、营销内容或可编辑 HTML 场景里,很多人把 Odoo 动态占位符理解成“往正文里塞一个字段路径”。但 html_editor 的 dynamic_placeholder_plugin.js 表明,它真正管理的是模型上下文、字段链选择、datetime 格式化、默认值兜底,以及最终落成 QWeb 节点的过程。

前端
进阶 开发者 1 分钟阅读
0 评论 0 点赞 0 收藏 5 阅读

很多人第一次用 Odoo 的动态占位符,会觉得它不过是一个“插字段”的小工具:

  • 选模型
  • 选字段
  • 插进正文
  • 结束

addons/html_editor/static/src/others/dynamic_placeholder_plugin.js 说明,事情没这么简单。

官方真正处理的是一条完整链路:

  1. 当前编辑上下文到底对应哪个模型;
  2. 用户选出的字段链如何表达;
  3. datetime 字段要不要带时区格式化;
  4. 默认值怎么做兜底;
  5. 最终不是插纯文本,而是落成一个可执行的 QWeb 节点。

所以动态占位符不是“文案编辑器的小糖果”,它其实站在 HTML Editor 与 QWeb 模板语义的交界处

一、动态占位符不是字符串替换,而是模板语义插入

onValidate() 里最关键的一段,是最终创建一个 T 节点:

  • 给它设 t-out
  • 如果有默认值,就把默认文本塞进去
  • 再通过 dom.insert(t) 插入编辑器

这说明 Odoo 插进去的不是一段“以后再正则替换”的标记串,而是一个真正带 QWeb 语义的节点。

这件事很重要,因为它决定了动态占位符的本质不是:

  • 存一段占位文本;
  • 渲染时做 replace。

而是:

编辑器阶段就把内容组织成模板语法的一部分。

这比字符串替换稳得多,也更符合 Odoo 模板体系。

二、为什么插件要先知道 defaultResModel

插件 setup 时会读 this.config.dynamicPlaceholderResModel,并提供 updateDphDefaultModel()

这说明动态占位符首先不是“任意字段浏览器”,而是在某个业务模型上下文里选字段链

这背后其实有很强的产品边界:

  • 当前在写邮件模板,模型可能是 crm.lead
  • 当前在写订单相关正文,模型可能是 sale.order
  • 当前在写联系人通知,模型可能是 res.partner

如果没有这个默认模型上下文,字段选择器根本不知道从哪个对象树开始走。

也正因为如此,源码在没有 resModel 时不是悄悄失败,而是直接通知:

你需要先选择模型,才能打开动态占位符选择器。

这说明模型上下文不是附加信息,而是动态占位符的前提条件。

三、字段链不是“一个字段”,而是 object.xxx.yyy.zzz 这样的访问路径

在非 datetime 情况下,源码会直接生成:

  • object.${chain}

这里的 chain 非常关键。它不是单字段名,而是用户从字段关系树里一路点出来的访问链。

这说明动态占位符真正支持的是:

  • 当前对象字段
  • 关联对象字段
  • 继续向下钻的关系字段

所以你看到的是“插一个变量”,底层其实是在构造QWeb 可执行访问路径

这比普通 CMS 的变量插入要强很多,因为它天然站在 Odoo 模型关系上。

四、datetime 单独处理,说明官方不把字段输出当成“一律原样打印”

fieldType === "datetime" 时,源码会走 _onValidateDatetime(),这里有两个非常关键的动作:

  1. 调 ORM 去拿 mail_get_partner_fields
  2. 生成 format_datetime(...) 表达式

这说明 datetime 字段在 Odoo 眼里不是“取值后直接输出”那么简单,而是带着显示语义:

  • 要不要按收件人时区显示
  • 没有关联伙伴时怎么回退
  • 默认值怎么接到表达式后面

如果有 partner fields,表达式会变成类似:

  • format_datetime(object.xxx, tz=object.partner_field.tz)

否则退回:

  • format_datetime(object.xxx)

这说明动态占位符不是在插“数据库值”,而是在插可执行显示逻辑

五、默认值不是前端占位文案,而是模板表达式的一部分兜底

很多人会把默认值理解成:“如果以后没数据,就在编辑器里先显示一点灰字提示。”

但源码的做法不是这样。

在 datetime 场景里,默认值会被拼成:

  • format_datetime(...) or 'fallback'

在普通场景里,则会把默认值作为节点文本留在 T 标签内部。

这说明默认值承担的不是纯视觉提示,而是渲染兜底策略

也就是说,Odoo 在设计动态占位符时,考虑的是:

  • 最终模板执行时可能没有数据;
  • 那页面、邮件或正文应该优雅退到什么内容。

这让动态占位符更接近“带兜底的模板表达式插入器”,而不是文案辅助器。

六、为什么要用 overlay / popover,而不是简单弹窗表单

插件依赖里有:

  • overlay
  • selection
  • history
  • dom

并用 DynamicPlaceholderPopover 打开选择器。

这体现了几个很关键的前端考虑:

1)插入动作必须贴着当前编辑上下文发生

用户是在正文里编辑,不是在后台配置页填表。popover 更适合“边看边插”的交互。

2)关闭后要回到编辑器焦点

onClose() 里会 selection.focusEditable(),说明官方很重视编辑连续性。

3)插入要进历史系统

onValidate() 最后会 history.addStep(),这保证动态占位符插入不是编辑器外的一次旁路改动。

这三点合起来说明:

动态占位符不是独立功能窗,而是编辑器命令系统的一部分。

七、为什么这个插件还注册进 powerbox 和 power buttons

源码把 openDynamicPlaceholder 同时挂进了:

  • user_commands
  • powerbox_categories
  • powerbox_items
  • power_buttons

这代表官方对动态占位符的定位并不是“高级功能藏在角落”,而是明确视为编辑器里的常用内容构造能力。

用户可以通过:

  • 命令系统
  • 空块旁边的 power button
  • powerbox 快速入口

去触发同一个动作。

这说明 Odoo 前端在这里贯彻的是“同一语义,多入口触发”的思路,而不是每个入口各做一套逻辑。

八、这条链路为什么特别适合拿来理解 QWeb 与编辑器的边界

很多人会把 QWeb 和编辑器理解成两套互不相干的东西:

  • 编辑器负责写 HTML
  • 模板系统负责后面渲染

动态占位符刚好告诉你,这个边界其实是连着的:

  • 编辑器里插入的不是死 HTML
  • 而是带 t-out 的模板节点
  • 并且已经带上字段链、格式化、兜底逻辑

也就是说,编辑器不是模板系统的上游文本框,而是模板语法的可视化构造器

这就是为什么这个插件虽小,却很能代表 Odoo 的前端设计哲学。

九、实战里最常犯的几个错

误区 1:把动态占位符做成纯文本标记

短期看着方便,长期会遇到转义、渲染、嵌套关系和默认值处理的一堆问题。

误区 2:忽略模型上下文

没有默认模型,字段链选择就会变成“看起来能选,实际落不稳”。

误区 3:datetime 直接原样输出

这样在跨时区通知、邮件、活动提醒里最容易把时间显示错。

误区 4:插入后不记历史步骤

用户一撤销,就会发现这个功能像“编辑器外面偷偷塞进来的东西”。

误区 5:默认值只做编辑态提示,不进入最终表达式逻辑

这样真实数据为空时,最终渲染页面仍然会出空洞。

十、结论

Odoo 动态占位符真正做的事,不是“给正文插个字段名”。

它做的是:

  • 在当前模型上下文里选择字段链
  • 针对 datetime 自动补显示逻辑
  • 为空值加兜底
  • 把结果生成为 QWeb 节点
  • 再作为编辑器正式历史步骤插入内容流

所以更准确的理解应该是:

动态占位符是一个把业务字段链翻译成可执行 QWeb 片段的编辑器命令。

理解这一点后,你再做邮件模板、营销内容、自动化通知、网站个性化文案相关的二开,才会真正知道自己是在扩展什么:不是一个“插变量小按钮”,而是一条连接编辑体验与模板执行的前端桥梁。

DISCUSSION

评论区

想参与讨论?先 登录 再发表评论。
还没有评论,你可以成为第一个留言的人。