My main view makes use of a directive called fileUploader. The fileUploader directive uses an ng-repeat to represent a list of objects (file details). I want this file-uploader directive to be reusable, so I wish to place use-specific UI in the view that is using it.
Here is my main view. In this case I want to see the standard file-uploader's template for each item plus two additional pieces of meta data I will find on the object (reference / destinationFolder).
<file-uploader class="col-md-10 file-uploader" file-uploader-upload-url="/x/upload">
<dl>
<dt>Reference</dt>
<!-- Note the binding on the next line, how do I evaluate it to the "item" in the directive's ng-repeat? -->
<dd>{{ item.reference }}</dd>
<dt>Folder</dt>
<dd>{{ item.destinationFolder }}</dd>
</dl>
</file-uploader>
My directive uses ng-transclude to include the <dl> contents above. It seems the {{ item.reference }} is evaluated in the main view and then inserted many times, what I want is for it to transclude it as-is and then evaluate the expression within the context of the directive's ng-repeat. The transclude etc is working correctly, but the binding is not working as I wish.
<ul class="file-upload-list">
<li ng-repeat="item in controller.fileUploader.queue">
<div class="file-upload-item" drag-container drag-data="controller.fileUploader.queue[$index]">
<div class="file-upload-icon">
<img src="/Icons/FileExtension/{{item.file.name | fileExtension}}" alt="Icon" class="file-upload-icon" />
</div>
<div class="file-upload-filename">
<a href="" title="{{ item.file.name }}">
{{ item.file.name | limitTo : -28 }}
</a>
</div>
<!-- Here is where I want the main view's template repeated and data-bound -->
<ng-transclude class="file-upload-meta-data"/>
<div class="progress">
<div class="progress-bar" role="progressbar" ng-style="{ 'width': item.progress + '%' }"></div>
</div>
</div>
</li>
</ul>
Sadly, this is not how ng-transclude transclusion works. What you are trying to do is create a container directive that makes use of its children as a "template" of what to stamp out inside your own directive.
The content that is transcluded is by definition bound to the scope of the place where the directive is instantiated; not to the scope of the directive's template.
So actually you don't really need to use transclusion here as what you really trying to do is simply inject the inner HTML into your own template. You can do this in the compile function like this:
app.directive('test', function(){
return {
restrict: 'E',
compile: function compile(tElement, tAttrs, tTransclude) {
// Extract the children from this instance of the directive
var children = tElement.children();
// Wrap the chidren in our template
var template = angular.element('<div ng-repeat="item in collection"></div>');
template.append(children);
// Append this new template to our compile element
tElement.html('');
tElement.append(template);
return {
pre: function preLink(scope, iElement, iAttrs, crtl, transclude) {
},
post: function postLink(scope, iElement, iAttrs, controller) {
}
};
}
};
});
-- Angular Issue #7874: ng-repeat problem with transclude
The solution was to use an ng-include inside the directive.
First I added an additional attribute on my directive in which the developer can specify the ID of the template, like so:
<file-uploader file-uploader-metadata-template-id="someID" class="col-md-10 file-uploader" file-uploader-upload-url="/x/upload">
Then inside the directive I just needed to use ng-include
<div class="file-upload-meta-data" ng-include="controller.fileUploaderMetadataTemplateId"/>
I can then use the directive like so
<file-uploader class="col-md-10 file-uploader" file-uploader-upload-url="/x/upload" file-uploader-metadata-template-id="file-meta-template"/>
<script type="text/ng-template" id="file-meta-template">
<dl>
<dt>Application</dt>
<dd>{{ item.meta.applicationName }} </dd>
<dt>Reference</dt>
<dd>{{ item.meta.referenceName }} </dd>
<dt>Folder</dt>
<dd>{{ item.meta.targetFolderName }} </dd>
</dl>
</script>
Note that I am binding to a controller, with controllerAs set to "controller" - here is the typescript for those interested....
module MyApp.Directives.FileUploaderDirective {
export class FileUploaderDirectiveController {
public uploadUrl: string;
public metaTemplateId: string;
}
class FileUploaderDirective implements angular.IDirective {
public restrict: string = "E";
public templateUrl: string = "/app/Directives/FileUploaderDirective/FileUploaderDirective.html";
public scope: any = {
uploadUrl: "#fileUploaderUploadUrl",
metaTemplateId : "#fileUploaderMetadataTemplateId"
};
public controller: string = "MyApp.Directives.FileUploaderDirective.FileUploaderDirectiveController";
public controllerAs: string = "controller";
public bindToController: boolean = true;
public transclude: boolean = true;
public replace: boolean = true;
}
export function register(app: angular.IModule) {
app.controller("MyApp.Directives.FileUploaderDirective.FileUploaderDirectiveController", FileUploaderDirectiveController);
app.directive("fileUploader", () => new FileUploaderDirective());
}
}
Related
I have created a ng-transclude sample which is not working when I use ng-include inside directive template property.Below are the code I have tried
angular.module("app",[]);
angular.module("app").controller('appController', function ($scope) {
$scope.message="Message from controller";
});
angular.module("app").directive('myFrame', function () {
return {
restrict: 'E',
template : '<div ng-include=\"getTemplateUrl()\"></div>',
controller:function($scope){
$scope.hidden=false;
$scope.close=function(){
$scope.hidden=true;
}
$scope.getTemplateUrl=function(){
//dynamic url
return "frame.html";
}
$scope.message="message from directive";
},
transclude:true,
}
});
angular.module("app").directive('mychildFrame', function () {
return {
restrict: 'E',
templateUrl:"childframe.html",
controller:function($scope){
},
}
});
childFrame.html
<h2>I am from Child</h2>
<div></div>
frame.html
<div class="well" style="width:350px;" ng-hide="hidden">
<div style="float:right;margin-top:-15px">
<i class="glyphicon glyphicon-remove" ng-click="close()" style="cursor:pointer"></i>
</div>
<div ng-transclude>
/*frame content goes here*/
</div>
</div>
index.html
<my-frame>
<mychild-frame></mychild-frame>
</my-frame>
https://plnkr.co/edit/O58voI?p=preview
When I change to the property template to templateUrl="frame.html" it's working fine. But the problem is, this HTML page inside the template is dynamic. So I end up with ng-include.
Could you please provide any possible workaround?
When using the templateUrl property you can also pass it a function to dynamically load a template.
There's no need to put the function in a controller, especially since it doesn't really belong there anyway: the controller is supposed to contain view logic, not a function to load the view itself.
I added the function in the templateUrl instead in your plunkr:
templateUrl : function() {
return 'frame.html';
}
https://plnkr.co/edit/HQHI9hkTojkZFK2Gjxfw?p=preview
As you can see this gives you the desired behavior
Directive template (items.html)
<li ng-repeat="item in itemCart">
{{item.title}} <br>
{{item.category}}  
{{ formatCurrencyFunction({cost: item.price}) }}
</li>
This custom directive is used in Second.html
<h1>
This is Second.
{{header}}
</h1>
<hr>
<ul>
<items-list item-cart="items" format-currency-function="formatPrice(cost)"></items-list>
</ul>
The code for Controller is:
myApp.directive("itemsList", function(){
return{
templateUrl:"contents/Views/Directives/Items.html",
replace: true,
scope:{
itemCart: "=",
formatCurrencyFunction: "&"
},
restrict:"EACM" // E-Element A-Attribute C-Class M-Comments
}
})
myApp.controller('secondController', ['$scope', '$log', '$http','$routeParams', function($scope, $log, $http, $routeParams) {
$scope.formatPrice = function(price){
return "₹ "+parseFloat(price).toFixed(2);
};
$scope.header = 'Second ' + ($routeParams.num || "");
$scope.testSecond = "Second";
$http.get("/items")
.success(function(data){
$scope.items = data;
});
}]);
The function formatPrice is not called from the directive Items.html
What has to be corrected in my code to get it working?
Inside the directive's link function, you'll want to call the method like so:
scope.someFn({arg: someValue});
myApp.directive("itemsList", function(){
return{
templateUrl:"contents/Views/Directives/Items.html",
replace: true,
scope:{
itemCart: "=",
formatCurrencyFunction: "&formatCurrencyFunction"
},
link:function(scope, element, attrs){
scope.formatPrice({arg: 35}); //scope.someFn({arg: 35});
},
restrict:"EACM" // E-Element A-Attribute C-Class M-Comments
}
})
Got this working by using ng-repeat in the Second.html page instead of the directive.
Directive template (items.html)
<li>
{{item.title}} <br>
{{item.category}}  
{{ formatCurrencyFunction({cost: item.price}) }}
</li>
This custom directive is used in Second.html
<h1>
This is Second.
{{header}}
</h1>
<hr>
<ul>
<items-list item="item" format-currency-function="formatPrice(cost)" ng-repeat="item in items"></items-list>
</ul>
Code in JS file
myApp.directive("itemsList", function(){
return{
templateUrl:"contents/Views/Directives/Items.html",
replace: true,
scope:{
item: "=",
formatCurrencyFunction: "&"
},
restrict:"EACM" // E-Element A-Attribute C-Class M-Comments
}
})
Inside the directive's link function, you need to call the method by applying some event to that attribute
Controller function 'formatCurrencyFunction()' can't be called by just placing it in an expression.
To use the function you need to call it based on an even, in your case ng-init will work.
<li ng-repeat="item in itemCart" ng-init="formatCurrencyFunction({cost: item.price})">
{{item.title}} <br>
{{item.category}}
</li>
I am trying to change html template after link is clicked. Value is boolean, initial value is true and appropriate template is loaded, but when value changed to false new template is not loaded, I don't know the reason. When initial value of boolean is true other template is loaded successfully, but on method called not. Please, help.
Here is my code:
TaskCtrl
app.controller('TasksCtrl', ['$scope', 'TaskService', function ($scope, TaskService) {
// initialize function
var that = this;
that.newTask = true;
that.name = "My name is Nedim";
that.templates = {
new: "views/task/addTask.html",
view: "views/task/viewTask.html"
};
// load all available tasks
TaskService.loadAllTasks().then(function (data) {
that.items = data.tasks;
});
$scope.$on('newTaskAdded', function(event, data){
that.items.concat(data.data);
});
that.changeTaskView = function(){
that.newTask = false;
console.log("New task value: " + that.newTask);
};
return $scope.TasksCtrl = this;
}]);
task.html
<!-- Directive showing list of available tasks -->
<div class="container-fluid">
<div class="row">
<div class="col-sm-6">
<entity-task-list items="taskCtrl.items" openItem="taskCtrl.changeTaskView()"></entity-task-list>
</div>
<div class="col-sm-6" ng-controller="TaskDetailCtrl as taskDetailCtrl">
<!-- form for adding new task -->
<div ng-if="taskCtrl.newTask" ng-include="taskCtrl.templates.new"></div>
<!-- container for displaying existing tasks -->
<div ng-if="!taskCtrl.newTask" ng-include="taskCtrl.templates.view"></div>
</div>
</div>
entityList directive
app.directive('entityTaskList', function () {
return {
restrict: 'E',
templateUrl: 'views/task/taskList.html',
scope: {
items: '='
},
bindToController: true,
controller: 'TasksCtrl as taskCtrl',
link: function (scope, element, attrs) {
}
};
});
directive template
<ul class="list-group">
<li ng-repeat="item in taskCtrl.items" class="list-group-item">
<a ng-click="taskCtrl.changeTaskView()">
<span class="glyphicon glyphicon-list-alt" aria-hidden="true"> </span>
<span>{{item.name}}</span>
<span class="task-description">{{item.description}}</span>
</a>
</li>
{{taskCtrl.newTask}}
Without any plunker or JSFiddle I can't tell for sure, but it might be issue with ng-if. I'm thinking of two workarounds.
First that I think is better. Use only 1 ng-include and only change the template.
HTML:
<entity-task-list items="taskCtrl.items" openItem="taskCtrl.changeTaskView('view')"></entity-task-list>
...
<div ng-include="taskCtrl.currentTemplate"></div>
JS:
that.currentTemplate = that.templates.new;
...
that.changeTaskView = function(template) {
that.currentTemplate = that.templates[template];
};
Or if you don't like this solution, try with ng-show instead of ng-if. With ng-show the elements will be rendered with display: none; property when the page loads, while with ng-if they will be rendered when the passed value is true.
Hope this helps you.
I have a partial view which is included on multiple pages. That Partial view has it's on NgController.
Is there a way I can access a scope variable of that specific nested controller when I'm inside of another?
For example:
<div ng-controller="fooController">
<a ng-click="changeScopeVariableOfBarController()">Click!</a>
<div ng-controller="barController">
{{ thisHasToChangePlease }}
</div>
</div>
Is that possible? I couldn't figure out how. I include the partial view with php's include, so I don't work with templates.
If you have a reusable "component" (template + functionality) you could refactor that into a directive instead of template + controller. Then, using an isolated scope with parameters you could have the outside world change the inside of the directive that way.
http://plnkr.co/edit/6JYVucuizmPkyt2CLQtr?p=preview
index.html
<div ng-controller="fooController">
<div>variable: {{ variable }}</div>
<button ng-click="changeScopeVariableOfBarController()">Click to increment variable on both scopes!</button>
<bar this-has-to-change-please="variable"></bar>
</div>
bar.html
<div>
thisHasToChangePlease: {{ thisHasToChangePlease }}
</div>
app.js
app.controller('fooController', function($scope) {
$scope.variable = 1;
$scope.changeScopeVariableOfBarController = function () {
$scope.variable++;
};
});
app.controller('barController', function ($scope) {
});
app.directive('bar', function () {
return {
restrict: 'E',
scope: {
thisHasToChangePlease: '='
},
controller: 'barController',
templateUrl: 'bar.html'
}
});
I am using Bootstrap UI's accordion directive. This uses transclusion under the hood. I have some logic that needs repeated, so I am trying to create a directive that wraps the accordian, which also uses transclusion.
<div>
<div accordion>
<div accordion-group is-open="isOpen">
<div accordion-heading>
<span class="glyphicon" ng-class="{'glyphicon-minus-sign': isOpen, 'glyphicon-plus-sign': !isOpen}"></span>
<strong>{{headerTitle}}</strong>
</div>
<div ng-transclude></div>
</div>
</div>
</div>
Here is the JavaScript:
application.directive('collapsePanel', ['baseUrl', function (baseUrl) {
return {
restrict: 'A',
templateUrl: baseUrl + 'content/templates/collapse_panel.html',
replace: true,
transclude: true,
scope: {
headerTitle: '#'
},
controller: ['$scope', function ($scope) {
$scope.isOpen = false;
}]
};
}]);
It should be as simple to use as:
<div collapse-panel header-title="Title">
{{scopeVariable}}
</div>
Assuming scopeVariable is in my controller, I would think its value would appear. From what I can tell, the scope belongs to the collapse-panel rather than the outer scope. It is almost like having nested transclusion directives is causing my problem.
Is there a trick to nesting transclusions like this?