Monday, November 15, 2010

Browser aware widget

In my work I recently stood before the problem of creating a widget to handle file uploading. The widgets should display only an upload button and when pressed the file chooser dialog box should pop up. When the user finished selecting a file the form should be submitted to the server.

The problem was that the file input element works differently for different browsers, so I had to create a widgets which produces html code for different browser. In GWT you accomplish this by Deferred binding.

File Uploading When googling around on the internet I found two solutions, for IE I could just have a button and when clicked it called the click method on an hidden input element. For the other browser I had to utilize a css hack where we place the input element in front of a button and make it transparent. For a better description of the css hack se the link: http://www.quirksmode.org/dom/inputfile.html

In creating browser dependent code you use deferred bindings, you do it by either having an interface or a class and then tell the compiler that when compiling for a specific browser you should use a specific implementation or subclass. It is good practice to encapsulate that code in a class so I started out with a class FileUploader which is a composite.
public class FileUploader extends Composite {
 
    FileUploaderImpl impl = GWT.create(FileUploaderImpl.class);

    public FileUploader(){
        GWT.log("FileUploader Constructor");
  
        initWidget(impl.getFormPanel());
  
    }

    public void setAction(String action){
        impl.getFormPanel().setAction(action);
    }
}
The important line above is the GWT.create(FileUploaderImpl.class). This is where the compiler will utilize deferred binding. We have the FileUploaderImpl be an abstract class providing access methods needed, and we create subclasses for each case where we need special implementation. In our case we have a FileUploaderImplIE for internet explorer and a FileUploaderImplStandard for all the other browsers.
public abstract class FileUploaderImpl {

    protected final FormPanel formPanel = new FormPanel();
    protected final FileUpload inputFile = new FileUpload();
    protected final Button button = new Button();

    public FileUploaderImpl(){
        getFormPanel().setEncoding(FormPanel.ENCODING_MULTIPART);
        getFormPanel().setAction("/uploadService");
        getFormPanel().setMethod(FormPanel.METHOD_POST);
        inputFile.addChangeHandler(new OnChangeHandler(getFormPanel()));
    }

    public abstract void setSize(String width, String height);

    public FormPanel getFormPanel() {
        return formPanel;
    }

    private static class OnChangeHandler implements ChangeHandler{

        private FormPanel form;
        public OnChangeHandler(FormPanel form){
            this.form = form;
        }

        @Override
        public void onChange(ChangeEvent event) {
            form.submit();
        }
    }
}
In abstract class FileUploaderImpl we have the element that should exists for all subclasses, the formpanel, the input field and the button. We also set the values on the form needed to upload files. And applying the OnChangeHandler on the input field making it so that when the user has selected a file it will be automatically uploaded.
public class FileUploaderImplIE extends FileUploaderImpl implements ClickHandler{
    
    public FileUploaderImplIE(){
        GWT.log("ImplIE Constructor");
        
        FlowPanel fp = new FlowPanel();
        getFormPanel().add(fp);
        
        inputFile.getElement().getStyle().setDisplay(Display.NONE);
        button.setText("IEImpl");
        fp.add(inputFile);
        fp.add(button);
        
        button.addClickHandler(this);
    }

    @Override
    public void onClick(ClickEvent event) {
        InputElement element = InputElement.as(inputFile.getElement());
        element.click();
    }

    @Override
    public void setSize(String width, String height) {
        button.setSize(width, height);
    }    
}
So in the IE implementation we bind the button click to the click of the input element and also hides the input element.
public class FileUploaderImplStandard extends FileUploaderImpl {
    
    FlowPanel fp = new FlowPanel();
    public FileUploaderImplStandard(){
        super();
        
        GWT.log("ImplStandard Constructor");
        
        getFormPanel().add(fp);
        
        fp.add(button);
        fp.add(inputFile);
        
        button.setText("Standard");
        
        
        // Apply style to input-field
        Style inpStyle = inputFile.getElement().getStyle();
        inpStyle.setOpacity(0.1);
        inpStyle.setProperty("filter", "alpha(opacity: 0)");;
        inpStyle.setZIndex(4);
        
        Style btnStyle = button.getElement().getStyle();
        btnStyle.setPosition(Position.ABSOLUTE);
        btnStyle.setTop(0, Unit.PX);
        btnStyle.setLeft(0,Unit.PX);
        btnStyle.setZIndex(0);
        
        Style panelStyle  = fp.getElement().getStyle();
        panelStyle.setPosition(Position.RELATIVE);
        panelStyle.setOverflow(Overflow.HIDDEN);
    }

    @Override
    public void setSize(String width, String height) {
        button.setSize(width, height);
        fp.setSize(width,height);
    }
}
In the case of the standard implementation we need to apply some styles to put the input element in front of the button and make it transparent. Now we have created code for the separate situations and we need to tell the compiler when to use what class wiht deferred binding. In the FileUploader class we have the GWT.create() statement which said that we should use deferred binding to get the right class for FileUploaderImpl and in the gwt.xml file we define how.
<replace-with class="se.patrik.FileUploaderImplStandard">
    <when-type-is class="se.patrik.FileUploaderImpl" />
</replace-with>

<replace-with class="se.patrik.FileUploaderImplIE">
    <when-type-is class="se.patrik.FileUploaderImpl" />
    <any>
        <when-property-is name="user.agent" value="ie6" />
        <when-property-is name="user.agent" value="ie8" />
    </any>
</replace-with>
This starts with saying that that FileUploaderImpl should be replaced with FileUploaderImplStandard, in the second case we override it if the user agent is ie6 or ie8, also note that ie6 also covers ie7. So if user agent is i6-i8 we should replace FileUploaderImpl with FileUploaderImplIE.