很多人第一次用 Odoo 的动态占位符,会觉得它不过是一个“插字段”的小工具:
- 选模型
- 选字段
- 插进正文
- 结束
但 addons/html_editor/static/src/others/dynamic_placeholder_plugin.js 说明,事情没这么简单。
官方真正处理的是一条完整链路:
- 当前编辑上下文到底对应哪个模型;
- 用户选出的字段链如何表达;
- datetime 字段要不要带时区格式化;
- 默认值怎么做兜底;
- 最终不是插纯文本,而是落成一个可执行的 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(),这里有两个非常关键的动作:
- 调 ORM 去拿
mail_get_partner_fields - 生成
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,而不是简单弹窗表单
插件依赖里有:
overlayselectionhistorydom
并用 DynamicPlaceholderPopover 打开选择器。
这体现了几个很关键的前端考虑:
1)插入动作必须贴着当前编辑上下文发生
用户是在正文里编辑,不是在后台配置页填表。popover 更适合“边看边插”的交互。
2)关闭后要回到编辑器焦点
onClose() 里会 selection.focusEditable(),说明官方很重视编辑连续性。
3)插入要进历史系统
onValidate() 最后会 history.addStep(),这保证动态占位符插入不是编辑器外的一次旁路改动。
这三点合起来说明:
动态占位符不是独立功能窗,而是编辑器命令系统的一部分。
七、为什么这个插件还注册进 powerbox 和 power buttons
源码把 openDynamicPlaceholder 同时挂进了:
user_commandspowerbox_categoriespowerbox_itemspower_buttons
这代表官方对动态占位符的定位并不是“高级功能藏在角落”,而是明确视为编辑器里的常用内容构造能力。
用户可以通过:
- 命令系统
- 空块旁边的 power button
- powerbox 快速入口
去触发同一个动作。
这说明 Odoo 前端在这里贯彻的是“同一语义,多入口触发”的思路,而不是每个入口各做一套逻辑。
八、这条链路为什么特别适合拿来理解 QWeb 与编辑器的边界
很多人会把 QWeb 和编辑器理解成两套互不相干的东西:
- 编辑器负责写 HTML
- 模板系统负责后面渲染
动态占位符刚好告诉你,这个边界其实是连着的:
- 编辑器里插入的不是死 HTML
- 而是带
t-out的模板节点 - 并且已经带上字段链、格式化、兜底逻辑
也就是说,编辑器不是模板系统的上游文本框,而是模板语法的可视化构造器。
这就是为什么这个插件虽小,却很能代表 Odoo 的前端设计哲学。
九、实战里最常犯的几个错
误区 1:把动态占位符做成纯文本标记
短期看着方便,长期会遇到转义、渲染、嵌套关系和默认值处理的一堆问题。
误区 2:忽略模型上下文
没有默认模型,字段链选择就会变成“看起来能选,实际落不稳”。
误区 3:datetime 直接原样输出
这样在跨时区通知、邮件、活动提醒里最容易把时间显示错。
误区 4:插入后不记历史步骤
用户一撤销,就会发现这个功能像“编辑器外面偷偷塞进来的东西”。
误区 5:默认值只做编辑态提示,不进入最终表达式逻辑
这样真实数据为空时,最终渲染页面仍然会出空洞。
十、结论
Odoo 动态占位符真正做的事,不是“给正文插个字段名”。
它做的是:
- 在当前模型上下文里选择字段链
- 针对 datetime 自动补显示逻辑
- 为空值加兜底
- 把结果生成为 QWeb 节点
- 再作为编辑器正式历史步骤插入内容流
所以更准确的理解应该是:
动态占位符是一个把业务字段链翻译成可执行 QWeb 片段的编辑器命令。
理解这一点后,你再做邮件模板、营销内容、自动化通知、网站个性化文案相关的二开,才会真正知道自己是在扩展什么:不是一个“插变量小按钮”,而是一条连接编辑体验与模板执行的前端桥梁。
DISCUSSION
评论区