Angular ng-transclude scope in repeat - javascript

I just started to study AngularJS and tries to implement customized table directive with the multiple slots transclude.
And faced situation that scope not transferred to transclude. There is a lot of solutions in other StackOverflow questions, but all of them works only when in directive template ng-repeat appears for top element, but that is not my case.
At least i can't adopt all that solutions.
Simplified version.
Directive:
<span>
<div>Some pagination</div>
<div style="display: inline"><input type="text" placeholder="Search"/></div>
<div style="display: inline">Some filters</div>
<table>
<tbody>
<tr ng-repeat="line in lines" ng-transclude="row">
</tr>
</tbody>
</table>
<div>Some pagination again</div>
</span>
Using of directive:
<my-table>
<row>
<td>{{line.col1}}</td>
<td>{{line.col2}}</td>
</row>
</my-table>
Full example with the script on Plunkr:
https://plnkr.co/edit/rg43ZdPMGHLBJCTLOoLC
Any advice very appreciated.

The simplest and probably cleanest way to directly reference the $scope object created by the ng-repeat in a transcluded template is through the $parent property:
<my-table>
<td>{{$parent.line.col1}}</td>
<td>{{$parent.line.col2}}</td>
</my-table>
The $parent property of the $scope created for a transcluded template points to the $scope of the target template into which such template is ultimately transcluded (in this case, the ng-repeat), even though such transcluded $scope is not a child of the target $scope in the usual sense as a result of the transclusion. See this wonderful blog post for a more complete discussion of this.
Working plunkr: https://plnkr.co/edit/LoqIMiQVZKlTt5epDnZF?p=preview.

You need to use $transclude function manually and create new child scope for each line. Other than that you need to pass lines to directive if you are using isolated scope (and you are using it).
Your linking function should look something like this:
link: function($scope, $element, $attrs, controller, $transclude) {
var tbody = $element.find('tbody');
$scope.$watch('lines', function (lines) {
tbody.empty();
lines.forEach(function (line) {
var childScope = $scope.$new();
childScope.line = line;
$transclude(childScope, function (content) {
tbody.append('<tr>');
tbody.append(content);
tbody.append('</tr>');
}, null, 'row');
});
});
}
Plunker: https://plnkr.co/edit/MLNZOmoQyMazgIpluMqO?p=preview
But that is bad idea anyway, cos it is hard to create table this way. As you can see child of is not elements. You would have to do a little bit of DOM manipulation to make it work.

i see you don't really need to use attribute
so code look more simple and clean:
<body ng-controller="tableCtrl">
<h1>Table test</h1>
<my-table lines="lines"></my-table>
</body>
your template:
<span>
<div>Some pagination</div>
<div style="display: inline"><input type="text" placeholder="Search"/></div>
<div style="display: inline">Some filters</div>
<table>
<tbody>
<tr ng-repeat="line in lines">
<td>{{line.col1}}</td>
<td>{{line.col2}}</td>
</tr>
</tbody>
</table>
<div>Some pagination again</div>
</span>
and angular directive:
angular.module('myApp', [])
.directive("myTable", function() {
return {
restrict: 'E',
transclude: true,
scope: {
lines:'=lines',
api: '#'
},
templateUrl: "template.html",
};
})
.controller("tableCtrl", ['$scope', function($scope) {
$scope.lines = [
{col1: "testCol1", col2: "testCol2"},
{col1: "testCol11", col2: "testCol21"}
];
}]);
working example in plunkr: https://plnkr.co/edit/iMxRoD0N3sUXqmViHAQh?p=preview

Related

ng-bind-html renders data as HTML but ignores children elements

I'm working on this function that generates HTML based on user input and I can render it as proper HTML inside a list item (instead of a string version) using ng-bind-as-html.
The only problem is that it doesn't render the span tag that is also a child element in the li tag. I'm an Angular n00b and could use any insight ya got.
The code below allows me to have my functions output as proper HTML, which is nice, but I need that span tag to show up as it allows my user to copy the contents of the text.
Basically, I can either rewrite without ng-bind-html and have the span render appropriately, or I can have my HTML output render and not get the span tag. I'm stuck with one or the other and not both... and I want both. Classic.
Thanks for your help!
<li
class="entry"
ng-repeat="entry in output track by $index"
ng-mouseenter="onEnter()"
ng-bind-html="entry">
{{entry}}
<span
class="copy"
ng-click="copyData(entry)"
ng-mouseenter="onEnter()">
{{message}}
</span>
</li>
You can create a custom directive to achieve this with transclude and ng-transclude as below.
Documentation for ng-transclude
var app = angular.module('app', []);
app.controller('TestController', ['$scope', function($scope) {
$scope.message = 'You are welcome!';
$scope.output = ['<h1>Hello user</h1>'];
}]);
app.directive('bindHtml', ['$sce', function($sce) {
return {
restrict: 'A',
transclude: true,
template: '<span><span ng-bind-html="html"></span><span ng-transclude></span></span>',
link: function (scope, element, attrs) {
scope.html = $sce.trustAsHtml(scope.$eval(attrs.bindHtml));
}
};
}]);
angular.bootstrap(document, ['app']);
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-controller="TestController">
<li
class="entry"
ng-repeat="entry in output track by $index"
ng-mouseenter="onEnter()"
bind-html="entry">
<span
class="copy"
ng-click="copyData(entry)"
ng-mouseenter="onEnter()">
{{message}}
</span>
</li>
</div>

Angular directive/child directive transclude inside ng-repeat

The problem is that child directive binds to parent however syntax {{name}} gets ignored by ng-repeat. What would be the right way achieving this?
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>{{name}}</span>
or
<span>{{item.name}}</span>
</display-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($timeout) {
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>
</div>
I can see that
<span>{{item.name}}</span>
<span>{{item.id}}</span>
Gets replaced with
<span></span>
or
<span>{{item.name}}</span>
and not with
<span>{{name}}</span>
or
<span>{{item.name}}</span>
Question: How do I get ng-repeat to respect html bindings syntax from child directive? Or is there is another way to achieve this?
If i am not wrong, then you are trying to create a list view such that the template of the list would be provided by the user, but the methods (click,etc) would already be made available through the directive.
Now, since angular 1.3, the transcluded scope is a child of the directive isolated scope,
so, in your case, if you follow the correct hierarchy, you can access the directive scope from within the template provided by the user.
Here is your scope hierarchy:
Directive isolated scope --> ng-repeat new scope for every row --> transcluded scope.
so, if you want to access directive scope from the transcluded scope,
you would need to do $parent (for ng-repeat) and then access item.name, like below:
<display-item-template>
<span>Item Name: {{$parent.item.name}}</span>
</display-item-template>
Also, you don't need the bindings inside your compact-select-tpl, because you want that content to come from transclusion:
<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>
</div>
The displayItemTemplate directive that you transclude inside the other directive has already it's data interpolated {{name}} and {{item.name}}.
If you don't have these variables in the $scope, it will transclude empty strings inside your spans.
Then in your compactSelect directive, the div that gets transcluded will have it's content overridden.
If you move the displayItemTemplate directive inside the other directive's template, the repeat will work. (you will need to remove ng(transclude and transclude: true
Fiddle
Additionally, if you use bindToController, don't put the attributes inside scope.
function compactSelect() {
return {
template : [
'<div class="compact-select-repeater-box" style="" >',
'<div ng-repeat="item in ctrl.csItems | filter:searchParam" class="compact-select-repeater" ng-class="ctrl.getHighlightedClass(item)" ng-click="ctrl.itemSelected(item)">',
'<display-item-template>',
'<span>{{item.name}}</span>',
'</display-item-template>',
'</div>',
'<div style="position:absolute;bottom:0">',
'+ Click here to add customer {{ctrl.message}}</div></div>',
].join(''),
bindToController: {
noItemSelectedText: '#',
noItemSelectedIcon: '#',
csItems: '=csItems'
},
scope: {},
controllerAs : 'ctrl',
controller : function($scope) {
}
}
}
function displayItemTemplate() {
return {
require: '^compactSelect',
restrict: 'E'
}
}
function SuperController() {
this.name = "a name";
this.contacts = [{name:"rob"}, {name:"jules"}, {name:"blair"}];
}
angular.module('myApp', []);
angular
.module('myApp')
.controller('SuperController', SuperController)
.directive('compactSelect', compactSelect)
.directive('displayItemTemplate', displayItemTemplate);
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.6/angular.min.js"></script>
<div ng-app="myApp">
<div ng-controller="SuperController as s">
<compact-select
no-item-selected-text="Add a Customer"
no-item-selected-icon="fa-user"
search-placeholder="Type a customer name"
cs-items="s.contacts">
</compact-select>
</div>
</div>

Angularjs - repeater appending elements in different parents in view

I know that if I use the directive ng-repeat, like below, I get every element inside and including the div to repeat on the DOM.
<div class="col col-3" ng-repeat="movie in popular" >
<figure>
<img ng-src="{{movie.backdropURL}}" alt="{{movie.code}}">
<div class="overlay"></div>
<figcaption>{{movie.code}}</figcaption>
<!-- <span class="extra-info">{{movie.extra}}</span> -->
<span class="price">{{movie.price}}</span>
</figure>
</div>
However now I want to have some parent elements that won't repeat but will use the same scope object for their children, that will then repeat.
So, I would like to do a repeater that would append the properties of the scope into their parent, something like this:
<ul class="parent1">
<li><img src={{myScope[0].imgUrl}}></li>
<li><img src={{myScope[1].imgUrl}}></li>
<li><img src={{myScope[2].imgUrl}}></li>
</ul>
<div class="parent2">
<span>{{myScope[0].description}}</span>
<span>{{myScope[1].description}}</span>
<span>{{myScope[2].description}}</span>
</div>
I would like to know if it is possible to reuse a native angular directive (I would prefer not to run the same repeater every time for every parent) where it could append the element to the parent. If not, do you have any suggestion for a solution. I've looked up some links for custom directives I haven't succeeded in applying them. So if you have a 'beginners' custom directive tutorial that could help me go on the right direction, it would be highly appreciated.
I don't know if I understand exactly what you are asking for.
Anyway, the ngRepeat directive is placed in the DOM under a particular parent, so you cannot run it once and append the leaves under different parents. The only way to do that is to create a custom directive that runs a loop internally and sets the leaf under the parent of your choice.
That is:
angular
.module('mymodule')
.directive('mydirective', mydirective);
function mydirective(){
var directive = {
restrict: 'A'
, link: link
}
return directive;
function link($scope, elem, attrs) {
for(var i=0;i<$scope.myScope.length;++i){
var el1 = angular.element('<li><img src='+$scope.myScope[i].imgUrl+'></li>'),
el2 = angular.element('<span>'+$scope.myScope[i].description+'</span>');
elem.find('.parent1').append(el1);
elem.find('.parent2').append(el2);
}
}
}
Please let me know if I misunderstood your goal.
Check this:
HTML:
<div ng-app="myApp" ng-controller="myCtrl">
<ul class="parent1">
<li ng-repeat="item in myScope">
<img ng-src={{item.imgUrl}}>
</li>
</ul>
<div class="parent2">
<p ng-repeat="item in myScope"><span>{{item.description}}</span></p>
</div>
</div>
Controller:
angular.module('myApp', [])
.controller('myCtrl', ['$scope', function($scope) {
$scope.myScope = [
{imgUrl:"someUrl1", description: "this is first url"},
{imgUrl:"someUrl2", description: "this is second url"}
]
}]);
Acceptable :) ?

How to pass parameters in nested controllers in Angular JS

I have a situation where I get some data from a REST API in a controller, I render that data using ng-repeat. Then in that loop, I need to run another controller, pass it data from earlier controller, do some operations on it and then again run an ng-repeat on it.
When I do it, "Inspect Element" shows value kept in parent controller's parameter. But the value which is being passed to the nested controller is actually the variable name.
Source code:
HTML:
<div class="checkbox" ng-repeat="bird in birds">
<table>
<tr>
<td>
<a ng-href="/birds/{{bird.Image}}" rel="shadowbox"><img ng-src="/birds/{{bird.Image}}" height="200" width="200"></img></a>
<div ng-controller="imageController" model="{{ bird.AdditionalImages }}">More Images: {{ imageString }}
<div ng-repeat="image in images">
<a ng-href="/birds/{{image}}" rel="shadowbox[{{ bird.Image }}]">a</a>
</div>
</div>
</td>
<td>
<table>
<tr>
<td>
<b>{{ bird.CommonName }}</b>
</td>
</tr>
<tr>
<td>
Spotted at: {{ bird.SpottedAt }}
</td>
</tr>
</table>
</td>
</tr>
</table>
</div>
JavaScript (for nested controller):
anekchidiya.controller('imageController', function($scope, $attrs) {
$scope.imageString = $attrs.model;
console.log("images: " + $scope.imageString);
});
You can perform it by passing your scope into a directive, and you will create an isolated scope.
For example :
Controller
(function(){
function Controller($scope) {
$scope.data = [{
name: 'john',
age: '26'
}, {
name: 'paul',
age: '24'
}, {
name: 'titi',
age: '32'
}];
}
angular
.module('app', [])
.controller('ctrl', Controller);
})();
Directive
(function(){
function customDirective() {
return{
restrict: 'AE',
template: '<h3>Age : {{age}}</h3>',
scope: {
age: '='
}
};
}
angular
.module('app')
.directive('customDirective', customDirective);
})();
And you can call your directive into the ngRepeat for example, by passing some data :
HTML
<body ng-app="app" ng-controller="ctrl">
<div ng-repeat="item in data">
<h2>Name : {{item.name}}</h2>
<custom-directive age="item.age"></custom-directive>
</div>
</body>
So, typical usage of an isolated scope , it is in a directive that creates a complete component, a widget, etc ...
So, you will be able to build some custom components, and to pass specific data.

Custom directive value with bindonce in Angularjs

I got a ng-repeat with thousands of item in it, so I decided to tryout bindonce to reduce the number of watches. But I couldn't figure out how to use it properly.
So now I got the following code:
<div ng-repeat="card in cards">
<div class="item-box" draggable="{{card.category}}" itemId="{{card._id}}">
<img ng-src="{{card.image}}" width="100%" height="100%">
</div>
</div>
As I read in the bindonce doc, I should add the directive and use the bo-* directives, so I fugured out this:
<div ng-repeat="card in cards" bindonce>
<div class="item-box" draggable="{{card.category}}" itemId="{{card._id}}">
<img bo-src="card.image" width="100%" height="100%">
</div>
</div>
So my question is how I can also use {{card.category}} and {{card._id}} using bind-once?
bo-attr bo-attr-draggable="card.category" bo-attr-itemId="card._id"
seems not to work, I'm not getting any errors, just nothing happens.
Result looks like
<div class="item-box ng-scope" bo-attr="" bo-attr-draggable="card.category" bo-attr-itemid="card._id" draggable="Pants" itemid="m--Pi">
bo-attr doesn't actually seem like what you want to be doing, you just want a directive to evaluate and bind data without creating any watches. I made a plnkr that I think is what you want: http://plnkr.co/edit/sFPAjlRCkDuXU5UiM1U1?p=preview
app.controller('MainCtrl', function($scope) {
$scope.name = 'World';
});
// html
<div directive="name"></div>
// Dummy directive
app.directive('directive', function() {
return {
template: '<div bindonce bo-text="val"></div>',
compile: function() {
return {
pre: function(scope, elt, attrs) {
scope.val = scope.$eval(attrs.directive);
}
};
}
}
})
Woo no watches!
Let me know if I misunderstood something.

Categories

Resources