Description
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.