You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@tapestry.apache.org by "Howard M. Lewis Ship (JIRA)" <ji...@apache.org> on 2008/10/17 23:37:44 UTC

[jira] Commented: (TAP5-280) Fields injected by AjaxFormLoop sometimes have incorrect values after submits where validation fails.

    [ https://issues.apache.org/jira/browse/TAP5-280?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=12640675#action_12640675 ] 

Howard M. Lewis Ship commented on TAP5-280:
-------------------------------------------

I'm not sure what a fix for this one is; There are certainly some problems with ajax updates, since we have to allocate unique ids when rendering the new content and that makes it difficult to match the components to the field inputs (and error messages) stored inside the ValidationTracker.

This is more noticeable because client validation is off; normally, most input validations would occur on the client and not even be sent to the server. 

Example: a text field may be injected (as part of AjaxFormLoop) with a name and client id of "username12345" (the 12346 is a unique id generated from the system clock).  When the whole page re-renders, the client id might instead by "username_2"  (i.e., the 4th time the username component renders, part of the series "username", "username_0", "username_1", "username_2", etc.).  

The ValidationTracker is keyed against control name (i.e., "username12345" is used to locate user input and related validation errors).  With the full-page re-render, a new and non-matching control name ("username_2") is used.

A fix for this is tricky; perhaps involving some additional object to arbitrate between the generated control names (whether full-page or ajax) and the names used inside the ValidationTracker.  I'm not quite sure what that would look like or how it would operate.

> Fields injected by AjaxFormLoop sometimes have incorrect values after submits where validation fails.
> -----------------------------------------------------------------------------------------------------
>
>                 Key: TAP5-280
>                 URL: https://issues.apache.org/jira/browse/TAP5-280
>             Project: Tapestry 5
>          Issue Type: Bug
>          Components: tapestry-core
>    Affects Versions: 5.0.15
>            Reporter: Shawn Brownfield
>
> Using the below .tml, .java. and .js files:
> 1. Click Add Group.
> 2. Add values "a" and "1" in the upper group, "c" and "3" in the lower group.
> 3. Click Save.
> 4. Add a row in the upper group and enter "b" and "b".
> 5. Click Save.
> ==> Values "c" and "3" are copied into the last row of the upper group (validation has failed).
> The only values ever lost/copied over are values of rows that were inserted via the addRowLink.  Upon failure of validation, it looks as though the ValidationTracker is trying to look up the values for the new rows using the control id from the render phase, but the submission had "mangled"/"uniquified" control ids (with :?????? added) for all the inputs, so that is how these values are stored in the ValidationTracker.
> AjaxFormLoopTest.tml:
> <html xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">
> 	<head/>
> 	<body>
> 		<h1>Nested AjaxFormLoop Test</h1>
> 		<t:form t:id="formLoopTestForm" t:clientValidation="false">	   
> 			<div t:type="AjaxFormLoop"
> 				 t:id="outerFormLoop"
> 				 t:source="outerList"
> 				 t:value="tempOuter"
> 				 t:encoder="outerEncoder"
> 			>
> 				<table style="background-color: #ece9d8; margin: 2px 0;">
> 					<tbody>
> 						<tr>
> 							<th>String</th>
> 							<th>Number</th>
> 						</tr>
> 						<tr t:type="AjaxFormLoop"
> 							t:id="innerFormLoop"
> 							t:source="tempOuter.list"
> 							t:value="tempInner"
> 						    t:context="tempOuter.id"
> 							t:encoder="innerEncoder"
> 						>
> 							<td><t:textField t:value="tempInner.string"/></td>
> 							<td><t:textField t:value="tempInner.number"/></td>
> 							<t:parameter name="addRow">
> 								<td colspan="2">
> 									<t:addRowLink>Add Row</t:addRowLink>
> 								</td>
> 							</t:parameter>
> 						</tr>
> 					</tbody>
> 				</table>
> 				<t:parameter name="addRow">
> 					<t:addRowLink>Add Group</t:addRowLink>
> 				</t:parameter>
> 			</div>
> 		
> 			<br/>
> 						
> 			<t:submit t:id="save" value="Save"/>
> 			<t:actionLink t:id="clear">Clear</t:actionLink>
> 		</t:form>
> 	</body>
> </html>
> AjaxFormLoopTest.java:
> @IncludeJavaScriptLibrary("context:javascript/script.js")
> public class AjaxFormLoopTest {	
> 	@Persist
> 	@Property
> 	private List<Outer> outerList;
> 	
> 	@Property
> 	private Outer tempOuter;
> 	
> 	@Property 
> 	private Inner tempInner;
> 	
> 	void beginRender() {
> 		if (outerList == null) {
> 			outerList = new ArrayList<Outer>();
> 			Outer outer = new Outer(0);
> 			outerList.add(outer);
> 		}
> 	}
> 	Object onAddRowFromOuterFormLoop() {
> 		int nextId = outerList.size() + 1;
> 		Outer outer = new Outer(nextId);
> 		outerList.add(outer);
> 		
> 		return outer;
> 	}
> 	
> 	Object onAddRowFromInnerFormLoop(Integer outerId) {
> 		Outer outer = getOuterById(outerId);
> 		List<Inner> list = outer.getList();
> 		Inner inner = new Inner();
> 		list.add(inner);
> 		inner.setOuter(outer);
> 		
> 		return inner;
> 	}
> 	
> 	void onActionFromClear() {
> 		outerList = null;
> 	}
> 	////////////////////////////////
> 	// Encoders
> 	////////////////////////////////
> 	private final PrimaryKeyEncoder<Integer, Outer> outerEncoder =
> 		new PrimaryKeyEncoder<Integer, Outer>() {
> 			public void prepareForKeys(List arg0) {}
> 			public Integer toKey(Outer outer) {
> 				return outer.getId();
> 			}			
> 			public Outer toValue(Integer key) {
> 				Outer ret = getOuterById(key);
> 				if (ret == null) throw new RuntimeException("outerEncoder could not retreive item for key:" + key);
> 				return ret;
> 			}
> 		};
> 	public PrimaryKeyEncoder<Integer, Outer> getOuterEncoder() { return outerEncoder; }
> 	private final PrimaryKeyEncoder<EncoderKey, Inner> innerEncoder =
> 		new PrimaryKeyEncoder<EncoderKey, Inner>() {
> 			public void prepareForKeys(List arg0) {}
> 			public EncoderKey toKey(Inner inner) {
> 				Outer outer = inner.getOuter();
> 				return new EncoderKey(outer.getId(), outer.getList().indexOf(inner));
> 			}			
> 			public Inner toValue(EncoderKey key) {
> 				Inner ret = null;
> 				
> 				Outer outer = getOuterById(key.getGroupId());
> 				if (outer != null) {
> 					ret = outer.getList().get(key.getIndex());
> 				}
> 				
> 				if (ret == null) throw new RuntimeException("innerEncoder could not retreive item for key:" + key);
> 				return ret;
> 			}
> 	};
> 	public PrimaryKeyEncoder<EncoderKey, Inner> getInnerEncoder() { return innerEncoder; }
> 	////////////////////////////////
> 	// Helpers
> 	////////////////////////////////
> 	private Outer getOuterById(Integer outerId) {
> 		Outer outer = null;
> 		for (Outer o : outerList) {
> 			if (o.getId().equals(outerId)) {
> 				outer = o;
> 				break;
> 			}
> 		}
> 		
> 		return outer;
> 	}
> 	
> 	// Referenced classes
> 	public static class Outer {
> 		private Integer id;
> 		private List<Inner> list;
> 		
> 		public Outer(Integer id) {
> 			this.id = id;
> 			list = new ArrayList<Inner>();
> 			Inner inner = new Inner();
> 			inner.setOuter(this);
> 			list.add(inner);
> 		}
> 		public Integer getId() { return id; }
> 		public void setId(Integer id) {	this.id = id; }
> 		public List<Inner> getList() { return list;	}
> 		public void setList(List<Inner> list) {	this.list = list; }
> 	}
> 	
> 	public static class Inner {
> 		private String string;
> 		private Integer number;
> 		private Outer outer;
> 		public String getString() { return string; }
> 		public void setString(String string) { this.string = string; }
> 		public Integer getNumber() { return number; }
> 		public void setNumber(Integer number) { this.number = number; }
> 		
> 		public Outer getOuter() { return outer; }
> 		public void setOuter(Outer outer) { this.outer = outer; }
> 	}
> 	public static class EncoderKey implements Serializable{
> 		private static final long serialVersionUID = 1L;
> 		
> 		private final Integer groupId;
> 		private final Integer index;
> 		
> 		public final static String SEPARATOR = "_";
> 		
> 		
> 		public EncoderKey(Integer q, Integer i){
> 			this.groupId = q;
> 			this.index = i;
> 		}
> 		
> 		public Integer getGroupId() {
> 			return groupId;
> 		}
> 		public Integer getIndex() {
> 			return index;
> 		}
> 		
> 		@Override
> 		public boolean equals(Object obj){
> 			if (!(obj instanceof EncoderKey)){
> 				return super.equals(obj);
> 			}
> 			EncoderKey other = (EncoderKey)obj;
> 			return this.groupId.equals(other.groupId) && this.index.equals(other.index);
> 		}
> 		
> 		@Override
> 		public int hashCode(){
> 			return this.groupId * this.index * 31;
> 		}		
> 		
> 		public int compareTo(EncoderKey other){
> 			int test = this.groupId.compareTo(other.groupId);
> 			return test != 0 ? test : this.index.compareTo(other.index);			
> 		}	
> 		public String toString(){
> 			return "Group: " + groupId + " Index: " +  index;
> 		}
> 		
> 		public String encode(){
> 			return this.getGroupId()+EncoderKey.SEPARATOR+this.getIndex();
> 		}
> 	}
> }
> script.js:
> /** Override to prevent client-side validation. */
> Tapestry.Initializer.validate = function (field, specs) { return; };

-- 
This message is automatically generated by JIRA.
-
You can reply to this email to add a comment to the issue online.


---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tapestry.apache.org
For additional commands, e-mail: dev-help@tapestry.apache.org