Initial work on rewriting Palette component

Project: http://git-wip-us.apache.org/repos/asf/tapestry-5/repo
Commit: http://git-wip-us.apache.org/repos/asf/tapestry-5/commit/c435bd2a
Tree: http://git-wip-us.apache.org/repos/asf/tapestry-5/tree/c435bd2a
Diff: http://git-wip-us.apache.org/repos/asf/tapestry-5/diff/c435bd2a

Branch: refs/heads/5.4-js-rewrite
Commit: c435bd2a5ac445dfdd882db708f8884baded691b
Parents: 97f89ae
Author: Howard M. Lewis Ship <[email protected]>
Authored: Tue Nov 6 11:47:11 2012 -0800
Committer: Howard M. Lewis Ship <[email protected]>
Committed: Tue Nov 6 13:33:01 2012 -0800

----------------------------------------------------------------------
 54_RELEASE_NOTES.txt                               |    6 +-
 .../META-INF/modules/core/palette.coffee           |  174 ++++++++++
 .../tapestry5/corelib/components/Palette.java      |  251 ++------------
 .../resources/META-INF/assets/core/palette.css     |   47 +++
 .../tapestry5/corelib/components/Palette.tml       |   46 ++-
 tapestry-core/src/test/app1/PaletteDemo.tml        |   41 ++-
 .../integration/app1/pages/PaletteDemo.java        |   27 +-
 7 files changed, 330 insertions(+), 262 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/c435bd2a/54_RELEASE_NOTES.txt
----------------------------------------------------------------------
diff --git a/54_RELEASE_NOTES.txt b/54_RELEASE_NOTES.txt
index 2c103eb..b5d42da 100644
--- a/54_RELEASE_NOTES.txt
+++ b/54_RELEASE_NOTES.txt
@@ -142,4 +142,8 @@ still exists, but the methods do nothing, and the service 
and interface will be
 In prior releases, adding _any_ JavaScript to the application would implicitly 
import the `core` JavaScript stack.
 This no longer occurs; in most cases, the core stack is provided automatically 
by other components.
 
-You may want to consider adding `@Import(stack="core")` to your applications' 
main layout component.
\ No newline at end of file
+You may want to consider adding `@Import(stack="core")` to your applications' 
main layout component.
+
+## Palette Component
+
+The selected property is now type `Collection`, not specifically type `List`. 
It is no longer allowed to be null.
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/c435bd2a/tapestry-core/src/main/coffeescript/META-INF/modules/core/palette.coffee
----------------------------------------------------------------------
diff --git 
a/tapestry-core/src/main/coffeescript/META-INF/modules/core/palette.coffee 
b/tapestry-core/src/main/coffeescript/META-INF/modules/core/palette.coffee
new file mode 100644
index 0000000..c257eeb
--- /dev/null
+++ b/tapestry-core/src/main/coffeescript/META-INF/modules/core/palette.coffee
@@ -0,0 +1,174 @@
+# Copyright 2012 The Apache Software Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http:#www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# ##core/palette
+#
+# Support for the `core/Palette` component.
+define ["core/dom", "_"],
+  (dom, _) ->
+    class PaletteController
+
+      constructor: (id) ->
+        @selected = (dom id)
+        container = @selected.findContainer ".t-palette"
+        @available = container.findFirst ".t-palette-available select"
+        @hidden = container.findFirst "input[type=hidden]"
+
+        @select = container.findFirst "[data-action=select]"
+        @deselect = container.findFirst "[data-action=deselect]"
+
+        @moveUp = container.findFirst "[data-action=move-up]"
+        @moveDown = container.findFirst "[data-action=move-down]"
+
+        # Track where reorder is allowed based on whether the buttons actually 
exist
+        @reorder = @moveUp isnt null
+
+        @valueToOrderIndex = {}
+
+        _.each @available.element.options, (option, i) =>
+          @valueToOrderIndex[option.value] = i
+
+        # This occurs even when the palette is disabled, to present the
+        # values correctly. Otherwise it looks like nothing is selected.
+        @initialTransfer()
+
+        unless @selected.element.disabled
+          @updateButtons()
+          @bindEvents()
+
+      initialTransfer: ->
+        # Get the values for options that should move over
+        values = JSON.parse @hidden.value()
+        valueToPosition = {}
+
+        _.each values, (v, i) -> valueToPosition[v] = i
+
+        e = @available.element
+
+        movers = []
+
+        for i in [(e.options.length - 1)..0] by -1
+          option = e.options[i]
+          value = option.value
+          pos = valueToPosition[value]
+          unless pos is undefined
+            movers[pos] = option
+            e.remove i
+
+        for option in movers
+          @selected.element.add option
+
+      updateHidden: ->
+        values = _.pluck(this.selected, "value")
+        hidden.value JSON.stringify values
+
+      bindEvents: ->
+        @select.on "click", =>
+          @doSelect()
+          return false
+
+        @deselect.on "click", =>
+          @doDeselect()
+          return false
+
+      updateButtons: ->
+        @select.element.disabled = @available.element.selectedIndex < 0
+
+        nothingSelected = @selected.element.selectedIndex < 0
+
+        @deselect.element.disabled = nothingSelected
+
+        if @reorder
+          @moveUp.disabled = nothingSelected or @allSelectionsAtTop()
+          @moveDown.disabled = nothingSelected or @allSelectionsAtBottom()
+
+      transferOptions: (from, to, atEnd) ->
+        if from.element.selectedIndex is -1
+          return
+
+        _(to.element.options).each (o) -> o.selected = false
+
+        movers = @removeSelectedOptions from
+
+        @moveOptions movers, to, atEnd
+
+      removeSelectedOptions: (select) ->
+        movers = []
+        e = select.element
+        options = e.options
+
+        for i in [(e.length - 1)..(e.selectedIndex)] by -1
+          o = options[i]
+          if o.selected
+            select.remove i
+            movers.unshift o
+
+        return movers
+
+      moveOptions: (movers, to, atEnd) ->
+        _.each movers, (o) =>
+          @moveOption o, to, atEnd
+
+        @updateHidden()
+        @updateButtons()
+
+      moveOptions: (option, to, atEnd) ->
+        before = null
+
+        unless atEnd
+          optionOrder = @valueToOrderIndex[option.value]
+          candidate = _.find to.element.options, (o) => 
@valueToOrderIndex[o.value] > optionOrder
+          if candidate
+            before = candidate
+
+        @addOption to, option, before
+
+      addOption: (to, option, before) ->
+        try
+          to.element.add option, before
+        catch ex
+          if before is null
+            # IE throws an exception about type mismatch; here's the fix:
+            to.add option
+          else
+            to.add option, before.index
+
+      indexOfLastSelection: (select) ->
+        e = select.element
+        if e.selectedIndex < 0
+          return -1
+
+        for i in  [(e.options.length -1)..(e.selectedIndex + 1)] by -1
+          if e.options[i].selected
+            return i
+
+        return -1
+
+      allSelectionsAtTop: ->
+        last = @indexOfLastSelection @selected
+        options = @selected.options
+
+        _(options[0..last]).all (o) -> o.selected
+
+      allSelectionsAtBottom: ->
+        last = @selected.element.selectedIndex
+
+        _(options[last..]).all (o) -> o.selected
+
+
+    initialize = (id) ->
+      new PaletteController(id)
+
+    # Export just the initialize function
+    return initialize
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/c435bd2a/tapestry-core/src/main/java/org/apache/tapestry5/corelib/components/Palette.java
----------------------------------------------------------------------
diff --git 
a/tapestry-core/src/main/java/org/apache/tapestry5/corelib/components/Palette.java
 
b/tapestry-core/src/main/java/org/apache/tapestry5/corelib/components/Palette.java
index 6c614ec..e3dc476 100644
--- 
a/tapestry-core/src/main/java/org/apache/tapestry5/corelib/components/Palette.java
+++ 
b/tapestry-core/src/main/java/org/apache/tapestry5/corelib/components/Palette.java
@@ -22,16 +22,11 @@ import org.apache.tapestry5.corelib.base.AbstractField;
 import org.apache.tapestry5.internal.util.SelectModelRenderer;
 import org.apache.tapestry5.ioc.annotations.Inject;
 import org.apache.tapestry5.ioc.annotations.Symbol;
-import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
 import org.apache.tapestry5.json.JSONArray;
 
-import java.util.Collections;
+import java.util.Collection;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
-
-import static org.apache.tapestry5.ioc.internal.util.CollectionFactory.newList;
-import static org.apache.tapestry5.ioc.internal.util.CollectionFactory.newSet;
 
 /**
  * Multiple selection component. Generates a UI consisting of two 
&lt;select&gt; elements configured for multiple
@@ -52,7 +47,6 @@ import static 
org.apache.tapestry5.ioc.internal.util.CollectionFactory.newSet;
  * Much of the look and feel is driven by CSS, the default Tapestry CSS is 
used to set up the columns, etc. By default,
  * the &lt;select&gt; element's widths are 200px, and it is common to override 
this to a specific value:
  * <p/>
- * <p/>
  * <pre>
  * &lt;style&gt;
  * DIV.t-palette SELECT { width: 300px; }
@@ -72,104 +66,9 @@ import static 
org.apache.tapestry5.ioc.internal.util.CollectionFactory.newSet;
  * @see Form
  * @see Select
  */
-@Import(library = "palette.js")
+@Import(stylesheet = "Palette.css")
 public class Palette extends AbstractField
 {
-    // These all started as anonymous inner classes, and were refactored out 
to here.
-    // I was chasing down one of those perplexing bytecode errors.
-
-    private final class AvailableRenderer implements Renderable
-    {
-        public void render(MarkupWriter writer)
-        {
-            writer.element("select", "id", getClientId() + "-avail", 
"multiple", "multiple", "size", getSize(), "name",
-                    getControlName() + "-avail");
-
-            writeDisabled(writer, isDisabled());
-
-            for (Runnable r : availableOptions)
-                r.run();
-
-            writer.end();
-        }
-    }
-
-    private final class OptionGroupEnd implements Runnable
-    {
-        private final OptionGroupModel model;
-
-        private OptionGroupEnd(OptionGroupModel model)
-        {
-            this.model = model;
-        }
-
-        public void run()
-        {
-            renderer.endOptionGroup(model);
-        }
-    }
-
-    private final class OptionGroupStart implements Runnable
-    {
-        private final OptionGroupModel model;
-
-        private OptionGroupStart(OptionGroupModel model)
-        {
-            this.model = model;
-        }
-
-        public void run()
-        {
-            renderer.beginOptionGroup(model);
-        }
-    }
-
-    private final class RenderOption implements Runnable
-    {
-        private final OptionModel model;
-
-        private RenderOption(OptionModel model)
-        {
-            this.model = model;
-        }
-
-        public void run()
-        {
-            renderer.option(model);
-        }
-    }
-
-    private final class SelectedRenderer implements Renderable
-    {
-        public void render(MarkupWriter writer)
-        {
-            writer.element("select", "id", getClientId(), "multiple", 
"multiple", "size", getSize(), "name",
-                    getControlName());
-
-            writeDisabled(writer, isDisabled());
-
-            putPropertyNameIntoBeanValidationContext("selected");
-
-            Palette.this.validate.render(writer);
-
-            removePropertyNameFromBeanValidationContext();
-
-            for (Object value : getSelected())
-            {
-                OptionModel model = valueToOptionModel.get(value);
-
-                renderer.option(model);
-            }
-
-            writer.end();
-        }
-    }
-
-    /**
-     * List of Runnable commands to render the available options.
-     */
-    private List<Runnable> availableOptions;
-
     /**
      * The image to use for the deselect button (the default is a left 
pointing arrow).
      */
@@ -222,8 +121,6 @@ public class Palette extends AbstractField
     @Property(write = false)
     private Asset moveUp;
 
-    private SelectModelRenderer renderer;
-
     /**
      * The image to use for the select button (the default is a right pointing 
arrow).
      */
@@ -235,12 +132,17 @@ public class Palette extends AbstractField
      * The list of selected values from the {@link 
org.apache.tapestry5.SelectModel}. This will be updated when the form
      * is submitted. If the value for the parameter is null, a new list will 
be created, otherwise the existing list
      * will be cleared. If unbound, defaults to a property of the container 
matching this component's id.
+     * <p/>
+     * Prior to Tapestry 5.4, this allowed null, and a list would be created 
when the form was submitted. Starting
+     * with 5.4, the selected list may not be null, and may not be a list (it 
may be, for example, a set).
      */
-    @Parameter(required = true, autoconnect = true)
-    private List<Object> selected;
+    @Parameter(required = true, autoconnect = true, allowNull = false)
+    private Collection<Object> selected;
 
     /**
      * If true, then additional buttons are provided on the client-side to 
allow for re-ordering of the values.
+     * This is only useful when the selected parameter is bound to a {@code 
List}, rather than a {@code Set} or other
+     * unordered collection.
      */
     @Parameter("false")
     @Property(write = false)
@@ -257,6 +159,7 @@ public class Palette extends AbstractField
     /**
      * Number of rows to display.
      */
+    @Property(write = false)
     @Parameter(value = BindingConstants.SYMBOL + ":" + 
ComponentParameterConstants.PALETTE_ROWS_SIZE)
     private int size;
 
@@ -274,41 +177,41 @@ public class Palette extends AbstractField
     @Symbol(SymbolConstants.COMPACT_JSON)
     private boolean compactJSON;
 
-    /**
-     * The natural order of elements, in terms of their client ids.
-     */
-    private List<String> naturalOrder;
 
-    public Renderable getAvailableRenderer()
+    public final Renderable mainRenderer = new Renderable()
     {
-        return new AvailableRenderer();
-    }
+        @Override
+        public void render(MarkupWriter writer)
+        {
+            SelectModelRenderer visitor = new SelectModelRenderer(writer, 
encoder);
+
+            model.visit(visitor);
+        }
+    };
 
-    public Renderable getSelectedRenderer()
+    public String getInitialJSON()
     {
-        return new SelectedRenderer();
+        return new JSONArray().toString(compactJSON);
     }
 
+
     @Override
     protected void processSubmission(String controlName)
     {
-        String parameterValue = request.getParameter(controlName + "-values");
-
-        validationTracker.recordInput(this, parameterValue);
+        String parameterValue = request.getParameter(controlName);
 
         JSONArray values = new JSONArray(parameterValue);
 
         // Use a couple of local variables to cut down on access via bindings
 
-        List<Object> selected = this.selected;
+        Collection<Object> selected = this.selected;
 
-        if (selected == null)
-            selected = newList();
-        else
-            selected.clear();
+        selected.clear();
 
         ValueEncoder encoder = this.encoder;
 
+        // TODO: Validation error if the model does not contain a value.
+
         int count = values.length();
         for (int i = 0; i < count; i++)
         {
@@ -334,40 +237,13 @@ public class Palette extends AbstractField
         removePropertyNameFromBeanValidationContext();
     }
 
-    private void writeDisabled(MarkupWriter writer, boolean disabled)
-    {
-        if (disabled)
-            writer.attributes("disabled", "disabled");
-    }
-
-    void beginRender(MarkupWriter writer)
+    void beginRender()
     {
-        JSONArray selectedValues = new JSONArray();
-
-        for (OptionModel selected : selectedOptions)
-        {
-
-            Object value = selected.getValue();
-            String clientValue = encoder.toClient(value);
-
-            selectedValues.put(clientValue);
-        }
-
-        JSONArray naturalOrder = new JSONArray();
-
-        for (String value : this.naturalOrder)
-        {
-            naturalOrder.put(value);
-        }
-
         String clientId = getClientId();
 
-        javaScriptSupport.addScript("new Tapestry.Palette('%s', %s, %s);", 
clientId, reorder, naturalOrder
-                .toString(compactJSON));
-
-        writer.element("input", "type", "hidden", "id", clientId + "-values", 
"name", getControlName() + "-values",
-                "value", selectedValues);
-        writer.end();
+        // The client side just need to know the id of the selected (right 
column) select;
+        // it can take it from there.
+        javaScriptSupport.require("core/palette").with(clientId);
     }
 
     /**
@@ -378,53 +254,6 @@ public class Palette extends AbstractField
         return false;
     }
 
-    @SuppressWarnings("unchecked")
-    void setupRender(MarkupWriter writer)
-    {
-        valueToOptionModel = CollectionFactory.newMap();
-        availableOptions = CollectionFactory.newList();
-        selectedOptions = CollectionFactory.newList();
-        naturalOrder = CollectionFactory.newList();
-        renderer = new SelectModelRenderer(writer, encoder);
-
-        final Set selectedSet = newSet(getSelected());
-
-        SelectModelVisitor visitor = new SelectModelVisitor()
-        {
-            public void beginOptionGroup(OptionGroupModel groupModel)
-            {
-                availableOptions.add(new OptionGroupStart(groupModel));
-            }
-
-            public void endOptionGroup(OptionGroupModel groupModel)
-            {
-                availableOptions.add(new OptionGroupEnd(groupModel));
-            }
-
-            public void option(OptionModel optionModel)
-            {
-                Object value = optionModel.getValue();
-
-                boolean isSelected = selectedSet.contains(value);
-
-                String clientValue = toClient(value);
-
-                naturalOrder.add(clientValue);
-
-                if (isSelected)
-                {
-                    selectedOptions.add(optionModel);
-                    valueToOptionModel.put(value, optionModel);
-                    return;
-                }
-
-                availableOptions.add(new RenderOption(optionModel));
-            }
-        };
-
-        model.visit(visitor);
-    }
-
     /**
      * Computes a default value for the "validate" parameter using
      * {@link org.apache.tapestry5.services.FieldValidatorDefaultSource}.
@@ -434,28 +263,20 @@ public class Palette extends AbstractField
         return this.defaultProvider.defaultValidatorBinding("selected", 
this.resources);
     }
 
-    // Avoids a strange Javassist bytecode error, c'est lavie!
-    int getSize()
-    {
-        return size;
-    }
-
     String toClient(Object value)
     {
         return encoder.toClient(value);
     }
 
-    List<Object> getSelected()
-    {
-        if (selected == null)
-            return Collections.emptyList();
-
-        return selected;
-    }
 
     @Override
     public boolean isRequired()
     {
         return validate.isRequired();
     }
+
+    public String getDisabledValue()
+    {
+        return disabled ? "disabled" : null;
+    }
 }

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/c435bd2a/tapestry-core/src/main/resources/META-INF/assets/core/palette.css
----------------------------------------------------------------------
diff --git a/tapestry-core/src/main/resources/META-INF/assets/core/palette.css 
b/tapestry-core/src/main/resources/META-INF/assets/core/palette.css
new file mode 100644
index 0000000..14a18d1
--- /dev/null
+++ b/tapestry-core/src/main/resources/META-INF/assets/core/palette.css
@@ -0,0 +1,47 @@
+DIV.t-palette {
+    display: inline;
+}
+
+DIV.t-palette SELECT {
+    margin-bottom: 2px;
+    width: 200px;
+}
+
+DIV.t-palette-title {
+    color: white;
+    background-color: #809FFF;
+    text-align: center;
+    font-weight: bold;
+    margin-bottom: 3px;
+    display: block;
+}
+
+DIV.t-palette-available {
+    float: left;
+}
+
+DIV.t-palette-controls {
+    margin: 5px 5px;
+    float: left;
+}
+
+DIV.t-palette-controls > DIV {
+    margin-top: 5px;
+}
+
+DIV.t-palette-controls > DIV:first-child {
+    margin-top: 0;
+}
+
+DIV.t-palette-selected {
+    float: left;
+    clear: right;
+}
+
+DIV.t-palette-spacer {
+    clear: left;
+}
+
+DIV.t-palette-title {
+    width: 200px;
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/c435bd2a/tapestry-core/src/main/resources/org/apache/tapestry5/corelib/components/Palette.tml
----------------------------------------------------------------------
diff --git 
a/tapestry-core/src/main/resources/org/apache/tapestry5/corelib/components/Palette.tml
 
b/tapestry-core/src/main/resources/org/apache/tapestry5/corelib/components/Palette.tml
index aff9513..1042188 100644
--- 
a/tapestry-core/src/main/resources/org/apache/tapestry5/corelib/components/Palette.tml
+++ 
b/tapestry-core/src/main/resources/org/apache/tapestry5/corelib/components/Palette.tml
@@ -1,31 +1,51 @@
 <div class="t-palette" xml:space="default" 
xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd";>
+    <t:remove>Contains the array of option ids for selected 
elements:</t:remove>
+    <input type="hidden" name="${controlName}" value="${initialJSON}" 
disabled="${disabledValue}"/>
+
     <div class="t-palette-available">
         <div class="t-palette-title">
             <t:delegate to="availableLabel"/>
         </div>
-        <t:delegate to="availableRenderer"/>
+        <select multiple="multiple" size="${size}" disabled="${disabledValue}">
+            <t:remove>
+                All the options, in their "natural" order, are rendered here, 
then selected elements
+                are transferred to the other select.
+            </t:remove>
+            <t:delegate to="mainRenderer"/>
+        </select>
     </div>
+
     <div class="t-palette-controls">
-        <button id="${clientId}-select" disabled="disabled">
-            <img src="${select}" alt="${message:core-palette-select-label}"/>
-        </button>
-        <button id="${clientId}-deselect" disabled="disabled">
-            <img src="${deselect}" 
alt="${message:core-palette-deselect-label}"/>
-        </button>
-        <t:if test="reorder">
-            <button id="${clientId}-up" disabled="disabled">
-                <img src="${moveUp}" alt="${message:core-palette-up-label}"/>
+        <div>
+            <button data-action="select" class="btn" 
disabled="${disabledValue}">
+                <img src="${select}" 
alt="${message:core-palette-select-label}"/>
             </button>
-            <button id="${clientId}-down" disabled="disabled">
-                <img src="${moveDown}" 
alt="${message:core-palette-down-label}"/>
+        </div>
+        <div>
+            <button data-action="deselect" class="btn" 
disabled="${disabledValue}">
+                <img src="${deselect}" 
alt="${message:core-palette-deselect-label}"/>
             </button>
+        </div>
+        <t:if test="reorder">
+            <div>
+                <button data-action="move-up" class="btn" 
disabled="${disabledValue}">
+                    <img src="${moveUp}" 
alt="${message:core-palette-up-label}"/>
+                </button>
+            </div>
+            <div>
+                <button data-action="move-down" class="btn" 
disabled="${disabledValue}">
+                    <img src="${moveDown}" 
alt="${message:core-palette-down-label}"/>
+                </button>
+            </div>
         </t:if>
     </div>
     <div class="t-palette-selected">
         <div class="t-palette-title">
             <t:delegate to="selectedLabel"/>
         </div>
-        <t:delegate to="selectedRenderer"/>
+        <select id="${clientId}" multiple="multiple" size="${size}" 
disabled="${disabledValue}">
+            <t:remove>Starts empty, populated on the client side.</t:remove>
+        </select>
     </div>
     <div class="t-palette-spacer"/>
 </div>

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/c435bd2a/tapestry-core/src/test/app1/PaletteDemo.tml
----------------------------------------------------------------------
diff --git a/tapestry-core/src/test/app1/PaletteDemo.tml 
b/tapestry-core/src/test/app1/PaletteDemo.tml
index 0c5acca..6d65f74 100644
--- a/tapestry-core/src/test/app1/PaletteDemo.tml
+++ b/tapestry-core/src/test/app1/PaletteDemo.tml
@@ -3,28 +3,37 @@
 <h1>Palette Demo</h1>
 
 
-<t:form>
-    <div>
-        <label class="checkbox">
-            <t:checkbox t:id="reorder"/>
-            Enable Reorder
-        </label>
+<t:form t:id="demo" class="form-horizontal">
+    <div class="control-group">
+        <div class="controls">
+
+            <label class="checkbox">
+                <t:checkbox t:id="reorder"/>
+                Enable Reorder
+            </label>
+        </div>
+
     </div>
 
 
-    <div>
+    <div class="control-group">
+
+        <t:label for="languages"/>
+
+        <div class="controls">
 
-        <t:palette t:id="languages" model="languageModel" reorder="reorder" 
encoder="languageEncoder"
-                   availableLabel="Languages Offered" validate="required">
-            <t:parameter name="selectedLabel" xml:space="default">
-                Selected
-                <t:if test="reorder">/ Ranked</t:if>
-                Languages
-            </t:parameter>
-        </t:palette>
+            <t:palette t:id="languages" model="languageModel" 
reorder="reorder" encoder="languageEncoder"
+                       availableLabel="Languages Offered" validate="required">
+                <t:parameter name="selectedLabel" xml:space="default">
+                    Selected
+                    <t:if test="reorder">/ Ranked</t:if>
+                    Languages
+                </t:parameter>
+            </t:palette>
+        </div>
     </div>
 
-    <div>
+    <div class="form-actions">
         <input type="submit" class="btn btn-primary"/>
     </div>
 

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/c435bd2a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/PaletteDemo.java
----------------------------------------------------------------------
diff --git 
a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/PaletteDemo.java
 
b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/PaletteDemo.java
index 96c0d60..7cf5dba 100644
--- 
a/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/PaletteDemo.java
+++ 
b/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/PaletteDemo.java
@@ -1,4 +1,4 @@
-// Copyright 2007 The Apache Software Foundation
+// Copyright 2007, 2012 The Apache Software Foundation
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -18,12 +18,14 @@ import org.apache.tapestry5.ComponentResources;
 import org.apache.tapestry5.SelectModel;
 import org.apache.tapestry5.ValueEncoder;
 import org.apache.tapestry5.annotations.Persist;
+import org.apache.tapestry5.annotations.Property;
 import org.apache.tapestry5.integration.app1.data.ProgrammingLanguage;
 import org.apache.tapestry5.ioc.annotations.Inject;
 import org.apache.tapestry5.ioc.services.TypeCoercer;
 import org.apache.tapestry5.util.EnumSelectModel;
 import org.apache.tapestry5.util.EnumValueEncoder;
 
+import java.util.ArrayList;
 import java.util.List;
 
 public class PaletteDemo
@@ -32,32 +34,23 @@ public class PaletteDemo
     private ComponentResources resources;
 
     @Persist
+    @Property
     private List<ProgrammingLanguage> languages;
 
     @Persist
+    @Property
     private boolean reorder;
 
     @Inject
     private TypeCoercer typeCoercer;
 
-    public boolean isReorder()
-    {
-        return reorder;
-    }
-
-    public void setReorder(boolean reorder)
-    {
-        this.reorder = reorder;
-    }
-
-    public List<ProgrammingLanguage> getLanguages()
-    {
-        return languages;
-    }
 
-    public void setLanguages(List<ProgrammingLanguage> selected)
+    void onPrepareFromDemo()
     {
-        languages = selected;
+        if (languages == null)
+        {
+            languages = new ArrayList<ProgrammingLanguage>();
+        }
     }
 
     public SelectModel getLanguageModel()

Reply via email to