自定义Faces组件中程序化修改组件树及子组件委托方案问询
自定义JSF组件库的组件树修改与子组件委托问题
我们维护着一个自定义JSF组件库,通过Bootstrap样式类装饰原生Faces组件,可以看作是已停止维护的BootFaces项目的继任者。我们希望实现表单的「一站式」主题效果:开发者添加输入组件时,组件能自动附带周边内边距、标签、输入框本身及输入提示信息。
最初考虑用复合组件实现,但Bootstrap的特殊特性要求我们在部分场景下严格控制渲染内容,因此放弃了这个方案。我们期望开发者只需编写:
<mf:inputText .../>
实际生成的组件树结构为:
- inputTextParent - padding component - label component - actual input field component - message component
现在有两个核心疑问:
- 在
inputTextParent中,有没有合规的方式修改组件树并添加子组件? - 如果开发者编写如下代码:
能否把<mf:inputText ...> <h:somethingElse ../> </mf:inputText><h:somethingElse ../>委托给内部的实际输入框组件,生成如下结构?- inputTextParent - padding component - label component - actual input field component - h:somethingElse - message component
目前我们尝试在组件构造函数中做类似操作,但无法正确把h:somethingElse委托给输入框子组件,而且我们也清楚构造函数并不是操作组件树的合法时机,当前代码如下:
public MFInputTextComponentParent() { setRendererType(getComponentName()); final FacesContext context = FacesContext.getCurrentInstance(); final Application application = context.getApplication(); final MFPadComponent pad = (MFPadComponent) application.createComponent(MFPadComponent.COMPONENT); pad.setPadType("y"); pad.setPad(1); super.getChildren().add(pad); final MFInternalLabelComponent label = (MFInternalLabelComponent) application .createComponent(MFInternalLabelComponent.COMPONENT); pad.getChildren().add(label); final MFInternalInputTextComponentParent inputText = createInternalInputText(application); pad.getChildren().add(inputText); final MFInternalMessageComponent message = (MFInternalMessageComponent) application .createComponent(MFInternalMessageComponent.COMPONENT); pad.getChildren().add(message); }
解决方案
一、合规修改组件树的时机:PostAddToViewEvent
构造函数确实不是操作组件树的正确时机——此时组件还未被添加到视图树中,Faces上下文可能还未完全初始化,而且无法处理开发者传入的子组件(因为这些子组件是在组件被解析后才会被添加到inputTextParent的子列表中)。
正确的时机是监听PostAddToViewEvent,这个事件在组件被成功添加到视图树后触发,此时可以安全地修改组件树:
- 给
MFInputTextComponentParent添加事件监听:
@NamedEvent(shortName = "postAddToView") public class MFInputTextComponentParent extends UIComponentBase { public MFInputTextComponentParent() { addSystemEventListener(new PostAddToViewEventListener() { @Override public void processEvent(SystemEvent event) throws AbortProcessingException { if (event instanceof PostAddToViewEvent) { buildComponentTree(); } } }); } private void buildComponentTree() { FacesContext context = FacesContext.getCurrentInstance(); Application application = context.getApplication(); // 清空现有子组件(避免重复构建,比如视图重建时) getChildren().clear(); // 创建padding组件 MFPadComponent pad = (MFPadComponent) application.createComponent(MFPadComponent.COMPONENT); pad.setPadType("y"); pad.setPad(1); getChildren().add(pad); // 创建标签组件 MFInternalLabelComponent label = (MFInternalLabelComponent) application.createComponent(MFInternalLabelComponent.COMPONENT); pad.getChildren().add(label); // 创建内部输入框组件 MFInternalInputTextComponentParent inputText = createInternalInputText(application); pad.getChildren().add(inputText); // 将开发者传入的子组件转移到内部输入框中 List<UIComponent> originalChildren = new ArrayList<>(getChildren()); // 移除刚创建的pad组件,剩下的就是开发者传入的子组件 originalChildren.remove(pad); inputText.getChildren().addAll(originalChildren); // 创建提示信息组件 MFInternalMessageComponent message = (MFInternalMessageComponent) application.createComponent(MFInternalMessageComponent.COMPONENT); pad.getChildren().add(message); } // 其他原有方法... }
二、子组件委托的实现逻辑
上面的代码中,核心是将inputTextParent原本的子组件(即开发者写的<h:somethingElse ../>)转移到内部的实际输入框组件中:
- 先通过
new ArrayList<>(getChildren())复制当前inputTextParent的子组件列表(此时列表里包含开发者传入的子组件,以及刚创建的pad组件) - 移除刚创建的pad组件,剩下的就是开发者传入的子组件
- 将这些子组件添加到内部输入框的
getChildren()中
这样就能实现把开发者传入的子组件委托给内部输入框的需求。
三、额外注意事项
- 避免重复构建组件树:在
buildComponentTree()开头先清空getChildren(),防止视图重建(比如AJAX请求时)导致组件重复添加。 - 组件ID的唯一性:如果内部组件需要设置ID,确保ID不会和现有组件冲突,可以用
context.getViewRoot().createUniqueId()生成唯一ID。 - 属性传递:如果需要把
inputTextParent上的属性(比如value、required等)传递给内部输入框组件,需要在buildComponentTree()中手动复制这些属性,或者通过组件绑定的方式处理。 - 渲染器的配合:确保自定义组件的渲染器能正确处理生成的组件树结构,特别是Bootstrap的样式类要正确应用到对应的内部组件上。
内容的提问来源于stack exchange,提问作者Jonathan S. Fisher
相关产品推荐
相关产品推荐

