AngularJS : How to access the directive's scope in transcluded content? - javascript

I have trouble getting my transcluding directive to work. I want to do the following: Create a directive that outputs a list where the content of each item is defined by transcluded content. E.g:
<op-list items="myItems">
<span class="item">{{item.title}}</span>
</op-list>
so I would use ng-repeat inside op-list's template and must be able to access the scope created by ng-repeat inside the transcluded content.
This is what I've done so far:
var myApp = angular.module('myApp', []);
myApp.controller('MyCtrl', ['$scope', function ($scope) {
$scope.myModel = {
name: 'Superhero',
items: [{
title: 'item 1'
}, {
title: 'item 2'
}]
};
}]);
myApp.directive('opList', function () {
return {
template: '<div>' +
'<div>items ({{items.length}}):</div>' +
'<div ng-transclude ng-repeat="item in items"></div>' +
'</div>',
restrict: 'E',
replace: true,
transclude: true,
scope: {
items: '='
}
};
});
<html ng-app="myApp">
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-controller="MyCtrl">
<div>Hello, {{myModel.name}}!</div>
<op-list items="myModel.items">
<span>title: {{item.title}}|{{$scope}}|{{scope}}|{{items}}</span>
</op-list>
</div>
</html>

Check if this works:
myApp.directive('opList', ['$scope', function ($scope) {
return {
template: '<div>' +
'<div>items ({{model.items.length}}):</div>' +
'<ng-transclude ng-repeat="item in model.items"></ng-transclude>' +
'</div>',
restrict: 'E',
replace: true,
transclude: true,
scope: {
items: '='
}
};
}]);
If it doesn't then try to inspect $scope in console and see if you're able to access your model.

Related

How to pass transclusion down through nested directives in Angular?

I am trying to figure out how to pass a transclusion down through nested directives and bind to data in the inner-most directive. Think of it like a list type control where you bind it to a list of data and the transclusion is the template you want to use to display the data. Here's a basic example bound to just a single value (here's a plunk for it).
html
<body ng-app="myApp" ng-controller="AppCtrl as app">
<outer model="app.data"><div>{{ source.name }}</div></outer>
</body>
javascript
angular.module('myApp', [])
.controller('AppCtrl', [function() {
var ctrl = this;
ctrl.data = { name: "Han Solo" };
ctrl.welcomeMessage = 'Welcome to Angular';
}])
.directive('outer', function(){
return {
restrict: 'E',
transclude: true,
scope: {
model: '='
},
template: '<div class="outer"><inner my-data="model"><div ng-transclude></div></div></div>'
};
})
.directive('inner', function(){
return {
restrict: 'E',
transclude: true,
scope: {
source: '=myData'
},
template :'<div class="inner" my-transclude></div>'
};
})
.directive('myTransclude', function() {
return {
restrict: 'A',
transclude: 'element',
link: function(scope, element, attrs, controller, transclude) {
transclude(scope, function(clone) {
element.after(clone);
})
}
}
});
As you can see, the transcluded bit doesn't appear. Any thoughts?
In this case you don't have to use a custom transclude directive or any trick. The problem I found with your code is that transclude is being compiled to the parent scope by default. So, you can fix that by implementing the compile phase of your directive (this happens before the link phase). The implementation would look like the code below:
app.directive('inner', function () {
return {
restrict: 'E',
transclude: true,
scope: {
source: '=myData'
},
template: '<div class="inner" ng-transclude></div>',
compile: function (tElem, tAttrs, transclude) {
return function (scope, elem, attrs) { // link
transclude(scope, function (clone) {
elem.children('.inner').append(clone);
});
};
}
};
});
By doing this, you are forcing your directive to transclude for its isolated scope.
Thanks to Zach's answer, I found a different way to solve my issue. I've now put the template in a separate file and passed it's url down through the scopes and then inserting it with ng-include. Here's a Plunk of the solution.
html:
<body ng-app="myApp" ng-controller="AppCtrl as app">
<outer model="app.data" row-template-url="template.html"></outer>
</body>
template:
<div>{{ source.name }}</div>
javascript:
angular.module('myApp', [])
.controller('AppCtrl', [function() {
var ctrl = this;
ctrl.data = { name: "Han Solo" };
}])
.directive('outer', function(){
return {
restrict: 'E',
scope: {
model: '=',
rowTemplateUrl: '#'
},
template: '<div class="outer"><inner my-data="model" row-template-url="{{ rowTemplateUrl }}"></inner></div>'
};
})
.directive('inner', function(){
return {
restrict: 'E',
scope: {
source: '=myData',
rowTemplateUrl: '#'
},
template :'<div class="inner" ng-include="rowTemplateUrl"></div>'
};
});
You can pass your transclude all the way down to the third directive, but the problem I see is with the scope override. You want the {{ source.name }} to come from the inner directive, but by the time it compiles this in the first directive:
template: '<div class="outer"><inner my-data="model"><div ng-transclude></div></div></div>'
the {{ source.name }} has already been compiled using the outer's scope. The only way I can see this working the way you want is to manually do it with $compile... but maybe someone smarter than me can think of another way.
Demo Plunker

How to tell angular where to render child directives inside parent template (multiple transclude)?

I have problem that is inability to render child directive (selected-item-template) in the parent template.
Code bellow:
HTML (Main/child directive)
<compact-select
no-item-selected-text="Add a Customer"
no-item-selected-icon="fa-user"
search-placeholder="Type a customer name"
cs-model="customer"
cs-items="contacts"
>
<display-item-template>
<span>{{$parent.item.id}}</span>
<span>{{$parent.item.name}}</span>
</display-item-template>
<selected-item-template>
Your have selected customer: {{$parent.item.name}}
</selected-item-template>
</compact-select>
Directive
angular.module('core').directive('compactSelect', [function($timeout) {
return {
templateUrl : 'modules/core/views/components/compact-select-tpl.html',
bindToController: true,
transclude: true,
scope: {
noItemSelectedText: '#',
noItemSelectedIcon: '#',
csModel: '=',
csItems: '=csItems'
},
controllerAs : 'ctrl',
controller : function($scope) {
}
};
}]).directive('displayItemTemplate', function() {
return {
require: '^compactSelect',
restrict: 'E'
}
}).directive('selectedItemTemplate', function() {
return {
require: '^compactSelect',
restrict: 'E'
}
});
Directive Template (modules/core/views/components/compact-select-tpl.html)
<div class="compact-select-repeater-box" style="" >
<div ng-transclude ng-repeat="item in ctrl.csItems | filter:searchParam" class="compact-select-repeater" ng-class="ctrl.getHighlightedClass(item)" ng-click="ctrl.itemSelected(item)">
<span>{{item.name}}</span>
<span>{{item.id}}</span>
</div>
<div style="position:absolute;bottom:0">
+ Click here to add customer {{ctrl.message}}
</div>
**HERE I WANT SELECTED ITEM TEMPLATE**
</div>
Question: How I can tell where child directive needs to be rendered?
Directive on ng-repeat works, but when I add two directives everything gets combined together and that's not what I want. Is there is a way to specify with ng-transclude where to render which directive? Like ng-transclude="displayItemTemplate" and ng-transclude="selectedItemTemplate" respectively?
This Example Based on Directives, and i want to show you how multi directives work together on one array.
I hope this helps you.
var app = angular.module("app", []);
app.controller("ctrl", function ($scope) {
$scope.selectedList = [];
$scope.data = [
{ name: "a", checked: true },
{ name: "b", checked: false }
];
$scope.getResult = function () {
console.log($scope.selectedList);
}
});
app.directive("directiveA", [function () {
return {
restrict: "E",
template: "<ul ng-repeat=\"item in items\">" +
"<li><directive-c data=\"item\"></directive-c> {{item.name}} <directive-b data=\"item\"></directive-b></li>" +
"</ul>",
scope: {
items: "="
}
};
}]);
app.directive("directiveB", function () {
return {
restrict: "E",
template: "<button ng-click=\"renderData(data)\">{{data.checked ? 'unchecked':'checked'}}</button>",
scope: {
data: "="
},
link: function (scope) {
scope.renderData = function (data) {
data.checked = !data.checked;
}
}
}
});
app.directive("directiveC", function () {
return {
restrict: "E",
template: "<input type=\"checkbox\" ng-model=\"data.checked\">",
scope: {
data: "="
}
}
});
app.directive("directiveD", function () {
return {
restrict: "E",
template: "<ul ng-repeat=\"item in items\">" +
"<li ng-if=\"item.checked\">{{item.name}}</li>" +
"</ul>",
scope: {
items: "=",
result: "="
},
link: function (scope) {
scope.$watch("items", function (newValue) {
scope.result = [];
if (newValue) {
angular.forEach(scope.items, function (item, index) {
if (item.checked) scope.result.push(item);
});
}
}, true);
}
}
});
<!DOCTYPE html>
<html ng-app="app" ng-controller="ctrl">
<head>
<title></title>
</head>
<body>
<h3>items</h3>
<directive-a items="data"></directive-a>
<h3>selected items</h3>
<directive-d items="data" result="selectedList"></directive-d>
<hr />
<button ng-click="getResult()">get selected list</button>
<small>after click, check your console</small>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
</body>
</html>
I have figured out how to achieve following. It can be done with multiple transcludsions, since angular 1.5.
I just need to define transcludeSlot.
Code bellow:
HTML (Main/child directive)
<compact-select
no-item-selected-text="Add a Customer"
no-item-selected-icon="fa-user"
search-placeholder="Type a customer name"
cs-model="customer"
cs-items="contacts"
>
<display-item-template>
<span>{{$parent.item.id}}</span>
<span>{{$parent.item.name}}</span>
</display-item-template>
<item-selected-template>
Your have selected customer: {{$parent.csModel.name}}
</item-selected-template>
</compact-select>
Directive
angular.module('core').directive('compactSelect', [function($timeout) {
return {
templateUrl : 'modules/core/views/components/compact-select-tpl.html',
bindToController: true,
transclude: {
'repeaterItemSlot': 'displayItemTemplate',
'itemSelectedTemplateSlot' : 'itemSelectedTemplate'
},
scope: {
noItemSelectedText: '#',
noItemSelectedIcon: '#',
csModel: '=',
csItems: '=csItems'
},
controllerAs : 'ctrl',
controller : function($scope) {
}
};
}]);
Directive Template (modules/core/views/components/compact-select-tpl.html)
<div class="compact-select-repeater-box" style="" >
<div ng-transclude="repeaterItemSlot" ng-repeat="item in ctrl.csItems | filter:searchParam" class="compact-select-repeater" ng-class="ctrl.getHighlightedClass(item)" ng-click="ctrl.itemSelected(item)">
<span>{{item.name}}</span>
<span>{{item.id}}</span>
</div>
<div style="position:absolute;bottom:0">
+ Click here to add customer {{ctrl.message}}
</div>
<div ng-transclude="itemSelectedTemplateSlot"></div>
</div>
So this works nicely similar to XAML.

Angular directive - isolated scope isn't showing

I trying to make a directive which accepting an attribute and hook it to the isolated scope, but the attribute value is not showing.
angular.module('app', [])
.controller('torrentController', [function() {
this.recommended = ['...'],
this.otherArray = ['...']
}])
.directive('torrentsTable', [function() {
return {
restrict: 'E',
templateUrl: 'templates/directives/torrentsTable.html',
scope: {
index: '='
},
controller: 'torrentController as torrentCtrl'
};
}]);
The idea is to use this directive to show different list of torrents with this syntax:
<torrents-table index="recommended"></torrents-table>
<torrents-table index="someOtherIndex"></torrents-table>
I wish this 2 almost same lines to show different "list" with results.
templates/directives/torrentsTable.html
<!-- I also tried with ng-repeat="torrent in torrentCtrl.recommended" -->
<!-- And is working as I excepted (It's shows the recommended array) -->
<div layout="row" ng-repeat="torrent in torrentCtrl[index]">
<div flex>Name: {{torrent.name}}</div>
<div flex>{{index}}</div>
</div>
{{index}} is not showing, and it's value is not showing.
While I actually make hardcoded ng-repeat arguments - it repeating but {{index}} is empty.
What I am doing wrong?
Your problem: how you pass key.
You use in directive:
scope: {
index: '='
},
so you should pass to directive expression, that evaluated to $scope property. So if you not inject scope - you pass undefined.
You can fix this two ways:
1) pass string instead something else
<torrents-table index="'recommended'"></torrents-table>
<torrents-table index="'someOtherIndex'"></torrents-table>
2) change directive definition to
scope: {
index: '#'
},
sample you can see in snippet below.
angular.module('app', [])
.controller('torrentController', [function() {
this.recommended = [1,2,3,4,5];
this.someOtherIndex = ['a','b','c','d','e'];
}])
.directive('torrentsTable', [function() {
return {
restrict: 'E',
template: '<div flex>{{index}}</div>'+
'<div layout="row" ng-repeat="torrent in torrentCtrl[index]">'+
' <div flex>Name: {{torrent}}</div>'+
'</div>',
scope: {
index: '='
},
controller: 'torrentController as torrentCtrl'
};
}])
.directive('torrentsTable2', [function() {
return {
restrict: 'E',
template: '<div flex>{{index}}</div>'+
'<div layout="row" ng-repeat="torrent in torrentCtrl[index]">'+
' <div flex>Name: {{torrent}}</div>'+
'</div>',
scope: {
index: '#'
},
controller: 'torrentController as torrentCtrl'
};
}]);
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.7/angular.js"></script>
<div ng-app='app'>
<torrents-table index="'recommended'"></torrents-table>
<torrents-table index="'someOtherIndex'"></torrents-table>
<hr/>
<torrents-table2 index="recommended"></torrents-table2>
<torrents-table2 index="someOtherIndex"></torrents-table2>
</div>

List of directives inside of an ng-repeat

I'm working on a page that is made up of 5 directives, for example:
<directive-one></directive-one>
<directive-two></directive-two>
<directive-three></directive-three>
<directive-four></directive-four>
<directive-five></directive-five>
I would like to be able to re-order these on demand so that a user can control how their page looks. The only way I could think of doing that was putting them in an ng-repeat:
$scope.directiveOrder = [{
name: "directive-one",
html: $sce.trustAsHtml('<directive-one></directive-one>'),
order: 1
}, ...
HTML:
<div ng-repeat="directive in directiveOrder" ng-bind-html="directive.html">
{{directive.html}}
</div>
This will give me the right tags, but they aren't processed as directives by angular. Is there a way around that? I'm assuming it's something to do with $sce not handling it, but I might be way off?
Try creating a new directive and using $compile to render each directive:
https://jsfiddle.net/HB7LU/18670/
http://odetocode.com/blogs/scott/archive/2014/05/07/using-compile-in-angular.aspx
HTML
<div ng-controller="MyCtrl">
<button ng-click="reOrder()">Re-Order</button>
<div ng-repeat="d in directives">
<render template="d.name"></render>
</div>
</div>
JS
var myApp = angular.module('myApp',[])
.directive('directiveOne', function() {
return {
restrict: 'E',
scope: {},
template: '<h1>{{obj.title}}</h1>',
controller: function ($scope) {
$scope.obj = {title: 'Directive One'};
}
}
})
.directive('directiveTwo', function() {
return {
restrict: 'E',
scope: {},
template: '<h1>{{obj.title}}</h1>',
controller: function ($scope) {
$scope.obj = {title: 'Directive Two'};
}
}
})
.directive("render", function($compile){
return {
restrict: 'E',
scope: {
template: '='
},
link: function(scope, element){
var template = '<' + scope.template + '></' + scope.template + '>';
element.append($compile(template)(scope));
}
}
})
.controller('MyCtrl', function($scope, $compile) {
$scope.directives = [{
name: 'directive-one'
}, {
name: 'directive-two'
}];
$scope.reOrder = function () {
$scope.directives.push($scope.directives.shift());
console.log($scope.directives);
};
});
I hope You can easily done it.
var myApp = angular.module('myApp',[])
.directive('directiveOne', function() {
return {
restrict: 'E',
scope: {},
template: '<h1>{{obj.title}}</h1>',
controller: function ($scope) {
$scope.obj = {title: 'Directive One'};
}
}
})
.directive('directiveTwo', function() {
return {
restrict: 'E',
scope: {},
template: '<h1>{{obj.title}}</h1>',
controller: function ($scope) {
$scope.obj = {title: 'Directive Two'};
}
}
});
myApp.controller('ctrl',function($scope){
$scope.data = [{name:'directive-one'},{name:'directive-two'}];
});
<html ng-app='myApp'>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.17/angular.min.js"></script>
</head>
<body ng-controller='ctrl'>
<div ng-repeat='item in data'>
<item.name></item.name>
<directive-one></directive-one>
</body>
</html>

Initialize text input fields in angular directive

I have a directive which shows input fields and I want to initialize those fields with data from the server. The problem is that I can't do that while using ng-model.
Before using a directive I used in the controller something like $scope.field1 = $scope.first.field1
Here's my code. I simplified it for the sake of readability but the idea's here.
In my controller I have this code:
app.controller('MyController',
['$scope', 'myData', function($scope, myData) {
myData.then(function(data) {
$scope.first = data.first;
$scope.second = data.second;
});
}]);
Inside first and second I have 2 field: field1 and field2.
In in html code, I have this bit:
<h1>First</h1>
<my-directive info="first"></my-directive>
<h1>Second</h1>
<my-directive info="second"></my-directive>
The directive is as follows:
app.directive('myDirective', function() {
return {
restrict: 'E',
scope: {
info: '='
},
templateUrl: 'static/js/myDirective.html',
link: function(scope, element, attrs) {
scope.doStuff = function() {
/* Do stuff with
scope.field1 and scope.field2 */
}
}
};
});
and the myDirective.html code:
<input type="text" ng-model="myfield1" />
<input type="text" ng-model="myfield2" />
<input type="submit" ng-click="doStuff()" />
If in myDirective.html I write:
<input type="text" value="info.field1" />
I can see the value fine.
Any ideas?
probably your directive initializes before your data loads. and your directive sees ng-model as an undefined variable. You are not using info directly in template so no auto $watch's for you :).
you need to $watch your info variable in directive and call your doStuff function on change.
note: i wouldn't recommend adding a controller to a directive just for this task. adding a controller to a directive is needed when you need to communicate with other directives. not for waiting async data
edit
you should do
app.directive('myDirective', function() {
return {
restrict: 'E',
scope: {
info: '='
},
templateUrl: 'static/js/myDirective.html',
link: function(scope, element, attrs) {
scope.doStuff = function() {
/* Do stuff with
scope.field1 and scope.field2 */
}
scope.$watch('info', function(newValue){
scope.doStuff();
});
}
};
});
Check working demo: JSFiddle.
Define a controller for the directive and do the initialization inside it:
app.directive('myDirective', function() {
return {
restrict: 'E',
scope: {
info: '='
},
controller: ['$scope', 'myData', function ($scope, myData) {
myData.then(function(data){
$scope.first = data.first;
$scope.second = data.second;
});
}],
...
},
Inside the directive, myfield1 and myfield2 don't exist. You are close to solving the issue by using info.field1 instead.
var myApp = angular.module('myApp', []);
//Faking myData here; in your example this would come from the server
var myData = {
first: {
field1: "FirstField1",
field2: "FirstField2"
},
second: {
field1: "SecondField1",
field2: "SecondField2"
}
};
myApp.controller('MyController', ['$scope', function($scope) {
$scope.first = myData.first;
$scope.second = myData.second;
}]);
myApp.directive('myDirective', function() {
return {
restrict: 'E',
scope: {
info: '='
},
template: '<input type="text" ng-model="info.field1" /><input type="text" ng-model="info.field2" /><input type="submit" ng-click="doStuff()" />',
link: function(scope, element, attrs) {
scope.doStuff = function() {
alert('Info: ' + scope.info.field1 + ', ' + scope.info.field2);
}
}
};
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.5/angular.min.js"></script>
<div ng-app="myApp" ng-controller="MyController">
<h1>First</h1>
<my-directive info="first"></my-directive>
<h1>Second</h1>
<my-directive info="second"></my-directive>
</div>

Categories

Resources