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 }