001 /*
002 * Apache License
003 * Version 2.0, January 2004
004 * http://www.apache.org/licenses/
005 *
006 * Copyright 2008-2010 by chenillekit.org
007 *
008 * Licensed under the Apache License, Version 2.0 (the "License");
009 * you may not use this file except in compliance with the License.
010 * You may obtain a copy of the License at
011 *
012 * http://www.apache.org/licenses/LICENSE-2.0
013 */
014
015 package org.chenillekit.tapestry.core.components;
016
017 import org.apache.tapestry5.Block;
018 import org.apache.tapestry5.ClientElement;
019 import org.apache.tapestry5.ComponentResources;
020 import org.apache.tapestry5.ValueEncoder;
021 import org.apache.tapestry5.annotations.BeginRender;
022 import org.apache.tapestry5.annotations.Component;
023 import org.apache.tapestry5.annotations.Environmental;
024 import org.apache.tapestry5.annotations.Parameter;
025 import org.apache.tapestry5.annotations.Persist;
026 import org.apache.tapestry5.corelib.components.Delegate;
027 import org.apache.tapestry5.corelib.components.Loop;
028 import org.apache.tapestry5.ioc.annotations.Inject;
029 import org.apache.tapestry5.services.ComponentDefaultProvider;
030 import org.apache.tapestry5.services.javascript.JavaScriptSupport;
031 import org.apache.tapestry5.util.StringToEnumCoercion;
032 import org.chenillekit.tapestry.core.internal.PagedSource;
033 import org.chenillekit.tapestry.core.internal.PagerPosition;
034
035 /**
036 * Provides a paged list similar to the grid component. However, this uses a ul
037 * instead of a table by default. The list and its items are configurable via
038 * parameters.
039 *
040 * @version $Id: PagedLoop.java 674 2010-07-29 12:47:25Z homburgs $
041 */
042 public class PagedLoop implements ClientElement
043 {
044 @Environmental
045 private JavaScriptSupport javascriptSupport;
046
047 @Parameter(value = "prop:componentResources.id", defaultPrefix = "literal")
048 private String clientId;
049
050 /**
051 * The element to render. If not null, then the loop will render the indicated element around its body (on each pass through the loop).
052 * The default is derived from the component template.
053 */
054 @Parameter(value = "prop:componentResources.elementName", defaultPrefix = "literal")
055 private String element;
056
057 /**
058 * Defines the collection of values for the loop to iterate over.
059 */
060 @SuppressWarnings("unused")
061 @Parameter(required = true)
062 private Iterable<?> source;
063
064 private PagedSource<?> pagedSource;
065
066 /**
067 * Defines where the pager (used to navigate within the "pages" of results)
068 * should be displayed: "top", "bottom", "both" or "none".
069 */
070 @Parameter(value = "bottom", defaultPrefix = "literal")
071 private String pagerPosition;
072
073 private PagerPosition internalPagerPosition;
074
075 /**
076 * The number of rows of data displayed on each page. If there are more rows than will fit, the Grid will divide
077 * up the rows into "pages" and (normally) provide a pager to allow the user to navigate within the overall result set.
078 */
079 @Parameter("25")
080 private int rowsPerPage;
081
082 @Persist
083 private int currentPage;
084
085 /**
086 * The current value, set before the component renders its body.
087 */
088 @SuppressWarnings("unused")
089 @Parameter
090 private Object value;
091
092 /**
093 * If true and the Loop is enclosed by a Form, then the normal state saving logic is turned off.
094 * Defaults to false, enabling state saving logic within Forms.
095 */
096 @SuppressWarnings("unused")
097 @Parameter(name = "volatile")
098 private boolean isVolatile;
099
100 /**
101 * The index into the source items.
102 */
103 @SuppressWarnings("unused")
104 @Parameter
105 private int index;
106
107 /**
108 * Value encoder for the value, usually determined automatically from the type of the property bound to the value
109 * parameter.
110 */
111 @Parameter(required = true)
112 private ValueEncoder encoder;
113
114 @SuppressWarnings("unused")
115 @Component(parameters = {"source=pagedSource",
116 "element=prop:element", "value=inherit:value",
117 "volatile=inherit:volatile", "encoder=inherit:encoder",
118 "index=inherit:index"})
119 private Loop loop;
120
121 @Component(parameters = {"source=pagedSource", "rowsPerPage=rowsPerPage",
122 "currentPage=currentPage"})
123 private Pager internalPager;
124
125 @SuppressWarnings("unused")
126 @Component(parameters = "to=pagerTop")
127 private Delegate pagerTop;
128
129 @SuppressWarnings("unused")
130 @Component(parameters = "to=pagerBottom")
131 private Delegate pagerBottom;
132
133 /**
134 * A Block to render instead of the table (and pager, etc.) when the source
135 * is empty. The default is simply the text "There is no data to display".
136 * This parameter is used to customize that message, possibly including
137 * components to allow the user to create new objects.
138 */
139 @Parameter(value = "block:empty")
140 private Block empty;
141
142 @Inject
143 private ComponentResources resources;
144
145 @Inject
146 private ComponentDefaultProvider defaultProvider;
147
148 private String assignedClientId;
149
150 public String getElement()
151 {
152 return element;
153 }
154
155 public Object getPagerTop()
156 {
157 return internalPagerPosition.isMatchTop() ? internalPager : null;
158 }
159
160 public Object getPagerBottom()
161 {
162 return internalPagerPosition.isMatchBottom() ? internalPager : null;
163 }
164
165 public PagedSource<?> getPagedSource()
166 {
167 return pagedSource;
168 }
169
170 public int getRowsPerPage()
171 {
172 return rowsPerPage;
173 }
174
175 public void setRowsPerPage(int rowsPerPage)
176 {
177 this.rowsPerPage = rowsPerPage;
178 }
179
180 public int getCurrentPage()
181 {
182 return currentPage;
183 }
184
185 public void setCurrentPage(int currentPage)
186 {
187 this.currentPage = currentPage;
188 }
189
190 ValueEncoder defaultEncoder()
191 {
192 return defaultProvider.defaultValueEncoder("value", resources);
193 }
194
195 @SuppressWarnings("unchecked")
196 Object setupRender()
197 {
198 if (currentPage == 0)
199 currentPage = 1;
200
201 assignedClientId = javascriptSupport.allocateClientId(clientId);
202 internalPagerPosition = new StringToEnumCoercion<PagerPosition>(
203 PagerPosition.class).coerce(pagerPosition);
204
205 pagedSource = new PagedSource(source);
206
207 int availableRows = pagedSource.getTotalRowCount();
208
209 // If there's no rows, display the empty block placeholder.
210 if (availableRows == 0)
211 {
212 return empty;
213 }
214
215 int startIndex = (currentPage - 1) * rowsPerPage;
216 int endIndex = Math.min(startIndex + rowsPerPage - 1,
217 availableRows - 1);
218
219 pagedSource.prepare(startIndex, endIndex);
220
221 return null;
222 }
223
224 @BeginRender
225 Object begin()
226 {
227 // Skip rendering of component (template, body, etc.) when there's
228 // nothing to display.
229 // The empty placeholder will already have rendered.
230 return (pagedSource.getTotalRowCount() != 0);
231 }
232
233 void onAction(int newPage)
234 {
235 // TODO: Validate newPage in range
236 currentPage = newPage;
237 }
238
239 /**
240 * Returns a unique id for the element. This value will be unique for any given rendering of a
241 * page. This value is intended for use as the id attribute of the client-side element, and will
242 * be used with any DHTML/Ajax related JavaScript.
243 */
244 public String getClientId()
245 {
246 return assignedClientId;
247 }
248 }