Skip to content

Commit 2515a76

Browse files
docs: Add Route-to-component docs
1 parent d87a6bb commit 2515a76

File tree

1 file changed

+320
-22
lines changed

1 file changed

+320
-22
lines changed

_guide/105.ng1-route-to-component.md

Lines changed: 320 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ permalink: /guide/ng1/route-to-component
88

99
{% include toc icon="columns" title="Route to Component" %}
1010

11-
UI-Router for Angular 1 version 1.0 introduced the ability to route directly to
11+
UI-Router v1.0 for Angular 1 introduced the ability to route directly to
1212
[Angular 1.5+ components](https://docs.angularjs.org/guide/component).
1313

1414
Although UI-Router has always allowed views to be defined as arbitrary `template` and `controller` combinations, we
@@ -17,66 +17,364 @@ highly recommend that the Angular 1.5 component model is adopted instead.
1717
In this guide, we'll cover how to route to Angular 1.5 components and how to supply them with resolve data.
1818

1919

20-
# Angular 1.5 .`component()`
20+
# Angular 1.5 `.component()`
2121

2222
An Angular 1.5 component is a reusable piece of a web page, which encapsulates both markup and behavior.
2323
A component is self-contained; it does not access `$scope` values from parent elements, nor does it provide values on `$scope` for children to access.
2424
A component gets all its input data from a parent component as explicit property bindings via attributes in the parent's template.
25-
Any outputs (for communication to the parent) is done using event callbacks, also via attributes in the parent template.
25+
Any outputs (for communication to the parent) are provided as event callbacks, likewise as attributes in the parent template.
2626

2727
In addition to inputs and output bindings, a component also has lifecycle hooks.
2828

2929
The component model from Angular 1.5 aligns closely with the component model from Angular 2.
30-
From a high level, it even has much in common with even React's or Web Component's models.
31-
This level of parity between frameworks allows you to reason about application structure in a similar way, no matter what framework you are using.
30+
From a high level, it even has much in common with even React or Web Component models.
31+
This parity between frameworks allows you to reason about application structure in a similar way, no matter which framework you are using.
3232
{: .notice--info}
3333

3434
To learn more about Angular 1.5 components, refer to the [official docs](https://docs.angularjs.org/guide/component)
3535
and read a [blog or two](https://www.google.com/search?q=angular+1.5+component+blog&oq=angular+1.5+component+blog).
3636

3737
# UI-Router Legacy
3838

39-
UI-Router applications are defined as a tree of states.
40-
Each state typically has one or more views.
41-
42-
In legacy UI-Router, the only way to define a view was to provide a `template` and a `controller`.
43-
The `template` provided the DOM layout, while the `controller` injected services and resolve data, and implemented logic required to make the view work.
39+
In legacy UI-Router, the only way to define a state's view is to using a `template` and a `controller`.
40+
The `template` defines the DOM layout, while the `controller` injects resolve data and services,
41+
and implements logic required to make the view work.
4442

4543
A typical legacy (non-component) state definition might look like this:
4644

4745
```js
4846
.state('fooState', {
4947
url: '/foo/:fooId',
48+
template: '/partials/foo.html',
49+
controller: 'FooController',
5050
resolve: {
51-
fooValue: function($stateParams, $http) {
52-
return $http.get('/api/foo/' + $stateParams.fooId)
53-
.then(function(resp) { return resp.data; });
51+
fooData: function(FooService, $stateParams) {
52+
return FooService.getFoo($stateParams.fooId)
5453
}
55-
},
56-
template: '/partials/foo.html',
57-
controller: 'FooController'
54+
}
5855
});
5956
```
6057

61-
Where `fooController.js` is:
58+
In `fooController.js`, the resolved `fooData` is injected and placed onto the `$scope` for the template to use.
59+
Additional services are also injected to wire up desired behavior in the view.
6260

6361
```js
64-
.controller('FooController', function(fooValue, $scope) {
65-
$scope.foo = fooValue;
62+
// resolve, scope, service
63+
.controller('FooController', function(fooData, $scope, SomeService) {
64+
$scope.foo = fooData;
65+
66+
$scope.clickHandler = function() {
67+
return SomeService.someFn();
68+
}
6669
});
6770
```
6871

69-
And `/partials/foo.html` is:
72+
In `/partials/foo.html` template, the data and functions bound to the `$scope` are used to render the page.
7073

74+
{% raw %}
7175
```html
7276
<h1>{{ foo.name }}</hi>
77+
<i>{{ foo.description }}</i>
78+
79+
<button ng-click="clickHandler()">Do something</button>
80+
7381
<ul>
74-
<li ng-repeat="bar in foo.bars">
82+
<li ng-repeat="bar in $ctrl.foo.bars">
83+
<a ui-sref=".bar({ barId: bar.id })"
84+
ng-class="{ active: bar.active }">
85+
{{ bar.name }}
86+
</a>
7587

76-
{{ bar.name }}
88+
<button ng-click="bar.active = true">
89+
{{ bar.active ? "Deactivate" : "Activate" }}
90+
</button>
7791
</li>
7892
</ul>
93+
94+
<div ui-view></div>
95+
```
96+
{% endraw %}
97+
98+
Legacy templates and controllers have access to scope variables from parent controllers.
99+
In the same way, a controller's $scope variables are accessible by its nested children.
100+
We may be tempted to use this pattern to share data up and down the DOM tree.
101+
However, sharing in this way makes it difficult understand where data comes from, or know what other code might modify the data.
102+
103+
It's also common to see a poorly designed view built as a single template with lots of markup,
104+
and a single controller with too many responsibilities.
105+
{: .notice--info }
106+
107+
# Migrate to components
108+
109+
To begin routing to components, the template and controller for a state is converted to an Angular 1.5 component.
110+
Creating a component from a template and controller will explicitly couple the markup and logic as a single, cohesive unit.
111+
The component can then be used as a logical building block for the application.
112+
113+
The component model enforces separation of concerns and encapsulation by using an isolate scope.
114+
Data from parent scopes can no longer be directly accessed.
115+
Instead, it needs to be explicitly wired in.
116+
This makes it easier to understand where the data comes from.
117+
{: .notice--info}
118+
119+
120+
## Create a component
121+
122+
- Create a `.component` which uses the `template` and `controller` from the state definition.
123+
- Instead of putting variables and functions on `$scope`, put them directly on controller using `this`.
124+
- In the template, reference the controller using `$ctrl`. This convention for components is effectively `controllerAs: '$ctrl'`.
125+
- Instead of injecting resolve data into the controller, use a one-way component input binding, i.e., `<`.
126+
Note that global services such as `SomeService` are still injected into the component's controller.
127+
128+
```js
129+
// This component can be used like: <foo></foo>
130+
.component('foo', {
131+
bindings: {
132+
// one-way input binding, e.g.,
133+
// <foo foo-data="$ctrl.someFoo"></foo>
134+
foo: '<fooData'
135+
},
136+
// controllerAs: $ctrl
137+
template: `
138+
<h1>{{ $ctrl.foo.name }}</hi>
139+
<i>{{ $ctrl.foo.description }}</i>
140+
141+
<button ng-click="$ctrl.clickHandler()">Do something</button>
142+
143+
<ul>
144+
<li ng-repeat="bar in $ctrl.foo.bars">
145+
<a ui-sref=".bar({ barId: $ctrl.bar.id })" ng-class="{ active: $ctrl.bar.active }">
146+
{{ $ctrl.bar.name }}
147+
</a>
148+
149+
<button ng-click="$ctrl.bar.active = !$ctrl.bar.active">
150+
{{ $ctrl.bar.active ? "Deactivate" : "Activate" }}
151+
</button>
152+
</li>
153+
</ul>
154+
155+
<div ui-view></div>
156+
`,
157+
controller: function(SomeService) {
158+
this.clickHandler = function() {
159+
return SomeService.someFn();
160+
}
161+
}
162+
});
163+
```
164+
165+
We use ES6 multi-line strings in this example for convenience.
166+
{: .notice--info }
167+
168+
Components should avoid injecting `$scope`.
169+
However, injecting `$scope` may still be necessary in some cases, such as `$scope.$watch` or `$scope.$on`.
170+
171+
## Update the state definition
172+
173+
In the state definition, replace the `template`/`controller` properties with a `component` property which refers to the name of the new component.
174+
175+
```js
176+
.state('fooState', {
177+
url: '/foo/:fooId',
178+
component: 'foo', // The component's name
179+
resolve: {
180+
fooData: function(FooService, $stateParams) {
181+
return FooService.getFoo($stateParams.fooId)
182+
}
183+
}
184+
});
185+
```
186+
187+
- When activating `fooState`, UI-Router begins routing to the `foo` component.
188+
- After the state's `fooData` resolve is fetched, the transition to `fooState` is successful.
189+
- The `foo` component is created. The resolve value of `fooData`'s is bound to the component's `fooData` input.
190+
- Finally, the controller is injected and instantiated.
191+
192+
Resolve values are bound to the routed component via the component's input bindings.
193+
Global services are still injected into the controller.
194+
{: .notice--info}
195+
196+
# Resolve Bindings
197+
198+
UI-Router automatically binds `resolve` data to a matching component input.
199+
In the example, the `fooState` has a `resolve` called `fooData`.
200+
When ready, the resolved data is bound to the `fooData` input of the component.
201+
202+
Look at the state and component definitions again:
203+
204+
```js
205+
.state('fooState', {
206+
url: '/foo/:fooId',
207+
component: 'foo',
208+
resolve: {
209+
// A resolve named `fooData`
210+
fooData: function(FooService, $stateParams) {
211+
return FooService.getFoo($stateParams.fooId)
212+
}
213+
}
214+
});
215+
```
216+
217+
```js
218+
.component('foo', {
219+
bindings: {
220+
// input binding from the `fooData` attribute
221+
// to the internal `foo` property of the controller
222+
foo: '<fooData'
223+
},
224+
...
225+
```
226+
227+
The resolve named `fooData` is automatically bound to the `fooData` input of the component.
228+
{: .notice--info}
229+
230+
In some cases, the resolve name may not exactly match the component input name.
231+
The mapping between resolve name and component input can be customized using a `bindings:` object on the state declaration.
232+
233+
```js
234+
.state('fooState', {
235+
url: '/foo/:fooId',
236+
component: 'foo',
237+
// the `foo` input binding on the component
238+
// receives `fooData` resolve value (from the state)
239+
bindings: { foo: 'fooData' },
240+
resolve: {
241+
// A resolve named `fooData`
242+
fooData: function(FooService, $stateParams) {
243+
return FooService.getFoo($stateParams.fooId)
244+
}
245+
}
246+
});
247+
```
248+
249+
```js
250+
.component('foo', {
251+
bindings: {
252+
foo: '<'
253+
},
254+
...
79255
```
80256
257+
The `bindings:` object on the state customizes the mapping of a resolve value to a component input.
258+
{: .notice--info }
259+
260+
# Named views/View Targeting
261+
262+
A named `ui-view` can also be targeted using a component.
263+
As usual, the `views:` property on a state declaration is used to target named `ui-view`(s).
264+
Each key on `views:` targets a specific named `ui-view`.
265+
The value for a key is the component that should be loaded into that named `ui-view`.
266+
267+
```js
268+
.state('fooState.barState', {
269+
views: {
270+
// When `fooState.barState` is active, `fooComponent`
271+
// is loaded into the <ui-view name="content" />
272+
// which was created by `fooState`.
273+
"content@fooState": "fooComponent"
274+
}
275+
...
276+
});
277+
```
278+
279+
The component receives `resolve` data bindings, just like with unnamed components.
280+
To customize bindings for named views, replace the name of the component with an object containing the `component` name and `bindings`.
281+
282+
```js
283+
.state('fooState.barState', {
284+
views: {
285+
// When `fooState.barState` is active, `fooComponent`
286+
// is loaded into the <ui-view name="content" />
287+
// which was created by `fooState`.
288+
// The component's `foo` input receives `fooData` resolve
289+
"content@fooState": {
290+
component: "fooComponent",
291+
bindings: { foo: 'fooData' }
292+
}
293+
}
294+
...
295+
});
296+
```
297+
298+
# Break out more components
299+
300+
You are routing to components now.
301+
However, don't stop there!
302+
Components offer a simple mental model for composing a UI out of nested components.
303+
If your existing code has a monolithic template and controller, take this opportunity to break it down into smaller components.
304+
305+
In our example, we are rendering one `foo` and summaries of its embedded `bars`.
306+
We can simplify the `foo` component by creating and using a separate "bar summary" component.
307+
308+
The bar summary component will be a "dumb component", meaning it only renders its inputs and emits events.
309+
It doesn't "own" any of its input data.
310+
Because of this, it also does not mutate the input data.
311+
Instead, it exposes an output, in the form of a callback.
312+
313+
Our new components look like this:
314+
315+
```js
316+
.component('foo', {
317+
bindings: {
318+
foo: '<fooData' // one-way input binding
319+
},
320+
template: `
321+
<h1>{{ $ctrl.foo.name }}</hi>
322+
<i>{{ $ctrl.foo.description }}</i>
323+
324+
<button ng-click="$ctrl.clickHandler()">Do something</button>
325+
326+
<ul>
327+
<bar-summary ng-repeat="oneBar in $ctrl.foo.bars"
328+
barValue="oneBar" on-toggle="$ctrl.onToggle(barId)"></bar-summary>
329+
</ul>
330+
331+
<div ui-view></div>
332+
`,
333+
controller: function(SomeService) {
334+
this.onToggle = (barId) => {
335+
var bar = $ctrl.foo.bars.find(bar => bar.id === barId);
336+
bar.active = !bar.active;
337+
}
338+
this.clickHandler = function() {
339+
return SomeService.someFn();
340+
}
341+
}
342+
})
343+
344+
.component('barSummary', {
345+
bindings: {
346+
bar: '<barValue',
347+
onToggle: '&'
348+
},
349+
template: `
350+
<li>
351+
<a ui-sref=".bar({ barId: $ctrl.bar.id })" ng-class="{ active: $ctrl.bar.active }">
352+
{{ $ctrl.bar.name }}
353+
</a>
354+
355+
<button ng-click="$ctrl.onToggle({ barId: $ctrl.barId })">
356+
{{ $ctrl.bar.active ? 'Deactivate' : 'Activate' }}
357+
</button>
358+
</li>
359+
`
360+
});
361+
```
362+
363+
Note that `barSummary` has no controller and instead of mutating the `bar` object,
364+
it calls the `onToggle` `&` output bindings when the button is clicked.
365+
{: .notice--info }
366+
367+
It would have been easier to allow the `barSummary` component to mutate its `bar.active` property.
368+
The `foo` controller also grew a little bit when the `onToggle` callback was added.
369+
However, by writing `barSummary` as a dumb component, the `foo` component retains "ownership" of its data.
370+
371+
372+
# Resources
81373
374+
- Angular 1.5 [`.component()` docs](https://docs.angularjs.org/guide/component)
375+
- UI-Router [`component:` docs](https://ui-router.github.io/docs/latest/interfaces/ng1.ng1viewdeclaration.html#component)
376+
- UI-Router [`bindings:` docs](https://ui-router.github.io/docs/latest/interfaces/ng1.ng1viewdeclaration.html#bindings)
377+
- UI-Router [`views:` docs](https://ui-router.github.io/docs/latest/interfaces/ng1.ng1statedeclaration.html#views)
378+
- UI-Router [sample application](https://github.com/ui-router/sample-app-ng1/) built with components
379+
- Todd Motto's [Angular Style Guide](https://github.com/toddmotto/angular-styleguide#components)
82380

0 commit comments

Comments
 (0)