Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Component tree: controller bindToParent? #14060

Closed
lucianogreiner opened this issue Feb 16, 2016 · 9 comments
Closed

Component tree: controller bindToParent? #14060

lucianogreiner opened this issue Feb 16, 2016 · 9 comments

Comments

@lucianogreiner
Copy link

Hello. I am thinking about having an application as a tree of components with 1.5.

Let's say we have:

<user-crud> // $crud controller
<user-list users="$crud.users" on-select="$ctrl.editUser"></user-list>
<user-editor user="$crud.user" on-save="$ctrl.saveUser"></user-editor>
</user-crud>

If we want to reuse somewhere else, we should not depend on user-crud. Also, we want to reduce or eliminate the $scope and $watch usage as a good practice.

So, what i miss is a better clean way of communication from parent to its child components (crud to the list and editor) other than using events or directive require.

function UserCrudController(UserService) {
 var self = this;
 ...
 self.editUser = function(user) {
  // So now i want to tell editor to edit this user:
  //1
  self.user = user; // user-editor needs to $watch for this
  //2
  $scope.broadcast('editUser', user); // both UserCrudController and UserEditControllers need to use $scope
  // Want to command our child here:
  self.userEditor.edit(user);
}
self.saveUser = function(user) {
  // same for save
  // do some saving logic then:
  self.userEditor.hide();

  var updatedUserList = ...;
  self.userList.update(updatedUserList );

  // or maybe
  self.$components.userEditor(...);
  self.$components.userList(...);
 }
}

I believe this approach is not absurd, but it's not the way we used to code it using Angular. Maybe there could be something as a 'bindToParent' property to expose child controller into parent:

<user-crud> // $crud controller
 <user-list users="$crud.users" on-select="$ctrl.editUser" var="list"></user-list> // $crud.list==UserListController
 <user-editor user="$crud.user" on-save="$ctrl.saveUser"></user-editor> //$crud.userEditor==UserEditorController
</user-crud>

module.component('userList', {
...
bindToParent : true // will bind component name // bindToParent : 'list' // bindToParent : function($attrs) { return $attrs.var; }
...
});

The problem i see with this approach is what could happen if we have a multiple user-list elements or a iteration of them inside an element.

What do you think?

@gkalpak
Copy link
Member

gkalpak commented Feb 16, 2016

I think it would be nice to have something equivalent to ng2's Query.
Although it's probably hard to implement something similar due to internal differences, maybe something equivalent in functionality (at least a static version of it) woud still be helpful.

In the meantime, you could query your children (e.g. in the post-linking phase or when needed) and access their controllers. (If templateUrl is used by the children, you can't do it in the post-linking phase 😃). This might be handled more consistently if/when we implement the $afterViewInit lifecycle hook.

Here is a hacky demo of what you can do today.

@drpicox
Copy link
Contributor

drpicox commented Feb 20, 2016

What about #14080 ?
It suits perfectly for the test case presented, ex:

<user-crud> // $crud controller
  <user-list as="$crud.users" users="$crud.users" on-select="$ctrl.editUser"></user-list>
  <user-editor as="$crud.editor" user="$crud.user" on-save="$ctrl.saveUser"></user-editor>
</user-crud>

And at the same time, you can reuse user-list directive in any other context. Even better, you can have two instances of the same directive without overwriting controllers.

Ops, and there is other inconvenience for bindToParent: it is polluting parent directive controller, so, you need to use it carefully or you may overwrite things.

Ops, an inconvenience for using $element.find() is you have to be sure that none of your children and children-children-... would have the directive that are you looking for, find just looks for all instances in the whole children tree. Uhmm I do not know for sure how it is working in ng2.


By the way, from what I understand from the template, you are using on-select/on-save without '&' notation. I think that something like:

<user-crud> // $crud controller
  <user-list as="$crud.users" users="$crud.users" on-select="$ctrl.editUser(user)"></user-list>
  <user-editor as="$crud.editor" user="$crud.user" on-save="$ctrl.saveUser($crud.user)"></user-editor>
</user-crud>

It should be nicer. You should do something like this in your component definition:

module.component('userList', {
  ...
  bindings : {
    fireSelect: '&onSelect',// notify changes with $ctrl.fireSelect({user: currentUser})
    ...
  },
  ...
});

@lucianogreiner
Copy link
Author

Thank you for the updates.

I've been working with this, and tried the approach @gkalpak suggested. It's a fact that we can use $element to find the child controllers and bind them. It works pretty well, but it still have some problems:

First that we need to make sure the child controllers have been initialized. It happens after parent initialization. So parent controller cannot make any initialization that depends on child. For example, cannot call this.editor.edit({}); while starting. Binding needs to be done after controller initialization into the link function.

Having a link function means we cannot use component, must be directive. And the logic to locate child controllers isn't really nice, and would be worst if we have multiple child nodes.

So turns out i found another solution to handle child components inputs and outputs in a normalized form, that does not rely on scope or any initialization order: RsJs Observables.

<user-crud> // $crud controller
  <user-list users="$crud.users" on-select="$ctrl.editUser"></user-list>
   <user-editor user="$crud.user" on-save="$ctrl.saveUser"></user-editor>
</user-crud>

So now under userCrud controller i have a few observables that will be passed on child attribute bindings. UserCrud controller will listen into some of them, that will turn out to be child component outputs, and will send values into others, that will be child component inputs. This solution seems pretty good, since it is a normalized form of communication for both inputs and outputs, and since i can start userCrudController sending values into "users" observable, it does not really matter if userList has not been initialized. As soon as it gets initialized it will handle that value.
Something like this:

function UserCrud() {
   this.users = new Rx.Subject();
   this.user = new Rx.Subject();
   this.editUser = new Rx.Subject();
   this.saveUser =   new Rx.Subject();

   this.editUser.subscribe(editUser);
   this.saveUser.subscribe(saveUser);

  function editUser(id) {
    // load user from backeden then
    this.user.onNext(loadedUser);
  }

   function saveUser(user) {
       // do something,  get the updated user link then:
       this.users.onNext(updatedUsers);
   }

}

// user-list and user-editor controllers will subscribe to userCrud.users and userCrud.user observables and submit values to editUser and saveUser observables.

I believe this is a pretty decent solution after all. I know Angular 2 uses RxJS observables. Not sure if it uses the same way, but this makes sense to me and results into really independent components without any need for scope/watch and element handling.

What do you think?

@gkalpak
Copy link
Member

gkalpak commented Feb 22, 2016

My approach was a work-around, so there are of course many cases that are not handled.

The downside of your approach is that the child controllers use stuff from a parent controller. I don't think observables offer much of a benefit in this case (although they are pretty cool in other cases 😃); you could just as well expose a function form the parent controller that the child controllers could use.
Either way, it creates a "contract" for the parent controller, which I thought you wanted to avoid.

The benefit of having children expose functionality and let the parent controller use it or ignore it, is that you your parent controllers aren't coupled to the children and don't need to have a specific "shape".

E.g. one implementation of user-crud might just want to display users (and use user-list for that), but it's not interested in editing them, so it doesn't need an editUser or saveUser observable.

@lucianogreiner
Copy link
Author

Indeed, the child to parent communication could be easily done by function binding from parent using '&', and the other way could be done by parent attribute watching (Maybe for this parent -> child observables might make more sense), but I prefered observables to be able to have a more common form of communication for both input and output. I believe that could be easier to teach the development team and maintain.

Regards.

@gkalpak
Copy link
Member

gkalpak commented Feb 22, 2016

Sure, any way that works for you and your team is fine.

The "problem" still remains:
It'd be nice to have a clean and easy way to expose a public API from components. Parents/siblings could utilize that API to compose more powerful components or complex UXes, without any coupling between the individual parts/components. It would be like an Interface, but for components instead of classes.

The proposed as directive would be one way to go. Query/ViewQuery/etc would be another.
Or something else, who know ? 😃

@lucianogreiner , thx for the insights !

@lucianogreiner
Copy link
Author

This works pretty well:

angular.module('test')
    .component('parent', {
        template : '<h1>Parent</h1><button ng-click="$ctrl.increment()">Increment child</button><child number="$ctrl.value" as="$ctrl.list"></child></div>',
        controller : function($timeout) {
            var self = this;
            var count = 0;

            self.value = 10;

            self.increment = function() {
                self.list.updateNumber(++count);
            }
        }
    });

angular.module('test')
    .component('child', {
        template : '<div>Child: {{$ctrl.number}}</div>',
        isolate : true,
        scope : {},
        bindings : {
            as : '=',
            number : '<'
        },
        controller : function() {
            var self = this;
            self.as = self;

            this.updateNumber = function(number) {
                self.number = number;
            }
        }
    });

So i think it´s all a matter of pre-processing "as" attribute binding, isn't it?

this.as = this;

@gkalpak
Copy link
Member

gkalpak commented Feb 23, 2016

The idea is that the as directive will grab the controller instance of the component it's placed on and assign it on the template scope (i.e. the parent of the component's scope).
The child component doesn't need to know any of that. (So no as binding is needed and no assignment.)

(BTW, .component doesn't need scope: {} - it will just be ignored anyway 😃)

@Narretz
Copy link
Contributor

Narretz commented Apr 27, 2017

As far as the API goes, #14080 is more flexible (and has a higher chance of landing). Closing.

@Narretz Narretz closed this as completed Apr 27, 2017
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

4 participants