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