
GrapesJS Trait Manager 完全指南组件设置面板的建模、定制与深度集成【免费下载链接】grapesjsFree and Open source Web Builder Framework. Next generation tool for building templates without coding项目地址: https://gitcode.com/GitHub_Trending/gr/grapesjsTrait特性是 GrapesJS 中描述组件参数与行为的核心机制用户在使用可视化编辑器时看到的组件设置Settings面板本质上就是当前选中组件的 Trait 列表。本文围绕 GrapesJS 官方文档 docs/modules/Traits.md 展开系统讲解如何为自定义组件定义 Trait、如何与组件属性双向绑定、如何使用内置的六种 Trait 类型、如何在运行时动态增删改 Trait以及如何通过定义新 Trait 类型甚至完全自定义 Trait Manager UI 来满足高级场景。文中所有示例均可在当前仓库源码中找到对应实现读者学完后应能独立完成从组件建模到自定义设置面板的完整闭环。适用版本本指南针对 GrapesJS v0.21.9 及以上版本当前仓库 packages/core 即为最新源码。建议先阅读 Components 模块文档 以理解组件模型基础。什么是 Trait在 GrapesJS 中Trait 定义了组件的不同参数和行为。用户通常把 Trait 视为组件的设置Settings。Trait 最常见的用途是自定义元素属性例如为input设置placeholder也可以绑定到组件的属性property并响应其变化。从源码结构看Trait 模型定义在 packages/core/src/trait_manager/model/Trait.ts它继承自统一的 Model 基类。默认情况下 Trait 值会写入组件属性attributes开启changeProp后则写入组件属性property这一行为在Trait.getTargetValue()与Trait.setTargetValue()两个方法中体现Trait.ts。注意默认所有组件都自带id和title两个 Trait。这个默认值定义在 Component.ts 的defaults中traits: [id, title]。因此选中任意组件并打开 Settings 面板都会看到这两个内置 Trait。为组件添加 Trait通常你是在定义新的自定义组件或扩展已有组件时声明 Trait。下面以让input元素变得更可配置为例演示。基础定义通过editor.Components.addType注册input组件类型并在model.defaults.traits数组中声明 Traiteditor.Components.addType(input, { isComponent: (el) el.tagName INPUT, model: { defaults: { traits: [ // 字符串会自动转换为 text 类型 name, // 等同于: { type: text, name: name } placeholder, { type: select, // Trait 的类型 name: type, // (必填) 要绑定到组件上的属性/特性名 label: Type, // Settings 面板中显示的标签 options: [ { id: text, label: Text }, { id: email, label: Email }, { id: password, label: Password }, { id: number, label: Number }, ], }, { type: checkbox, name: required, }, ], // 默认情况下 Trait 绑定到属性(attributes)上 // 因此初始值通过 attributes 定义 attributes: { type: text, required: true }, }, }, });其中name、placeholder这样的字符串写法会被 TraitFactory 自动转换为{ type: text, name: xxx }的对象形式见buildFromString。另外target这个特殊名字会由工厂自动生成一个select类型并带有optionsTarget默认选项[{ value: false }, { value: _blank }]该默认选项配置定义在 packages/core/src/trait_manager/config/config.ts。动态定义 Trait如果 Trait 列表需要依据组件的其他特征动态生成可以把traits定义成函数它会在组件初始化时执行editor.Components.addType(input, { isComponent: (el) el.tagName INPUT, model: { defaults: { traits(component) { const result []; // 一些业务逻辑示例 if (component.get(draggable)) { result.push(name); } else { result.push({ type: select, // .... }); } return result; }, }, }, });该函数的参数即当前组件实例返回值是 Trait 定义数组。相关执行逻辑可见 Component.tstraits为函数时会先以组件实例调用再构建 Traits 集合。响应 Trait 变化Trait 默认绑定在组件属性上因此可以通过属性监听器响应变化editor.Components.addType(input, { model: { defaults: { // ... }, init() { this.on(change:attributes:type, this.handleTypeChange); }, handleTypeChange() { console.log(Input type changed to: , this.getAttributes().type); }, }, });绑定到组件属性changeProp默认 Trait 修改的是组件 attributes但也可以通过changeProp: 1将 Trait 绑定到组件 property 上。切换后监听事件由change:attributes:*变为change:*editor.Components.addType(input, { model: { defaults: { // ... traits: [ { name: placeholder, changeProp: 1, }, // ... ], // 从 attributes 切换到 properties 后 // 初始值应从 property 上设置 placeholder: Initial placeholder, }, init() { // 监听事件也随之从 change:attributes:* 变为 change:* this.on(change:placeholder, this.handlePlhChange); }, // ... }, });这一机制在 Trait.ts 中实现changeProp为真时读取component.get(name)并写入component.set(props, opts)否则读写component.getAttributes()/component.addAttributes()。Trait 分类Categories可以将 Trait 分组展示未指定category的 Trait 渲染在列表底部const category1 { id: first, label: First category }; const category2 { id: second, label: Second category, open: false }; editor.Components.addType(input, { model: { defaults: { // ... traits: [ { name: trait-1, category: category1 }, { name: trait-2, category: category1 }, { name: trait-3, category: category2 }, { name: trait-4, category: category2 }, // 未指定 category 的 Trait 会渲染在底部 { name: trait-5 }, { name: trait-6 }, ], }, }, });分类对象支持id唯一标识、label显示名、open是否默认展开默认true。分类的实现建立在通用抽象类 CollectionWithCategories 与 ModuleCategory 之上Traits 集合在add时调用initCategory为每个 Trait 挂载分类Traits.ts视图层再依据分类将条目渲染进不同的分组容器TraitsView.ts。内置 Trait 类型GrapesJS 内置六种 Trait 类型注册于 trait_manager/index.ts 的types映射中text、number、select、checkbox、color、button其默认属性定义在 Trait.ts。Text文本简单的文本输入框{ type: text, // 不指定 type 时默认就是 text name: my-trait, // 必填所有类型通用 label: My trait, // 输入框旁显示的标签 // label: false, // 设为 false 会移除标签列 placeholder: Insert text, // 输入框内显示的占位符 }文本输入框由基础视图 TraitView 直接生成占位符取自placeholder || defaultmin/max属性会被同步到 DOM同时会读取 i18n 配置traitManager.traits.attributes.name作为额外的 DOM 属性。Number数字数字输入框支持范围与步进{ type: number, // ... placeholder: 0-100, min: 0, // 最小值 max: 100, // 最大值 step: 5, // 步进值 }对应的视图是 TraitNumberViewmin、max、step均会写入原生input的对应属性。Checkbox复选框简单的复选开关{ type: checkbox, // ... valueTrue: YES, // 勾选时写入的值默认: true valueFalse: NO, // 取消勾选时写入的值默认: false }其值转换逻辑见 Trait.tsgetTargetValue的useType分支与setTargetValue勾选状态写入valueTrue未勾选写入valueFalse同时字符串true/false会被自动转为布尔值。Select下拉选择带选项的下拉选择框{ type: select, // ... options: [ // 选项数组 { id: opt1, label: Option 1}, { id: opt2, label: Option 2}, ] }渲染逻辑在 TraitSelectView选项支持字符串name/value相同或对象id、label、value、style当当前值不在选项列表中时会回退到default。getOptionIdoption.id || option.value与getOptionLabellabel || name || id的解析规则定义在 Trait.ts。Color颜色内置颜色选择器底层复用了样式管理器中的 InputColor 组件见 TraitColorView{ type: color, // ... }Button按钮带命令绑定的按钮{ type: button, // ... text: Click me, full: true, // 全宽按钮 command: editor alert(Hello), // 或者直接指定命令 ID command: some-command, }按钮点击后会执行command字符串会被em.Commands.run(command)当作命令 ID 执行函数则直接调用Trait.ts。按钮视图 TraitButtonView 额外支持labelButton优先于text作为按钮文案并通过eventCapture [click button]捕获点击事件。运行时更新 TraitTrait 本质上是组件的一个属性因此可以通过 Component API 在任意时刻读取与修改。获取当前选中组件的全部 Traitconst component editor.getSelected(); // 画布中选中的组件 const traits component.getTraits(); traits.forEach((trait) console.log(trait.props()));获取单个 Trait按name查找const component editor.getSelected(); console.log(component.getTrait(type).props());getTraits()与getTrait()的实现见 Component.tsgetTrait同时匹配id和name找不到返回null。更新 Trait 的某个属性// 更新 Input 组件 type trait 的 options const component editor.getSelected(); component.getTrait(type).set(options, [ { id: opt1, label: New option 1}, { id: opt2, label: New option 2}, ]); // 或一次更新多个属性 component.getTrait(type).set({ label: My type, options: [...], });增删 Trait 使用addTrait/removeTrait实现见 Component.ts// 新增 Trait const component editor.getSelected(); component.addTrait({ name: type, ... }, { at: 0 }); // at 选项指定插入位置索引 // 不传时新 Trait 追加到列表末尾 // 删除 Trait component.removeTrait(type); // 也支持批量删除 component.removeTrait([title, id]);补充说明getTraitIndex(id)可获取 Trait 的当前索引updateTrait(id, props)可快捷更新属性setTraits(array)可整体替换 Trait 集合详见 Component.ts。国际化I18nTrait 相关的文案可通过 I18n 模块 配置引用以下结构{ en: { traitManager: { empty: Select an element before using Trait Manager, label: Component settings, categories: { categoryId: Category label, }, traits: { // 以 trait 的 name 属性作为键 labels: { href: Href label, }, // 内置类型如 text会把这些属性应用到输入 DOM 上 attributes: { href: { placeholder: eg. https://google.com }, }, // select 类型用于翻译选项标签 options: { target: { // 这里的键是 option 的 id _blank: New window, }, }, }, }, } }这些键在源码中的读取位置标签em.t(traitManager.traits.labels.name)TraitView.ts 与 Trait.ts输入 DOM 属性em.t(traitManager.traits.attributes.name)TraitView.ts选项标签em.t(traitManager.traits.options.name.optionId)Trait.ts分类标签em.t(traitManager.categories.categoryId)Trait.ts。自定义扩展内置类型能覆盖大部分常规需求但遇到更复杂的 UI 时可以选择定义全新 Trait 类型或者完全从零实现一个自定义 Trait Manager。定义新 Trait 类型创建自定义元素createInput以扩展默认link组件为例。默认链接组件的 Trait 很基础现在我们用一个新类型href-next替换全部 Trait让用户可以选择 href 的类型如 url、email 等// 更新组件 editor.Components.addType(link, { model: { defaults: { traits: [ { type: href-next, name: href, label: New href, }, ], }, }, });此时因为href-next类型尚未定义渲染出的只是一个普通文本输入框。下面通过editor.Traits.addType注册它editor.Traits.addType(href-next, { // 期望返回一个 HTML 字符串或 HTML 元素 createInput({ trait }) { // 这里可以读取 trait 上的属性做决策 const traitOpts trait.get(options) || []; const options traitOpts.length ? traitOpts : [ { id: url, label: URL }, { id: email, label: Email }, ]; // 创建容器元素并填充内容 const el document.createElement(div); el.innerHTML select classhref-next__type ${options.map((opt) option value${opt.id}${opt.label}/option).join()} /select div classhref-next__url-inputs input classhref-next__url placeholderInsert URL/ /div div classhref-next__email-inputs input classhref-next__email placeholderInsert email/ input classhref-next__email-subject placeholderInsert subject/ /div ; // 让内容可交互切换 url/email 时显示对应输入区 const inputsUrl el.querySelector(.href-next__url-inputs); const inputsEmail el.querySelector(.href-next__email-inputs); const inputType el.querySelector(.href-next__type); inputType.addEventListener(change, (ev) { switch (ev.target.value) { case url: inputsUrl.style.display ; inputsEmail.style.display none; break; case email: inputsUrl.style.display none; inputsEmail.style.display ; break; } }); return el; }, });从实现上看addType内部会把自定义方法扩展在基础视图TraitView上trait_manager/index.ts因此你可以覆盖createInput、createLabel、onEvent、onUpdate、templateInput、noLabel、eventCapture等全部钩子。自定义标签与布局Trait 由标签列 输入列组成两者都可定制。用createLabel自定义标签渲染editor.Traits.addType(href-next, { // 期望返回一个 HTML 字符串或 HTML 元素 createLabel({ label }) { return div divBefore/div ${label} divAfter/div /div; }, // ... });单个 Trait 定义中可用label: false移除标签列若想让该类型的所有实例都强制无标签则使用noLabeleditor.Traits.addType(href-next, { noLabel: true, // ... });默认情况下 GrapesJS 会在输入框外面包一层容器简单输入没问题但复杂自定义 Trait 可能不需要它。用templateInput移除或替换默认包裹层editor.Traits.addType(href-next, { // 完全移除包裹层 templateInput: , // 使用新包裹层用 data-input 属性指定输入容器位置 templateInput: div classcustom-input-wrapper Before input div>editor.Traits.addType(href-next, { // ... // 根据元素变化更新组件 // elInput 是 createInput 返回的 HTMLElement onEvent({ elInput, component, event }) { const inputType elInput.querySelector(.href-next__type); let href ; switch (inputType.value) { case url: const valUrl elInput.querySelector(.href-next__url).value; href valUrl; break; case email: const valEmail elInput.querySelector(.href-next__email).value; const valSubj elInput.querySelector(.href-next__email-subject).value; href mailto:${valEmail}${valSubj ? ?subject${valSubj} : }; break; } component.addAttributes({ href }); }, });事件捕获机制默认基础视图在输入容器上监听change事件TraitView.prototype.eventCapture [change]见 TraitView.ts捕获到的事件需能冒泡然后触发onEvent。如果想改为监听input事件通过eventCapture声明即可editor.Traits.addType(href-next, { eventCapture: [input], // 数组内可声明多个事件 // ... });注意事件委托是通过this.events[event] onChange注册的TraitView.ts随后onChange会先同步输入值到模型再调用onEventTraitView.ts。反向同步onUpdate组件上已有href属性时初次渲染可能没有正确回填输入框。这一步应在onUpdate中完成editor.Traits.addType(href-next, { // ... // 组件变化时更新输入元素 onUpdate({ elInput, component }) { const href component.getAttributes().href || ; const inputType elInput.querySelector(.href-next__type); let type url; if (href.indexOf(mailto:) 0) { const inputEmail elInput.querySelector(.href-next__email); const inputSubject elInput.querySelector(.href-next__email-subject); const mailTo href.replace(mailto:, ).split(?); const email mailTo[0]; const params (mailTo[1] || ).split().reduce((acc, item) { const items item.split(); acc[items[0]] items[1]; return acc; }, {}); type email; inputEmail.value email || ; inputSubject.value params.subject || ; } else { elInput.querySelector(.href-next__url).value href; } inputType.value type; inputType.dispatchEvent(new CustomEvent(change)); }, });此后即使从外部修改组件Trait 也会同步更新editor.getSelected().addAttributes({ href: mailto:new-emailtest.com?subjectNewSubject });在组件侧onUpdate的触发链路是组件属性变化 → Trait 模型targetUpdated()→ 触发trait:value/trait:update事件Trait.ts→ 视图onValueChange回填输入并调用postUpdate()→onUpdateTraitView.ts。小结定义一个自定义 Trait 类型只需要三个核心方法createInput—— 定义自定义 HTML 元素onEvent—— 输入变化时如何更新组件onUpdate—— 组件变化时如何更新输入集成外部 UI 组件原生 DOM API 写起来比较繁琐。如果使用现代 UI 框架Vue、React 等集成会简单得多。以下是把 Vue Slider 组件集成成 Trait 的示例editor.Traits.addType(slider, { createInput({ trait }) { const vueInst new Vue({ render: (h) h(VueSlider) }).$mount(); const sliderInst vueInst.$children[0]; sliderInst.$on(change, (ev) this.onChange(ev)); // 用 onChange 触发 onEvent this.sliderInst sliderInst; return vueInst.$el; }, onEvent({ component }) { const value this.sliderInst.getValue() || 0; component.addAttributes({ value }); }, onUpdate({ component }) { const value component.getAttributes().value || 0; this.sliderInst.setValue(value); }, });集成外部组件只需遵循三个核心要点组件渲染new Vue({ render: ... })。依赖具体框架例如 React 中应是ReactDOM.render(element, ...)变更传播sliderInst.$on(change, ev this.onChange(ev))。框架需要提供订阅变更的机制并且组件应暴露该变更事件。注意这里使用的是onChange方法来手动触发onEvent——需要时不应直接调用onEvent而应通过onChange属性读写sliderInst.getValue()/sliderInst.setValue(value)。组件实例需要支持读取与写入数据。自定义 Trait Manager默认的 Trait Manager UI 能处理大部分常见任务但需要更高级的逻辑或元素时可以从零构建自己的 Trait Manager。做法在初始化配置中声明traitManager.custom: true然后订阅trait:custom事件该事件会在任何需要刷新 UI 的时刻触发const editor grapesjs.init({ // ... traitManager: { custom: true, // ... }, }); editor.on(trait:custom, (props) { // props.container (HTMLElement) - 默认容器元素可将自定义 UI 挂载其中 // 在这里编写渲染/更新 UI 的逻辑 });配置项custom定义在 config/config.ts默认false该模块其余配置还有stylePrefix样式前缀默认trt-、appendTo容器元素默认空为空时不渲染以及optionsTargettargetTrait 的默认选项。从源码看trait:custom事件的触发路径是组件选中变化 →state.set({ component, traits })→__trgCustom()→em.trigger(trait:custom, { container })trait_manager/index.ts。事件对象中container是模块内部维护的容器元素__ctn来自appendTo配置或首次触发时传入的opts.container。在自定义 UI 中可以利用 Trait Manager 提供的高层 API 读取状态editor.Traits.getTraits()—— 当前选中组件的 Trait 数组editor.Traits.getComponent()—— 当前选中的组件editor.Traits.getCategories()—— 当前组件的分类列表editor.Traits.getTraitsByCategory()—— 按分类分组的 Trait 列表返回形如{ category?: Category, items: Trait[] }的数组无分类的条目放入category为空的组每个 Trait 实例则提供getValue()/setValue()、getType()、getLabel()、getOptions()/getOptionId()/getOptionLabel()、runCommand()等方法详见 Trait.ts。这些方法均以 JSDoc 形式标注在 trait_manager/index.ts 中也可通过editor.Traits在运行时直接调用。事件Trait Manager 会触发以下事件事件常量定义于 packages/core/src/trait_manager/types.ts事件触发时机回调数据trait:select选中新的 Trait例如切换选中组件{ traits, component }trait:valueTrait 值被更新{ trait, component, value }trait:updateTrait 任意属性被更新{ trait, component, value }trait:category:updateTrait 分类被更新{ category, changes }trait:custom自定义 Trait Manager UI 需要刷新{ container }trait上述全部事件的兜底catch-all事件{ event, trait?, component?, value?, ... }示例editor.on(trait:value, ({ trait, component, value }) { console.log(Trait value updated:, trait.getName(), value); }); editor.on(trait:select, ({ traits, component }) { ... });小结Trait Manager 是 GrapesJS 连接组件模型与用户设置界面的桥梁字符串或对象形式的 Trait 定义经 TraitFactory 规范化为 Trait 模型再由视图层按类型分派到六种内置视图渲染changeProp决定了值写入属性还是特性分类、国际化、运行时 API 与自定义类型扩展则共同构成了完整的面板定制能力。参考本文示例你可以为自己的业务组件设计出即开即用的设置面板也可以将既有 UI 框架组件无缝接入编辑器。【免费下载链接】grapesjsFree and Open source Web Builder Framework. Next generation tool for building templates without coding项目地址: https://gitcode.com/GitHub_Trending/gr/grapesjs创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考