Tag instances are populated with these properties:
http://plnkr.co/edit/p1I2rQiD4MpUofd9PC18?p=preview
I find this confusing. Maybe riot3 could have better defined and documented apis for accessing child tag instances and element nodes? While I am not sure what to propose, I think that exposing both (named nodes, tags and all child tags) is very useful when working with custom tags, but it would be nice to have a more intuitive api. Btw., riot3 behavior feels logical after thinking about it, but I think it will be very confusing for newcomers.
I agree I could be confusing, I thought to add the ref property to cache all the Nodes instances without polluting the tag properties. What do you think guys?
cc: @tipiirai @aMarCruz @cognitom @rsbondi @rogueg
my concern is that this can be a big breaking change and all the riotjs ecosystem will be damaged by this decision. I can imagine that all the videos/tutorials/articles produced will be deprecated for such a small change
So in this discussion we should also talk about how much stuff riot3 can break. Changes in the next branch will already break lots of custom tags (just look at my plnkr above) - I am concerned about compatibility here.
Yeah, I found the refs really confusing as well, which prompted the change in next. I'd describe it like this:
id or name gets attached to the tag with id or name as the key.<div id='foo' /> => parent.foo = DOM element<my-tag name='bar' /> => parent.bar = Tag(my-tag)tags collection contains every child tag, keyed by the type of tag.<my-tag name='bar' /> => parent.tags['my-tag'] = Tag(my-tag)The goal was to making tag references more predictable, and easier for newcomers to remember. You're right that it will require some code changes:
parent.bar._tag becomes just parent.bar
parent.bar becomes parent.bar.root
parent.tags.bar is now undefined
Looking at the online material, the common case seems to be referencing inputs, and that behavior hasn't changed.
@rogueg: sorry, but the changes you describe look like just another shade of confusing :)
Neither riot2 nor current riot3 is intuitive in this regard, both behaviors require quite a bit of familiarity before they are understandable.
Paraphrasing your description, here is what we currently have in master:
<div id='foo' /> => parent.foo = DOM element<my-tag name='bar' /> => parent.tags['my-tag'] = parent.tags.bar = Tag(my-tag)I find it easier to explain the old behavior. I only find it confusing that id is preferred over name in riot2, but I guess this can be fixed without breaking compatibility.
Btw., riot-mui would break with riot3: https://github.com/kysonic/riot-mui/blob/master/src/material-elements/material-combo/material-combo.tag#L29
Well, perhaps both ways take a bit of learning. I think your description of master is subtly off, because
<my-tag name='bar' /> => parent.tags['my-tag'] = undefined
In your original plunker, there are two child-tag elements, but this.tags['child-tag'] only points to one of them for riot 2. To me, that's very unexpected. In riot 3, it's an array with both child-tags.
@rogueg: you are right, I missed the part where named custom tags aren't added to this.tags[tagName]. So back were I started: both behaviors can be confusing.
But still, can't we think of something without introducing breaking changes?
Maybe just fix inconsistencies:
this.tags and append to this.tags[tagName].this.tags.name (and should not be accessed via this.name._tag).Or break it hard: this.rootNode, this.parent, this.nodes, this.tags, no named elements directly in the tag instance.
Which behavior do others prefer? master or next (keeping in mind that the latter breaks compatibility)?
I'm a fan of the "hard break" approach you just described. It seems much cleaner than having everything in the tag instance, which is quite messy. It is 3.0.0 after all, and I think replacing references isn't a huge refactor in most projects.
A 3.0.0 migration guide could probably fit into 1 paragraph and would address any concerns in this regard, imho.
Please make it simple... only one of this.tags[] or this.tags.name and no DOM elements in riot tags, we have querySelector.
I'm not 100% clear about this, but I suspect querySelector isn't a perfect substitution, because you might want to do manipulations on the virtual DOM before the nodes are mounted onto the real DOM, and the way to do that is to use the automatically bound named elements, as discussed here #1233. In that scenario, I believe querySelector would return undefined. Please correct me if I'm wrong, as I haven't entirely wrapped my head around the inner workings of the vdom vs real dom and tag lifecycle.
I've also experienced a hard to re-produce behavior where the DOM isn't 100% mounted in the 'mount' event, and document.getElementById would return undefined. I can't seem to recreate this, and it only ever happened on iOS for me, so I have yet to report it. Named elements would alleviate this too.
I have updated the demo using the latest alpha release http://plnkr.co/edit/bd0R6RG9LXT1f2mXC8bV?p=preview I think there are still some issues
this.child as HTMLElement does not make a whole lot of sense, it would be the same as this.tags.child.root
so here is the way I see it
{
"this.child": "Don't care, probably can go away",
"this.tags.child": "Tag",
"this.tags['child-tag']": "Array",
"this.input": "HTMLInputElement",
"this.tags['input']": "undefined",
"this.parent": "Tag",
"this.root": "HTMLElement"
}
so you do have a tag named child and you also have 2 tags of type child-tag, which to me seems consistent, a named tag always shows by name, and a tag always shows by tagname. Of course there would be overlap with 2 references to the same thing, but we seemed to have survived all these years with an html element having references like parentNode and parentElement which could be the same element.
Hi. This is the latest version d3cbc92afbfa03c641fbaaeb5365ed3ed927a55b. Riot 3 will drop this[name-or-id-attr] and add this.refs[ref-attr]:
| property | Riot 2 | Riot 3 |
| :-- | :-- | :-- |
| 1 - this.root | DOM | DOM |
| 2 - this.parent | Tag | Tag |
| 3 - this.tags[custom-tag-name] | Tag / [Tag] | Tag / [Tag] |
| 4 - this.refs[ref-attr] | undefined | DOM / [DOM] / Tag / [Tag] |
| 5 - this.tags[name-or-id-attr] | Tag | undefined |
| 6 - this[name-or-id-attr] | DOM / [DOM] | undefined |
But this.refs seems still confusing. I think DOM / [DOM] is enough. Was this fully discussed already?
@cognitom you are totally right. Would you mind making another pull request please?
I think if you put a ref on a tag, 90% of the time you want the tag instance, not the root DOM element.
@rogueg I am not sure, other frameworks like react and vue do not group instances and DOM nodes under the same key. Vue for example uses v-el:name for the DOM nodes and v-ref:name for the instances (even if it has some bugs). I would prefer to clean a bit the riot apis' with the newest release and having 4 different kind of types grouped under the same key seems to me really too much
React works exactly the way I'm describing. From the docs:
When attaching a ref to a DOM component like
<div />, you get the DOM node back; when attaching a ref to a composite component like<TextInput />, you'll get the React class instance. In the latter case, you can call methods on that component if any are exposed in its class definition.
I can see how the "sometimes an array, sometimes not" could be confusing. I don't have a strong opinion on how we handle that.
@rogueg I would like to hear also the @rsbondi opinion about this. I guess he will be back next week so let's keep this decision on hold
Is it a possible option to drop tags and refs at all? We can handle them by querySelector like @aMarCruz said.
Or we could easily wrap querySelector like this:
<my-tag>
<input class="first" value="a">
<input class="first" value="b">
<input-special class="second" value="x" />
<p>{ $('.first').value }</p>
<script>
this.querySelector('.first').value // a
this.querySelectorAll('.first')[1].value // b
this.$('.first') // DOM
this.$('.first', true)// undefined
this.$('.second') // DOM
this.$('.second', true) // Tag
</script>
</my-tag>
$: alias to querySelector$$: alias to querySelectorAllquerySelector(selector, true): gets a Tag instance instead of DOMupdate process$('input-special') --> $('input-special'),$([data-is='input-special'])I tried writing the code for proof of concept. #1934
I know it could be a dangerous change, but I'm starting to like it honestly...
Sorry, I went on tangent a bit. The idea about querySelector has been landed on the independent mixin. No need to implement it into the core.
By the way, after week-long thought, I'd like to agree with @rogueg's comment. For the tag manipulation, we have native querySelect() API, so it seems a better idea to focus on Tag for me. And much consistency between this.tags and this.refs.
I'd like to wait the opinion from @rsbondi, too.
The way I see it,
refs/tags gives overhead on tag creation, whether or not they are used, and adds complexity to the code baseI would prefer not having the true flag for tag selection but this.$tag would seem nicer to me but not a deal breaker
So I am in line with @cognitom 's original train of thought, but I have been off the grid so I have not given it the "week long thought" so I may end up arriving at a different conclusion. And it would be quite a drastic change
With the correction to this.refs having DOM / [DOM] only, the table from @cognitom seems good to me, or we can remove this.refs keeping a less confusing API. Anyway, riot3 is not riot2 anymore.
What about getting rid of this.tags, and only having refs?
<my-tag ref='foo' />
<input ref='input' />
this.refs.foo // => Tag(my-tag)
this.refs.input // => DOM
Maybe I've just been looking at it for too long, but this seems really straightforward to me.
I think at this point we have 2 plausible possibilities:
refs and tags properties (my favorite)refs and tags properties as arrays internally and let them behave like the current riot using getter:Object.defineProperty(this.tags, 'my-tag', {
get: function() {
var tags = this._internal.tags['my-tag']
return tags.length > 1 ? tags : tags[0]
}
})
I like always arrays because you always know what's coming out of them. If you expect only one, then as a programmer you can just this.refs['myref'][0], otherwise just this.refs['myref'].forEach(doSomething)
It's probably slow, but I would just this.refs['myref'].forEach(doSomething) all the time
I am also for always returning an array.
And I agree with @rogueg: this.refs[ref-attr] -> [DOM] / [Tag].
I need to access all child tags in a form helper mixin and I think there is no way to do it besides this.tags[custom-tag-name] -> [Tag], so I need this.tags to stay.
@rogueg mixing [DOM] / [Tag] looks _not_ good to me. When we need to handle the DOM and Tag equally, we can wrap native <input> with <custom-input-or-something>. It keeps the code more consistent, I think:
<custom-input ref="name" value={ name } />
<date-picker ref="date" value={ date } />
this.refs['name'].on('change', item => {...})
this.refs['date'].on('change', item => {...})
@GianlucaGuarini "always arrays" is my favorite, but backward compatibility is important, too. Then, how about the separated methods? (your _getter_ idea seems also nice)
<my-tag ref="first" />
this.tags['my-tag'], this.tags['value-of-name-attr'] // Tag or [Tag] - same as Riot 2
this.getTagsByTagName('my-name') // [Tag] - always array
this.getTagsByRef('first') // [Tag] - always array
^^^ heheheh that's what I get for going to a meeting before finishing a comment on github
side-thought: what's our standard way to tell if something is a tag vs. dom? Like if this.refs could return either, we should be able to tell right?
Personally I'd rather request what I want in the first place than request something in general and then check to see what it is. my rather verbose preference would be
| over-verbose property | returns |
| --- | --- |
| this.tagsNamed[tag-name] | [tag] |
| this.tagNamed[tag-name] | tag |
| this.domsByRef[ref-attr] | [dom] |
| this.domByRef[ref-attr] | dom |
| this.tagsByRef[ref-attr] | [tag] |
| this.tagByRef[ref-attr] | tag |
or something like that. like yeah it's more stuff, but you know what you're getting when you call it.
this.refs as an always array seems perfect to me as well.
Makes things predictable.
For backward compatibility, as is already said in this thread, a document on upgrading to riot3 mentioning this change should suffice.
I actually like the way it is. It's so simple and small, I don't really see the issue. You have very little to learn and remember already. Which is great.
this.root[.querySelector] is great! Please don't remove it.
@GianlucaGuarini, as you posted earlier, I also find confusing that
<my-tag name='bar' /> => parent.tags['my-tag'] = undefined
Imagine something like
<my-field name="checkbox | text | radio" />
Here I would like to use the name attribute without that eliminating one way of access for the parent on its children. Specificically, It'd be convenient to
parent.tags['my-field'].forEach ...
Perhaps there could be a way to maintain access to all children of a specific type.
I think the we will clean up the ref and tags behavior in the next riot major release at moment any change to the riot api will be a breaking change. I am closing this issue appreciating the feedback received thank you
Most helpful comment
I like always arrays because you always know what's coming out of them. If you expect only one, then as a programmer you can just this.refs['myref'][0], otherwise just this.refs['myref'].forEach(doSomething)
It's probably slow, but I would just this.refs['myref'].forEach(doSomething) all the time