Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Keyed children #147

Merged
merged 3 commits into from
Oct 1, 2017
Merged

Keyed children #147

merged 3 commits into from
Oct 1, 2017

Conversation

pdf
Copy link
Contributor

@pdf pdf commented Sep 16, 2017

Initial pass at keyed children. This one was pretty tough due to having to handle keys and insertions for nested List children.

Working through this did show up some bugs in the existing implementation of List handling, primarily: the *HTML nil type coercion from #138 was not applying to List children, so I've moved that into *HTML.reconcileChildren(), as I need to handle Lists there anyway.

Fixes #25

@pdf
Copy link
Contributor Author

pdf commented Sep 16, 2017

The implementation relies on the new Keyer interface, as well as introducing a vecty.Key() Applyer. Elements must use the latter, and Components must use the former.

The reason for requiring Components to implement the interface, rather than just relying on vecty.Key() in the Component's return from Render() is that we need to access the key before we perform the render/reconciliation, so that we can obtain the correct child from the previous render.

I considered alternative approaches to avoid requiring the interface, as I recall you raising that preference last time we looked at this, but the caveats were too severe for them to be viable.

@codecov-io
Copy link

codecov-io commented Sep 16, 2017

Codecov Report

Merging #147 into master will decrease coverage by 5.55%.
The diff coverage is 15.43%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #147      +/-   ##
==========================================
- Coverage   48.02%   42.46%   -5.56%     
==========================================
  Files           4        4              
  Lines         456      551      +95     
==========================================
+ Hits          219      234      +15     
- Misses        212      290      +78     
- Partials       25       27       +2
Impacted Files Coverage Δ
domutil.go 75% <ø> (+25%) ⬆️
dom.go 41.15% <15.28%> (-6.59%) ⬇️
markup.go 46.15% <20%> (-1.68%) ⬇️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update e309004...4bc6fbc. Read the comment docs.

dom.go Outdated
@@ -69,6 +69,13 @@ type Unmounter interface {
Unmount()
}

// Keyer is an optional interface that a Component can implement in order to
// uniquely identify the component amongst its siblings. If implemented, all
// sibling components and HTML must also be keyed.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As it stands:

If implemented, all sibling components and HTML must also be keyed.

Makes it sounds like "all sibling components" and "HTML" must be keyed, so maybe even children of children? I think that's not the intent, so better wording would be:

If implemented, all siblings, both component and HTML, must also be keyed.

dom.go Outdated
// uniquely identify the component amongst its siblings. If implemented, all
// sibling components and HTML must also be keyed.
type Keyer interface {
Key() string
Copy link
Member

@slimsag slimsag Sep 17, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add docstring:

// Key returns the key value that uniquely identifies this component amongst its siblings.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is a Keyer using a string key and not an interface{} key? I think that cases like this will be very common, and interface{} equality works just the same for us internally as string equality would (i.e. we don't need to convert to a string).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could be an interface{}, string was just the most sensible concrete type to me. I'm okay with changing it though.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, let's change it

Copy link
Contributor Author

@pdf pdf Sep 18, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll have to try and document very clearly that the returned value's type must be comparable, maybe include a link to https://golang.org/ref/spec#Comparison_operators, or maybe specify that it must be a valid map key and link to https://golang.org/ref/spec#MapType, though that might be even more confusing.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am still confident that interface{} is a better choice than string

I think that saying:

// Key returns a value that will be compared against sibling key values for ordering purposes.

Is plenty enough. I don't think we need to even link to the spec about comparison operators.

If an interface{} value is not comparable to another interface{} value, nothing bad will happen (i.e. no panic) aside from the comparison becoming false / not working as the user may incorrectly expect. For example: https://play.golang.org/p/7uK4AQbER-

Anecdotally, although not exactly the same situation, the new sync.Map type also doesn't document requirements for keys: https://golang.org/pkg/sync/#Map.Load

Copy link
Contributor Author

@pdf pdf Sep 23, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't strictly compare different key values, just look them up, and check for conflicts. That said, a value that is invalid as a map key will panic: https://play.golang.org/p/HjI8bscDdz.

And sync.Map is a rather different scenario - it's obvious that you must adhere to map semantics there, whereas here, the fact that the key is stored in a map internally is an implementation detail, and is not obvious to the user.

Maybe we can get away with just saying that the provided key's type must be comparable.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I see, I had forgotten that. How about just:

// Key returns a value that will be used for ordering purposes. The type must be valid as
// a map key, or a panic will occur.

Clear and to the point. Also, we should make sure to add something like:

_ = map[interface{}]struct{}{key: struct{}{}}

At any location where we receive a map key from the user of Vecty, as this would ensure they get a stack trace that shows exactly the location where they passed the invalid key. Presumably the compiler will optimize this out completely (but I didn't check, and it doesn't matter).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, we should make sure to add something like:

I don't think we bother with this, components can implement Keyer however they like anyway, so would be of limited utility.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fine for now, I'll think about it more.

dom.go Outdated
}

// Node returns the underlying JavaScript Element or TextNode.
func (h *HTML) Node() *js.Object { return h.node.(wrappedObject).j }

// Key satisfies the Keyer interface.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace satisfies with implements

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In Go terminology, interfaces are satisfied not implemented. However, it's good to keep the documentation consistent for this code base.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm happy to be proven wrong, but I've never seen <Method> satisfies the <Interface> interface. over <Method> implements the <Interface> interface. in a docstring.

https://gotools.org/net/http#transport.go-L337 seems to agree with me.

@@ -81,6 +88,7 @@ func (p *ItemView) Render() *vecty.HTML {
"completed": p.Item.Completed,
"editing": p.editing,
},
prop.ID(p.Item.Title),
Copy link
Member

@slimsag slimsag Sep 17, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this for ? I assume it's a quick fix for some bug? Or added here unintentionally?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My bad, I left this in from my testing - just made it easier to identify items from devtools, will remove.

dom.go Outdated
if keyer, ok := nextChild.(Keyer); ok {
nextKey = keyer.Key()
if len(h.keyMap) > 0 && nextKey == "" {
panic("vecty: all siblings must have keys when using keyed elements")
Copy link
Member

@slimsag slimsag Sep 17, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with the panic, but nextKey == "" places an unusual constraint that "" is not a valid key? As mentioned above, I'd prefer we switch to interface{} key types, in which case we can use nil here I suppose.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Zero value needs to be invalid for whatever type is used, yes.

@pdf
Copy link
Contributor Author

pdf commented Sep 17, 2017

Let me know when you're ready for me to do another pass on this, I may have some time one night during the week.

dom.go Outdated
func (l childList) remove(next *HTML, insertBeforeNode jsObject) {
l.html.node = next.node
l.html.insertBeforeNode = next.insertBeforeNode
l.html.removeChildren(l.html.children)
Copy link
Member

@slimsag slimsag Sep 18, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation on this function and its parameters do not make sense to me.

If the goal is to remove the childLists's elements from the DOM node, then why does it also take a next parameter?

Why is there an unused insertBeforeNode parameter?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll rename next (the target for the operation) and update the docs. The insertBeforeNode was used before this commit, but now is not, will remove.

dom.go Outdated
}
case List:
nextChild = childList{html: &HTML{children: v}}
h.children[i] = nextChild
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

h.children's data type is []ComponentOrHTML which does not include childList. It feels wrong to ignore this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ComponentOrHTML is exported, but this is an unexported type, what would you like to do?

Copy link
Member

@slimsag slimsag Sep 22, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we change it to an interface{} type without requiring extensive* copying / conversion all over the place? (I am not sure).

Otherwise, maybe we just need a doc comment on the definition of children now:

// important: children is actually ComponentOrHTML OR childList here, from an internal perspective.

dom.go Outdated
// Massage concrete type if necessary.
switch v := nextChild.(type) {
case *HTML:
if v == nil {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Document.

// The next child's type is *HTML but it is nil. We want the same effective
// handling as if it was a nil ComponentOrHTML, so explicitly set nextChild to
// nil now.

dom.go Outdated
h.children[i] = nextChild
}
case List:
nextChild = childList{html: &HTML{children: v}}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Document. There is a reason why we are replacing List types with our own childList type, but I do not know what it is.

dom.go Outdated
}
if !keyed {
keyed = len(h.keyMap) > 0
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does keyed need to be persistent outside the scope of the for loop?

i.e., why don't we just declare keyed := len(h.keyMap) > 0 before the first usage below here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is fine.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By "I think this is fine" do you mean the current code, or what I suggested?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the change is fine.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does need to exist outside this scope, since we use it to determine how to remove children. I've slightly changed how this works in the next push any way, since we need to track this across renders too now.

dom.go Outdated
prevChildList.remove(h, h.insertBeforeNode)
if h.insertBeforeNode != nil {
h.insertBeforeNode = prevChildList.html.insertBeforeNode
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand what this if block is doing. Please document.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first line is documented at diff, the last three are the same as diff, I'll copy the comment.

dom.go Outdated
keyMap map[string]ComponentOrHTML
nestedKeys bool
insertBeforeNode jsObject
lastRenderedChild *HTML
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fields you've added here are not self-explanatory like the ones prior to this change. key is self explanatory, but:

  • keyMap
  • nestedKeys
  • insertBeforeNode
  • lastRenderedChild

are not. Can you document each of them with an explanation of what they are / what state they carry?

dom.go Outdated
var (
new = h.node != prev.node
nextKey string
stableKey bool
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we not define this variable closer to where it is used down below?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you'd prefer to split these up I can.

dom.go Outdated
new = h.node != prev.node
nextKey string
stableKey bool
insertBeforeKeyedNode jsObject
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same with this one

dom.go Outdated
keyed = len(h.keyMap) > 0
}

if (i >= len(prev.children) && !keyed) || new {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. This block is purely responsible for handling creation of new child elements that the previous DOM node's children did not have, so we should document that now.

  2. Why does this condition on !keyed ? Why would new children that are keyed not be handled here? I don't understand.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Ack
  2. Because position can not be used for anything when dealing with keyed children - the feature is designed to deal with children that may move position, and that means they may appear at a higher index on the current render, than the last render had. Once we know we're dealing with keyed children, the key is the only authoritative index.

var prevChild ComponentOrHTML
if len(prev.children) > i {
prevChild = prev.children[i]
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change introduces a lot of uncertainty for me about whether or not at this point in the loop we're dealing with children that have a direct relationship to prev or not. It seems this is only the case when keyed == true, so I think it'd be more clear to have this prevChild declaration be inside the if below and then keep the old prevChild declaration that you removed but have it defined below this keyed if statement.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The if check here is required, because if we're dealing with keyed children, the earlier condition may not have continued the children loop when we get here - we still want to assign this var in either case, it just needs this check now to avoid overflowing the prev.children array when keyed.

@slimsag
Copy link
Member

slimsag commented Sep 18, 2017

  1. Excluding string -> interface{}, I like the public API and am fine with using the Keyer interface.
  2. I'm fine with / agree with the *HTML nil change.

.. with the above said; I'm afraid of this change right now because I've spent a considerable amount of time trying to grok this but still do not fully understand the implementation. With 2-3 hours yesterday, and 2 hours just now, I'm out of time and probably won't have time to take another look until next weekend.

I feel confident you've done your due diligence, and that this is a sound change, but this currently adds an additional level of complexity that I currently cannot reason about. I'm not saying we shouldn't merge it, and also not saying we should block it on testing. But in its current state I will need more time (maybe 1 - 3 weekends) to fully parse and internalize how this is all working together.

I think the main thing I do not understand is how, after this change, the overall reconciliation flow is operating. Which conditions are handling new or existing children, keyed or not, etc. is all very overwhelming currently.

I'll come back to this when I have some more spare time, hopefully next weekend. If you'd like to take a second pass on this in the meantime, feel free to of course.

@pdf
Copy link
Contributor Author

pdf commented Sep 18, 2017

I can understand that, Lists in particular make things somewhat complicated. If you think it might help, we could schedule a call on Slack or something and talk it through one weekend, let me know.

In the mean time, I may take another pass and at least address the notes here, see if there's any way to make things easier to parse.

The silver lining here though, is that I believe this is the last outstanding major architectural change, based on the current issue list.

@pdf
Copy link
Contributor Author

pdf commented Sep 21, 2017

We got a little side-tracked with details, but when I pushed this I also wanted to discuss some alternative implementation details. There's no need to understand the current implementation to discuss these.

The implementation I've pushed here requires all siblings to be keyed, including those that are rendered as part of a vecty.List. This doesn't feel ideal to me, since one of the nice things about vecty.List otherwise is that it provides a means to segregate a set of children that may change in length from those around them, without requiring an additional DOM node.

The only options I could think of to allow a separate keyspace for List children are:

  1. Disallow List as a sibling to keyed children, I'm not sure I like having to impose this restriction, but it might be a valid constraint, and has the lowest implementation cost (with a couple of minor caveats), it's simpler than what I've done here. Users can work around the constraint using consecutive Lists instead of mixing Lists with bare keyed siblings.
  2. Try to treat Lists separately when mixed with keyed siblings. The problem with this is that if the List moves position, there's no opportunity to reconcile against the previous List, so we'd end up re-rendering the entire List's contents, defeating the purpose of keying the List children. One strategy we might employ to try and deal with this is to mark prev Lists as processed, and do a reverse-search, then a forward-search through children to try and find a List to work from. This approach is the most error-prone (as far as render-efficiency, and persistence), but I think it feels best for the user without adding restrictions... when it works. Not sure this can be reliable enough to consider.
  3. Find some method to provide a key to a List itself. This feels kind of weird as a user though - generally keys are going to be assigned to a set of children based on some common property derived programmatically, but the list container will not usually fit into this model. I also don't know exactly how we'd enable setting a key on a list, if we wanted to go this way.

I'll find some time to post some examples of what I'm talking about, if this isn't clear enough. Please let me know your thoughts @slimsag, and if there are particular parts you would like clarified.

@slimsag
Copy link
Member

slimsag commented Sep 21, 2017

The implementation I've pushed here requires all siblings to be keyed, including those that are rendered as part of a vecty.List. This doesn't feel ideal to me, since one of the nice things about vecty.List otherwise is that it provides a means to segregate a set of children that may change in length from those around them, without requiring an additional DOM node.

Good point; thank you for flagging this.

I agree that this does not sound ideal, one reason in particular is because a vecty.List may be generated by a library and have keys that conflict with other siblings keys.

  1. This option does not sound particularly appealing, but it may be the situation that I will become convinced after seeing the difficulty in finding a better solution.
  2. The trouble with this solution is that the semantics are not clear to the user, I think? If we do this, we're essentially left with multiple sibling lists and we're trying to derive a key from their actual contents? I'd love to hear your thoughts on how we would concretely define equality for a list in this regard. Maybe we would count HTML tag/text types and the list with the closest match is equal? Not sure..
  3. I actually think that this option sounds the most sane, aside from figuring out an API to provide the key. In this regard, a list is treated similar to e.g. creating another sibling as a ele.Div for example. Could you provide some examples where you think this API would be weird to interact with ? i.e. what are you talking about here?:

This feels kind of weird as a user though - generally keys are going to be assigned to a set of children based on some common property derived programmatically, but the list container will not usually fit into this model.

@pdf
Copy link
Contributor Author

pdf commented Sep 22, 2017

  1. Level of difficulty is not necessarily the driver here, but also obviousness of behaviour, at least after the first time the user discovers this limitation.
  2. Yeah, as much as I would like this approach to work, I just don't think there's any way to make this reliable/predictable enough.
  3. Example below the break.

type KeyedComponent struct {
        vecty.Core
        ID string `vecty:"prop"`
}

func (c *KeyedComponent) Key() interface{} {
        return c.ID
}

func (c *KeyedComponent) Render() *vecty.HTML {
        return elem.Div(vecty.Text(c.ID))
}

type MyComponent struct {
        vecty.Core
        data []string
}

func (c *MyComponent) Render() *vecty.HTML {
        var contents []vecty.MarkupOrChild
        for _, v := range c.data {
                contents = append(contents, &KeyedComponent{ID: v})
        }
        contents = append(contents, vecty.List(some_footer_elements)) // <- this will panic with missing key error on the List when rendered
        return elem.Div(contents...)
}

So, the user has a bunch of keyed components (could just as easily be keyed elements), that are generated from some data store. This is the most common reason to use keys - dealing with arbitrary data that the developer doesn't control, and may change between renders. With option 3, the user is left with the requirement to add some arbitrary key they make up to the List sibling, and hope that it doesn't conflict with any of the keys in the generated content.

Of course, it could be argued that this is poorly written, and to avoid needing to make up a key for the footer List, the same thing could be written as:

 func (c *MyComponent) Render() *vecty.HTML {
-        var contents []vecty.MarkupOrChild
+        var contents vecty.List
         for _, v := range c.data {
                 contents = append(contents, &KeyedComponent{ID: v})
         }
-       contents = append(contents, vecty.List(some_footer_elements)) // <- this will panic with missing key error on the List when rendered
-       return elem.Div(contents...)
+       return elem.Div(
+               contents,
+               vecty.List(some_footer_elements),
+       )
 }

But in that case, option 1 works just as well, and the panic in the original implementation would be more explicit about what the problem is - vecty.List can not be mixed with keyed children.

@slimsag
Copy link
Member

slimsag commented Sep 22, 2017

I think I understand. I'll be brief since I don't have much time -- so weigh this comment with less weight than usual as there could be a critical piece I have not understood yet.

My thinking was that we might add something like vecty.ListWithKey (poor name, but not important), which would make your first example look like this:

type KeyedComponent struct {
        vecty.Core
        ID string `vecty:"prop"`
}

func (c *KeyedComponent) Key() interface{} {
        return c.ID
}

func (c *KeyedComponent) Render() *vecty.HTML {
        return elem.Div(vecty.Text(c.ID))
}

type MyComponent struct {
        vecty.Core
        data []string
}

func (c *MyComponent) Render() *vecty.HTML {
        var contents []vecty.MarkupOrChild
        for _, v := range c.data {
                contents = append(contents, &KeyedComponent{ID: v})
        }
-        contents = append(contents, vecty.List(some_footer_elements)) // <- this will panic with missing key error on the List when rendered
+        contents = append(contents, vecty.ListWithKey("listkey", some_footer_elements)) // <- this does not panic because vecty.ListWithKey was used instead of vecty.List
        return elem.Div(contents...)
}

The idea being that you assign a key for the entire list itself, rather than for individual elements of the list. In this case "listkey" just needs to be unique against siblings, i.e. the keys of whatever is in contents (ID: v in this case).

Maybe i've misunderstood something, though?

@pdf
Copy link
Contributor Author

pdf commented Sep 23, 2017

The idea being that you assign a key for the entire list itself, rather than for individual elements of the list. In this case "listkey" just needs to be unique against siblings, i.e. the keys of whatever is in contents (ID: v in this case).

I'm not entirely certain how to make it consistent, ie - what is the method signature of ListWithKey here? I suppose something like this might be acceptable?

type KeyedList struct {
    key interface{}
    list LIst
}

func ListWithKey(key interface{}, List) KeyedList {}

Maybe i've misunderstood something, though?

My example may have been based on a lack of imagination, and being too close to the code, now that I think about it further. I had not considered the possibility that a user may want to generate a list of keyed List, and so I'd assumed that the only time a List would be used in conjunction with other keyed children would be if it was instantiated manually, with an arbitrary key, and my example was trying to demonstrate that there's no way to safely manually key such a sibling, if the data source for the other key data is outside the user's control. I think this was bad reasoning though, and there may be some valid use-cases that I had not considered, so I believe we should disregard this line of thinking, humble apologies for the distraction.

@slimsag
Copy link
Member

slimsag commented Sep 23, 2017

I suppose something like this might be acceptable?

Yes, I think that a signature like that would be ideal. Other semantics which probably aren't important for validating that this idea would work:

  • I will suggest the name ListKey now.
  • KeyedList probably does not need to be exported. Maybe just use the interface that accepts this type. vecty.MarkupOrChild
  • Given the name, key should probably come last.
type keyedList struct {
    list LIst
    key interface{}
}

// ListKey wraps the given List with the given key for ordering purposes.
func ListKey(list List, key interface{}) MarkupOrChild { return keyedList{list, key} }

I'm not super confident about the name, but it seems better than ListWithKey and KeyList to me currently (the latter implies it's a list of keys or something).

I think having ListKey return an interface instead of a published type is useful because it pushes people to pass around a List instead and, for example, return List from APIs that produce data and wrap them with ListKey(l, "somekey") at the usage site instead of at the data production site.

What do you think of this approach overall? Good enough idea to take a try at implementing it?

@pdf
Copy link
Contributor Author

pdf commented Sep 23, 2017

I'm not super confident about the name, but it seems better than ListWithKey and KeyList to me currently (the latter implies it's a list of keys or something).

Not sure I'm in love with ListKey either. What do you think about a different approach?

// WithKey wraps the List in a Keyer using the given key, for ordering purposes.
func (l List) WithKey(key interface{}) vecty.MarkupOrChild {}

What do you think of this approach overall? Good enough idea to take a try at implementing it?

I think I can see how this might fit together, can take a pass at it.

@slimsag
Copy link
Member

slimsag commented Sep 23, 2017

I like that API/name approach a lot! That's beautiful :) Can't believe I didn't think of that myself!

@pdf
Copy link
Contributor Author

pdf commented Sep 29, 2017

New pass addresses previous comments, and implements the keyed list with separate keyspace, and WithKey() method for List.

@pdf pdf force-pushed the keyed_children branch 2 times, most recently from 5680da3 to bfc1f63 Compare September 29, 2017 01:28
@slimsag
Copy link
Member

slimsag commented Sep 29, 2017

Thanks @pdf ! I'll do my best to review this ASAP so we can land it in this weekend.

@pdf
Copy link
Contributor Author

pdf commented Sep 29, 2017

I'm not sure it's much easier to understand now than in the last pass, so please fire any questions you have at me. I'm not sure how much more time I'll have to work on this over the weekend, but I'll certainly try to clarify anything you need me to.

@pdf
Copy link
Contributor Author

pdf commented Sep 30, 2017

I have a little time today if you have thoughts/queries @slimsag.

@slimsag
Copy link
Member

slimsag commented Oct 1, 2017

Hi @pdf ! Sorry for the delayed response to your above messages!

I've just spent the last ~6 hours working through your PR case by case and feel I have a very solid understanding of how everything works now. :)

I've created a separate PR to your branch at pdf#2 which has very individual commits + good commit messages with my suggestions. Once you've had a chance to look over them and feel good about the suggestions we can squash merge to keep a clean git history.

This change is very good! I like it quite a lot! 😃

@pdf
Copy link
Contributor Author

pdf commented Oct 1, 2017

No problem, glad you could make sense of it now. I'll take a look over your proposed changes now.

This change fixes a regression introduced by PR hexops#134.

The basic issue is that when `nil` is encountered, it is treated correctly as a nil render,
but when `(*HTML)(nil)` is encountered, it is not. I discovered this while porting
Go-Package-Store (which uses Vecty) over to the new API.

The following code effectively demonstrates the issue:

```Go
package main

import (
        "github.com/gopherjs/vecty"
        "github.com/gopherjs/vecty/elem"
)

func main() {
        vecty.RenderBody(&PageView{})
}

type PageView struct {
        vecty.Core
}

func (p *PageView) Render() *vecty.HTML {
        return elem.Body(
                // this works:
                //nil,

                // this does not:
                p.returnsNil(),
        )
}

func (p *PageView) returnsNil() *vecty.HTML {
        return nil
}
```
dom.go Outdated
switch v := prevChild.(type) {
case keyedList:
pendingMounts = l.html.reconcileChildren(v.html)
case *HTML, Component, nil:
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay with this?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great!

@slimsag
Copy link
Member

slimsag commented Oct 1, 2017

Looks like you've just merged my changes + made the change #147 (comment)

If there's nothing else changed (I didn't spot anything), then I'm happy to merge this as soon as you feel good to :)

dom.go Outdated
}
// WithKey wraps the List in a Keyer using the given key.
func (l List) WithKey(key interface{}) ComponentOrHTML {
return keyedList{key: key, html: &HTML{children: l}}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My only concern with this is that MarkupOrChild/ComponentOrHTML may end up with this keyedList in them, which makes the documentation inaccurate, amongst other things.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes that is a good point actually...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could publish a type KeyedList interface{} value + update the docs of MarkupOrChild/ComponentOrHTML?

Then nothing else has to change.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, just make keyedList public with no exported fields / methods

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was my inclination, too.

@pdf
Copy link
Contributor Author

pdf commented Oct 1, 2017

I shuffled the documentation for KeyedList around a little, since it was aimed at internal use. Let me know if you think the public documentation is adequate.

@slimsag
Copy link
Member

slimsag commented Oct 1, 2017

Ready to merge once you are. I will update doc/CHANGELOG.md to add a line that MarkupOrChild and ComponentOrHTML can both now contain KeyedList once we've merged.

@pdf
Copy link
Contributor Author

pdf commented Oct 1, 2017

:shipit:

@slimsag slimsag merged commit e30eda9 into hexops:master Oct 1, 2017
@slimsag
Copy link
Member

slimsag commented Oct 1, 2017

🎉

slimsag added a commit that referenced this pull request Oct 1, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants