Type: New Feature
Resolution: Won't Fix
Affects Version/s: 2.1.2
Fix Version/s: Future
Contrary to component-oriented frameworks , Struts2 architecture does not try to ignore what the web is, it just tries to make web programming easier without introducing complex component lifecycles.
However, there are a few things that the component-oriented frameworks got right, and I believe struts2 should build on its architecture to enable users to create UI components easily.
These needs arise from the project I am currently working on, and I describe the hacks that I currently do to 'simulate' components. I also try to envision the features that struts2 could add to make writing components easier. This 'request' should be seen as a meta-feature request that can be used as a basis for discussing what should be included. Once everybody agrees, we can create smaller issues that target individual concerns.
So, let's define what a reusable UI component needs to work decently:
- Markup template. (the view)
- Custom CSS for the component
- Access to helper Java code
- Access to a given model, expressed as a Java class that has to be pushed on the value stack, so that it can catch URL/form parameters related to the component
- When the component displays part of a form, the action has to delegate part of the validation to the component's model, using @VisitorFieldValidator
- a Custom interceptor to set parameters on the action
- Custom properties file for I18N
Frameworks such as tapestry have built-in features for all these concerns. Here is the struts2-way to achieve the same, with the current feature set :
- Markup/View: Markup template expressed as FTL, either using the Struts2 Component / Taglib infrastructure, or just plain FTL files that have nothing to do with struts2
- Helper java functions have to be registered using a Custom Freemarker Manager. Adding helpers to the freemarker context is pretty straightforward, but adding the services to the ValueStack too is a little tricky. You have to make sure you don't push the same service twice in the same request, because populateContext() is called several times...
- The component's java model has to be pushed by the action. pretty much like the action's view has to reference the head tag in the head section. I use a MultipleModelDrivenInterceptor that works like ModelDrivenInterceptor, except that is pushes a list of objects, instead of just one. In case the component adds fields to the form, the action is responsible for adding @VisitorFieldValidator.
- For important parameters and session attributes that are considered globally accessible on the website, specific interceptors + *Aware interfaces can be used to ease the actions' job. Things like Users, etc, can be directly injected to the action to avoid boilerplate copy/pasted code.
- Each component has its own properties file, that is registered in struts.xml as a global resource bundle.
OK, now, why I think all this stuff is suboptimal :
- There is no common concept in struts that defines what a component is. so all the pieces are available, but it looks like tricks and hacks that we try to get to work. It would definitely be less worrying for the developer if struts provided an 'official' way to do it.
- the developer is responsible for adding head to the HTML head section, for creating the component's Java Model, for pushing it on the value stack, and for delegating validation to the component. WAO !!! In most cases, if you're not the one who developed the component, you will forget one of these steps.
- The developer is responsible for making sure keys are globally unique among components... Hum... Once again, this gives the impression of a mere hack...
Pushing some resource bundle using the I18N tag is not an option since it renders the use of <#nested> useless. For instance, if a component did :
<@s.text name="whatever.key.in.component.resource.bundle" />
then if the component is called with <@s.text /> code in the <#nested /> section, the <@s.text would not find its key in the component resource bundle, and would then fail. Implementing a fallback mechanism that would go down the value stack could partly work, but would still not fix the duplicate key problem.
- When we want to do some processing depending on the parameters, and inject some variables to the current action thanks to a custom interceptor, we don't have access to type converters, we basically need to do the job by hand (or probably instantiate the type converters, or do whatever hardcore low level struts or OGNL stuff to achieve the same). So, the only real way to bind parameters is to use a custom model class that is instantiated and pushed on the value stack by the action, but then the action has too much stuff to do regarding the component. See previous section regarding custom java component models.
So, what I think struts could add in order to make writing components more consistent and easier :
1] A small wiki section that describes the 'preferred' struts approach to creating reusable components, whatever approach is chosen. Even if the wiki section is considered as a mere suggestion, it would allow users to realize that it is in fact possible to write components using struts2, and would certainly help struts2 beginners.
2] A kind of generic behavior that would allow components (Struts 2 Tag Components) to register custom CSS and JS code, that would be printed in the default struts2 themes. (<@s.head />). In order to select which components are used on a page, I guess that parsing the FTL would be too weak ? So, it would be acceptable to force the action to declare the list of the components that are used. Do some people have an idea regarding this ?
3] A way for Struts2 components to declare :
- A method to create the associated java model object, for every request where the component is used. This model object should be pushed to the value stack automatically.
- the ModelDriven-validation.xml rules that should be included when the component is used. The main problem I see with that is that is to allow switching validation off, because the action would probably want to validate the model for the execute() method, but not the input() one.
- An interface that an action could implement to receive the populated model (MyModelAware.class), as well as some callback method that is used to set the model on the implementing action. A generic interceptor could then use whatever ThreadLocal/request-scoped object to retrieve all the callback methods that have to be called on the current action, depending on the types implemented by the action.
- The name of a spring/whatever IoC service that should be registered in the freemarker + OGNL context, so that FTL views can refer to it
- The name of a .properties file to use for I18N keys lookup. To make this to work, my guess is that @s.text would need to be completly reworked to be component aware, since both global keys, and trying eveyr textprovider in the value stack both don't fix the duplicate key problems. Maybe some experts have a better idea to solve this issue ?
That's it ! (I think)
So yeah, still a lot of questions, it's definitely a tough problem, but I really believe it's worth thinking a little bit about that.
In my project, I am progressively starting to extract abstractions to create components, so don't hesitate to bring your ideas to complement what I suggested. I would happily share the code that implements these ideas whenever it is available.