Friday, February 8. 2008Zend_Form Advanced FeaturesI've been working on for the past few weeks, and it's nearing release readiness. There are a number of features that Cal didn't cover in his DevZone coverage (in part because some of them weren't yet complete) that I'd like to showcase, including:
This post will serve primarily as a high-level overview of some of these
features; if you're looking for more in-depth coverage, please review the
unit tests. InternationalizationWhen using form components in many libraries, internationalization (i18n) is often tricky to accomplish. There are many potential translation targets: labels, submit and reset buttons, and error messages all potentially need to be treated. Zend_Form allows setting a translation object at both the element and form level, and also allows setting a default translation object for all forms and elements. I personally feel this latter is the most flexible; in most projects, you'll have a single set of translation files, so why not simply re-use the same object throughout? // Create your translation object $translate = new Zend_Translate(...); // Set it as the default object for all forms and elements: Zend_Form::setDefaultTranslator($translate); What do you get for this?
In other words, translation in Zend_Form is pretty well integrated. Element GroupingIn Zend_Form, we distinguish between two types of element grouping: grouping for display purposes (DisplayGroups) and grouping for logistical purposes (Sub Forms) With DisplayGroups, you're basically saying you want to visually or semantically group elements together on the page. Usually (and by default) this is done with fieldsets. DisplayGroups provide a simple mechanism for doing this. The elements remain children of the parent form object, but are rendered within the display group. Other times, you want to group the elements logically. For instance, you might want to group a billing address separately from a shipping address. This grouping may be simple namespacing under array keys (I'll cover this more later), shared filters or decorators, or, in advanced use cases, separate pages of a multi-page form. Zend_Form's answer to these situations are "Sub Forms". They are actually a subclass of Zend_Form, and the only real difference is the class and the default decorators used (by default, they render in a fieldset). Since they share the same functionality as a regular form, this means they can validate their elements, render themselves, etc. However, Zend_Form itself cannot iterate over or render a sub forms elements; only the sub form can do that. One potentially powerful use case for sub forms is for multi-page forms. You could easily create a form consisting of several sub forms, and display a single sub form per page, persisting data in the session between form submissions; only when all pages have received their data would the parent form be valid, allowing you to finally pass the data to the model. Form grouping at the display and logical level both are powerful tools, and this functionality is trivial with Zend_Form. Array SupportMany developers like to namespace their form elements under nested arrays. This allows for groupings of related data, as well as having several groups with similar data on the same page. As an example, imagine a form showing both a shipping and a billing address: <form action="/foo/bar" method="post"> <fieldset> <legend>Shipping Address</legend> <dl> <dt>Address:</dt> <dd><input name="shipping[address]" type="text" value="" /></dd> <dt>City:</dt> <dd><input name="shipping[city]" type="text" value="" /></dd> <dt>Postal:</dt> <dd><input name="shipping[postal]" type="text" value="" /></dd> </dl> </fieldset> <fieldset> <legend>Billing Address</legend> <dl> <dt>Address:</dt> <dd><input name="billing[address]" type="text" value="" /></dd> <dt>City:</dt> <dd><input name="billing[city]" type="text" value="" /></dd> <dt>Postal:</dt> <dd><input name="billing[postal]" type="text" value="" /></dd> </dl> </fieldset> </form> PHP will receive two arrays from the submitted form, 'shipping' and 'billing'. Zend_Form now allows this (as of today). To keep all existing features, and to allow elements and sub forms to stay de-coupled from their parent forms, you need to do a little configuration: $shipping = new Zend_Form_SubForm('shipping'); // This next line tells the elements, validators, and decorators that they are // part of an array; by default, the sub form name is used: $shipping->setIsArray(true); // This can also be accomplished by explicitly setting the array name: $shipping->setElementsBelongTo('shipping'); The fun part is that this can be arbitrarily deep, by specifying the array key as it would appear in the form. So, as an example, if we wanted the entire form returned in the 'demographics' array, and 'shipping' and 'billing' were keys in that array, we could do the following: // Set base key for entire form: $form->setElementsBelongTo('demographics'); // Set subkey for shipping sub form: $shipping->setElementsBelongTo('demographics[shipping]'); // Set subkey for billing sub form: $billing->setElementsBelongTo('demographics[billing]'); When you set or retrieve values, or validate, these array keys are honored. What's more, since they are configurable, you can leave them out of your generic forms, and only set them in your concrete instances -- allowing re-use and re-purposing. ConclusionThis post is mainly to serve as high-level overview of some of the more advanced features of Zend_Form. In the coming weeks, more thorough documentation will be present in the Zend Framework repository, allowing developers to understand the functionality in more depth. Hopefully I've whetted some people's appetites out there, and we'll get more of you experimenting with the current code base. Update: fixed array notation HTML example to show separate billing and shipping addresses. Comments
Display comments as
(Linear | Threaded)
Great work. I am looking forward to getting rid of QuickForm when 1.5 is released. Zend_Form is turning out to be an excellent solution that is capable of handling a very diverse range of Form related problems.
I think you made a mistake in the HTML code for the "Array Access" section. Shouldn't one of the fieldsets be Billing and the other Shipping? Glad you're liking what you see. I've traditionally stayed away from most form libraries as I've found them not configurable enough.
Thanks for pointing out the cut-and-paste error; I've now corrected it. Yes I agree. I am particularly looking forward to being able to set up an entire form with a Zend_Config object.
Thank you for this information. I found the sections about i18n and arrays particularly useful, as I was trying to hack this together myself.
Actually, I've got a follow-up question to the i18n part...
I'm wondering if I'm doing things wrong, or if the result I get is by design. In your post, it says: * Error messages. Validation error messages will be translated, with any value substitutions as provided by Zend_Validate. I have subclassed Zend_Form, and in the constructor I use $this->setTranslator($translate), then go on to make elements and add them to the form (without setting translator object for any of them). This all works fine, but on errors, value substitution dos not occur. The reason for this, as far as I've been able to backtrack, is that the translation itself occurs in the Errors decorator, at which point values have already been substituted by Zend_Validate_Abstract. I've tried setting translator for elements too, but this results in no translating (also, the render method in Zend_Form sets translator to null in all elements if no translator is set in the form). Is there any way to make the translation happen before value substitution? I'm using Zend Framework 1.5.0 Preview Release. Most of the i18n improvements have been made since the last Preview Release. If you give a nightly snapshot or current trunk a try, I think your results will be much better.
I tried the latest snapshot, but that didn't work either. It's almost implemented, but not quite. The isValid() method in Zend_Form_Element calls setTranslator() on all validators if the method exists, but setTranslator() is not a method in Zend_Validate_Abstract.
My fix: - Added $_translator as a member in Zend_Validate_Abstract - Added accessors setTranslator(Zend_Translate $translator = null) and getTranslator() to Zend_Validate_Abstract - Modified the method _createMessage() in Zend_Validate_Abstract: Changed the following line: $message = $this->_messageTemplates[$messageKey]; to: if (null !== $this->_translator) { $message = $this->_translator->getAdapter()->translate($messageKey); } else { $message = $this->_messageTemplates[$messageKey]; } - Removed translation part from the method render() in Zend_Form_Decorator_Errors Now it works, but this hack is sub-optimal, so I'm looking forward to the stable release, and hope it will be fixed till then. There's something wrong with your install, as the addition of setTranslator() to Zend_Validate_Abstract was necessary for me to even attempt these i18n features; please try checking out directly from subversion.
Also, if you continue having issues, please report to the fw-mvc list instead of my blog; there will be more people who can help you, including helping verify issues. Okay, I checked out the latest trunk from subversion, and this helped a lot. Now, everything is translated in form elements and validators, but the error messages are still rendered without value substitution.
I followed your advice and posted the issue to the fw-mvc list, so I guess the discussion continues there from now on (this comment is for notifying other potentional googlers that the discussion has moved http://www.nabble.com/I18n-problem-with-Zend_Form_Decorator_Errors-td15422825s16154.html
#3.1.1.1.1
Robin Skoglund
on
2008-02-11 18:12
(Reply)
I have a question about using a radio button with a config object, I'm setting it up in my config like so:
elements.call_me.type = "radio" elements.call_me.options.multiOptions.morning = "9:00-5:00" elements.call_me.options.multiOptions.afternoon = "5:00-10:00" elements.call_me.options.validators.alnum.validator = "alnum" elements.call_me.options.validators.alnum.options.messages.stringEmpty = "..." elements.call_me.options.validators.alnum.options.messages.notAlnum = "..." elements.call_me.options.required = true elements.call_me.options.label = "Call Me" But when there are errors with other elements on the form neither of the radio buttons get selected when the form gets displayed again. All of the other elements on the form seem to work fine, any ideas? Who do you guys have writing the error messages, robots? I really think they could use some humanization. I realize they can be overridden, but you'd think the default ones would be somewhat understandable. "value is empty, but a non-empty value is required". 90% of users are going to think "what the hell does that mean" when reading that. Also, I'm still a little on the fence about mixing all the logic into one component (rendering, validation, etc) Other than that... nice work!
For the line:
$shipping->setElementsInArray(true); Zend_Form_SubForm does not contain a definition for setElementsInArray Also, using setElementsBelongTo() does not seem to have an effect for elements in display groups, is there a way to make that work? setElementsInArray() existed at the time I wrote this, which was before the API stabilized. I've updated it now to use setIsArray().
Re: Zend_Form_DisplayGroup::setElementsBelongTo(), this is a known bug that I will be fixing in an upcoming release. Oh no! I guess I will have to defer creating my multi page form.
Thanks for the prompt reply; your articles and responses have been remarkably helpful. You can still create your multi-page forms, just be careful using display groups within sub forms.
Does that mean don't use them at all or is there a workaround I can use?
#6.1.1.1.1
Christopher
on
2008-04-21 17:16
(Reply)
Hi Matthew,
Do you have an idea how to combine two form elements ? For example I want to create drop down boxes for time. The first one will be for hours and the second one for minutes. I use displaygroups items to group elements in fieldset so the solution should be something else. I solved the issue using jQuery time picker but I am just curious how one can combine two elements that will be rendered together. What if I create My_Form_Element_Time ? Any other elegant solutions ? Slavi Please ask questions such as this on the fw-mvc list; providing support in comments is too difficult.
Is it correct, that I still have to create one SubForm for each namespace/nested array I want on the final rendered element?
In other words, in order to have a name attribute of My[verbose][namespace][value] on a Zend_Form_Element I will have to create a Zend_Form 'My' with two SubForms ('My[verbose]' and 'My[verbose][namespace]'). I cannot simply tell the Form itself or only one SubForm that all elements belong to 'My[verbose][namespace]', right? That is correct -- it is part of the design, and is intended to force the developer to group elements semantically.
Thanks for clearifying this.
I think it's a bit awkward to use this way. Often I simply add a depth level for a more verbose structure on the resulting Post data. While this does add some semantics to the data structure, having to add an otherwise functionless SubForm for that feels like wrapping the input elements in DIVs, just to represent they are nested. It would be nice to have some Zend_Form handle these things in a more convenient or automatic way. The implementation is also a bit misleading, since I can very much call setElementsBelongTo('My[foo][bar]') on a SubForm without having the other required forms and declarations in place. The elements will render with the full name attribute then but the form will not read in any data following this array structure. The problem here is mapping input data to objects. Without using subforms, it becomes very difficult to do this in a meaningful way within the structure of the PHP objects utilized. While I understand what you are getting at, I will in turn ask you to ask yourself this question: what do you gain from arbitrarily nesting the data structure? and is there another way that may have more semantic sense?
I see what your getting at and I mostly agree.
Still, there might be usecases, where using a structure not required by the immediate form, might be a valid approach. I might want to turn post data from several forms into a single Zend_Config. One form might represent data at a deeper level in the Config, while another might be more at the root. If the form elements know this structure, I could simply merge any forms into the Zend_Config without needing to write additional code. But I admit, this is a quite specific usecase and it is also not necessarily the concern of the form to be aware of the data structure in a Zend_Config and if I really need to do it, I can. I just have to add the subForms. So, it's fine I guess. Thanks again!
#9.1.1.1.1
Gordon
on
2009-02-04 16:48
(Reply)
Add Comment
|
CalendarQuicksearchLinks
ArchivesCategoriesSyndicate This BlogShow tagged entries |






In a new post to his blog today, Matthew O'Phinney dives ...
Tracked: Feb 11, 10:21