Merge lp:~salgado/lazr-js/bug-482235 into lp:lazr-js

Proposed by Guilherme Salgado
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
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.

To post a comment you must log in.
Revision history for this message
Guilherme Salgado (salgado) wrote : Posted in a previous version of this proposal

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

Revision history for this message
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.plug(TextFieldPickerPlugin('#text-field'));

Compare that to the existing API which looks something like this:

    var picker = new Y.Picker({get_initial_value_from: '#text-field'});
    picker.subscribe('save', function (e) {
        Y.DOM.byId('#text-field').value = e.details[Y.Picker.SAVE_RESULT].value;
    });

review: Abstain
Revision history for this message
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.plug(TextFieldPickerPlugin('#text-field'));
>

You do this:

Y.Picker.plug(TextFieldPickerPlugin);
var picker = Y.Picker({
  ...
  use_value_from: '#foobar' // Our new attribute
});

The class-level plugin could then add the attributes it needs dynamically.

review: Abstain
Revision history for this message
Guilherme Salgado (salgado) wrote : Posted in a previous version of this proposal
Download full text (3.8 KiB)

> 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.plug(TextFieldPickerPlugin('#text-field'));

Below is what I came up with, but notice the ID of the text field is not passed to TexFieldPickerPlugin (I didn't find out how to do that).

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/widgets/templates/vocabulary-picker.js'
--- lib/canonical/widgets/templates/vocabulary-picker.js 2009-09-21 12:27:14 +0000
+++ lib/canonical/widgets/templates/vocabulary-picker.js 2009-12-11 19:54:11 +0000
@@ -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 TextFieldPickerPlugin(config) {
+ TextFieldPickerPlugin.superclass.constructor.apply(this, arguments);
+ }
+
+ TextFieldPickerPlugin.NAME = 'TextFieldPickerPlugin';
+ TextFieldPickerPlugin.NS = 'txtpicker';
+
+ Y.extend(TextFieldPickerPlugin, Y.Plugin.Base, {
+ 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(args.input_id);
+ this.doAfter('save', function (e) {
+ result = e.details[Y.Picker.SAVE_RESULT];
+ input.value = result.value;
+ });
+ this.doAfter('show', function() {
+ if ( input.value ) {
+ this.get('host')._search_input.set('value', input.value);
+ }
+ });
+ }
+ });
+
     // The vocabulary picker, created when used for the first time.
     var picker = null;
     function make_picker() {
- var save = function (result) {
- Y.DOM.byId(args...

Read more...

Revision history for this message
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.

Revision history for this message
Björn Tillenius (bjornt) wrote :

On Fri, Dec 18, 2009 at 06:30:31PM -0000, Guilherme Salgado wrote:
> === modified file 'src-js/lazrjs/picker/picker.js'
> --- src-js/lazrjs/picker/picker.js 2009-11-24 00:09:41 +0000
> +++ src-js/lazrjs/picker/picker.js 2009-12-18 18:30:30 +0000
> @@ -863,10 +863,39 @@
> }
> };
>
> +
> +function TextFieldPickerPlugin(config) {
> + TextFieldPickerPlugin.superclass.constructor.apply(this, arguments);
> +}
> +
> +TextFieldPickerPlugin.NAME = 'TextFieldPickerPlugin';
> +TextFieldPickerPlugin.NS = 'txtpicker';
> +
> +Y.extend(TextFieldPickerPlugin, Y.Plugin.Base, {
> + 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(config.input_id);

This is not quite true. You can use CSS3 selectors if you use attribute
selectors. For example:

    var input = Y.one('[id="some.id"]');

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://launchpad.net/~bjornt

lp:~salgado/lazr-js/bug-482235 updated
167. By Guilherme Salgado

Move Picker and TextFieldPickerPlugin 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.TextFieldPickerPlugin

Revision history for this message
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(TextFieldPickerPlugin, Y.Plugin.Base, {
> > + 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(config.input_id);
>
> This is not quite true. You can use CSS3 selectors if you use attribute
> selectors. For example:
>
> var input = Y.one('[id="some.id"]');

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://paste.ubuntu.com/344218/
        Example: http://paste.ubuntu.com/344220/

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://paste.ubuntu.com/344216/

How does it look now?

--
Guilherme Salgado <email address hidden>

Revision history for this message
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.getDOMNode().

- 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

review: Approve
Revision history for this message
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.

lp:~salgado/lazr-js/bug-482235 updated
170. By Guilherme Salgado

A couple minor tweaks suggested by Maris

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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: '&lt;h2&gt;Picker example&lt;/h2&gt;',
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

Subscribers

People subscribed via source and target branches