001    // Copyright 2008, 2010, 2011 The Apache Software Foundation
002    //
003    // Licensed under the Apache License, Version 2.0 (the "License");
004    // you may not use this file except in compliance with the License.
005    // You may obtain a copy of the License at
006    //
007    // http://www.apache.org/licenses/LICENSE-2.0
008    //
009    // Unless required by applicable law or agreed to in writing, software
010    // distributed under the License is distributed on an "AS IS" BASIS,
011    // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012    // See the License for the specific language governing permissions and
013    // limitations under the License.
014    
015    package org.apache.tapestry5.corelib.components;
016    
017    import org.apache.tapestry5.*;
018    import org.apache.tapestry5.annotations.Parameter;
019    import org.apache.tapestry5.annotations.SupportsInformalParameters;
020    import org.apache.tapestry5.dom.Element;
021    import org.apache.tapestry5.ioc.annotations.Inject;
022    import org.apache.tapestry5.services.javascript.JavaScriptSupport;
023    
024    /**
025     * Turns any arbitrary (X)HTML element into a component. The element's start and end
026     * tags are rendered, including any informal parameters and possibly an id
027     * attribute.  The id is provided by {@link JavaScriptSupport#allocateClientId(String)}
028     * (so it will be unique on the client side) and is available after the component
029     * renders using {@link #getClientId()}. The Any component has no template of its
030     * own but does render its body, if any.
031     * <p>
032     * Some common uses are:
033     * <ul>
034     * 
035     * <li>Applying a mixin to an ordinary HTML element. For example,
036     * the following turns an <i>img</i> element into a component that, via the
037     * {@link org.apache.tapestry5.corelib.mixins.RenderNotification RenderNotification} mixin, triggers event
038     * notifications when it enters the BeginRender and EndRender phases:
039     * 
040     * <pre>&lt;img t:type="any" t:mixins="renderNotification"&gt;</pre>
041     * 
042     * And the following renders a <i>td</i> element with the
043     * {@link org.apache.tapestry5.corelib.mixins.NotEmpty NotEmpty} mixin to ensure
044     * that a non-breaking space (&amp;nbsp;) is rendered if the td element would
045     * otherwise be empty:
046     * 
047     * <pre>&lt;td t:type="any" t:mixins="NotEmpty"&gt;</pre>
048     * </li>
049     * 
050     * <li>Providing a dynamically-generated client ID for an HTML element
051     * in a component rendered in a loop or zone (or more than once in a page), for
052     * use from JavaScript. (The component class will typically use
053     * {@link org.apache.tapestry5.annotations.InjectComponent InjectComponent}
054     * to get the component, then call {@link #getClientId()} to retrieve the ID.)
055     * 
056     * <pre>&lt;table t:type="any" id="clientId"&gt;</pre>
057     * 
058     * As an alternative to calling getClientId, you can use the
059     * {@link org.apache.tapestry5.corelib.mixins.RenderClientId RenderClientId}
060     * mixin to force the id attribute to appear in the HTML:
061     * 
062     * <pre>&lt;table t:type="any" t:mixins="RenderClientId"&gt;</pre>
063     * </li>
064     * 
065     * <li>Dynamically outputting a different HTML element depending on
066     * the string value of a property. For example, the following renders an element
067     * identified by the "element" property in the corresponding component class:
068     * 
069     * <pre>&lt;t:any element="prop:element" ... &gt;</pre>
070     * </li>
071     * 
072     * <li>As the base component for a new custom component, especially convenient
073     * when the new component should support informal parameters or needs a dynamically
074     * generated client ID:
075     * 
076     * <pre>public class MyComponent extends Any { ... }
077     * </li>
078     * </ul>
079     * 
080     * @tapestrydoc
081     */
082    @SupportsInformalParameters
083    public class Any implements ClientElement
084    {
085        /**
086         * The name of the element to be rendered, typically one of the standard (X)HTML
087         * elements, "div", "span", "a", etc., although practically any string will be
088         * accepted. The default comes from the template, or is "div" if the template
089         * does not specify an element.
090         */
091        @Parameter(defaultPrefix = BindingConstants.LITERAL)
092        private String element;
093    
094        /**
095         * The desired client id, which defaults to the component's id.
096         */
097        @Parameter(value = "prop:componentResources.id", defaultPrefix = BindingConstants.LITERAL)
098        private String clientId;
099    
100        private Element anyElement;
101    
102        private String uniqueId;
103    
104        @Inject
105        private ComponentResources resources;
106    
107        @Inject
108        private JavaScriptSupport javascriptSupport;
109    
110        String defaultElement()
111        {
112            return resources.getElementName("div");
113        }
114    
115        void beginRender(MarkupWriter writer)
116        {
117            anyElement = writer.element(element);
118    
119            uniqueId = null;
120    
121            resources.renderInformalParameters(writer);
122        }
123    
124        /**
125         * Returns the client id. This has side effects: this first time this is called (after the Any component renders
126         * its start tag), a unique id is allocated (based on, and typically the same as, the clientId parameter, which
127         * defaults to the component's id). The rendered element is updated, with its id attribute set to the unique client
128         * id, which is then returned.
129         * 
130         * @return unique client id for this component
131         */
132        public String getClientId()
133        {
134            if (anyElement == null)
135                throw new IllegalStateException(String.format(
136                        "Unable to provide client id for component %s as it has not yet rendered.", resources
137                                .getCompleteId()));
138    
139            if (uniqueId == null)
140            {
141                uniqueId = javascriptSupport.allocateClientId(clientId);
142                anyElement.forceAttributes("id", uniqueId);
143            }
144    
145            return uniqueId;
146        }
147    
148        void afterRender(MarkupWriter writer)
149        {
150            writer.end(); // the element
151        }
152    
153        void inject(JavaScriptSupport javascriptSupport, ComponentResources resources, String element, String clientId)
154        {
155            this.javascriptSupport = javascriptSupport;
156            this.resources = resources;
157            this.element = element;
158            this.clientId = clientId;
159        }
160    }