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

Route to component: Allow binding to "&" for routed components #3239

Closed
christopherthielen opened this issue Jan 3, 2017 · 5 comments
Closed

Comments

@christopherthielen
Copy link
Contributor

christopherthielen commented Jan 3, 2017

As requested in #2627 (comment) , #3099 , and elsewhere, it could be useful to bind to a routed component's "&" output bindings.

Current Behavior: Map resolves to inputs

Currently, routed components only receive input bindings from resolve data. We currently map resolve data to the following types of bindings:

.component('foo', {
  bindings: {
    input1: '<',
    input2: '@',
    input3: '=',
  }
});

When routing to this component, resolve data is mapped to the bindings. Route-to-component uses a templateProvider which generates a template such as:

<foo input1="::$resolve.input1" input2="{{ ::$resolve.input2 }} input3="::$resolve.input3"></foo>

This binds resolve data (named input1, input2, and input3 to the component's input bindings.


The problem with output bindings

Output bindings for components in angular 1 are declared using a "&". This instructs angular to create a proxy function on the component controller. When an output event occurs, the function is invoked.

.component('withOutput', {
  bindings: { onEvent: '&' },
  template: "<button ng-click="$ctrl.onEvent({ foo: 123, bar: 456 })">button</button>"
});

When creating an instance of the component in a parent component, the child component's output is wired to some functionality on the parent by writing an expression (which usually invokes a handler function on the parent component controller).

In the parent component:

.component('parent', {
  template: "<with-output on-event="$ctrl.handleEvent(foo, bar)"><with-output>"
});

When the button on the child is clicked, it calls the proxy function with the parameters (which it evaluates in the child component scope). Then the proxy function evaluates the wiring expression ($ctrl.handleEvent(foo, bar)), interpolating using the variables from the child (foo and bar), and finally invokes the expression in the parent component's scope (so handleEvent is evaluated in the parent component's scope).

It's common to write components using this pattern where the parent component wires up event handlers in the parent to events emitted from the child. The child informs the parent of events and allows the parent to handle them. I.e., <with-output on-event="$ctrl.handleEvent(foo, bar)"><with-output>

However, once you decide that the child component withOutput should be routed, you can no longer use the same pattern because UI-Router does not allow bindings to cross the ui-view boundary. It currently only allows input bindings to receive resolve data.

Proposal

To make this component routing model more flexible, the proposed solution is to pass through bindings which the child component declares, and which the parent component has wired up through the ui-view.

Example 1

The states using these components:

.state('parent', { component: 'someParent' })
.state('parent.child', { component: 'someChild' })

For example, if the child component declares an input component:

.component('someChild', {
  bindings: { someInput: '<' }
});

and the parent component has wired up some-input on the ui-view:

.component('someParent', {
  template: 
    '<h1>parent</h1>' +
    '<ui-view some-input="$ctrl.data"></ui-view>'
});

Then the template generated for some-child by route-to-component will be:

<some-child some-input="$ctrl.data"></some-child>

These wired bindings on the ui-view will take precedence over the existing auto-wiring of resolve data to component inputs.

Example 2

This example shows how to wire up an event emitted from a routed child component to a parent component, through the ui-view:

The states using these components:

.state('parent', { component: 'someParent' })
.state('parent.child', { component: 'someChild' })
.component('someChild', {
  bindings: { onSomeEvent: '&' },
  template: '<a ng-click="$ctrl.onSomeEvent({ data: $ctrl.childData })">notify parent</a>'
});

and the parent component has wired up a handler, via the ui-view:

.component('someParent', {
  template: 
    '<h1>parent</h1>' +
    '<ui-view on-some-event="$ctrl.handleSomeEvent(data)"></ui-view>',
  controller: function() {
    this.handleSomeEvent = function(data) {
      alert("parent got " + data + " from child");
    }
  }
});

When the button in some-child is clicked, the handleSomeEvent in the some-parent is invoked.

The html in the DOM when parent.child is active would be:

<ui-view>
  <some-parent>
    <h1>parent</h1>
    <ui-view on-some-event="$ctrl.handleSomeEvent(data)">
      <some-child on-some-event="$ctrl.handleSomeEvent(data)">
        <a ng-click="$ctrl.onSomeEvent({ data: $ctrl.childData })">notify parent</a>
      </some-child>
    <ui-view>
  </some-parent>
</ui-view>
<some-child some-input="$ctrl.data"></some-child>

Note that the extra on-some-event wiring on the ui-view remains, but causes no bad behavior because the ui-view itself doesn't process it.

Limitations

Not all attributes on the ui-view from the parent component will be passed through to the child component. Only attributes on the ui-view which match a child component's bindings will be passed through. This is so you can use attributes or directives specific to the ui-view.

This will probably not work with any sort of isolate scope on the ui-view. For example, it probably does not work if the ui-view has an ng-if toggling it.

@christopherthielen christopherthielen added this to the 1.0.0-beta.4 milestone Jan 3, 2017
@christopherthielen
Copy link
Contributor Author

See also #3111 for binding "&" to functions returned by resolve

@orneryd
Copy link

orneryd commented Jan 9, 2017

I commented on #3111 I think the usage and API of resolve is becoming overloaded and clunky supported this way.

@christopherthielen
Copy link
Contributor Author

@rexhxiao
Copy link

rexhxiao commented Jan 14, 2017

How can do this on ng-metadata?

@aestheticsdata
Copy link

aestheticsdata commented Jan 17, 2017

Using ui-router RC1,

does the child need to have a child state of the parent in order to get the foo-binding on the <div ui-view foo-binding="$ctrl.bar"> ?

Is it possible with:
parent : .state('hello, ...
child : .state('world', ...

or do we need to have:
parent: .state('hello', ...
child: .state('hello.world', ...

I have <div ui-view foo-binding="$ctrl.bar"> on my root component template, but the routed child is not receiving the fooBinding.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants