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><img t:type="any" t:mixins="renderNotification"></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 (&nbsp;) is rendered if the td element would
045 * otherwise be empty:
046 *
047 * <pre><td t:type="any" t:mixins="NotEmpty"></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><table t:type="any" id="clientId"></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><table t:type="any" t:mixins="RenderClientId"></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><t:any element="prop:element" ... ></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 }