"Element wrapper" draft notes

4 views
Skip to first unread message

kangax

unread,
Aug 19, 2008, 11:46:50 PM8/19/08
to Prototype: Core
As per a recent discussion http://groups.google.com/group/prototype-core/browse_thread/thread/16d0517ecc605a00
I have made a few notes regarding a potential semantics of an "element
wrapper" implementation : )

== Notes:

- The notion of `NodeWrapper` is used to refer to such "element
wrapper".
- `$W` function is assumed to accept an "id" string and return a new
`NodeWrapper` instance "over an element" with that "id".
- Each statement is followed by a brief example (for clarity).
- Naming conventions are a subject of change.

== Semantics:

1) NodeWrapper always exposes a public `source` property, which is a
reference to a wrapped element:

$W('foo').source; // An actual element with id="foo"

2) NodeWrapper's mutator methods always return a reference to a
NodeWrapper they act upon:

$W('foo').addClassName('foo'); // NodeWrapper around an element with
id="foo"

3) NodeWrapper's methods that act upon an element other than the
original one, return that element's NodeWrapper:

$W('foo').wrap('div', { className: 'bar' }); // NodeWrapper around a
newly created `div` element with class="bar"

4) NodeWrapper always exposes public `get`/`set` methods which are
accessor/mutator methods for properties of a wrapped element:

$W('foo').get('innerHTML'); // returns a value of `innerHTML`
property of an element with id="foo"
$W('foo').set('tabIndex', 1); // sets `tabIndex` property of an
element with id="foo" to 1

5) `Element`, when called as a constructor, always returns a
NodeWrapper for a newly created element:

new Element('div', { className: 'foo' }); // a wrapper for a newly
created <div class="foo"></div>

Any comments/objections are welcomed ; )

--
kangax

Walter Lee Davis

unread,
Aug 20, 2008, 12:02:35 AM8/20/08
to prototy...@googlegroups.com

On Aug 19, 2008, at 11:46 PM, kangax wrote:

> $W('foo').wrap('div', { className: 'bar' }); // NodeWrapper around a
> newly created `div` element with class="bar"


Sorry to barge in the middle here, but could you clarify what 'foo'
means here? Is 'foo' the ID of the newly-created div.bar? Or is the
existing div#foo now wrapped with a div.bar?

Walter

kangax

unread,
Aug 20, 2008, 12:21:39 AM8/20/08
to Prototype: Core
Walter, sorry for the confusion.

$W('foo') returns a wrapper of "id='foo' element".
`wrap` then wraps the original element with <div class="bar"></div>
and returns a wrapper around this newly created element (class=bar).

An analogy with our current way of doing things would be:

$('foo').wrap('div', { className: 'bar' });

You can see how it looks almost identical (or fully identical if we
were to use `$` as a wrapper helper). The only difference is the
internal implementation.

-- kangax

John-David Dalton

unread,
Aug 20, 2008, 10:13:07 AM8/20/08
to Prototype: Core
Kangax looks good.
I think that it should be $() instead of $W(), but for the purpose of
clarity during the
discussion the use of $W is fine.

I think that any method that returned element, will instead return
NodeWrapper.
Any method that returned an array of elements will return a
NodeListWrapper.
(In implementation I think $$() should be used but for discussion $
$W() will be used
to avoid confusion)

$$W('input:not([disabled])').highlight();
$$('input:not([disabled])').invoke('highlight');

I think we should follow jQuery's lead here and extend the
Element.Methods and Enumerable methods
to the NodeListWrapper.
$$W(...).each, $$(...).without, ... $$(...).hide(), $$(...).observe

If the node list returns 1 element then methods like $$
(...).getValue() should return the value of that element.
(this is how jQuery handles it)


I think $A should return a ListWrapper, maybe ListWrapper could be the
$super for NodeListWrapper.
ListWrapper would have methods of Array Additions and Enumerable
methods.

kangax

unread,
Aug 20, 2008, 10:45:06 AM8/20/08
to Prototype: Core
On Aug 20, 10:13 am, John-David Dalton <John.David.Dal...@gmail.com>
wrote:
> Kangax looks good.
> I think that it should be $() instead of $W(), but for the purpose of
> clarity during the
> discussion the use of $W is fine.
>
> I think that any method that returned element, will instead return
> NodeWrapper.
> Any method that returned an array of elements will return a
> NodeListWrapper.
> (In implementation I think $$() should be used but for discussion $
> $W() will be used
> to avoid confusion)
>
> $$W('input:not([disabled])').highlight();
> $$('input:not([disabled])').invoke('highlight');

I'm in favor of such syntax.
Besides a performance increase (comparing to `invoke`) this makes the
code somewhat more concise.

>
> I think we should follow jQuery's lead here and extend the
> Element.Methods and Enumerable methods
> to the NodeListWrapper.
> $$W(...).each, $$(...).without, ... $$(...).hide(), $$(...).observe

I think it does make sense to mixin Enumerable - NodeList is a
collection after all. I just hope there won't be name conflicts
between Enumerable methods and user-defined ones.

>
> If the node list returns 1 element then methods like $$
> (...).getValue() should return the value of that element.
> (this is how jQuery handles it)

This could lead to ambiguity. We should probably discuss it more.

>
> I think $A should return a ListWrapper, maybe ListWrapper could be the
> $super for NodeListWrapper.
> ListWrapper would have methods of Array Additions and Enumerable
> methods.

Definitely a possibility if we stop extending Array.prototype.
Otherwise, I don't see much usefulness in this. NodeListWrapper
encapsulates an array of `NodeWrapper`s and its methods operate
directly on those `NodeWrapper`. A custom `_each` and mixed-in
Enumerable would take care of iteration. Am I missing something?

--
kangax

Mislav Marohnić

unread,
Aug 20, 2008, 6:43:55 PM8/20/08
to prototy...@googlegroups.com
I like where all this is heading -- thumbs up, Kangax.

Ken Snyder

unread,
Aug 20, 2008, 7:11:10 PM8/20/08
to prototy...@googlegroups.com
kangax wrote:
> ...

>> If the node list returns 1 element then methods like $$
>> (...).getValue() should return the value of that element.
>> (this is how jQuery handles it)
>
> This could lead to ambiguity. We should probably discuss it more.
I agree that it is ambiguous. I find the selector result object one of
the most curious and confusing aspects of jQuery. I like the
functionality to deal with one element separate from the functionality
to deal with multiple elements. I like the concept of having methods
like NodeListWrapper#addClassName() or NodeListWrapper#setAttribute().
Once you start trying to merge the functionality of $() and $$(), code
becomes hard to follow. Furthermore, unless you use $$('#mydiv') instead
of $('mydiv') there are few cases that it is safe to assume that the
NodeListWrapper object contains only one item.

>> I think $A should return a ListWrapper, maybe ListWrapper could be the
>> $super for NodeListWrapper.
>> ListWrapper would have methods of Array Additions and Enumerable
>> methods.
>
> Definitely a possibility if we stop extending Array.prototype.
> Otherwise, I don't see much usefulness in this. NodeListWrapper
> encapsulates an array of `NodeWrapper`s and its methods operate
> directly on those `NodeWrapper`. A custom `_each` and mixed-in
> Enumerable would take care of iteration. Am I missing something?
>
> --
> kangax

I think he means that it appears that the Enumerable mix-in concept is
necessitated the fact that native Array objects can't inherit from
Enumerable. Consider this hierarchy assuming we use a ListWrapper
instead of Array.prototype:

Enumerable (each, collect, inject, invoke, etc.)
+--ListWrapper (Enumerable + first, last, reduce, reverse, etc.)
+--NodeListWrapper (ListWrapper + addClassName, setAttribute, etc.)
+--ObjectRange (ListWrapper + overrides)
+--Ajax.Responders (ListWrapper + register, unregister, dispatch)
+--Hash (Enumerable + get, set, keys, toQueryString, etc.)


- Ken Snyder


John-David Dalton

unread,
Aug 20, 2008, 10:33:11 PM8/20/08
to Prototype: Core
@Key Snyder - correct I was assuming Array.prototype was not extended
and that we were using wrappers.

@kangax

I don't think there would be confusion with things like $$
(..).getValue() here.

Keep in mind users do use the equiv of $$('#myId') and that in those
cases you are expecting 1 result
so the desired behavior of a readAttribute, or getValue or anything
getting a property
is to return from the only matched item.

jQuery takes $('input').val(), (our $$('input').getValue()) and only
returns the value of the first item regardless of how many elements
are matched.
If you want an array of values then $$('input').invoke('getValue')
would do.

I think millions of downloads and that its popularity among casual/new
programmers
speaks to its ease of use.

Anyway I like the direction things are headed,
can't wait to start hammering on code :).

- JDD

kangax

unread,
Aug 20, 2008, 11:40:20 PM8/20/08
to Prototype: Core
On Aug 20, 10:33 pm, John-David Dalton <John.David.Dal...@gmail.com>
wrote:
> @Key Snyder - correct I was assuming Array.prototype was not extended
> and that we were using wrappers.

Well, if `ListWrapper` is Enumerable + `Array.prototype` extensions,
then yes, it does make sense : )

>
> @kangax
>
> I don't think there would be confusion with things like $$
> (..).getValue() here.
>
> Keep in mind users do use the equiv of  $$('#myId') and that in those
> cases you are expecting 1 result
> so the desired behavior of a readAttribute, or getValue or anything
> getting a property
> is to return from the only matched item.

True.
We could reduce ambiguity by having some kind of a generic rule - e.g.
"all accessor methods always act on the first item". This should be
fine, as long as there are no (or not many) exceptions.

>
> jQuery takes $('input').val(), (our $$('input').getValue()) and only
> returns the value of the first item regardless of how many elements
> are matched.
> If you want an array of values then $$('input').invoke('getValue')
> would do.
>
> I think millions of downloads and that its popularity among casual/new
> programmers
> speaks to its ease of use.

I wouldn't focus on general appeal here, but rather on increasing
compatibility and performance : )
Nevertheless, I agree that such syntax could be useful.

>
> Anyway I like the direction things are headed,
> can't wait to start hammering on code :).

Cool.
I forked prototype, and started working on NodeWrapper/NodeListWrapper
- http://github.com/kangax/prototype/tree/master/src/element.js
Accompanying tests are there as well (although the test suite is far
from being complete).

--
kangax

Ken Snyder

unread,
Aug 21, 2008, 12:15:58 AM8/21/08
to prototy...@googlegroups.com
...

> I forked prototype, and started working on NodeWrapper/NodeListWrapper
> - http://github.com/kangax/prototype/tree/master/src/element.js
> Accompanying tests are there as well (although the test suite is far
> from being complete).
>
> --
> kangax
Exciting to see in code! What about adding caching instances and maybe even using a double-duty constructor instead of Class.create?  See what I mean below.
- Ken Snyder
Prototype.Element = function(element) {
  if (this instanceof Prototype.Element) {
    // constructor
    element.__prototypeWrapper = this;
    this.raw = element;
  } else {
    // "getInstance" function
    // allow element to be string, dom node, or Prototype.Element instance
    element = (typeof element === 'string' ? document.getElementById(element) : element);
    if (element instanceof Prototype.Element) {
      return element;
    }
    if (element && element.__prototypeWrapper) {
      return element.__prototypeWrapper;
    } else {
      return new Prototype.Element(element)
    }
  }
};

$W = Prototype.Element;

var myDiv = document.getElementById('someDiv');
var myWrappedDiv = $W('someDiv');
myWrappedDiv === $W(myDiv); // true
myWrappedDiv === $W(myWrappedDiv); // true
// although $W() is called 3 times, only one instance is created

kangax

unread,
Aug 21, 2008, 1:37:45 AM8/21/08
to Prototype: Core
On Aug 21, 12:15 am, "Ken Snyder" <kendsny...@gmail.com> wrote:
> ...> I forked prototype, and started working on NodeWrapper/NodeListWrapper
> > -http://github.com/kangax/prototype/tree/master/src/element.js
Ken,

I wanted to keep $W consistent with the rest of the library - be a
simple shortcut for creating a new instance of NodeWrapper. Just like `
$H` returns `new Hash` and $R returns `new ObjectRange`, $W should
probably return `new NodeWrapper`. $W is also responsible for
"transforming" string parameter into an actual element (via
`document.getElementById`):

...
this.$W = function(element) {
if (typeof element == 'string') {
element = document.getElementById(element);
}
return new Prototype.Node(element);
};
...

Using such "proxy" helper ($W) also ensures NodeWrapper is always
called as a constructor. There are less chances that a user will
mistakenly call `NodeWrapper` as a function and pollute the global
scope. It, therefore, allows us to get rid of "this instanceof
Prototype.Node" check:

new Prototype.Node('foo'); // returns a wrapper
Prototype.Node('foo'); // boom! returns undefined and creates a global
`source`/`raw` property

Good point about passing a wrapper into a wrapper : ) Would it make
sense to return a wrapper itself or a "clone" of it? Would there ever
be a need for a cloned wrapper?

As far as caching, I'm afraid that storing reference to a wrapper on a
node itself could lead to circular references (which as we know "leak"
in IE's JScript). Having a separate cache-table and map elements to
wrappers by an element's id (or a custom property of an element) seems
more appropriate.

I also think that #update, #insert, #replace, etc. should allow to
accept a wrapper (besides an element or a string):

$W('foo').insert($W('bar'));

This should be as easy as giving wrapper `toElement`/`toHTML` methods
(since `update`/`insert` delegate to those when available)

--
kangax

T.J. Crowder

unread,
Aug 21, 2008, 7:13:20 AM8/21/08
to Prototype: Core
Hey folks,

Weighing in briefly (being hammered at work and at home, and not in
that fun way). Great stuff so far! Thoughts/opinions/questions:

1. IMHO, $$() should _always_ return a list/array/whatever, even if
there's only one element; it should never return the NodeWrapper
itself. Two reasons: A) Having a function that returns a list in
some situations and a single element in another situations just means
that every use of it has to check what it got back, leading to
unnecessarily-verbose code and confusion on the part of users. If
people are doing things like $$("#thingy"), tell them to use $(),
that's what it's for. :-) If there's a really really good reason
they're using $$() (for instance, they don't know what the selector
is, it's supplied from elsewhere), well, then they should always be
expecting to get back a list. B) That's what it does now.

> > I don't think there would be confusion with things like $$
> > (..).getValue() here.
>
> > Keep in mind users do use the equiv of $$('#myId') and that in those
> > cases you are expecting 1 result
> > so the desired behavior of a readAttribute, or getValue or anything
> > getting a property
> > is to return from the only matched item.
>
> True.
> We could reduce ambiguity by having some kind of a generic rule - e.g.
> "all accessor methods always act on the first item". This should be
> fine, as long as there are no (or not many) exceptions.

2. FWIW, it seems to me that calling an accessor on a list should
return a list of the accessor results for each list element. Always.
Similarly, calling a setter on the list [e.g., $$
('.blarg').update('yada')] should call the setter on all list
elements.

> 1) NodeWrapper always exposes a public `source` property, which is a
> reference to a wrapped element:

3. Still don't like "source". That word is just way too overloaded,
and again, it's as much a target (e.g., of an update() call) as a
source. "raw" is the best general term I've thought of, but I'm not
married to it. Whatever we use should be *short* (so not
"wrappedThingamajig") and as clear as feasible. Maybe we should give
up on the generic term (I *know* it was my idea, but...) and just use
something meaningful for each wrapper, e.g. NodeWrapper.node,
ListWrapper.list, etc. Keep 'em short, document 'em, and if people
forget what the property for the wrapped thingy inside a
NodeListWrapper is, too bad, at least the terms are short and clear.

4. get() and set() methods: Great to have them, but I think it's
important that we not expect/require that people _use_ them. We're
not, right? I don't want to make a function call to access tagName,
I'll want to use "wrapper.source.tagName" (or "wrapper.raw.tagName")
without the wrapper getting confused somehow.

5. How will the list wrapper handle indexing into the list? E.g.:

var items;
var n;
items = $$('div.item');
for (n = 0; n < items.length; ++n)
{
items[n].update('This is item ' + n);
}

I'm not aware of a standard, widely-supported JavaScript feature that
lets us define the subscript operator, so I'm guessing the above is
out. Again I hate to introduce a function call for simple access
[e.g., list.item(n), a'la Microsoft collections], so would I go to the
underlying "raw/source"?

6. Question on $$(): I think the idea so far is that it returns a
NodeListWrapper. I'm guessing that that's a list of NodeWrappers
around the results of the query, right? So I could reliably rewrite
my code above like this (I'll use 'source' rather than 'raw' here):

var items;
var list;
var n;
items = $$('div.item');
list = items.source;
for (n = 0; n < list.length; ++n)
{
list[n].update('This is item ' + n);
}

...because although I'm accessing the raw list, it contains wrapped
nodes. Is that the idea?
--
T.J. Crowder
tj / crowder software / com

kangax

unread,
Aug 21, 2008, 7:46:25 AM8/21/08
to Prototype: Core
This is tricky : )
On one hand, having accessor act on all elements seems most intuitive
and consistent with the current API. On the other hand, I can clearly
see a case when getting "first value" is much more convenient.
Considering that NodeList items can not be accessed via brackets,
wouldn't we end up with a somewhat verbose syntax?

$$('#sidebar #expandAll')[0].observe('click', function(){});

becomes:

$W('#sidebar #expandAll').source[0].observe('click', function(){});

Perhaps, if we give NodeList a `first` method, it will make things a
little less cryptic:

$W('#sidebar #expandAll').first().observe('click', function(){});

>
> > 1) NodeWrapper always exposes a public `source` property, which is a
> > reference to a wrapped element:
>
> 3. Still don't like "source".  That word is just way too overloaded,
> and again, it's as much a target (e.g., of an update() call) as a
> source.  "raw" is the best general term I've thought of, but I'm not
> married to it.  Whatever we use should be *short* (so not
> "wrappedThingamajig") and as clear as feasible.  Maybe we should give

I really like `source` but let's keep the naming aside for now : )

> up on the generic term (I *know* it was my idea, but...) and just use
> something meaningful for each wrapper, e.g. NodeWrapper.node,
> ListWrapper.list, etc.  Keep 'em short, document 'em, and if people
> forget what the property for the wrapped thingy inside a
> NodeListWrapper is, too bad, at least the terms are short and clear.
>
> 4. get() and set() methods:  Great to have them, but I think it's
> important that we not expect/require that people _use_ them.  We're
> not, right?  I don't want to make a function call to access tagName,
> I'll want to use "wrapper.source.tagName" (or "wrapper.raw.tagName")
> without the wrapper getting confused somehow.

Sure. We should recommend using them wisely and whenever it makes
sense.

>
> 5. How will the list wrapper handle indexing into the list?  E.g.:
>
>     var items;
>     var n;
>     items = $$('div.item');
>     for (n = 0; n < items.length; ++n)
>     {
>         items[n].update('This is item ' + n);
>     }
>
> I'm not aware of a standard, widely-supported JavaScript feature that
> lets us define the subscript operator, so I'm guessing the above is
> out.  Again I hate to introduce a function call for simple access
> [e.g., list.item(n), a'la Microsoft collections], so would I go to the
> underlying "raw/source"?

"each" always passes index as a second argument to iterator function:

$$('div.item').each(function(node, n) {
node.update('This is item ' + n);
});

>
> 6. Question on $$():  I think the idea so far is that it returns a
> NodeListWrapper.  I'm guessing that that's a list of NodeWrappers
> around the results of the query, right?  So I could reliably rewrite
> my code above like this (I'll use 'source' rather than 'raw' here):
>
>     var items;
>     var list;
>     var n;
>     items = $$('div.item');
>     list = items.source;
>     for (n = 0; n < list.length; ++n)
>     {
>         list[n].update('This is item ' + n);
>     }
>
> ...because although I'm accessing the raw list, it contains wrapped
> nodes.  Is that the idea?

Yep, `source` of NodeList is an array of `Node`s.

--
kangax

T.J. Crowder

unread,
Aug 21, 2008, 9:21:39 AM8/21/08
to Prototype: Core
Hiya,

> > 2. FWIW, it seems to me that calling an accessor on a list should
> > return a list of the accessor results for each list element. Always.
...
>
> This is tricky : )
> On one hand, having accessor act on all elements seems most intuitive
> and consistent with the current API. On the other hand, I can clearly
> see a case when getting "first value" is much more convenient.
> Considering that NodeList items can not be accessed via brackets,
> wouldn't we end up with a somewhat verbose syntax?
>
> $$('#sidebar #expandAll')[0].observe('click', function(){});
>
> becomes:
>
> $W('#sidebar #expandAll').source[0].observe('click', function(){});

Maybe I'm not understanding the example; I'm not a CSS guru by any
means. To me that looks like you're selecting an element with the ID
'expandAll' if and only if it exists within an element with the ID
'sidebar'. If so, that seems like quite an edge case to me (wouldn't
you know whether it was inside the sidebar?), but again, I think I
probably just don't get it.

> Perhaps, if we give NodeList a `first` method, it will make things a
> little less cryptic:

That sounds good. But separately, if we think it's common to need to
select a single element on the basis of a CSS selector, I'd suggest an
explicit means of doing that (e.g., a separate function -- $$First()
comes to mind) that returns a NodeWrapper (or undefined, of course)
and is optimized for that operation -- specifically, it stops as soon
as it's found one. More readable, perhaps slightly faster.

> > 3. Still don't like "source". That word is just way too overloaded,
...
>
> I really like `source` but let's keep the naming aside for now : )

No problem. But you know I'll come back to it. :-)

> > 4. get() and set() methods: Great to have them, but I think it's
> > important that we not expect/require that people _use_ them.
...
> Sure. We should recommend using them wisely and whenever it makes
> sense.

Good good, just checking.

> > 5. How will the list wrapper handle indexing into the list? E.g.:
>
> > var items;
> > var n;
> > items = $$('div.item');
> > for (n = 0; n < items.length; ++n)
> > {
> > items[n].update('This is item ' + n);
> > }
...
>
> "each" always passes index as a second argument to iterator function:

I don't use each() unless I have a really good reason for the added
overhead or I know I'm dealing with so few elements that the added
overhead (dramatic, in IE's case) doesn't matter. I don't want people
to think I'm dissing each() and I don't want to start an each()
conversation. But it seems like people get so caught up in the
coolness of this stuff they they forget that function calls have a
cost, inline closures have an even higher cost. each() is really cool
and I love it, love the idea of it, love the expressiveness of it.
And implementations are making progress. But for now my default mode
will continue to be boring old-fashioned for/next loops (backward if
it doesn't matter and time is *really* of the essence) barring a
strong reason to do something else.

> > 6. Question on $$(): I think the idea so far is that it returns a
> > NodeListWrapper. I'm guessing that that's a list of NodeWrappers
> > around the results of the query, right?
...
> Yep, `source` of NodeList is an array of `Node`s.

Nodes? Or NodeWrappers?

John-David Dalton

unread,
Aug 21, 2008, 10:44:25 AM8/21/08
to Prototype: Core

@T. J. Crowder take some time to look at the jQuery source, it might
help with the wrapper discussion.
I was not suggesting that $$() return a list in some cases and a
single item in others. I was saying that
getter/setter methods would execute on the first matched item.

In jQuery (jQuery syntax):
$('#footer') -> returns a jQuery instance (in our case it would be a
NodeListWrapper);

If you example the object it looks like:
NodeListWrapper[0] -> div#footer (the html element)
NodeListWrapper.length -> 1
NodeListWrapper.observe -> method
NodeListWrapper.toggle -> method
NodeListWrapper.each -> method
NodeListWrapper._each -> method
NodeListWrapper.show -> method
NodeListWrapper.getValue -> method
NodeListWrapper.update -> method

the jQuery object and thus our NodeListWrapper will mimic an array,
it has a length, and index properties.

When you call a getter method then it executes on the first matched
item
NodeListWrapper.getHeight -> execute on the first matched element and
not iterate over the list
NodeListWrapper.getStyle -> execute on ...
NodeListWrapper.getDimensions() -> ...
so $$W('#id').getHeight() -> is the same as -> $$W('#id')
[0].getHeight();

$$W('#id')[0] is an element in jQuery but if we wrap ours in a
NodeWrapper too then
$$W('#id')[0] would be the NodeWrapper
$$W('#id')[0] instanceof NodeWrapper would be true.
$$W('#id')[0].raw() would be the element,

now we can talk about if thats too many wrappers or what not,
but I think the approach of $$(selector)<method> is a good one.

Now we could do this:
NodeWrapper.raw() -> gives you the element;
NodeWrapper.raw(element) -> sets the element;
and the internal var _raw is private;

so that by the rules above if Prototype were modified $$W -> $$, $W ->
$
$('id').raw() -> the element
$$('#id').raw() -> the element (its a getter/setter so it executes on
the first matched)

$$('.panels').hide();
$$('.panels:first').hide();

$('myPanel).raw().tagName
$('myPanel).get('tagName');

There would be no internal raw item for the NodeListWrapper because
like the jQuery object
its items are part of its indexed properties.

- JDD





kangax

unread,
Aug 21, 2008, 10:45:20 AM8/21/08
to Prototype: Core
On Aug 21, 9:21 am, "T.J. Crowder" <t...@crowdersoftware.com> wrote:
> Hiya,
>
>
>
>
>
> > > 2. FWIW, it seems to me that calling an accessor on a list should
> > > return a list of the accessor results for each list element.  Always.
> ...
>
> > This is tricky : )
> > On one hand, having accessor act on all elements seems most intuitive
> > and consistent with the current API. On the other hand, I can clearly
> > see a case when getting "first value" is much more convenient.
> > Considering that NodeList items can not be accessed via brackets,
> > wouldn't we end up with a somewhat verbose syntax?
>
> > $$('#sidebar #expandAll')[0].observe('click', function(){});
>
> > becomes:
>
> > $W('#sidebar #expandAll').source[0].observe('click', function(){});
>
> Maybe I'm not understanding the example; I'm not a CSS guru by any
> means.  To me that looks like you're selecting an element with the ID
> 'expandAll' if and only if it exists within an element with the ID

Correct.

> 'sidebar'.  If so, that seems like quite an edge case to me (wouldn't
> you know whether it was inside the sidebar?), but again, I think I
> probably just don't get it.

This happens anytime one needs to reference an element without id:

$$('[name="'+prop+'"]')[0]; // get element by name
$$('tbody tr:first-child')[0]; // get first row of a table
$$('img[src^="data:image"]')[0]; // get image with specified src
attribute

etc.

>
> > Perhaps, if we give NodeList a `first` method, it will make things a
> > little less cryptic:
>
> That sounds good.  But separately, if we think it's common to need to
> select a single element on the basis of a CSS selector, I'd suggest an
> explicit means of doing that (e.g., a separate function -- $$First()
> comes to mind) that returns a NodeWrapper (or undefined, of course)
> and is optimized for that operation -- specifically, it stops as soon
> as it's found one.  More readable, perhaps slightly faster.

Another global function? ; )

>
>
>
> > > 3. Still don't like "source".  That word is just way too overloaded,
> ...
>
> > I really like `source` but let's keep the naming aside for now : )
>
> No problem.  But you know I'll come back to it. :-)

I know : )
JS1.7 introduces iterators but as of now they are of little use (in a
web environment) : /
If `each` is out of the question, then yes, your previous example
should be fine.

>
>
>
> > > 6. Question on $$():  I think the idea so far is that it returns a
> > > NodeListWrapper.  I'm guessing that that's a list of NodeWrappers
> > > around the results of the query, right?
> ...
> > Yep, `source` of NodeList is an array of `Node`s.
>
> Nodes?  Or NodeWrappers?

NodeWrappers.

--
kangax

Nick Stakenburg

unread,
Aug 21, 2008, 11:24:40 AM8/21/08
to Prototype: Core
On the naming I'd prefer 'source' over 'raw'. It better describes what
you get from it and is easier to use in a sentence. Talking about the
'source' of a Node(List)Wrapper makes more sense then 'raw' of a
Node(List)Wrapper.

--
Nick

T.J. Crowder

unread,
Aug 21, 2008, 11:41:59 AM8/21/08
to Prototype: Core
@JDD:

Thanks. I understood your suggsetion about the accessors, I thought
I'd also (separately) seen a suggestion that $$() return a single
wrapped element when it only finds one, but I'm not immediately
finding what it was that made me thing that. Sounds like we're all on
the same page wrt always returning a list. Apologies for muddying the
waters there.

I disagree on the list applying accessors to only the first element.
From an API perspective I just don't see that it makes sense. I'd
much rather see a first() method or property, or better yet (as I
suggested to kangax) an explicit means of saying "give me the first
(and only the first) element that matches this CSS spec). If I see
the code "list.highlight()", I'm going to expect the highlight method
to be applied to the list, not to the first entry on the list.

> Now we could do this:
> NodeWrapper.raw() -> gives you the element;
> NodeWrapper.raw(element) -> sets the element;
> and the internal var _raw is private;

We'll be using the raw underlying item a LOT, the last thing we want
to do is make accessing it a function call.

> There would be no internal raw item for the NodeListWrapper because
> like the jQuery object
> its items are part of its indexed properties.

If it's not literally a wrapper, if it's a replacement, that's great
and it's certainly the simplest solution. But we keep calling it a
wrapper, which is the only reason for the question about indexed
access.
--
T.J. Crowder
tj / crowder software /

On Aug 21, 3:44 pm, John-David Dalton <John.David.Dal...@gmail.com>
wrote:

Ryan Gahl

unread,
Aug 21, 2008, 11:54:59 AM8/21/08
to prototy...@googlegroups.com
I like the wrapper tack (same way Ext does it). .raw doesn't sound intuitive to me though. Why not just use .dom for the underlying element (also like Ext)? Or .el? .raw just doesn't make for a beautiful API, IMHO :)

And no, don't make it a function call.
--
Ryan Gahl
Manager, Senior Software Engineer
Nth Penguin, LLC
http://www.nthpenguin.com
--
WebWidgetry.com / MashupStudio.com
Future Home of the World's First Complete Web Platform
--
Inquire: 1-920-574-2218
Blog: http://www.someElement.com
LinkedIn Profile: http://www.linkedin.com/in/ryangahl

darrin

unread,
Aug 21, 2008, 11:59:57 AM8/21/08
to Prototype: Core

>
> I disagree on the list applying accessors to only the first element.
> From an API perspective I just don't see that it makes sense.  I'd
> much rather see a first() method or property, or better yet (as I
> suggested to kangax) an explicit means of saying "give me the first
> (and only the first) element that matches this CSS spec).  If I see
> the code "list.highlight()", I'm going to expect the highlight method
> to be applied to the list, not to the first entry on the list.
>

I agree with that disagreement. Although it shortens up the syntax, to
me it's counter intuitive for a list to operate on the first element
for item related functions.

Darrin

T.J. Crowder

unread,
Aug 21, 2008, 12:00:03 PM8/21/08
to Prototype: Core
> This happens anytime one needs to reference an element without id:
>
> $$('[name="'+prop+'"]')[0]; // get element by name
> $$('tbody tr:first-child')[0]; // get first row of a table
> $$('img[src^="data:image"]')[0]; // get image with specified src
> attribute

Sure, that makes much more sense to me than '#sidebar #expandAll'.

> > ...But separately, if we think it's common to need to
> > select a single element on the basis of a CSS selector, I'd suggest an
> > explicit means of doing that (e.g., a separate function -- $$First()
> > comes to mind)...
>
> Another global function? ; )

Nah, we're namepsacing it, remember? ;-) Seriously, though, if it's
common to need to select a single element not by ID and act on it,
let's make that an explicit and efficient thing, rather than bending
the API of the node list thingy to suit the case of only *really*
wanting the first element. Doesn't matter (much) whether it's another
top-level function (although please, not literally with the name "$
$First()" -- blech) or an extension to something else (although I'd
leave $() -- which is otherwise very tempting -- lean and mean),
whatever. But using $$() to go get a single element seems wasteful to
me, it'll continue its recursive descent even after it has what you
want, and then it goes and puts a list around it, all to be thrown
away because you're just going to use the first element anyway.

FWIW.

-- T.J. :-)

kangax

unread,
Aug 21, 2008, 12:09:18 PM8/21/08
to Prototype: Core
On Aug 21, 10:44 am, John-David Dalton <John.David.Dal...@gmail.com>
wrote:
> @T. J. Crowder take some time to look at the jQuery source, it might
> help with the wrapper discussion.
> I was not suggesting that $$() return a list in some cases and a
> single item in others. I was saying that
> getter/setter methods would execute on the first matched item.
>
> In jQuery (jQuery syntax):
> $('#footer') -> returns a jQuery instance (in our case it would be a
> NodeListWrapper);
>
> If you example the object it looks like:
>   NodeListWrapper[0] -> div#footer (the html element)
>   NodeListWrapper.length -> 1
>   NodeListWrapper.observe -> method
>   NodeListWrapper.toggle -> method
>   NodeListWrapper.each -> method
>   NodeListWrapper._each -> method
>   NodeListWrapper.show  -> method
>   NodeListWrapper.getValue -> method
>   NodeListWrapper.update -> method
>
> the jQuery object and thus our NodeListWrapper will mimic an array,
> it has a length, and index properties.
>

Are we going to make `ListWrapper` pretend it's an array?
I agree that it's convenient to be able to access single elements of a
list with brackets, but wouldn't exposing all these properties break
the integrity/abstraction of a `ListWrapper`?

jQuery "fills" its instance with such properties and seems to adjust
`length` accordingly, but it's far from being an array:

var j = $('div');
j.length; // 14
j[14] = 'foo';
j.length; // 14 (not 15) - no "magic" length behavior obviously
j.push; // undefined
j.pop; // undefined
j.concat; // undefined

I think the only array-like method they have is `slice`.

--
kangax

John-David Dalton

unread,
Aug 21, 2008, 12:32:07 PM8/21/08
to Prototype: Core

// if source is a method
$$(css).source() -> equiv
$$(css)[0].source() -> equiv
$$(css).first().source() -> equiv
you could always cache it
var source = $$(css).source();

// an array of sources
$$(css).invoke('source'); // OR
$$(css).each($$.source); //notice static $$.source method
$$(css)._each($$.source); // faster than all of them :P

if source is a property
//no soup for $$().<method>
$$(css)[0].source
$$(css).first().source
$$(css).pluck('source');

//get height of first matched item (equivs)
$$(css).first().getHeight(),
$$(css)[0].getHeight(), and
$$(css).getHeight() are equivs.

// array of heights (equivs)
$$(css).invoke('getHeight');
$$(css).each($$.getHeight);
$$(css)._each($$.getHeight);

//setters
$$(css).update(...); //sets all matched items
$$(css).setStyle(...) //sets all matched items
$$(css).addClassName(...) // does it to all matched items
$$(css).show().observer('click', ...); // shows all matches and
attached an onclick observer to all of them.

// get element by id
$(id).hide();
$(id).source() or $(id).source (whichever we decide)
$(id).source()._prototypeWrapperID //the id that the cache looks at
$(id).source()._prototypeWrapperID -> [2]
// its an array with a number in it
because it wont transfer when you clone the node, this is how
its done in the current event system when element._prototypeEventID is
set.

We mod Class.create to allow the "initialize" method to return a value
so then:
Prototype.NodeWrapper = Class.create({
initialize: function(...) {
// ...
if (Prototype.isWrapper(element)) { // some check for wrapper
return element;
}

// ... resolve element from ID or whatnot

var cacheID = (element._prototypeWrapperID || [null])[0];
if (cacheID) {
return Prototype.NodeWrapper.cache[cacheID];
}
// set source, however that is decided on.
this._source = element;
}, // ...
})

The reason we name it "source" instead of "element" or "dom"
is so we can always know what the source of a wrapper is without
having to guess if its an
Element wrapper to check for "element" if its an event wrapper to
check for .event.
we would know its always .source.

- JDD

Ken Snyder

unread,
Aug 21, 2008, 2:05:31 PM8/21/08
to prototy...@googlegroups.com
kangax wrote:
> ...

> Are we going to make `ListWrapper` pretend it's an array?
> I agree that it's convenient to be able to access single elements of a
> list with brackets, but wouldn't exposing all these properties break
> the integrity/abstraction of a `ListWrapper`?
>
> jQuery "fills" its instance with such properties and seems to adjust
> `length` accordingly, but it's far from being an array:
>
> var j = $('div');
> j.length; // 14
> j[14] = 'foo';
> j.length; // 14 (not 15) - no "magic" length behavior obviously
> j.push; // undefined
> j.pop; // undefined
> j.concat; // undefined
>
> I think the only array-like method they have is `slice`.
>
> --
> kangax
I agree that ListWrapper should not try to be like an array.

In fact, I think it would be faster to have a ListWrapper#item method
instead of extending the ListWrapper with numbered indexes:

$$W('div p').item(5);

ListWrapper.prototype.item = function(index) {
// in this example, the element isn't even wrapped until it is accessed
return this.raw[index] ? $W(this.raw[index]) : undefined;
};

Length could be a property like a browser NodeList object, but we might
find more consistency with a ListWrapper#size method instead.

- Ken Snyder

Ken Snyder

unread,
Aug 21, 2008, 2:32:40 PM8/21/08
to prototy...@googlegroups.com
kangax wrote:
> ...

> Ken,
>
> I wanted to keep $W consistent with the rest of the library - be a
> simple shortcut for creating a new instance of NodeWrapper. Just like `
> $H` returns `new Hash` and $R returns `new ObjectRange`, $W should
> probably return `new NodeWrapper`. $W is also responsible for
> "transforming" string parameter into an actual element (via
> `document.getElementById`):
>
> ...
> this.$W = function(element) {
> if (typeof element == 'string') {
> element = document.getElementById(element);
> }
> return new Prototype.Node(element);
> };
> ...
>
> Using such "proxy" helper ($W) also ensures NodeWrapper is always
> called as a constructor. There are less chances that a user will
> mistakenly call `NodeWrapper` as a function and pollute the global
> scope. It, therefore, allows us to get rid of "this instanceof
> Prototype.Node" check:
>
> new Prototype.Node('foo'); // returns a wrapper
> Prototype.Node('foo'); // boom! returns undefined and creates a global
> `source`/`raw` property
>
That makes sense. I was just thinking where it might be confusing that:

$W('foo') instanceof Prototype.Node; // true

but seeing it written out like that does make sense.


> Good point about passing a wrapper into a wrapper : ) Would it make
> sense to return a wrapper itself or a "clone" of it? Would there ever
> be a need for a cloned wrapper?
>

The only case I can think of is when there is an item in memory with the
same id as one on the page. I don't know how we would handle that case.
Maybe the caching system could handle that somehow.


> As far as caching, I'm afraid that storing reference to a wrapper on a
> node itself could lead to circular references (which as we know "leak"
> in IE's JScript). Having a separate cache-table and map elements to
> wrappers by an element's id (or a custom property of an element) seems
> more appropriate.
>

What about this caching idea: http://gist.github.com/6609

> I also think that #update, #insert, #replace, etc. should allow to
> accept a wrapper (besides an element or a string):
>
> $W('foo').insert($W('bar'));
>
> This should be as easy as giving wrapper `toElement`/`toHTML` methods
> (since `update`/`insert` delegate to those when available)
>

Definitely.
> --
> kangax
Ken Snyder

kangax

unread,
Aug 21, 2008, 3:44:37 PM8/21/08
to Prototype: Core
On Aug 21, 12:00 pm, "T.J. Crowder" <t...@crowdersoftware.com> wrote:
[snip]
> Nah, we're namepsacing it, remember? ;-) Seriously, though, if it's
> common to need to select a single element not by ID and act on it,
> let's make that an explicit and efficient thing, rather than bending
> the API of the node list thingy to suit the case of only *really*

I'm more and more leaning towards accessors acting upon the entire
collection. Too much magic is confusing. Some method names might not
clearly reflect whether they are getters or setters.

Freshly baked YUI 3 does exactly that - any method on a NodeList
(which is a wrapper) acts on *every* element in a collection (as far
as I can see from examples page). I like their approach - it's
intuitive and robust. The way they solve single item "cherry-picking"
is via `item` method and `length` is obtained via `size` method. I
know that some people are not too fond of such syntax but I actually
find it elegant.

--
kangax

John-David Dalton

unread,
Aug 21, 2008, 5:49:16 PM8/21/08
to Prototype: Core

Ya YUI 3 is something to check out:
YUI({combine: true, timeout: 10000}).use("node-style", function(Y) {
var nodes = Y.all('.panels');
nodes.each(....);
});

YUI({combine: true, timeout: 10000}).use("node-base", function(Y) {
var onClick = function(e) {
var node = e.currentTarget;
node.get('parentNode').removeChild(node);
};

Y.all('#demo li').on('click', onClick);
});

They don't seem to expose the wrapped element.
But does expose the wrappers unique ID: ._yuid = "yuid-4-8"
The element has a property added to it:
element._yuid = "yuid-4-8"

Y.all('*').get('value') returns an array of values (most are undefined
but its an array)
Y.get('*').get('tagName') returns "HTML" and thats it (single match)

I do dig Y.all('.panels').on('click', function(e){...});

- JDD

kangax

unread,
Aug 21, 2008, 10:14:50 PM8/21/08
to Prototype: Core
Good points, Ken.

This made me think about lazy initialization of NodeListWrapper's. The
two options seem to be:
1) store an array of "pure" elements and turn them into wrappers "on
the fly"
2) turn elements into wrappers when instantiating `NodeListWrapper`
and then return these (already "prepared") wrappers.

I'm in favor of putting all the hard work on constructor (#2) so that
`NodeListWrapper` methods don't have to spend time wrapping elements.
This way, you could create `NodeListWrapper` outside of intensive
areas of application (such as frequent event handlers) and have a
higher performance later on:

Btw, the performance of wrappers looks very optimistic:

// 217 elements total; "base_test.html" from the prototype's "test/
unit"
// FF 3.0.1 / OSX 10.5

var elements = $$('*'), nodeListWrapper = $$W('*');

console.time('elements - invoke');
for (var i=0; i<100; i++) {
elements.invoke('show');
}
console.timeEnd('elements - invoke');


console.time('elements - each');
for (var i=0; i<100; i++) {
elements.each(Element.show);
}
console.timeEnd('elements - each');

console.time('wrapper - each');
for (var i=0; i<100; i++) {
nodeListWrapper.show();
}
console.timeEnd('wrapper - each');


console.time('wrapper - for');
for (var i=0; i<100; i++) {
nodeListWrapper._show();
}
console.timeEnd('wrapper - for');

/*
elements - invoke: 718ms
elements - each: 282ms
wrapper - each: 250ms
wrapper - for: 220ms
*/

--
kangax

T.J. Crowder

unread,
Aug 22, 2008, 7:14:59 AM8/22/08
to Prototype: Core
> This made me think about lazy initialization of NodeListWrapper's. The
> two options seem to be:
> 1) store an array of "pure" elements and turn them into wrappers "on
> the fly"
> 2) turn elements into wrappers when instantiating `NodeListWrapper`
> and then return these (already "prepared") wrappers.
>
> I'm in favor of putting all the hard work on constructor (#2)...

NodeLists are live, though, aren't they?
http://www.w3.org/TR/2004/REC-DOM-Level-3-Core-20040407/core.html#ID-536297177
http://www.w3.org/TR/2004/REC-DOM-Level-3-Core-20040407/core.html#td-live

-- T.J. :-)

kangax

unread,
Aug 22, 2008, 8:15:55 AM8/22/08
to Prototype: Core
On Aug 22, 7:14 am, "T.J. Crowder" <t...@crowdersoftware.com> wrote:
> > This made me think about lazy initialization of NodeListWrapper's. The
> > two options seem to be:
> > 1) store an array of "pure" elements and turn them into wrappers "on
> > the fly"
> > 2) turn elements into wrappers when instantiating `NodeListWrapper`
> > and then return these (already "prepared") wrappers.
>
> > I'm in favor of putting all the hard work on constructor (#2)...
>
> NodeLists are live, though, aren't they?http://www.w3.org/TR/2004/REC-DOM-Level-3-Core-20040407/core.html#ID-...http://www.w3.org/TR/2004/REC-DOM-Level-3-Core-20040407/core.html#td-...

Of course, but our selector implementation turns them into an array.

--
kangax

T.J. Crowder

unread,
Aug 22, 2008, 9:09:25 AM8/22/08
to Prototype: Core
> > NodeLists are live, though, aren't they?http://www.w3.org/TR/2004/REC-DOM-Level-3-Core-20040407/core.html#ID-......
>
> Of course, but our selector implementation turns them into an array.

Ah, okay, sorry about that. I was thinking live. I've read
http://github.com/kangax/prototype/tree/master/src/element.js
now and see where you're coming from. Should have done that earlier.

So if I'm using Prototype without enabling automatic extension, I can
use $$() to get an array of unextended elements and do whatever I want
(e.g., I know it's going to be a big list and don't want them extended
until/unless I actually use them). If I use $$W(), I'll get the new
Prototype.NodeList with all of the elements pre-wrapped. Is that the
idea going forward?

-- T.J. :-)

On Aug 22, 1:15 pm, kangax <kan...@gmail.com> wrote:
> On Aug 22, 7:14 am, "T.J. Crowder" <t...@crowdersoftware.com> wrote:
>
> > > This made me think about lazy initialization of NodeListWrapper's. The
> > > two options seem to be:
> > > 1) store an array of "pure" elements and turn them into wrappers "on
> > > the fly"
> > > 2) turn elements into wrappers when instantiating `NodeListWrapper`
> > > and then return these (already "prepared") wrappers.
>
> > > I'm in favor of putting all the hard work on constructor (#2)...
>
> > NodeLists are live, though, aren't they?http://www.w3.org/TR/2004/REC-DOM-Level-3-Core-20040407/core.html#ID-......

John-David Dalton

unread,
Aug 22, 2008, 10:33:41 AM8/22/08
to Prototype: Core

T.J. $$W and $$ are the same thing (just named $$W so that we know we
are talking about the new implementation).

I kind of like the idea of lazy wrapping.
That would speed up the initial selector while offering a one time hit
later on,
on only the elements chosen (which is usually all of them being
iterated over by
"each" or "include") but for those cases were it iterates against
"find" it would help because
the un-iterated items wont be wrapped. Wrapping is by no means as slow
as extending the elements manually (ala IE), you create an instance
and assign it the passed element so the speed hit is still minimal.
Having to iterate over a list of matches elements is just another hit
to performance.
By using lazy wrapping we can avoid that and keep it as streamline and
as few steps between the native selector methods as possible.

$$(css).each(...);
$$(css).item(2).addClassName('awesome');
The $$(...)._each would iterate over the internal list
so it wouldn’t need to call item(i) during its iterations.

kangax

unread,
Aug 22, 2008, 10:37:29 AM8/22/08
to Prototype: Core
On Aug 22, 9:09 am, "T.J. Crowder" <t...@crowdersoftware.com> wrote:
> > > NodeLists are live, though, aren't they?http://www.w3.org/TR/2004/REC-DOM-Level-3-Core-20040407/core.html#ID-......
>
> > Of course, but our selector implementation turns them into an array.
>
> Ah, okay, sorry about that.  I was thinking live.  I've readhttp://github.com/kangax/prototype/tree/master/src/element.js
> now and see where you're coming from.  Should have done that earlier.
>
> So if I'm using Prototype without enabling automatic extension, I can
> use $$() to get an array of unextended elements and do whatever I want
> (e.g., I know it's going to be a big list and don't want them extended
> until/unless I actually use them).  If I use $$W(), I'll get the new
> Prototype.NodeList with all of the elements pre-wrapped.  Is that the
> idea going forward?

Well, for now I want to keep $W and $$W as a separate addition. It's
very unlikely that wrappers are to replace element extension anytime
before ver. 2.0. Having a working implementation now will help us test
it thoroughly (before moving it into the core).
I'm not sure if we should "pre-wrap" elements or do the "lazy init".
First approach "hits" the memory consumption, while second - run-time
performance. We'll need benchmarks for this.

--
kangax

T.J. Crowder

unread,
Aug 22, 2008, 11:30:27 AM8/22/08
to Prototype: Core
> I'm not sure if we should "pre-wrap" elements or do the "lazy init".
> First approach "hits" the memory consumption, while second - run-time
> performance. We'll need benchmarks for this.

Yeah. I know you were initially thinking pre-wrap, and my first
instinct was lazy-init (of course!), which was part of the point of my
question. If I have $$() for my lazy-init needs, I'm less likely to
weigh in and say $$W() should be lazy-init as well.

That said, lazy-init seems pretty cheap; essentially an extra
comparison on each item() call. Thinking out loud:

initialize: function(source) {
this.source = source;
this.wrappers = [];
},

size: function() {
return this.source.length;
},

item: function(index) {
return this.wrappers[index] || (this.wrappers[index] =
$W(this.source[index]));
},

_each: function(iterator) {
var w, s, index, length, value;
w = this.wrappers;
s = this.source;
index = 0;
length = s.length;
while (index < length) {
value = w[index] || (w[index] = $W(s[index]));
iterator(value, index++);
}
}

I repeated the item() logic within _each() because it's such a common
op it seems worth avoiding the extra function call, but anyone who
wants to accuse me of premature optimization at the cost of
maintainability might be on reasonably solid ground. ;-)

But I agree with you: We need benchmarks.

-- T.J. :-)

kangax

unread,
Aug 22, 2008, 11:37:57 AM8/22/08
to Prototype: Core
On Aug 22, 10:33 am, John-David Dalton <John.David.Dal...@gmail.com>
wrote:
> T.J. $$W and $$ are the same thing (just named $$W so that we know we
> are talking about the new implementation).
>
> I kind of like the idea of lazy wrapping.
> That would speed up the initial selector while offering a one time hit
> later on,
> on only the elements chosen (which is usually all of them being
> iterated over by
> "each" or "include") but for those cases were it iterates against
> "find" it would help because
> the un-iterated items wont be wrapped. Wrapping is by no means as slow
> as extending the elements manually (ala IE), you create an instance
> and assign it the passed element so the speed hit is still minimal.

John,

I'm not worried about initial performance "hit". I'm worried about
initial memory "hit". Lazy initialization uses constant amount of
memory - i.e. creating 1 Object object (instance of
Prototype.NodeList) and 1 Array object (actual elements). If you were
to use ".item(n)" afterwords, it would result in creating another
Object object at run-time (i.e. an instance of Prototype.Node).

The problem here is that accessing 1 item is something that doesn't
happen often. In most cases, some operation needs to be performed on
the entire collection right after "collecting it":

$$W('h2.toggle').observe('click', function(){
this.next().toggle();
});

$$W('tbody tr:nth-child(odd)').addClassName('odd');

$$W('#nav li
a').removeClassName('active').first().addClassName('active');

// etc.

I think that, most of the time, the memory consumption difference will
be negligible. The run-time performance hit, on the other hand, is
something that could hog things down. Even if $W were to wrap element
only once, there is still some overhead:

When iterating over a `source` items, we don't know if an item is a
wrapper or an element. We still need to either explicitly check for
it, or delegate the check to $W:

*/
$w('show hide update')._each(function(method) {
Prototype.Nodelist.prototype[method] = function() {
for (var i=0, l=this.source.length; i<l; i++) {
// wrap elements at run time
if (this.source[i] instanceof Prototype.Node) {
this.source[i][method].apply(this.source[i], arguments);
}
else {
(this.source[i] = $W(this.source[i]))
[method].apply(this.source[i], arguments);
}
}
}
});

// or delegating

$w('show hide update')._each(function(method) {
Prototype.Nodelist.prototype[method] = function() {
for (var i=0, l=this.source.length; i<l; i++) {
// wrap elements at run time
(this.source[i] = $W(this.source[i]))
[method].apply(this.source[i], arguments);
}
}
});

// and $W would take care of the rest

function $W(element) {
if (element instanceof Prototype.Node) {
return element;
}
if (typeof element == 'string') {
element = document.getElementById(element);
}
return new Prototype.Node(element);
};

There is an overhead of an extra function call (which could
potentially be performed thousands of times) and all these
conditionals.

Anyway, let's wait till we have some benchmarks to support these
claims : )

--
kangax

T.J. Crowder

unread,
Aug 22, 2008, 1:52:49 PM8/22/08
to Prototype: Core
> When iterating over a `source` items, we don't know if an item is a
> wrapper or an element.

My _each implementation deals with that case pretty efficiently. The
ongoing runtime cost of item() and _each() is pretty much one extra
comparison.

Before we can do benchmarks, we should ask people for usage guidance.
For instance, is the statement "In most cases, some operation needs to
be performed on the entire collection right after 'collecting it':"
true? It may be, or may not. (In cases where it's not, we can
probably give people guidance about honing their CSS...)

-- T.J. :-)

kangax

unread,
Aug 22, 2008, 4:07:25 PM8/22/08
to Prototype: Core
On Aug 22, 11:30 am, "T.J. Crowder" <t...@crowdersoftware.com> wrote:
> > I'm not sure if we should "pre-wrap" elements or do the "lazy init".
> > First approach "hits" the memory consumption, while second - run-time
> > performance. We'll need benchmarks for this.
>
> Yeah.  I know you were initially thinking pre-wrap, and my first
> instinct was lazy-init (of course!), which was part of the point of my
> question.  If I have $$() for my lazy-init needs, I'm less likely to
> weigh in and say $$W() should be lazy-init as well.
>
> That said, lazy-init seems pretty cheap; essentially an extra
> comparison on each item() call.  Thinking out loud:
>
>     initialize:  function(source) {
>         this.source = source;
>         this.wrappers = [];

Not sure if having two arrays in each `NodeList` compensates for the
load-time save : ) Definitely an interesting idea, though. I'll think
about it.

>     },
>
>     size:  function() {
>         return this.source.length;
>     },
>
>     item:  function(index) {
>         return this.wrappers[index] || (this.wrappers[index] =
> $W(this.source[index]));
>     },
>
>     _each:  function(iterator) {
>         var w, s, index, length, value;
>         w = this.wrappers;
>         s = this.source;
>         index = 0;
>         length = s.length;
>         while (index < length) {
>             value = w[index] || (w[index] = $W(s[index]));
>             iterator(value, index++);
>         }
>     }
>
> I repeated the item() logic within _each() because it's such a common
> op it seems worth avoiding the extra function call, but anyone who
> wants to accuse me of premature optimization at the cost of
> maintainability might be on reasonably solid ground. ;-)
>
> But I agree with you:  We need benchmarks.

--
kangax

Ken Snyder

unread,
Aug 22, 2008, 4:56:39 PM8/22/08
to prototy...@googlegroups.com
kangax wrote:
> On Aug 22, 11:30 am, "T.J. Crowder" <t...@crowdersoftware.com> wrote:
>
>>> I'm not sure if we should "pre-wrap" elements or do the "lazy init".
>>> First approach "hits" the memory consumption, while second - run-time
>>> performance. We'll need benchmarks for this.
>>>
>> Yeah. I know you were initially thinking pre-wrap, and my first
>> instinct was lazy-init (of course!), which was part of the point of my
>> question. If I have $$() for my lazy-init needs, I'm less likely to
>> weigh in and say $$W() should be lazy-init as well.
>>
>> That said, lazy-init seems pretty cheap; essentially an extra
>> comparison on each item() call. Thinking out loud:
>>
>> initialize: function(source) {
>> this.source = source;
>> this.wrappers = [];
>>
>
> Not sure if having two arrays in each `NodeList` compensates for the
> load-time save : ) Definitely an interesting idea, though. I'll think
> about it.
> ...
> --
> kangax
>
I think you may have already mentioned this, but what about lazy
wrapping but altering the raw array? (see below)

Of course an argument /against/ lazy wrapping is that we would have to
make "this.raw" a private property because you wouldn't know if it
contains wrapped or unwrapped elements.

- Ken


initialize: function(array) {
this.raw = array;
this.length = this.raw.length;
}...
item: function(index) {
if (index < 0 || index >= this.length) return null;
// $W either instantiates a new wrapper, or returns the value
// if the value is already an instance of Prototype.ElementWrapper
this.raw[index] = $W(this.raw[index]);
return this.raw[index];
}...
_each: function(iterator) {
var index = 0;
while (index < this.length)
iterator(this.item(index), index++);
}

T.J. Crowder

unread,
Aug 22, 2008, 5:25:33 PM8/22/08
to Prototype: Core
@JDD: Sorry, missed your note in the melee.

> T.J. $$W and $$ are the same thing (just named $$W so that we know we
> are talking about the new implementation).

Depending on how $$W is implemented, I may pretty strongly want
Prototype's excellent selectors but without automatic element
wrapping. Let's see how things play out...

-- T.J. :-)

On Aug 22, 3:33 pm, John-David Dalton <John.David.Dal...@gmail.com>
wrote:

T.J. Crowder

unread,
Aug 22, 2008, 5:58:20 PM8/22/08
to Prototype: Core
@Ken:

> Of course an argument /against/ lazy wrapping is that we would have to
> make "this.raw" a private property because you wouldn't know if it
> contains wrapped or unwrapped elements.

Yeah, that was my primary reason for keeping it separate; originally I
was thinking integrated as well but I'm a bare metal kind of guy and
want access to a raw list. (Can you tell I spent my formative years
as a C programmer in performance-challenged environments?) But it
definitely takes more memory. For a 1,000 node list, it's 1,000 extra
references (duh), which best case is 4k and more likely 8-24k; it
totally depends on how an implementation implements JavaScript's
sparse arrays.

@All:

I have a kookie idea for you guys. How 'bout not making this decision
for the users (except as a default)? In fact, how 'bout not
conflating selection and wrapping? This is totally off the top of my
head and may look really stupid in the morning, but:

Usage:

// I want a raw list:
var rawList = $$('.foo');

// I want a wrapped list of wrapped elements, so I wrap it:
var wrappedList = $L($$('.foo'));

// I want a lazily-wrapped list:
var lazyList = $L($$('.foo'), "lazy");

Doesn't have to be $L(), whatever, I'm just using that a as
placeholder for a convenient list wrapper like $W() is for elements.

Concept source excerpts for NodeList:

initialize: function(source, style) {
style = style || "prewrap";
this.length = source.length;
if (style == "lazy") {
this.source = source;
this.wrappers = [];
this.item = this._lazyitem;
this._each = this._lazyeach;
}
else /* assume "prewrap" */ {
this.wrappers = source.map($W);
}
},

size: function() {
return this.length;
},

item: function(index) {
return this.wrappers[index];
},

_lazyitem: function(index) {
return this.wrappers[index] || (this.wrappers[index] =
$W(this.source[index]));
},

_each: function(iterator) {
this.wrappers._each(iterator);
}

_lazyeach: function(iterator) {
var w, s, index, length, value;
w = this.wrappers;
s = this.source;
index = 0;
length = s.length;
while (index < length) {
value = w[index] || (w[index] = $W(s[index]));
iterator(value, index++);
}
}

So there are two concepts in the above:

1. Why do we need to replace $$() or create $$W()? Keep $$() as it is
and offer a way to wrap it. I think that's kangax's current thought
anyway.

2. Other than item() and _each(), what's the difference between a
prewrapped list and a lazy list? Provided we ensure that the other
methods use one of those methods rather than direct access? Sure, the
lazy ones (given the source above) end up having their own item() and
_each() references rather than sharing the prototype's, but hey, a
whole new class just for this seems a bit over the top.

I have a tendency to go too far on options, and this may be an
example. But no one knows better than the ultimate author what better
suits what they're doing.

But again the above hinges on the question: Is it really a valid use
case that people will want a list of lazily-wrapped elements? I know
I'll want lists of unwrapped elements (for which I can just keep using
$$() if it doesn't change), and I'm pretty sure I'll want lists of
wrapped ones (and so I'll use the nifty new stuff). But I'm not sure
I'll want a list where some elements end up wrapped and others don't.
Maybe if I'm doing some massive kind of big select where it's really
important to me that we only traverse the DOM once, and then I'll walk
the array making decisions; that's the only time I can think of and in
that case I'd just use $$() anyway and wrap things as/when I needed
the sugar.

-- T.J. :-)

T.J. Crowder

unread,
Aug 23, 2008, 2:49:50 AM8/23/08
to Prototype: Core
Folks,

Sorry, this is probably a better implementation of _lazyeach:
http://pastie.org/258491

wrapAll() converts the lazy-mode list into a wrapped-mode list by
making sure all elements are wrapped and reverting item() and _each()
to the prototype's versions; _lazyeach() just calls wrapAll() and
returns _each().

-- T.J. :-)

kangax

unread,
Aug 23, 2008, 12:23:23 PM8/23/08
to Prototype: Core
Last thing I want is to introduce confusion with all this optional
wrapping : )
I think we're diving too deep for the sake of elusive performance
gains.

The wrapper approach is already more superior than the one we
currently use. While `$` needs to iterate over the whole bunch of
methods (from Element.Methods, Form.Methods, etc.) and assign all of
them to an element (in certain crappy browsers - e.g. IE), `$W`
*always* creates one single object with one single reference to an
element. Having that reference to an element is what gives us a huge
performance boost. The familiar "element = $(element)" line in the
beginning of every method is the one that hogs things down. Calling
wrapper's method is like calling a method on an element itself -
performance gets as close to the "native" one as possible.

The other concern is obviously a memory usage. Caching element wrapper
could definitely help. We just have to make sure there are no unused
references after an element is destroyed.

Also, T.J, could you kindly explain when you would use plain `$$` but
not `$$W`?

--
kangax

T.J. Crowder

unread,
Aug 26, 2008, 4:28:17 AM8/26/08
to prototy...@googlegroups.com
> Also, T.J, could you kindly explain when you would use plain
> `$$` but not `$$W`?

Any time I don't need the sugar.

-- T.J. :-)

Reply all
Reply to author
Forward
0 new messages