Revision: 6125 Author: rj...@google.com Date: Fri Sep 11 16:12:54 2009 Log: Moved here from Incubator http://code.google.com/p/google-web-toolkit/source/detail?r=6125
Added: /wiki/UiBinder.wiki ======================================= --- /dev/null +++ /wiki/UiBinder.wiki Fri Sep 11 16:12:54 2009 @@ -0,0 +1,513 @@ +#summary UiBinder use cases + += GWT UiBinder Use Cases = + +Ray Ryan + +This document provides various use cases for the use of the UiBinder, a proposed service to generate Widget and DOM structures from XML markup. + +The samples here ignore binder's localization features. See UiBinderI18n. + += Background = + +There are problems with the declarative ui template service as it was [DeclarativeUi originally proposed] + + * A template-based UI must be instantiated via GWT.create(), causing an implementation detail to be visible as public api + * Within a template, only widgets with a zero arg constructor can be used + * CssResource and other ImmutableResourceBundle variants cannot be used + * Template xml files are found by magical name matching conventions, and applying more than one xml template to a class is impossible + +In addressing these issues, we have talked about encouraging a proxy style of use (basically, use Composite to wrap whatever widget gets GWT.create()'d), but dislike the extra object creation implied. We also hope for a system that can choose to use innerHTML, cloning, or DOM assembly as makes sense per browser type. These shortcomings could be addressed by a combination of developer discipline (yuck) and perhaps the builder pattern, but we still found ourselves faced with the likelihood of hurried developers wrapping an unneeded, generated object. + +Emily hit upon the idea of the Configurator (here rechristened UiBinder). It’s like a factory, but responsible for filling in the fields of a Widget (or other object) that someone else instantiates, rather than instantiating one itself. This seems to offer all the benefits of a builder, with no concerns of extra object creation, and as a nice side effect avoids a lot of boilerplate. This document illustrates its application in various use cases. + +{{{ +/** + * Interface implemented by classes that generate DOM or Widget structures from + * ui.xml template files, and which inject portions of the generated UI into the + * fields of an owner. + * <p> + * The generated UiBinder implementation will be based on an xml file resource + * in the same package as the owner class, with the same name and a "ui.xml" + * suffix. For example, a UI owned by class {...@code bar.baz.Foo} will be sought + * in {...@code /bar/baz/Foo.ui.xml}. (To use a different template file, put the + * {...@link UiTemplate} annotation on your UiBinder interface declaration to point + * the code generator at it.) + * + * @param <U> The type of the root object of the generated UI, typically a + * subclass of {...@link com.google.gwt.dom.client.Element} or + * {...@link com.google.gwt.user.client.ui.UIObject} + * @param <O> The type of the object that will own the generated UI + */ +public interface UiBinder<U, O> { + + /** + * Creates and returns the root object of the UI, and fills any fields of owner + * tagged with {...@link UiField}. + * + * @param owner the object whose {...@literal @}UiField needs will be filled + */ + U createAndBindUi(O owner); +} +}}} + += Hello World = + +Make a simple generated UI, with a named element, and without widgets. + +{{{ +<!-- HelloWorld.ui.xml --> + +<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'> + <div> + Hello, <span ui:field='nameSpan'/>. + </div> +</ui:UiBinder> +}}} + +{{{ +public class HelloWorld extends UIObject { // Could extend Widget instead + + interface MyUiBinder extends UiBinder<DivElement, HelloWorld> {} + private static MyUiBinder uiBinder = GWT.create(MyUiBinder.class); + + @UiField SpanElement nameSpan; + + public HelloWorld(String name) { + // call to createAndBindUi sets this.nameSpan + setElement(uiBinder.createAndBindUi(this)); + nameSpan.setInnerText(name); + } +} + +// Use: + +SpanElement helloWorld = new HelloWorld("World").getElement(); +}}} + += Hello Composite World = + +Make a simple widget-based UI + +{{{ +<!-- HelloWidgetWorld.ui.xml --> + +<ui:UiBinder + xmlns:ui='urn:ui:com.google.gwt.uibinder' + xmlns:g='urn:import:com.google.gwt.user.client.ui'> + <g:HTMLPanel> + Hello, <g:ListBox ui:field='listBox'/>. + </g:HTMLPanel> +</ui:UiBinder> +}}} + +{{{ +public class HelloWidgetWorld extends Composite { + + interface MyUiBinder extends UiBinder<Widget, HelloWidgetWorld> {} + private static MyUiBinder uiBinder = GWT.create(MyUiBinder.class); + + @UiField ListBox listBox; + + public HelloWidgetWorld(String... names) { + // sets listBox + initWidget(uiBinder.createAndBindUi(this)); + for (String name : names) { listBox.addItem(name); } + } +} + +// Use: + +HelloWidgetWorld helloWorld = + new HelloWidgetWorld("able", "baker", "charlie"); +}}} + +=Putting a label on a checkbox (referring to generated ids within a template)= + +_Not yet implemented._ You want to make your personal variant on the single most common widget, a checkbox with a nice, accessible HTML label element tied to it: + +{{{ +<!-- LabeledCheckBox.ui.xml --> + +<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'> + <span> + <input type='checkbox' ui:field='myCheckBox'> + <label ui:for='myCheckBox' ui:field='myLabel'/> + </span> +</ui> +}}} + +{{{ +public class LabeledCheckBox extends Widget { + interface MyUiBinder extends UiBinder<Widget, LabeledCheckbox> {} + private static MyUiBinder uiBinder = GWT.create(MyUiBinder.class); + + @UiField InputElement myCheckBox; + @UiField LabelElement myLabel; + + public LabeledCheckBox() { setElement(uiBinder.createAndBindUi(this)); } + + public void setValue(boolean b) { myCheckBox.setChecked(b); } + + public boolean getValue() { return myCheckBox.isChecked(); } + + public void setName(String name) { myLabel.setInnerText(name); } + + public String getName() { return myLabel.getInnerText(); } +} +}}} + +The proposal here is that a ui: prefix on any attribute other than id fills it with the id generated for a corresponding ui:field. + +There are type matching issues here. The ui:field of a DOM element is a string id, while that for a UIObject is typed. So, this should fail with a type mismatch: + +{{{ +<some:WidgetOfSomeKind ui:field='theWidget'> <label g:for='theWidget' /> +}}} + +<blockquote> + The use of attribute prefixing for this would be a mistake, a bad + use of XML. In particular, it fights the use of XML tools to enforce + things like, "all labels must have a 'for' attribute." + + Instead, we should bite the bullet and adopt a mini-expression + language, something like: + +{{{ +<ui:UiBinder + xmlns:ui='urn:ui:com.google.gwt.uibinder'> + <span> + <input type='checkbox' ui:field='myCheckBox'> + <label for='{myCheckBox}' ui:field='myLabel'/> + </span> +</ui:UiBinder> +}}} + +</blockquote> + +=Using an ImmutableResourceBundle (e.g. CssResource) with a UiBinder= + +{{{ +<!-- LogoNamePanel.ui.xml --> +<ui:UiBinder + xmlns:ui='urn:ui:com.google.gwt.uibinder' + xmlns:g='urn:import:com.google.gwt.user.client.ui' + xmlns:res='urn:with:com.my.app.widgets.logoname.Resources'> + <g:HTMLPanel> + + <img res:apply='logoImage'> + + <div res:class='style.mainBlock'> + <div res:apply='style.userPictureSprite'> + Well hello there + <span res:class='style.nameSpan' ui:field='userNameField'/> + </div> + </div> + </g:HTMLPanel> +</ui:UiBinder> +}}} + +{{{ +public class LogoNamePanel extends Composite { + interface MyUiBinder extend UiBinder<Widget, LogoNamePanel> {} + private static MyUiBinder uiBinder = GWT.create(MyUiBinder.class); + + @UiField SpanElement nameSpan; + + public LogoNamePanel() { + initWidget(uiBinder.createAndBindUi(this)); + } + + public void setUserName(String userName) { + nameSpan.setInnerText(userName); + } +} + +public interface Resources extends ClientBundle { + @Resource("Style.css") + Style style(); + + @Resource("Logo.jpg") + ImageResource widgetyImage(); + + public interface Style extends CssResource { + String mainBlock(); + String nameSpan(); + Sprite userPictureSprite(); + } +} +}}} + +The with: uri type marks an object whose methods can be called to fill +in attribute values. If no public api is provided to set the "with" +argument (as in this example), it must be instantiable by +GWT.create(). + +An element can be passed as an argument to a method on such resource +class via an apply attribute, as illustrated above with the Sprite and +ImageResource uses. + +Note that there is no requirement that a with: class implement the +ClientBundle interface. + +<blockquote> + As above, this is an abuse of attribute prefixing, and a bad idea. + Here again we should use our little expression language: + +{{{ +<ui:UiBinder + xmlns:ui='urn:ui:com.google.gwt.uibinder' + xmlns:g='urn:importcom.google.gwt.user.client.ui' + xmlns:res='urn:with:com.my.app.widgets.logoname.Resources'> + + <g:HTMLPanel> + + <div class='{res.style.mainBlock}'> + Well hello there + <span class='{res.style.nameSpan}' ui:field='userNameField'/> + </div> + </g:HTMLPanel> +</ui:UiBinder> +}}} + +</blockquote> + +=Share ImmutableResourceBundle instances= + +Extends LogoNamePanel (from the example above) to allow its bundle to be passed in. + +{{{ +public class LogoNamePanel extends Composite { + interface MyUiBinder extend UiBinder<Widget, LogoNamePanel> {} + private static MyUiBinder uiBinder = GWT.create(MyUiBinder.class); + + @UiField SpanElement nameSpan; + final Resources resources; + + public LogoNamePanel(Resources resources) { + initWidget(uiBinder.createAndBindUi(this)); + this.resources = resources; + } + + public void setUserName(String userName) { + nameSpan.setInnerText(userName); + } + + @UiFactory + public Resources getResources() { + return resources; + } +} +}}} + +This can be even more concise: + +{{{ +public class LogoNamePanel extends Composite { + interface MyUiBinder extend UiBinder<Widget, LogoNamePanel> {} + private static MyUiBinder uiBinder = GWT.create(MyUiBinder.class); + + @UiField SpanElement nameSpan; + @UiField(provided = true) + final Resources resources; + + public LogoNamePanel(Resources resources) { + initWidget(uiBinder.createAndBindUi(this)); + this.resources = resources; + } + + public void setUserName(String userName) { + nameSpan.setInnerText(userName); + } +} +}}} + + +=Using a widget that requires constructor args= + +You have an existing widget that needs constructor arguments. + +{{{ +public CricketScores(String... teamNames) {...} +}}} + +You use it in a template. + +{{{ +<!-- UserDashboard.ui.xml --> +< ui:UiBinder + xmlns:ui='urn:ui:com.google.gwt.uibinder' + xmlns:g='urn:import:com.google.gwt.user.client.ui' + xmlns:my='urn:import:com.my.app.widgets' > + + <g:HTMLPanel> + <my:WeatherReport ui:field='weather'/> + <my:Stocks ui:field='stocks'/> + <my:CricketScores ui:field='scores' /> + </g:HTMLPanel> +</ui:UiBinder> +}}} + +{{{ +public class UserDashboard extends Composite { + interface MyUiBinder extends UiBinder<Widget, UserDashboard> {} + private static MyUiBinder uiBinder = GWT.create(MyUiBinder.class); + + public UserDashboard() { + initWidget(uiBinder.createAndBindUi(this)); + } +} +}}} + +An error results: + +{{{ + +UserDashboard.ui.xml:7:2 [ERROR] com.my.app.widgets.CricketScores +has no default (zero args) constructor. You can define a +...@uifactory annotated method on UserDashboard to create an instance; +mark a CrickectScores field of UserDashboard with @UiField(provided=true) +and put an instance there; or annotate a constructor of CricketScores with +...@uiconstructor to allow its arguments to be provided by the template. + +}}} + +So you either make the @UiFactory method: + +{{{ +public class UserDashboard extends Composite { + interface MyUiBinder extends UiBinder<Widget, UserDashboard>; + private static final MyUiBinder uiBinder = GWT.create(MyUiBinder.class); + + private final String[] teamNames; + + public UserDashboard(String... teamNames) { + this.teamNames = teamNames; + initWidget(uiBinder.createAndBindUi(this)); + } + + /** Used by MyUiBinder to instantiate CricketScores */ + @UiFactory CricketScores makeCricketScores() { // method name is insignificant + return new CricketScores(teamNames); + } +} +}}} + +or perhaps: + +{{{ +public class UserDashboard extends Composite { + interface MyUiBinder extends UiBinder<Widget, UserDashboard>; + private static final MyUiBinder uiBinder = GWT.create(MyUiBinder.class); + + public UserDashboard() { + this.teamNames = teamNames; + initWidget(uiBinder.createAndBindUi(this)); + } + + /** + * Used by MyUiBinder to instantiate CricketScores. + * Arguments to be filled in the template + */ + @UiFactory CricketScores(String... teamNames) { + return new CricketScores(teamNames); + } +} +}}} + +{{{ +<!-- UserDashboard.ui.xml --> +<g:HTMLPanel xmlns:ui='urn:ui:com.google.gwt.uibinder' + xmlns:g='urn:import:com.google.gwt.user.client.ui' + xmlns:my='urn:import:com.my.app.widgets' > + + <my:WeatherReport ui:field='weather'/> + <my:Stocks ui:field='stocks'/> + <my:CricketScores ui:field='scores' teamNames='AUS, SAF, WA, QLD, VIC'/> +</g:HTMLPanel> +}}} + +or annotate the constructor: + +{{{ +public @UiConstructor CricketScores(String... teamNames) {...} +}}} + +{{{ +<!-- UserDashboard.ui.xml --> +<g:HTMLPanel xmlns:ui='urn:ui:com.google.gwt.uibinder' + xmlns:g='urn:import:com.google.gwt.user.client.ui' + xmlns:my='urn:import:com.my.app.widgets' > + + <my:WeatherReport ui:field='weather'/> + <my:Stocks ui:field='stocks'/> + <my:CricketScores ui:field='scores' teamNames='AUS, SAF, WA, QLD, VIC'/> +</g:HTMLPanel> +}}} + +or fill in a field marked with @UiField(provided=true): + +{{{ +public class UserDashboard extends Composite { + interface MyUiBinder extends UiBinder<Widget, UserDashboard>; + private static final MyUiBinder uiBinder = GWT.create(MyUiBinder.class); + + @UiField(provided=true) + final CricketScores cricketScores; // cannot be private + + public UserDashboard(CricketScores cricketScores) { + // DI fans take note! + this.cricketScores = cricketScores; + initWidget(uiBinder.createAndBindUi(this)); + } +} +}}} + +=Apply different xml templates to the same widget= + +You're an MVC developer. You have a nice view interface, and a templated Widget that implements it. How might you use several different xml templates for the same view? + +{{{ +public class FooPickerController { + public interface Display { + HasText getTitleField(); + SourcesChangeEvents getPickerSelect(); + } + + public void setDisplay(FooPickerDisplay display) { ... } +} + +public class FooPickerDisplay extends Composite + implements FooPickerController.Display { + + @UiTemplate("RedFooPicker.ui.xml") + interface RedBinder extends UiBinder<Widget, FooPickerDisplay> {} + private static RedBinder redBinder = GWT.create(MyUiBinder.class); + + @UiTemplate("BlueFooPicker.ui.xml") + interface BlueBinder extends UiBinder<Widget, FooPickerDisplay> {} + private static BlueBinder blueBinder = GWT.create(MyUiBinder.class); + + @UiField HasText titleField; + @UiField SourcesChangeEvents pickerSelect; + + public HasText getTitleField() { + return titleField; + } + public SourcesChangeEvents getPickerSelect() { + return pickerSelect; + } + + protected FooPickerDisplay(UiBinder<Widget, FooPickerDisplay> binder) { + initWidget(uiBinder.createAndBindUi(this)); + } + + public static FooPickerDisplay createRedPicker() { + return new FooPickerDisplay(redBinder); + } + + public static FooPickerDisplay createBluePicker() { + return new FooPickerDisplay(blueBinder); + } +} +}}} --~--~---------~--~----~------------~-------~--~----~ http://groups.google.com/group/Google-Web-Toolkit-Contributors -~----------~----~----~----~------~----~------~--~---