Accessing controller scope from transcluded content - angularjs

I am working on my first Angular app (yay!). I'm trying to have a generic modal component which can declare a title, custom content and any number action buttons. Background setup follows, then my questions at the bottom.
Here's how I'd like to use the directive.
<!-- in app/views/library/index.html -->
<!-- outside <modal> #i_work works great -->
<select id="i_work">
<option value="{{libraryType.id}}" ng-repeat="libraryType in libraryTypes">{{libraryType.name}}</option>
</select>
<modal id="library_new" title="My Custom Title">
<form>
<!-- inside <modal> #i_dont_work doesn't work -->
<select id="i_dont_work">
<option value="{{libraryType.id}}" ng-repeat="libraryType in libraryTypes">{{libraryType.name}}</option>
</select>
</form>
<modal-buttons>
<!-- [toggle-modal] is another directive that shows/hides a given <modal> -->
<button type="button" toggle-modal="library_new">cancel</button>
<button type="submit" ng-click="addLibrary()">save</button>
</modal-buttons>
</modal>
Here's the directive code:
// in app/scripts/directives/modal.js
'use strict';
angular.module('sampleAngularApp')
.directive('modal', [function () {
return {
templateUrl: '/scripts/directives/modal.html',
restrict: 'E',
transclude: true,
link: {
pre: function preLink(scope, element, attrs) {
scope.id = attrs.id;
scope.title = attrs.title;
},
post: function preLink(scope, element, attrs) {
// hacky-hack to transclude specific content into other targets.
// bound event handlers should be preserved (as implemented they are).
// this actually works
var buttonWrap = element.find('modal-buttons');
buttonWrap.children().each(function (index, button) {
element.find('.modal-footer').append(button);
});
buttonWrap.remove();
}
}
};
}]);
... and the directive template:
<!-- in app/scripts/directives/modal.html -->
<div class="modal" role="dialog">
<div class="modal-header">
<span class="title">{{title}}</span>
<button type="button" class="close" toggle-modal="{{id}}">close</button>
</div>
<!-- the contents of .modal-body should be everything inside <modal> above, -->
<!-- except for <modal-buttons> -->
<div class="modal-body" ng-transclude />
<!-- the contents of .modal-footer should be the contents of <modal-buttons> -->
<div class="modal-footer" />
</div>
Questions:
1) Consider the <select> elements in the controller's view above. #i_work renders correctly with <option>s just fine. #i_dont_work renders a <select> with no <option>s. libraryTypes seems to not be in scope inside <modal>. Why is that, and how can I fix it?
2) Is there a better way to transclude specific content into multiple targets? Google's Polymer project provides <content /> with an optional [select] attribute to provide multiple insert targets. Does Angular have anything like this? (See Polymer's website for more information.)

Related

ng-repeat data binding with custom directive

I have a list and on each item in list I am calling a modal window (custom directive) which should have details about that item being clicked , but the data does not change and remains same across each item. Please find the code below.
angular
.module('Testapp')
.directive('testDirective', function () {
return {
restrict: "AE",
templateUrl: "/Apps/templates/mytem/testdir.html",
translucent: true,
scope: {item:'=data'},
link: function (scope, element, attribute) {
console.log(scope.sequence);
}
};
});
Directive
<div class="modal fade" id="modalAddFilters">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-body tree">
{{item}}
</div>
</div>
</div>
</div>
Calling Template
<div>
<div ng-repeat="items in TestList>
<test-Directive id="directive_modalAddFilters" data="items"></test-Directive>
</div>
I am able to see the data correctly loaded in DOM but directive template doesnt change the data.
You code works fine, except that you forget to close you ng-repeat with a quotation mark.
I think you just didn't properly resolved you data into the modal view.
I have made a plunk based on your (partial) code, I added a modal and everything works fine. I've used ui-bootstrap to show the modal with the repeated data injected.

AngularJS: Using directives as templating

I'm using a directive to provide a basic template for many of the pages in my Angular app. It looks like this:
angular.module('app.basicLayout', [])
.directive('basicLayout', function () {
return {
restrict: 'E',
transclude: true,
templateUrl: '/assets/views/templates/basicLayout.html'
}
});
And HTML:
<basic-layout>
<h1>My layout goes here</h1>
</basic-layout>
On some of these pages I would like to add a sidebar and still be able to use the layout from my <basic-layout> directive. Is it possible to make something like the following?
<basic-layout>
<h1>My content goes here</h1>
<the-sidebar>
<h2>Sidebar content here</h2>
</the-sidebar>
</basic-layout>
Update
My template file of the directive currently look like this:
<div class="container basic-layout">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<div ng-transclude></div>
</div>
</div>
</div>
If <the-sidebar> is set, I would like to change the template file to something like this:
<div class="container basic-layout">
<div class="row">
<div class="col-md-8">
<!-- The content here -->
<div ng-transclude></div>
</div>
<div class="col-md-4">
<!-- The sidebar here -->
<div ng-transclude></div>
</div>
</div>
</div>
That's the exact case for transclusion. You can parametrize your directive layout with some variable layout (sidebar in this case). To do this your have to set the transclude property in the directive config object to true and also specify where in your directive's layout the changing content should be injected by using the ng-transclude directive. Like this:
return {
...
transluce: true,
...
}
and now in the directive template:
//other layout here
<div ng-transclude></div>
This way all the content you put inside the <basic-layout> directive will be transfered into the element on which you use ng-transclude.
For this to work, you need to manually transclude using the transclude function passed as a 5th parameter to the link function. To make it easier, I would change the template to have placeholders:
<div>
<content-placeholder></content-placeholder>
<div>
<sidebar-placeholder></sidebar-placeholder>
</div>
</div>
Then, place each content where it belongs:
transclude: true,
link: function(scope, element, attrs, ctrls, transclude){
transclude(function(clone){
var sidebar = clone.find("the-sidebar").remove();
element.find("content-placeholder").replaceWith(clone);
element.find("sidebar-placeholder").replaceWith(sidebar);
});
}
This should work for you, but it's not clear to me why you want to build a directive for a general layout.
If you have many pages in the Web app (in a classical non-SPA sense), then it's probably better to create the scaffolding in a "master page" on the server.
Otherwise, if you mean that you have many "views" of the same app, the I suggest looking into ui-router - specifically into a section of Nested States and Nested Views.

Manually moving transcluded content

I have a directive foo whose template includes a list via ng-repeat:
<div>
<h5>I want to insert transcluded template into body of the li:</h5>
<ul>
<li ng-repeat='item in items'>
<!-- need item template here -->
</li>
</ul>
</div>
I want the template for each item to (optionally) to be specifiable at the point of usage of the directive:
<foo items='people'>
<h5>{{item.name}}</h5>
</foo>
<foo items='people'>
{{item.name}} is {{item.age}} years old.
</foo>
So I need the innerHTML of the directive (e.g. <h5>{{item.name}}</h5>) to be copied to the marked location in the directive template.
<ng-transclude> does this, but it gives the transcluded items the scope of the directive rather than the item. I also need to be able to optionally pull the item template from somewhere else. So I need to do the transclusion manually.
I have access to the transcluded content during link:, but at that point the list item in question is gone!
<div>
<h5>I need to insert transcluded template into body of the li:</h5>
<ul>
<!-- ngRepeat: item in items -->
</ul>
</div>
I think I need to do it during compile, but the transclusion function passed to the compile function is deprecated.
Found a way to do it with a second directive, but that seems unnecessary...
You can achieve that using $interpolate service and changing a bit your approach please see demo here http://plnkr.co/edit/bSb7fEWiMTdNVJYyiXD8?p=preview
set your dynamic template as attribute in directive.
<foo template="'<h1>{{item.name}}</h1>'" items="people"></foo>
and change your directive to :
app.directive('foo', function($interpolate) {
return {
scope: {
items: '=',
template:'='
},
restrict: 'E',
transclude: true,
templateUrl: 'foo-template.html',
link: function(scope, element, attributes, controller, transclude) {
//interpolate your template like below
scope.getValue= function(item) {
var exp = $interpolate(scope.template);
var result =exp({item:item})
return result;
}
}
}
});
and in your template use ng-bind-html
<li ng-repeat='item in items'>
<div ng-bind-html="getValue(item)"></div>
</li>
don't forget to add ngSanitize module to your app

AngularJS: can I use transcluded content in directive without redefining controller?

I have a tab control which enables content within the control to be shown or hidden based on the selected tab.
This works well enough, but the content of the tab panes starts behaving oddly with the scope. Referencing the scope within the transcluded content doesn't seem to refer to the defined controller scope any more.
This can be seen with any directive which has transcluded content. Given the specific controller:
app.controller("MainCtrl", function ($scope) {
$scope.getTestText = function () {
alert($scope.testText);
alert(this.testText);
}
});
Which is used in the following markup:
<h1>Outside Tab</h1>
<div>
<input type="text" ng-model="testText" />
<button ng-click="testText = 'outside'">Set Test Text</button>
<button ng-click="getTestText()">Get Test Text</button>
{{testText}}
</div>
<simple-directive>
<h1>Inside Directive</h1>
<div>
<input type="text" ng-model="testText" />
<button ng-click="testText = 'inside'">Set Test Text</button>
<button ng-click="getTestText()">Get Test Text</button>
{{testText}}
</div>
</simple-directive>
See this plunker.
If you click the top "Set Test Text" button then the "Get Test Text" button, the two alerts show the same thing ("outside"). If you click the second "Set Test Text" button then "Get Test Text", there is a different result: although "this" has the expected "inside" value, the value on the scope is still "outside".
One workaround is to define the controller inside the transcluded content, like so:
<simple-directive>
<h1>Inside Directive</h1>
<div ng-controller="MainCtrl">
<input type="text" ng-model="testText" />
<button ng-click="testText = 'inside'">Set Test Text</button>
<button ng-click="getTestText()">Get Test Text</button>
{{testText}}
</div>
</simple-directive>
But I'd rather avoid this if possible.
So my question: is there a better way of doing this that doesn't change the scope? Is this just expected behaviour?
ngTransclude creates a child (non-isolated) scope :
The $transclude function creates a new child scope by default, source code:
transcludedScope = scope.$new();
The best solution is to avoid referring to primitives on the scope:
Why not use primitives? see similar answers...
Directive - controller data binding in AngularJS
bound element inside ngIf does not update binding
Directives inside ng-include
A demo plunker: http://plnkr.co/edit/LuU6ard1JYsYCOvlP5iY?p=preview
app.controller("MainCtrl", function ($scope) {
$scope.test = {};
$scope.getTestText = function () {
alert($scope.test.text);
alert(this.test.text);
}
});
template:
<input type="text" ng-model="test.text" />
<button ng-click="test.text = 'inside'">Set Test Text</button>
<button ng-click="getTestText()">Get Test Text</button>
{{test.text}}
Another solution is to manually transclude without creating a new child scope:
app.directive('simpleDirective', function () {
return {
restrict: 'E',
replace: true,
transclude: true,
scope: {},
link: function(scope,elm,attrs,ctrl,$transclude){
$transclude(scope.$parent, function(clone){
elm.after(clone);
})
}
};
})

AngularJS composite view with the same directive

I have a question, about composite view in AngularJS, that is close to that one : AngularJS Composite Components
However, I would like to know if it is possible to have a directive that includes a list of the same directive, like this :
<mydirective name="thecontainer">
<mydirective name="a"/>
<mydirective name="b"/>
<mydirective name="c"/>
</mydirective>
Thanks,
David.
Edit
Answering blesh, I will be more precise. I'm trying to display boxes (a simple square) that can have one or many boxes, themselves can have many boxes, etc.
Here is the directive
myApp.directive('box', function () {
return {
restrict:'E',
replace:true,
templateUrl:"partials/box.html",
scope: {
name : '#'
},
link:function (scope, element, attrs, ctrl) {
console.log("trying to log " + attrs.name);
}
}
});
Here is the template :
<div class="box">
<div class="box-header">
<div>{{ name }}</div>
</div>
<div class="box-container">
<!-- display other boxes here-->
</div>
</div>
Here is the interesting code in the view :
<box name="TOP" >
<box name="SUB" >
</box>
<box name="SUB" >
</box>
</box>
Obviously something is missing to tell the sub-boxes "hey please display in the right place into your parent and please, parent, adapt your size to the number of children you have"
David.
The answer was really simple, without ng-transclude, angular has no way to interpret correctly the content of the innner HTML tags. So the correct way to do it is to correct the template by adding a ng-transclude like this:
<div class="box">
<div class="box-header">
<div>{{ name }}</div>
</div>
<div class="box-container" ng-transclude>
<!-- display other boxes here-->
</div>
</div>

Resources