Merge lp:~salgado/lazr-js/bug-482235 into lp:lazr-js
- bug-482235
- Merge into toolchain
Status: | Merged |
---|---|
Merged at revision: | not available |
Proposed branch: | lp:~salgado/lazr-js/bug-482235 |
Merge into: | lp:lazr-js |
Diff against target: |
248 lines (+107/-18) 3 files modified
examples/picker/index.html (+30/-12) src-js/lazrjs/picker/picker.js (+41/-2) src-js/lazrjs/picker/tests/picker.js (+36/-4) |
To merge this branch: | bzr merge lp:~salgado/lazr-js/bug-482235 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Māris Fogels (community) | Approve | ||
Björn Tillenius | Pending | ||
Review via email: mp+16349@code.launchpad.net |
This proposal supersedes a proposal from 2009-12-10.
Commit message
Description of the change
Guilherme Salgado (salgado) wrote : Posted in a previous version of this proposal | # |
Björn Tillenius (bjornt) wrote : Posted in a previous version of this proposal | # |
The feature itself looks very useful. I have some comments and questions, though. First of all, reading the diff, it looks like it only sets the search text, but doesn't actually search. But I assume that it does perform an actual search, no?
I'm also not so sure about the API. I can see how you chose to add a parameter, since that fits into the current API. However, I think it's worth thinking about, whether we can do something better. The reason I'm a bit skeptic to having yet another parameter, is that you mostly want to use this when you have a text input field, right? And when you have a text input field, you mostly always want this functionality, and you mostly always want to put pack the search result from the picker into the text field, right? With the current API, that means that every time you connect the picker to a text field, you have to do two things, specifying the same text field twice.
I want us to specify the text field only once. A possible solution would be to use the plugin API to achieve this. So that you have something like:
var picker = new Y.Picker();
picker.
Compare that to the existing API which looks something like this:
var picker = new Y.Picker(
picker.
});
Māris Fogels (mars) wrote : Posted in a previous version of this proposal | # |
I agree, and as I stated in the original bug report, the API could be improved.
I would not mind building the initial_value attribute as described in the bug report into the lazr-js class, but I think Björn's suggestion works too.
Björn's idea can be simplified a bit. Assuming we want the functionality across all of Launchpad, you can plug into the Picker class itself using the class-level static .plug() method.
Instead of this:
>
> var picker = new Y.Picker();
> picker.
>
You do this:
Y.Picker.
var picker = Y.Picker({
...
use_value_from: '#foobar' // Our new attribute
});
The class-level plugin could then add the attributes it needs dynamically.
Guilherme Salgado (salgado) wrote : Posted in a previous version of this proposal | # |
> The feature itself looks very useful. I have some comments and questions,
> though. First of all, reading the diff, it looks like it only sets the search
> text, but doesn't actually search. But I assume that it does perform an actual
> search, no?
Currently, it doesn't but that's trivial to add.
>
> I'm also not so sure about the API. I can see how you chose to add a
> parameter, since that fits into the current API. However, I think it's worth
> thinking about, whether we can do something better. The reason I'm a bit
> skeptic to having yet another parameter, is that you mostly want to use this
> when you have a text input field, right? And when you have a text input field,
> you mostly always want this functionality, and you mostly always want to put
> pack the search result from the picker into the text field, right? With the
> current API, that means that every time you connect the picker to a text
> field, you have to do two things, specifying the same text field twice.
>
> I want us to specify the text field only once. A possible solution would be to
> use the plugin API to achieve this. So that you have something like:
>
> var picker = new Y.Picker();
> picker.
Below is what I came up with, but notice the ID of the text field is not passed to TexFieldPickerP
One thing that's not clear to me is the advantage of using the plugin API instead of just subclassing the picker and adding the functionality to the subclass? AFAICT, subclassing would work just fine and it would be certainly simpler.
=== modified file 'lib/canonical/
--- lib/canonical/
+++ lib/canonical/
@@ -1,4 +1,4 @@
-YUI().use('node', 'lp.picker', function(Y) {
+YUI().use('node', 'lp.picker', 'plugin', function(Y) {
if (Y.UA.ie) {
return;
}
@@ -6,18 +6,39 @@
// Args from python.
var args = %s;
+ function TextFieldPicker
+ TextFieldPicker
+ }
+
+ TextFieldPicker
+ TextFieldPicker
+
+ Y.extend(
+ initializer: function(config) {
+ // Y.one() uses CSS3 selectors which don't work with ids
+ // containing a period, so we have to use Y.DOM.byId().
+ var input = Y.DOM.byId(
+ this.doAfter(
+ result = e.details[
+ input.value = result.value;
+ });
+ this.doAfter(
+ if ( input.value ) {
+ this.get(
+ }
+ });
+ }
+ });
+
// The vocabulary picker, created when used for the first time.
var picker = null;
function make_picker() {
- var save = function (result) {
- Y.DOM.byId(args...
Māris Fogels (mars) wrote : Posted in a previous version of this proposal | # |
On Mon, Dec 14, 2009 at 2:32 PM, Guilherme Salgado
<email address hidden> wrote:
>
> One thing that's not clear to me is the advantage of using the plugin API instead of just subclassing the picker and adding the functionality to the subclass? AFAICT, subclassing would work just fine and it would be certainly simpler.
>
YUI widgets were not designed to be subclassed for the reason are thinking.
Subclassing a YUI widget means literally a new widget built on top of
the old functionality. You must give the widget a new .NAME
attribute, new CSS, a new renderUI, bindUI, and syncUI methods, etc.
YUI leans towards composing widgets to add features, rather than
subclassing to add them. That is what the Plugin API and
Y.Base.build() are for. Thus, that is why I suggested using the
Plugin API or the Extension API. In this case, a Plugin is simplest.
Björn Tillenius (bjornt) wrote : | # |
On Fri, Dec 18, 2009 at 06:30:31PM -0000, Guilherme Salgado wrote:
> === modified file 'src-js/
> --- src-js/
> +++ src-js/
> @@ -863,10 +863,39 @@
> }
> };
>
> +
> +function TextFieldPicker
> + TextFieldPicker
> +}
> +
> +TextFieldPicke
> +TextFieldPicke
> +
> +Y.extend(
> + initializer: function(config) {
> + // Y.one() uses CSS3 selectors which don't work with ids
> + // containing a period, so we have to use Y.DOM.byId().
> + // XXX: For that reason we need callsites to provide an element ID
> + // rather than a selector here.
> + var input = Y.DOM.byId(
This is not quite true. You can use CSS3 selectors if you use attribute
selectors. For example:
var input = Y.one('
The rest looks good in general, it looks nice. Although I'm missing some
documenation. Do you plan to add some?
--
Björn Tillenius | https:/
- 167. By Guilherme Salgado
-
Move Picker and TextFieldPicker
Plugin under tha lazr (Y.lazr) namespace - 168. By Guilherme Salgado
-
Docs and minor changes suggested by Bjorn
- 169. By Guilherme Salgado
-
Add an example showing how to use Y.lazr.
TextFieldPicker Plugin
Guilherme Salgado (salgado) wrote : | # |
Hi Bjorn,
On Mon, 2009-12-21 at 12:12 +0000, Björn Tillenius wrote:
> On Fri, Dec 18, 2009 at 06:30:31PM -0000, Guilherme Salgado wrote:
[...]
> > +Y.extend(
> > + initializer: function(config) {
> > + // Y.one() uses CSS3 selectors which don't work with ids
> > + // containing a period, so we have to use Y.DOM.byId().
> > + // XXX: For that reason we need callsites to provide an element ID
> > + // rather than a selector here.
> > + var input = Y.DOM.byId(
>
> This is not quite true. You can use CSS3 selectors if you use attribute
> selectors. For example:
>
> var input = Y.one('
I didn't know that I could use attribute selectors to workaround that,
and the comment was just copied from the LP code that does what is now
done by the plugin.
I've changed it the plugin to expect any CSS selector, instead of an ID,
added some docs and an example.
http://
Example: http://
And I've also moved the Picker from the Y namespace into the Y.lazr one,
after checking with Maris that that's where it belongs.
http://
How does it look now?
--
Guilherme Salgado <email address hidden>
Māris Fogels (mars) wrote : | # |
Hi Salgado,
Your changes look good. I have some comments about the implementation.
- You should use Y.one() instead of Y.get()
- Are you comfortable using a private member reference to get the raw DOM node (node._node)? There is a proper API function for that: Y.Node.
- Just a comment: not in your code, but in the picker example page, I do not like the way the save event callback functions use array references and class-static variables to access the event details. They should be using objects. Do not worry about fixing it. The picker will have to be cleaned up and refactored at some point in the future.
Looks good. With changing Y.get() to Y.one(), r=mars.
Maris
Guilherme Salgado (salgado) wrote : | # |
Thanks Maris!
I changed my code (and test) to use Y.Node.getDOMNode() instead of
accessing _node directly. And replaced Y.get with Y.one -- for some
reason I always forget which one is deprecated.
- 170. By Guilherme Salgado
-
A couple minor tweaks suggested by Maris
Preview Diff
1 | === modified file 'examples/picker/index.html' |
2 | --- examples/picker/index.html 2009-11-30 19:07:28 +0000 |
3 | +++ examples/picker/index.html 2009-12-22 16:14:11 +0000 |
4 | @@ -21,9 +21,11 @@ |
5 | |
6 | <script type="text/javascript"> |
7 | |
8 | -YUI(LAZR_YUI_CONFIG).use('lazr.picker', 'dump', function(Y) { |
9 | - // Parse the content of the textarea in the data structure |
10 | - // expected by the widget. |
11 | + YUI(LAZR_YUI_CONFIG).use('lazr.picker', 'dump', 'plugin', function(Y) { |
12 | + |
13 | + var Picker = Y.lazr.Picker; |
14 | + // Parse the content of the textarea in the data structure |
15 | + // expected by the widget. |
16 | function parse_items () { |
17 | var items_text = Y.Lang.trim(Y.get('#items').get('value')); |
18 | var lines = items_text.split(/\r\n|\r|\n/); |
19 | @@ -71,7 +73,7 @@ |
20 | } |
21 | |
22 | |
23 | - var picker = new Y.Picker({ |
24 | + var picker = new Picker({ |
25 | picker_activator: '#show-widget', |
26 | clear_on_cancel: true, |
27 | align: { |
28 | @@ -84,9 +86,11 @@ |
29 | zIndex: 1000 |
30 | }); |
31 | |
32 | + picker.plug( |
33 | + Y.lazr.TextFieldPickerPlugin, {input_element: '#initval'}); |
34 | picker.subscribe('save', function (e) { |
35 | Y.log('Got save event.'); |
36 | - var result = e.details[Y.Picker.SAVE_RESULT]; |
37 | + var result = e.details[Picker.SAVE_RESULT]; |
38 | Y.get('#selected-item').set( |
39 | 'innerHTML', result['title'] + " (" + result['value'] + ')'); |
40 | }); |
41 | @@ -106,14 +110,14 @@ |
42 | // the demo look better. A real application wouldn't delay the |
43 | // results just so you can the spinner. |
44 | var spin_interval; |
45 | - if (e.details[Y.Picker.SELECTED_BATCH_VALUE] !== undefined) { |
46 | + if (e.details[Picker.SELECTED_BATCH_VALUE] !== undefined) { |
47 | spin_interval = 500; |
48 | } else { |
49 | spin_interval = 2000; |
50 | } |
51 | Y.later(spin_interval, null, function () { |
52 | - var results = search_items(e.details[Y.Picker.SEARCH_STRING]); |
53 | - var selected_batch = e.details[Y.Picker.SELECTED_BATCH_VALUE]; |
54 | + var results = search_items(e.details[Picker.SEARCH_STRING]); |
55 | + var selected_batch = e.details[Picker.SELECTED_BATCH_VALUE]; |
56 | var simple_batching = Y.get('#page-label-1').get('checked'); |
57 | |
58 | if (simple_batching) { |
59 | @@ -228,6 +232,9 @@ |
60 | <b>Last item selected:</b> <span id="selected-item">None</span> |
61 | </div> |
62 | <div> |
63 | + <b>Initial value:</b> <input type="text" id="initval" value="" /> |
64 | + </div> |
65 | + <div> |
66 | <b>Minimum search characters:</b> |
67 | <input type="text" id="min-search-chars" value="3" /> |
68 | </div> |
69 | @@ -301,7 +308,7 @@ |
70 | rather than creating a new Picker every time you want to display it. |
71 | </p> |
72 | <pre> |
73 | - var picker = new Y.Picker({ |
74 | + var picker = new Picker({ |
75 | progressbar: true, |
76 | progress: 100, |
77 | headerContent: '<h2>Picker example</h2>', |
78 | @@ -370,8 +377,8 @@ |
79 | </p> |
80 | <pre> |
81 | picker.after('search', function(e) { |
82 | - var search_text = e.details[Y.Picker.SEARCH_STRING]; |
83 | - var selected_batch = e.details[Y.Picker.SELECTED_BATCH_VALUE]; |
84 | + var search_text = e.details[Picker.SEARCH_STRING]; |
85 | + var selected_batch = e.details[Picker.SELECTED_BATCH_VALUE]; |
86 | var result_batches = my_perform_search(search_text); |
87 | |
88 | if (selected_batch === undefined) { |
89 | @@ -406,7 +413,7 @@ |
90 | </p> |
91 | <pre> |
92 | picker.subscribe('save', function(e) { |
93 | - var result = e.details[Y.Picker.SAVE_RESULT]; |
94 | + var result = e.details[Picker.SAVE_RESULT]; |
95 | Y.log('Got save event with result: ' + result); |
96 | }); |
97 | </pre> |
98 | @@ -420,5 +427,16 @@ |
99 | Y.log('Got cancel event'); |
100 | }); |
101 | </pre> |
102 | + <h3>Associating the picker to a text input</h3> |
103 | + <p>Sometimes it's desirable to have a picker associated to a text input so |
104 | + that the value selected from the picker's search results is copied back |
105 | + to that text input when the save event is fired. The |
106 | + TextFieldPickerPlugin does that and also extends the picker to take the |
107 | + initial value of its search input from the associated text input. |
108 | + </p> |
109 | + <pre> |
110 | + picker.plug( |
111 | + Y.lazr.TextFieldPickerPlugin, {input_element: '#initval'}); |
112 | + </pre> |
113 | </body> |
114 | </html> |
115 | |
116 | === modified file 'src-js/lazrjs/picker/picker.js' |
117 | --- src-js/lazrjs/picker/picker.js 2009-11-24 00:09:41 +0000 |
118 | +++ src-js/lazrjs/picker/picker.js 2009-12-22 16:14:11 +0000 |
119 | @@ -863,10 +863,49 @@ |
120 | } |
121 | }; |
122 | |
123 | -Y.Picker = Picker; |
124 | + |
125 | +/** |
126 | + * This plugin is used to associate a picker instance to an input element of |
127 | + * the DOM. When the picker is shown, it takes its initial value from that |
128 | + * element and when the save event is fired, the value of the chosen item |
129 | + * (from the picker's list of results) is copied to that element. |
130 | + * |
131 | + * Also, this plugin expects a single attribute (input_element) in the |
132 | + * config passed to its constructor, which defines the element that will be |
133 | + * associated with the picker. |
134 | + * |
135 | + * @class TextFieldPickerPlugin |
136 | + * @extends Y.Plugin.Base |
137 | + * @constructor |
138 | + */ |
139 | + |
140 | +function TextFieldPickerPlugin(config) { |
141 | + TextFieldPickerPlugin.superclass.constructor.apply(this, arguments); |
142 | +} |
143 | + |
144 | +TextFieldPickerPlugin.NAME = 'TextFieldPickerPlugin'; |
145 | +TextFieldPickerPlugin.NS = 'txtpicker'; |
146 | + |
147 | +Y.extend(TextFieldPickerPlugin, Y.Plugin.Base, { |
148 | + initializer: function(config) { |
149 | + var input = Y.Node.getDOMNode(Y.one(config.input_element)); |
150 | + this.doAfter('save', function (e) { |
151 | + result = e.details[Y.lazr.Picker.SAVE_RESULT]; |
152 | + input.value = result.value; |
153 | + }); |
154 | + this.doAfter('show', function() { |
155 | + if ( input.value ) { |
156 | + this.get('host')._search_input.set('value', input.value); |
157 | + } |
158 | + }); |
159 | + } |
160 | +}); |
161 | + |
162 | +Y.lazr.Picker = Picker; |
163 | +Y.lazr.TextFieldPickerPlugin = TextFieldPickerPlugin; |
164 | |
165 | }, "0.1", {"skinnable": true, |
166 | - "requires": ["oop", "event", "event-focus", "node", |
167 | + "requires": ["oop", "event", "event-focus", "node", "plugin", |
168 | "substitute", "widget", "widget-stdmod", |
169 | "lazr.overlay", "lazr.anim", "lazr.base"] |
170 | }); |
171 | |
172 | === modified file 'src-js/lazrjs/picker/tests/picker.js' |
173 | --- src-js/lazrjs/picker/tests/picker.js 2009-11-30 19:07:28 +0000 |
174 | +++ src-js/lazrjs/picker/tests/picker.js 2009-12-22 16:14:11 +0000 |
175 | @@ -39,7 +39,7 @@ |
176 | name: 'picker_basics', |
177 | |
178 | setUp: function() { |
179 | - this.picker = new Y.Picker(); |
180 | + this.picker = new Y.lazr.Picker(); |
181 | }, |
182 | |
183 | tearDown: function() { |
184 | @@ -48,7 +48,7 @@ |
185 | |
186 | test_picker_can_be_instantiated: function() { |
187 | Assert.isInstanceOf( |
188 | - Y.Picker, this.picker, "Picker failed to be instantiated"); |
189 | + Y.lazr.Picker, this.picker, "Picker failed to be instantiated"); |
190 | }, |
191 | |
192 | test_picker_is_stackable: function() { |
193 | @@ -406,7 +406,7 @@ |
194 | }, |
195 | |
196 | test_save_does_not_clear_widget_when_clear_on_save_is_false: function () { |
197 | - picker = new Y.Picker({clear_on_save: false}); |
198 | + picker = new Y.lazr.Picker({clear_on_save: false}); |
199 | picker.render(); |
200 | |
201 | picker._search_input.set('value', 'foo'); |
202 | @@ -427,7 +427,7 @@ |
203 | }, |
204 | |
205 | test_cancel_event_clears_widget_when_clear_on_cancel_is_true: function () { |
206 | - picker = new Y.Picker({clear_on_cancel: true}); |
207 | + picker = new Y.lazr.Picker({clear_on_cancel: true}); |
208 | picker.render(); |
209 | |
210 | picker._search_input.set('value', 'foo'); |
211 | @@ -841,6 +841,38 @@ |
212 | this.picker.set('search_mode', false); |
213 | Assert.isTrue(got_focus, "focus didn't go to the search input."); |
214 | } |
215 | +})); |
216 | + |
217 | +suite.add(new Y.Test.Case({ |
218 | + |
219 | + name: 'picker_text_field_plugin', |
220 | + |
221 | + setUp: function() { |
222 | + var node = Y.one(document.body).appendChild( |
223 | + Y.Node.create('<input id="field.initval" value="foo" />')); |
224 | + this.picker = new Y.lazr.Picker(); |
225 | + this.picker.plug(Y.lazr.TextFieldPickerPlugin, |
226 | + {input_element: '[id="field.initval"]'}); |
227 | + }, |
228 | + |
229 | + tearDown: function() { |
230 | + cleanup_widget(this.picker); |
231 | + }, |
232 | + |
233 | + test_TextFieldPickerPlugin_initial_value: function () { |
234 | + this.picker.render(); |
235 | + this.picker.show(); |
236 | + Assert.areEqual('foo', this.picker._search_input.get('value')); |
237 | + }, |
238 | + |
239 | + test_TextFieldPickerPlugin_selected_item_is_saved: function () { |
240 | + this.picker.set('results', [{'title': 'Object 1', value: 'first'}]); |
241 | + this.picker.render(); |
242 | + simulate( |
243 | + this.picker.get('boundingBox'), '.yui-picker-results li', 'click'); |
244 | + Assert.areEqual( |
245 | + 'first', Y.Node.getDOMNode(Y.one('[id="field.initval"]')).value); |
246 | + }, |
247 | |
248 | })); |
249 |
Add a new attribute (get_initial_ value_from) to the picker. When that attribute is set, the initial value for the picker's search input will come from the page element identified by it