I have top menu in my app, and I would like to have different content there depending on controller. In Rails it is easy with content_for
, but how to achieve it with angular? I already know this solution: AngularJS: How can I pass variables between controllers? but maybe there is better way to do this?
4 Answers
One of the fun things about Angular is there is, often, no "better way" without understanding the context of your application. There are, "other" ways, which is best depends tremendously. I guess more info might help to customise or recommend a particular answer, too.
However here's my thought stream on this topic:
First thoughts
More defined service
The answer you found is not bad, though I would probably take it a little further and have some sort of 'menu service' rather than a highly generic 'property' store. This menu service could be manipulated by the controllers that ng-view
instantiates.
Via the route mappings
Taking it even further, it would be possible to include menu information within the route provider declarations and then, on $routeChangeSuccess
or $routeChangeStart
have the menu controller update itself based on the data from the routes (perhaps maintaining the service as well so that controllers can contribute "special" menu options, thereby allowing a degree of customisation).
A few more options
If shared services (a Angular best practice, fyi) aren't to your liking or setup and playing with the routes isn't, either (could be tricky) then I can see a few more options:
$rootScope
One is to inject $rootScope
(the great grand-daddy of all scopes) and have a collection on there that is your menu items; each controller could then just update that manually.
Custom events
Here $rootScope.$emit()
is your friend - you could emit some sort of event and supply menu configuration data. A controller would then be listening ($rootScope.$on()
) for the event and update/clear-out/replace it's own list of menu items with the newly-emitted menu list.
Advanced routing
Getting even funkier, you could even try and see if including functions in the resolve
part of the routes would do the trick.
References
Info on playing with the scope is on Angular's documentation: http://docs.angularjs.org/api/ng.$rootScope.Scope
Info on complex routing is here: http://www.yearofmoo.com/2012/10/more-angularjs-magic-to-supercharge-your-webapp.html#additional-features-of-controllers-and-routes (yearofmoo are Rails fans, so their opinions might match your own)

- 1,238
- 10
- 20
-
Base of my app is angular tutorial, so usecase would be adding menu to app/index.html from this step: http://docs.angularjs.org/tutorial/step_07 – Sławosz May 13 '13 at 09:15
For a similar task I wrote an angular js directive that mimics rails content_for
Sorry for CoffeeScript:
App.directive "contentFor", ["$sce", ($sce) ->
compile: (e, attrs, transclude) ->
pre: (scope, iElement, iAttrs, controller) ->
varname = iAttrs["contentFor"]
# Looks for the closest scope where 'varname' is defined
# or pick the top one
targetScope = scope
while targetScope.$parent
if ( if targetScope.hasOwnProperty then targetScope.hasOwnProperty(varname) else targetScope[varname] )
break
else
targetScope = targetScope.$parent
# Store html inside varname
html = iElement.html()
targetScope[varname] = $sce.trustAsHtml(html)
# remove the directive element
iElement.remove()
]
You can use it like this
<span content-for="menuHtml">
<ul>
<li>Menu Item 1</li>
<li>Menu Item 2</li>
<li>Menu Item 3</li>
</ul>
</span>
And here is the equivalent of <%= yield(:menuHtml) %>
:
<nav ng-bind-html="menuHtml"></nav>
In the end you could reset menu to a default binding to $routeChangeSuccess
event in your controller:
$scope.$on '$routeChangeSuccess', ->
$scope.menuHtml = $sce.trustAsHtml("<ul>...</ul>")

- 678
- 6
- 12
Bind your top level menu to an object in top level scope. Create a method in that controller to set that variable. Within your child controllers call the method to set the appropriate variable that will change the menu based on your controller.
You can leverage services
to store menu items if they are not coming from the server.

- 5,861
- 3
- 32
- 39
I think making a direct reference to the div which will be replaced instead of using angular's scopes as in mcasimir's answer is much simpler. Also it has the added benefit of allowing angular to compile the directives which in the other answer will give you an 'untrusted' error. This is what the directive looks like:
.directive('appContentFor', ($compile) -> {
replace: true,
compile: (element,attr,transclude)->
return (scope, element, attrs)->
element.remove()
node = $(attrs.node)
$(node[0]).replaceWith($compile(element.html())(scope))})
By injecting the $compile service angular can do it's job on the html before inserting it where it belongs. Now you can use this directive like this:
<div app-content-for node="div#awesomeContentFor">
<ul id="awesome_inserted_list">
<li>{{1+2}}</li>
<li>{{something_on_current_scope}}</li>
</ul>
</div>
then somewhere else just add the div with id awesomeContentFor:
<div id="awesomecontentfor"></div>

- 1,133
- 11
- 19