Refresh bindings in parent view when parent value is changed from child through DI
Asked Answered
S

2

6

The goal is to be able to update "heading" on the SharedParent and/or in the SharedState view, whenever it is changed from the child through DI.

I suspect there's a problem with the layout-view and layout-view-model on the Aurelia router custom element.

I need this since the SharedParent in the real code, will be abstractung general calculation functions (saved in variables in the SharedParent, which currently is not updated in the view) etc which the children will be calling (to avoid every child to duplicate this functionality). Ideally referencing the DI reference of the SharedParent.

The ideal solution enables direct modification on the parent. The SharedState was just an extra way of testing, and can be ignored, if you can see a way to get it working with option 1)

Option 1 is my highest priority to get working

See the live running gist here: https://gist.run/?id=66eeff540a4665694a31482b790bf01e

Update

I've made yet another gist to show another way I've tried in order to get the parent/child relationship working: https://gist.run/?id=080d4ac3f4d8677d344140a7827aea94 - in this example there's just another issue with the current route not being set correctly, due to duplicate "route" attributes in the router. But here, at least we're able to update the parent property from the children. The ideal solution is to get both working in 1 solution. So that the active route is set correctly, and children will be able to update the dependency injected parent's properties. And lastly the parent view needs to refresh on this change. This gist is by the way from another question: How to dynamically build navigation menu from routes linking to parent/child views/controllers - but slightly modified to help illustrate that what isn't working in this questions' gist, is indeed working here.

To sum it up.

I first tried to solve my problem this way: https://gist.run/?id=080d4ac3f4d8677d344140a7827aea94 - almost everything worked as it should (set heading in parent from child) and reflect change of dates/language from parent in child-view - both getting and setting of parent properties from the children. The only thing not working was the routing. I couldn't get the active route displayed, since multiple routes shared the same "route" attribute.

So I looked towards the layout-view(-model) solution (whick I proposed in this question) - which seemed to fix the routing issue, but broke the bindings between parent / child.

Sweeney answered 17/3, 2017 at 11:49 Comment(6)
Have you considered using the event aggregator rather than injecting your classes into the child components?Unravel
Since I need to both 1) get properties from parent 2) modify parent properties - from the child, I think the least messy way of doing so has to be by injecting the parent into the child. I've tried using signals, but the problem is that the parent view will not "re-render"/update, when a child modifies a parent property. At least not for the "layout-view(-model approach). Funny enough it works in the other example i've given under "update". I don't see how the event aggregator could help in this regard, but thanks for your suggestion. :-)Sweeney
Can you clarify one thing for me, please? What else does the parent have that the child needs that requires you injecting it there? I mean, besides the heading property that you want to update? I there something more that the child component needs (or will need in the future) from the parent?Unravel
Within each parent a lot of statistics is calculated. Right now the functions that calculate these statistics, counts the progress and displays the progress is defined in the the parent. The children calls those functions by referencing the injected parent.functionname(params,..,). I once had the same functionality defined with each of the children, but I moved it to the parent to reduce duplicate code. I can read just fine from the parent, but it would seem as if the layout-view/layout-view-model causes an issue so you cannot update the layout-view-model and view from its child :/Sweeney
I just noticed that if I remove the NewInstance.of I can make 2 work. But it really is 1) I hope to get working somehow.Sweeney
You called it SharedState for that reason, right? :) Maybe try and new that class up in the child class constructor to see if that works better (just a guess). The IoC container must be treating it as a singleton.Unravel
C
1

ChildA is constructed by the container before the layout-view and layout-view-model indicated on the router-view are constructed. That means the container has to create a new instance of SharedParent (lets call it "instance 1") to give to ChildA's constructor.

Later, the router-view's layout view-model is constructed (SharedParent). When custom attributes, custom elements and router view models are constructed by the container, the templating system's default behavior is to tell the container "give me a fresh instance, don't reuse an existing one". This makes a lot of sense, most of the time you don't want all the the 10 <my-number-input> that appear on your form to all be the same instance no more than you would want 10 standard html <input> elements on your form to be the same... you want them to be independent. So a second instance of SharedParent is constructed, which is why your header code doesn't work... ChildA has a different instance of SharedParent than the router view.

Using @singleton on SharedParent will resolve this, but a better approach is to have SharedParent, ChildA and ChildB take a dependency on a SharedState class (eg @inject(SharedState)). This will negate the need to override the default behavior, make it more clear what the shared dependencies are, and preserve the nice separation of concerns between SharedParent and ChildA.

class SharedState {
  heading = '';
  ... other shared items ...
}

here's an example: https://gist.run/?id=dfe291e3da67854d143d264a2edd5ade

Cassareep answered 25/3, 2017 at 16:26 Comment(5)
Hi Jeremy, thanks for your answer, however I can't see how the SharedState class can be used in this case, since both parent and child should be able to change the state (state is only going to be shared among parent and child - not subsequent children) and reflect the change. As you can see here: gist.run/?id=1abfca51c905d7a9a84a5133aefc55c0 it seems to work one way only. The heading is never updated from the child to the parent.Sweeney
So I guess by using sharedState you refer to the other way of doing it parent/child view/viewmodel relationships ( #40976625 ) - while I do believe layout view would be the cleaner way, there's also 1 other problem with the other approach which eventually lead me to: #42832755 - at that point I think i turn quite "hacky" and there has to be better waySweeney
I've edited your gist and pasted the link in my answer. Remember ChildA is constructed before SharedParent in your use-case.Cassareep
Thanks a lot for your example and explanation. I guess due to the @singleton decorator not behaving like it should with babel 6 (would explain why it works in your rjs-bundle and not in the current skeletons), using your shared-state approach is the best option for now.Sweeney
cool- glad it worked for you- would you mind updating the status of the github issue and closing (or leaving open). Just want to make sure we keep everyone involved up to date.Cassareep
P
3

I got it to work here: https://gist.run/?id=6c112829bb42a5ed86b78b4c8917a72c

  • I set @singleton(true) decorator on SharedParent => DI Basics
  • I used the existing BindingSignaler from the injected Parent
Pension answered 20/3, 2017 at 9:55 Comment(6)
Awesome... even after quickly looking through the DI basics link I don't have the brains to figure out that @singleton(true) solves absolutely EVERYTHING. Not even the BindingSignaler is necessary now, and the properties can be put outside the constructor as well in the "SharedParent". Thank you so very much for this - it was very important for me to get this working and I've tried for a whole week now. Brilliant! gist.run/?id=a1721aa07aa35330138408a28b2a0ce8 (see it's working now - even without the signaler) :DSweeney
here is another way of doing it => patrickwalters.net/my-best-practices-in-aureliaPension
Cool. It's definitely relevant, however I think the @singleton(true) + router-view, layout-view and layout-view-model approach is the better one in this case, since it allow you to route directly to the children, and at the same time wrap the layout-view around the routed layout-child-view. @singleton(true) really made all the difference. :-)Sweeney
@Pension Good information! Thanks for showing several options, too.Unravel
@zedl it seem to only work in the gist, but not in the esnext/webpack skeletons due to a babel6 issue most likely. Any ideas?Sweeney
Sorry, but I am only use the CLIPension
C
1

ChildA is constructed by the container before the layout-view and layout-view-model indicated on the router-view are constructed. That means the container has to create a new instance of SharedParent (lets call it "instance 1") to give to ChildA's constructor.

Later, the router-view's layout view-model is constructed (SharedParent). When custom attributes, custom elements and router view models are constructed by the container, the templating system's default behavior is to tell the container "give me a fresh instance, don't reuse an existing one". This makes a lot of sense, most of the time you don't want all the the 10 <my-number-input> that appear on your form to all be the same instance no more than you would want 10 standard html <input> elements on your form to be the same... you want them to be independent. So a second instance of SharedParent is constructed, which is why your header code doesn't work... ChildA has a different instance of SharedParent than the router view.

Using @singleton on SharedParent will resolve this, but a better approach is to have SharedParent, ChildA and ChildB take a dependency on a SharedState class (eg @inject(SharedState)). This will negate the need to override the default behavior, make it more clear what the shared dependencies are, and preserve the nice separation of concerns between SharedParent and ChildA.

class SharedState {
  heading = '';
  ... other shared items ...
}

here's an example: https://gist.run/?id=dfe291e3da67854d143d264a2edd5ade

Cassareep answered 25/3, 2017 at 16:26 Comment(5)
Hi Jeremy, thanks for your answer, however I can't see how the SharedState class can be used in this case, since both parent and child should be able to change the state (state is only going to be shared among parent and child - not subsequent children) and reflect the change. As you can see here: gist.run/?id=1abfca51c905d7a9a84a5133aefc55c0 it seems to work one way only. The heading is never updated from the child to the parent.Sweeney
So I guess by using sharedState you refer to the other way of doing it parent/child view/viewmodel relationships ( #40976625 ) - while I do believe layout view would be the cleaner way, there's also 1 other problem with the other approach which eventually lead me to: #42832755 - at that point I think i turn quite "hacky" and there has to be better waySweeney
I've edited your gist and pasted the link in my answer. Remember ChildA is constructed before SharedParent in your use-case.Cassareep
Thanks a lot for your example and explanation. I guess due to the @singleton decorator not behaving like it should with babel 6 (would explain why it works in your rjs-bundle and not in the current skeletons), using your shared-state approach is the best option for now.Sweeney
cool- glad it worked for you- would you mind updating the status of the github issue and closing (or leaving open). Just want to make sure we keep everyone involved up to date.Cassareep

© 2022 - 2024 — McMap. All rights reserved.