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