If you haven't already I strongly suggest you first read John Papa's AngularJS styleguide. It's a magnificent document of an evolving set of AngularJS development best practices. Among them there's also one that says to abolish $scope
use and rather provide view model data as part of controller instance because view bindings become more contextual amid other reasons. To accomplish this you need to use the ControllerType as instanceName
syntax. This is usually a blessing but sometimes it may seem to be a curse especially when you give controllers contextual instance names (i.e. UserController as user
) instead of some common name (i.e. UserController as vm
). This is especially useful if you're nesting controllers and don't want to access parent controllers using scope's $parent
property.
Contextual instance naming plays along nicely until you introduce shared views. Now when you want to bind your shared view to a controller instance you don't really know its name. It can be any controller instance name that will be using this shared view. Now what?
If you strictly follow John Papa's guidelines, this is trivial as he suggests to always name controller contexts vm (shorter for view model). In this case you know that controller's context name is always the same so it will simply work. But this somehow beats the purpose of having contextual values to some degree. What if we don't name them vm? What if we rather give controllers better contextual instance names like QuestionController as question
and AnswerController as answer
? What happens when we use a shared view that displays comments? It should of course bind to each of these as both have an array of comments?
What do we need for our scenario
If we continue on the upper premise we have two controllers
QuestionController
andAnswerController
comments
array property. Then we also need three views:
- shared
comments.html
view that displays comments, question.html
details view that displays question specific properties and includes comments view andanswer.html
details view that works analogous to question details view
If you're in a particular hurry or you came here just to see the code go and run my Plunkr example where this is implemented. It's simplified to the very bone so it shouldn't be too hard or too long to comprehend.
Comments shared view
This is a simplified code example of the shared comments view
1: <ul>
2: <li ng-repeat="comment in context.comments">
3: <span ng-bind="comment"></span>
4: </li>
5: </ul>
As you can see we're binding to some arbitrary context
property that should represent our controller instance context. So instead of saying question
or answer
we rather use something common. I used context
but you may use whatever seems suitable for your needs.
Note: As you'll see later where this context
gets created it would be much better for this specific example that comments view would rather consume comments directly and not whole controllers. It would make more sense and we could use the same technique to accomplish for the same. I encourage you to only pass through as much data as is relevant for the included view. But let's stick to this example where we need to bind to various controller instance names.
The magic
Now that we have our shared view we'll have to use some common AngularJS binding attributes that we normally don't use aside ng-include
directive. The thing is that we need to map our controller instance to the common context
scope value so the shared view template can use it. So we'll create and initialize a new scope variable with appropriate name that shared view uses.
Views question.html
and answer.html
will have an include of the comments shared view with this definition:
<div ng-include="'comments.html'" ng-init="context = question"></div>
So what have we done here? We've simply (created and) initialized a new scope variable context
and set its value to controller instance. Shared comments view is therefore able to access our controller regardless of its instance name as we're assigning it to a common scope variable in a non-shared view (question and answer view).
What are the main benefits
The main benefit is that we don't have to deal with this in our controllers' code. Just create controllers and views as you usually do. The only additional thing that needs to be done is mapping on the ng-include
element where you have to initialize the shared view's common context scope variable. This means that this is completely code independent and done in view-only on the shared view insertion point.
But this can't work everywhere, can it?
Of course not. This is applicable to ng-included shared views. There are two caveats I would like to point out though:
- You can't use the same trick with routing; if you define two routes with a shared view but different controller as instance names, you can't simply map controller instance to an additional scope variable without doing it in controller code itself (may be dynamic with hints from route definition
resolve
property but still) - I've italicized "completely code independent" in the upper text because if you already have a scope property with the same name as this common controller instance name we're mapping to, you'll end up with variable value overriding if nothing else
These caveats may put you off using this technique, but if not, I hope it's helpful. The proper way would of course be to componentize shared view as a directive with its own isolated scope as can be seen in this blog post.
Let's see some code
As linked earlier in the post this is a working example on Plunkr. I'll add just the important bits, leaving CSS out and stuff that's superfluous to understanding this concept. I'm not going to show you the main page HTML that has the ng-view
, because that's trivial and you can also check it on Plunkr.
I've already shown a stripped down version of the shared comments view. SO the following are going to be other missing parts. Let's first see routing configuration.
1: $routeProvider
2: .when("/question", {
3: templateUrl: "question.html",
4: controller: "QuestionController as question"
5: })
6: .when("/answer", {
7: templateUrl: "answer.html",
8: controller: "AnswerController as answer"
9: })
10: .otherwise({
11: redirectTo: "/question"
12: });
So these are the view templates question and answer.
1: <h1>Question</h1>
2: <h2 ng-bind="question.title"></h2>
3: <p ng-bind="question.body"></p>
4: <div ng-include="'comments.html'" ng-init="context = question"></div>
5: <div>This question has <span ng-bind="question.answers.length"></span> answer(s).</div>
1: <h1>Answer</h1>
2: <h2>This is an answer to <em ng-bind="answer.question.title"></em></h2>
3: <p ng-bind="answer.body"></p>
4: <div ng-include="'comments.html'" ng-init="context = answer"></div>
The outstanding part now are only the controllers.
1: function QuestionController() {
2: this.title = "How do we do this anyway?";
3: this.body = "I was wondering how can I solve this thing using my brain only? It seems my brain isn't enough hence the question.";
4: this.comments = [
5: "I think this has already been answered elsewhere.",
6: "Please provide a source link.",
7: "Here you go: http://some.place.com/else",
8: "Thanks, but that resource is dealing with a similar yet different problem."];
9: this.answers = [{
10: body: "This is the answer to your problems.",
11: comments: [
12: "I don't think so.",
13: "Well maybe.",
14: "I'm pretty sure you're on the right track.",
15: "Brilliant solution! +1"]
16: }];
17: }
1: function AnswerController() {
2: this.question = {
3: title: "What's the answer to the ultimate question of life, the universe, and everything?"
4: };
5: this.body = "The answer is 42.";
6: this.comments = [
7: "Are you sure, you've calculated it right?",
8: "Well I've invested a week into this and tripple-checked my results.",
9: "Seems legit, thanks..."];
10: }
Awesome post!!
ReplyDelete