Custom Field

I am quite new to this. So far, everything is looking excellent. Well thought out set of components, good documentation etc. Thanks for the excellent work behind this tool kit.

I am trying to create a custom Field for me. Initially, I thought it is a CustomComponent and it is easy to do because all I want is to have a small horizontal layout with few existing components laid out. However, I want to use it as a Field in the Form because I will be doing data-binding into this component. This means, I have to inherit it from AbstractField rather than from CustomComponent. If I derive it from AbstractField, I don’t know how to achieve the UI part (layout with sub-components). Any help is appreciated.

I wonder, a CustomField similar to CustomComponent is nice to have?

Wow! It was easy. I just extended AbstractField and overridden the painting part!

Hehe. Nicely done. You got it fixed before anyone got around to answer the question.

Anyhow, welcome aboard!

Yes, you got it right.

For a general rule of thumb: Inherit the
CustomComponent
to create logical UI blocks that cannot be mapped to a single value and you don’t want to publish any implementation details (for example complete views, editors, etc). Inheriting the
AbstractField
is good is you intend to use your component inside
Forms
and there is a logical way the getValue/setValue functions work.

I am also trying to create a custom component by combining the already existing ones. Specifically I want to have a textfield with a button just beside it.
So I’m trying to achieve this without creating a new gwt component, as mentioned in this topic, by extending AbstractField.
Is this feasible with the current version of vaadin (6.1)?

This is what I got so far, (is this approach correct??)


public class TestComponent extends AbstractField {

    private TextField field = new TextField();
    private Button button = new Button("button");
    private GridLayout gridLayout = new GridLayout(2, 1);

    public TestComponent() {
        super();
        gridLayout.addComponent(field);
        gridLayout.addComponent(button);

        button.addListener(new Button.ClickListener() {

            public void buttonClick(ClickEvent event) {
//                requestRepaint();
            }
        });
    }

    public Button getButton() {
        return button;
    }

    public TextField getField() {
        return field;
    }

    @Override
    public void paintContent(PaintTarget target) throws PaintException {
        gridLayout.paintContent(target);
    }

    @Override
    public String getTag() {
        return gridLayout.getTag();
    }

    @Override
    public Class getType() {
        return field.getType();
    }
}

The component gets rendered correctly but I get an out of sync error when i click twice the button. I realised that i can avoid it if i place a requestRepaint inside the clicklistener.
However I still get this error if for example i add another separate button on the same panel/window, which has nothing to do with the custom field, and shows a sub window when clicked. When the sub window closes and i click the button of the custom field i get an out of sync error (which i also avoid if i place a requestrepaint on the custom field from the close listener of the window — but this is not a good solution since I can’t call repaint from other components…).

Any advice please?

Thank you

In order to do a composite component, base it to some exiting ComponentContainer. In most cases CustomComponent is a good base component.

Maybe something along these lines:


package com.example.compositetest;

import com.vaadin.Application;
import com.vaadin.ui.Window;

public class CompositetestApplication extends Application {
    @Override
    public void init() {
        Window mainWindow = new Window("Compositetest Application");
        for (int i = 0; i < 5; i++) {
            mainWindow.addComponent(new TestComponent("Test " + i));
        }
        setMainWindow(mainWindow);
    }

}

package com.example.compositetest;

import com.vaadin.ui.Button;
import com.vaadin.ui.HorizontalLayout;
import com.vaadin.ui.TextField;
import com.vaadin.ui.Button.ClickEvent;

public class TestComponent extends com.vaadin.ui.CustomComponent {

    private TextField field = new TextField();
    private Button button = new Button("clear");

    public TestComponent(String caption) {
        HorizontalLayout layout = new HorizontalLayout();
        layout.setSpacing(true);
        layout.addComponent(field);
        field.setCaption(caption);
        layout.addComponent(button);
        layout.setComponentAlignment(button, "b");
        setCompositionRoot(layout);
        button.addListener(new Button.ClickListener() {

            public void buttonClick(ClickEvent event) {
                field.setValue("");

            }
        });

    }

    String getValue() {
        return field.toString();
    }
}

(Above component doesn’t do anything too useful, but it is easy to develop further).

Note that CustomComponent is designed to have minimal API. This allows you to hide API from wrapped components and only expose API you want to. (in this case - getValue()).

Thank you for the response Joonas!!
The problem is that just as in Syam’s case, I too need to use it inside a form and use it for databinding. Thus, it has to be a Field type right?? That’s why I tried AbstractField.

I suspect (
haven’t done any really low debugging yet…
) that I get the out of sync error because the button and textfield components do not get painted after the call of an event (
such as click event
), only the layout component gets painted (
perhaps because of the getTag method that returns the getTag method of the layout component? but if not used the custom component does not get rendered
), that’s why when i call manually request repaint all works fine.

Any ideas plz??

Ok. Now I see the problem.

You could implement Field interface in that TestComponent, but unfortunately that would require implementing quite a few methods. Good side is that you could implement them to proxy to TextField. Something like this:


package com.example.compositetest;

import java.util.Collection;

import com.vaadin.data.Property;
import com.vaadin.data.Validator;
import com.vaadin.data.Validator.InvalidValueException;
import com.vaadin.ui.Button;
import com.vaadin.ui.Field;
import com.vaadin.ui.HorizontalLayout;
import com.vaadin.ui.TextField;
import com.vaadin.ui.Button.ClickEvent;

public class TestComponent extends com.vaadin.ui.CustomComponent implements
        Field {

    private TextField field = new TextField();
    private Button button = new Button("clear");

    public TestComponent(String caption) {
        HorizontalLayout layout = new HorizontalLayout();
        layout.setSpacing(true);
        layout.addComponent(field);
        field.setCaption(caption);
        layout.addComponent(button);
        layout.setComponentAlignment(button, "b");
        setCompositionRoot(layout);
        button.addListener(new Button.ClickListener() {

            public void buttonClick(ClickEvent event) {
                field.setValue("");

            }
        });

    }

    public String getRequiredError() {
        return field.getRequiredError();
    }

    public boolean isRequired() {
        return field.isRequired();
    }

    public void setRequired(boolean required) {
        field.setRequired(required);
    }

    public void setRequiredError(String requiredMessage) {
        field.setRequiredError(requiredMessage);

    }

    public boolean isInvalidCommitted() {
        return field.isInvalidCommitted();
    }

    public void setInvalidCommitted(boolean isCommitted) {
        field.setInvalidCommitted(isCommitted);

    }

    public void commit() throws SourceException, InvalidValueException {
        field.commit();
    }

    public void discard() throws SourceException {
        field.discard();

    }

    public boolean isModified() {
        return field.isModified();
    }

    public boolean isReadThrough() {
        return field.isReadThrough();
    }

    public boolean isWriteThrough() {

        return field.isWriteThrough();
    }

    public void setReadThrough(boolean readThrough) throws SourceException {
        field.setReadThrough(readThrough);

    }

    public void setWriteThrough(boolean writeThrough) throws SourceException,
            InvalidValueException {
        field.setWriteThrough(writeThrough);

    }

    public void addValidator(Validator validator) {
        field.addValidator(validator);

    }

    public Collection<?> getValidators() {

        return field.getValidators();
    }

    public boolean isInvalidAllowed() {
        return field.isInvalidAllowed();
    }

    public boolean isValid() {
        return field.isValid();
    }

    public void removeValidator(Validator validator) {
        field.removeValidator(validator);

    }

    public void setInvalidAllowed(boolean invalidValueAllowed)
            throws UnsupportedOperationException {
        field.setInvalidAllowed(invalidValueAllowed);

    }

    public void validate() throws InvalidValueException {
        field.validate();
    }

    public Class<?> getType() {
        return field.getType();
    }

    public void setValue(Object newValue) throws ReadOnlyException,
            ConversionException {
        field.setValue(newValue);

    }

    public void addListener(ValueChangeListener listener) {
        field.addListener(listener);

    }

    public void removeListener(ValueChangeListener listener) {
        field.removeListener(listener);

    }

    public void valueChange(com.vaadin.data.Property.ValueChangeEvent event) {
        field.valueChange(event);

    }

    public Property getPropertyDataSource() {
        return field.getPropertyDataSource();
    }

    public void setPropertyDataSource(Property newDataSource) {
        field.setPropertyDataSource(newDataSource);

    }

    public void focus() {
        field.focus();
    }

    public int getTabIndex() {
        return field.getTabIndex();

    }

    public void setTabIndex(int tabIndex) {
        field.setTabIndex(tabIndex);

    }

    public Object getValue() {
        return field.getValue();
    }
}

(not tested)

Hi All, that work very well. There is a minor rendering problem: the caption of the TextField is placed on top, but it should be on the left. I think this is due to the horizontal layout used inside the CustomComponent.

Any hint on how to get rid of this?

Thank you very much,

Ivan

I made a step forward adding getter and setter for the caption property to the custom component.


    public void setCaption(String caption) {
        field.setCaption(caption);
    }
    
    public String getCaption() {
        return field.getCaption();
    }

Now I have both captions displayed: the top one plus the left one. :blink:

Ivan

Ok, I get it work. I change the getter/setter to use its own capion, leaving the TextField caption blank (inside the CustomComponent of course).

For sure it is a workaround, but it works. :bashful:


    private String caption;

    public void setCaption(String caption) {
    	this.caption = caption;
    }
    
    public String getCaption() {
    	return caption;
    }

Ivan

The parent layout renders the caption in the place it sees fit. A FormLayout will render it to the left, a Horizontal/VerticalLayout will render it above the field. In your case you need special caption handling in your composite so the caption for the inside TextField will not be rendered in your HorizontalLayout. On the other hand you want the FormLayout (containing your composite) to render the caption of your composite, i.e. the composite should have a caption, but the contained field should not. Just as you have described.

Thank you very much Joonas and also the rest of the guys for your great help!! :slight_smile:

Joonas, my initial approach was very similar to your solution (extend Component implement Field), the only difference was that i extended Panel instead of custom or abstract component. Big mistake!! As a result it didn’t get painted at all so i followed the other route (field only).

Thanks again, works great!!

p.s. i’m really enjoying your framework!!! :slight_smile:

Hi All,

as I told you in the last post the major issues was resolved. Now I’d like to see components aligned as in the “Information” panel (see the image below, that is just an example found on google to give you the idea).

The problem is that if I set the width of the custom component to 100% the childs components are distrybuted equally in the horizontal layout: no matter what… I cannot control the width of the TextField, only the Button can be changed.

Below is my actual constructor, but I tryied a lot of things. So probably I’m missing some basic concept here. :*)

    public ActionTextField(String caption, String buttonCaption, Button.ClickListener listener) {
        super.setWidth(100, UNITS_PERCENTAGE);

        layout.addComponent(field);        
        this.setCaption(caption);     
        button.setCaption(buttonCaption);
        button.addListener(listener);
        
        layout.addComponent(button);
        layout.setComponentAlignment(button, "br");
        layout.setExpandRatio(field, 2);
        layout.setExpandRatio(button, 1);

        // This one, work but it's not what I had in mind ;)
        //field.setWidth(290, UNITS_PIXELS);
        
        layout.setSpacing(true);
        setCompositionRoot(layout);
    }

Thank you very much for the support.

Ivan

Try to make layout also 100% wide. If you want to expand only the textfield, do not give expandratio to button. This should give you button with just enough width to fit the button caption and textfield that takes the rest of the space.

I gave it a try, but it doesn’t work. :frowning:

The only way it work is to actually fix the dimension of the embedded TextField. I will post my actual use case this evening. Just to be able to discuss on a common play-groud.

Cheers,

Ivan

Hi All,

i post my complete test app. The test app work nicely but i have problems with the alignment of the TextField inside the custom component.

VaadinComplexFrameApp

package org.is.sample.complex;

import com.vaadin.Application;
import com.vaadin.ui.Window;

public class VaadinComplexFrameApp extends Application {
	private static final long serialVersionUID = 6581573252353452472L;

	private final Window mainWindow = new Window("VaadinComplexFrameApp");

	@Override
	public void init() {
		ComplexLayout root = new ComplexLayout();
		mainWindow.setContent(root);
		setMainWindow(mainWindow);		
	}

}

ComplexLayout panel

package org.is.sample.complex;

import com.vaadin.data.util.BeanItem;
import com.vaadin.ui.Form;
import com.vaadin.ui.Panel;

public class ComplexLayout extends Panel {
	private static final long serialVersionUID = -9015184748756501389L;

	public ComplexLayout() {
		super();
		initComponents();
	}

	private void initComponents() {
		Person person = new Person("Jhon", "Smith", 21, "Lincoln Avenue", "1234", "NY");
		Form form = getForm(person);		
		this.addComponent(form);
	}
	
	private Form getForm(Person person) {
		Form form = new Form();
		form.setFormFieldFactory(new MyFormFieldFactory());
		form.setCaption("Person");
		form.setWidth("400px");
		form.setItemDataSource(new BeanItem(person));
		form.setVisibleItemProperties(new String[]{"name", "surname", "age"});
		form.setImmediate(true);	
		
		return form;
	}	
}

Person bean

package org.is.sample.complex;

import java.io.Serializable;


public class Person implements Serializable {
	private static final long serialVersionUID = -3798892320151628159L;
	private String name = "";
	private String surname = "";
	private int age = -1;
	private String address = "";
	private String number = "";
	private String city = "";

	
	public Person(String name, String surname, int age, String address,
		String number, String city) {
		super();
		this.name = name;
		this.surname = surname;
		this.age = age;
		this.address = address;
		this.number = number;
		this.city = city;
	}
	
	public String getName() {
		return name;
	}	
	public void setName(String name) {
		this.name = name;
	}
	public String getSurname() {
		return surname;
	}
	public void setSurname(String surname) {
		this.surname = surname;
	}
	public int getAge() {
		return age;
	}
	public void setAge(int age) {
		this.age = age;
	}

	public String getAddress() {
		return address;
	}

	public void setAddress(String address) {
		this.address = address;
	}

	public String getNumber() {
		return number;
	}

	public void setNumber(String number) {
		this.number = number;
	}

	public String getCity() {
		return city;
	}

	public void setCity(String city) {
		this.city = city;
	}
	
	
}

MyFormFieldFactory

package org.is.sample.complex;

import java.text.MessageFormat;

import com.vaadin.data.Item;
import com.vaadin.ui.Button;
import com.vaadin.ui.Component;
import com.vaadin.ui.DefaultFieldFactory;
import com.vaadin.ui.Field;
import com.vaadin.ui.Form;
import com.vaadin.ui.FormFieldFactory;
import com.vaadin.ui.Button.ClickEvent;

public class MyFormFieldFactory extends DefaultFieldFactory implements FormFieldFactory {
	private static final long serialVersionUID = 5845192054295782123L;

	public Field createField(Item item, final Object propertyId, Component uiContext) {
		Field f;
		if ("name".equals(propertyId)) {
			f = new ActionTextField("name", "...",
				new Button.ClickListener() {
					private static final long serialVersionUID = 600786528349423366L;

					public void buttonClick(ClickEvent event) {
						event.getButton().getApplication().getMainWindow().showNotification(
							MessageFormat.format("Button:{0} clicked!", propertyId)
						);
					}				
				}
			);
		}
		else {
			f = super.createField(item, propertyId, uiContext);
		}		
		f.setParent(((Form)uiContext).getLayout());
		f.setWidth("100%");
		return f;		
	}

}

and last but not least the actual custom component
ActionTextField

package org.is.sample.complex;

import java.util.Collection;

import com.vaadin.data.Property;
import com.vaadin.data.Validator;
import com.vaadin.data.Validator.InvalidValueException;
import com.vaadin.ui.Button;
import com.vaadin.ui.CustomComponent;
import com.vaadin.ui.Field;
import com.vaadin.ui.HorizontalLayout;
import com.vaadin.ui.TextField;

public class ActionTextField extends CustomComponent implements Field {
	private static final long serialVersionUID = 6116613180668324589L;
	
	private HorizontalLayout layout = new HorizontalLayout();
	private TextField field = new TextField();
    private Button button = new Button();
    private String caption;

    public ActionTextField() {
    }
    
    public ActionTextField(String caption, String buttonCaption, Button.ClickListener listener) {
    	super();
        super.setWidth(100, UNITS_PERCENTAGE);
        layout.setWidth(100, UNITS_PERCENTAGE);

        layout.addComponent(field);        
        this.setCaption(caption);     
        layout.setComponentAlignment(field, "l");        
        
        button.setCaption(buttonCaption);
        button.addListener(listener);
        
        layout.addComponent(button);
        layout.setComponentAlignment(button, "mr");
        layout.setExpandRatio(field, 3);
               
        layout.setSpacing(false);
        setCompositionRoot(layout);
    }

	public ActionTextField(String caption, String value, String buttonCaption, Button.ClickListener listener) {
    	this(caption, buttonCaption, listener);
        this.setValue(value);
    }
    
    public String getRequiredError() {
        return field.getRequiredError();
    }

    public boolean isRequired() {
        return field.isRequired();
    }

    public void setRequired(boolean required) {
        field.setRequired(required);
    }

    public void setRequiredError(String requiredMessage) {
        field.setRequiredError(requiredMessage);

    }

    public boolean isInvalidCommitted() {
        return field.isInvalidCommitted();
    }

    public void setInvalidCommitted(boolean isCommitted) {
        field.setInvalidCommitted(isCommitted);

    }

    public void commit() throws SourceException, InvalidValueException {
        field.commit();
    }

    public void discard() throws SourceException {
        field.discard();

    }

    public boolean isModified() {
        return field.isModified();
    }

    public boolean isReadThrough() {
        return field.isReadThrough();
    }

    public boolean isWriteThrough() {

        return field.isWriteThrough();
    }

    public void setReadThrough(boolean readThrough) throws SourceException {
        field.setReadThrough(readThrough);

    }

    public void setWriteThrough(boolean writeThrough) throws SourceException,
            InvalidValueException {
        field.setWriteThrough(writeThrough);

    }

    public void addValidator(Validator validator) {
        field.addValidator(validator);

    }

    public Collection<?> getValidators() {

        return field.getValidators();
    }

    public boolean isInvalidAllowed() {
        return field.isInvalidAllowed();
    }

    public boolean isValid() {
        return field.isValid();
    }

    public void removeValidator(Validator validator) {
        field.removeValidator(validator);

    }

    public void setInvalidAllowed(boolean invalidValueAllowed)
            throws UnsupportedOperationException {
        field.setInvalidAllowed(invalidValueAllowed);

    }

    public void validate() throws InvalidValueException {
        field.validate();
    }

    public Class<?> getType() {
        return field.getType();
    }

    public void setValue(Object newValue) throws ReadOnlyException,
            ConversionException {
        field.setValue(newValue);

    }

    public void addListener(ValueChangeListener listener) {
        field.addListener(listener);

    }

    public void removeListener(ValueChangeListener listener) {
        field.removeListener(listener);

    }

    public void valueChange(com.vaadin.data.Property.ValueChangeEvent event) {
        field.valueChange(event);

    }

    public Property getPropertyDataSource() {
        return field.getPropertyDataSource();
    }

    public void setPropertyDataSource(Property newDataSource) {
        field.setPropertyDataSource(newDataSource);

    }

    public void focus() {
        field.focus();
    }

    public int getTabIndex() {
        return field.getTabIndex();

    }

    public void setTabIndex(int tabIndex) {
        field.setTabIndex(tabIndex);

    }

    public Object getValue() {
        return field.getValue();
    }

    public void setCaption(String caption) {
    	this.caption = caption;
    }
    
    public String getCaption() {
    	return caption;
    }
    
    public float getWidth() {
    	if (layout != null)
    		return layout.getWidth();
    	else
    		return -1;
    }
    
    public void setWidth(float width, int unit) {
    	if (layout != null)
    		layout.setWidth(width, unit);
    }
    
    public void setWidth(String width) {
    	if (layout != null)
    		layout.setWidth(width);
    }    
    
}

Can anyone give me an hint?

Thank you,

Ivan

It is not that easy to create a CustomField any more (6.2 changes) since we have to go through the widgetset creation procedure.
Isn’t it a good idea to have a component in the core itself called CustomField that extended from AbstractField but has similar functionality of CustomComponent so that those who want to quickly create customized Field components can use it?

If anyone is interested I was able to make it work properly just using the following constructor:


    public ActionTextField(String caption, String buttonCaption, Button.ClickListener listener) {
        super();
        setWidth("100%");
        layout.setWidth("100%");

        field.setWidth("100%");
        layout.addComponent(field);       
        this.setCaption(caption);                 
       
        button.setCaption(buttonCaption);
        button.addListener(listener);
       
        layout.addComponent(button);
        
        layout.setComponentAlignment(button, "middle");
        layout.setExpandRatio(field, 1.0f);
        layout.setComponentAlignment(button, "middle right");
               
        layout.setSpacing(true);
        setCompositionRoot(layout);
    }

Cheers

I implement quite similar way but encounter an annoying issue with required field indicator. Used in the FormLayout, the indicator is rendered next to the underlying field instead of custom field’s caption.

Does anyone know how to get around this? I’m using Vaadin 6.4.1 by the way.

Thanks,