I made a custom directive ("wizard") based on the JQuery Steps plugin. But im facing problems when using ngRepeat inside my directive. The diretive uses ngTransclude so i can provide the steps content inside the page markup.
Im nowhere near an Angular expert, but what i can find is that it's a bug/freakaccident in Angular where an ngRepeat is used inside a directive with isolated scope and transclusion. Like mentioned here
https://github.com/angular/angular.js/issues/7874 but i can't seem to get any of the suggestions working :\
The directive looks like this:
Directives.wizard = ['$compile', function ($compile) {
return {
restrict: 'E',
scope: {
cancel: '&',
finish: '&',
stepChanged: '&'
},
template: '<div class="wizard" data-ng-transclude></div>',
replace: true,
transclude: true,
link: function ($scope, element, attrs, ctrl) {
// directive has to be wrapped in order to ngModel on sections to work with transclude: true
element.wrapInner('<div class="steps-wrapper">');
var opts = {
headerTag: "h3",
bodyTag: "section",
transitionEffect: "slideLeft",
onCanceled: function (event, currentIndex) {
$scope.cancel();
},
onFinished: function (event, currentIndex) {
$scope.finish();
},
onStepChanged: function (event, currentIndex) {
$scope.stepChanged();
}
};
var stepsEl = element.children('.steps-wrapper').steps(opts);
// wrap 'n compile..
$compile(stepsEl)($scope);
}
};}];
markup
<wizard>
<h3>Title</h3>
<section>
<p><DG:TextControl runat="server" Key="MineSuccesHistorier.Step1"/></p>
<input data-ng-model="$parent.newData.Title"></input> <!-- outputs array just fine -->
<!-- test stuff here -->
<!-- 1. regular ng-repeat - doesnt work (also tried with $parent.$parent which Batarang says is the correct path -->
<ul>
<li data-ng-repeat="item in $parent.newData.Competences">{{item.Title}}</li>
</ul>
<!-- 2. try with ng-init - doesnt work -->
<ul data-ng-init="comp = $parent.newData.Competences">
<li data-ng-repeat="item in comp">{{item.Title}}</li>
</ul>
</section>
<h3>Tab</h3>
<section>
<!-- next wizard tab here -->
</section>
How data is set in controller
$scope.newData = {
"Competences": [
{
"IsSelected": true,
"Title": "Hest"
},
{
"IsSelected": false,
"Title": "Kat"
},
{
"IsSelected": true,
"Title": "Ko"
},
{
"IsSelected": false,
"Title": "Ged"
}
],
"Id": "905c1285-d58b-4f65-8df5-52986c70a820",
"Situation": null,
"Title": null}
any ideas are much appreciated, thanks in advance!
UPDATE 25/3:
Added plnkr here http://plnkr.co/edit/kNl4UEoUa7zU4CgWaGSa?p=preview
When i add isolated scope in the directive, the repeaters stop working. If i leave out the isolated scope, it seems as if the ngRepeats are compiled multiple times.
UPDATE 2 25/3:
Added new plunkr with Vinays compiling suggestion - ng-repeat is now compiled only once. But two-way binding with ngModel in controller scope isn't working http://plnkr.co/edit/TR3XxvV4IYI66h4pY5hx?p=preview
Content is getting repeated because, ng-repeat will compile the contents once and $compile will compile it once again.
Directive
app.directive('wizard', function($timeout, $compile) {
return {
restrict: 'E',
replace: true,
template: '<div class="mywizard"></div>',
compile: function (tEl) {
return function(scope, element, attrs) {
var stepsEl;
element.wrapInner('<div class="steps-wrapper">');
element.children('.steps-wrapper').append(tEl.context.innerHTML);
var content = $compile(element.contents())(scope);
$timeout(function () {
element.children('.steps-wrapper').steps(opts);
})
}
}
};
});
Working Plnkr
Related
It seems like I get confused by isolated scopes in directives and hope you can help me out.
I tried to wrap a piece of code (which contains some custom directives) into a new directive to reduce code duplication. Obviously I needed to give some attributes like ng-model into my new directive as a parameter to make the directive reusable. ng-model does not like expressions though (I tried ng-model="{{myVariableWhichContainsDesiredNgModelString}}" at first) and thus I ended up at this article: AngularJS - Create a directive that uses ng-model.
While the accepted answer seems to work for a simple setup, I edited the plunker from the accepted answer to test out if it would work with nested directives as well: (in my app I need to wrap directives from a third party-library which I can not edit) Plunker. In my code each directive seems to generate its own scope and two-way-databinding by using = in the scope definition does not seem to work out as desired.
EDIT: Since it was not clear what i am asking I edited the Plunker above and will rephrase the question: In the Plunker I have three input-fields which are supposed to bind to the same model-value. This works initially, but as soon as I edit the third input-field it generates its own variable in its isolated scope instead of updating the initial value. Obviously the third input field refers to the new variable from that point on. How can I avoid that behaviour and keep the input linked to $scope.model.name?
Observation: removing the isolated-scope-directive from the template makes everything work as expected...
template: '<div><my-input ng-model="myDirectiveVar"></my-input></div>',
instead of
template: '<div><my-isolated-scope-directive><my-input ng-model="myDirectiveVar"></my-input></my-isolated-scope-directive></div>',
Plunker
HTML:
<!-- this binds to the model which i would like all my inputs to bind to.-->
<input ng-model="name">
<!-- Example 1: This seems to generate a new modelvalue in the isolated-scope directive. Can I avoid this without modifying that directive?-->
<my-isolated-scope-directive><my-input ng-model="name"></my-input></my-isolated-scope-directive>
<!-- Example 2: This is what i would like my code to look like in the end: One directive which uses the code-snippet of Example 1 as template and passes some parameters into that template.-->
<my-wrapper-directive my-directive-var="name"></my-wrapper-directive>
Directives:
my-input contains a modified input-field:
app.directive('myInput', function() {
return {
restrict: 'E',
replace: true,
require: 'ngModel',
template: '<input class="some">',
link: function($scope, elem, attr, ctrl) {
console.debug($scope);
}
};
})
my-isolated-scope-directive is a placeholder-directive with its own isolated scope to simulate the behaviour for nested directives:
.directive('myIsolatedScopeDirective', function() {
return {
restrict: 'E',
transclude: true,
replace: true,
scope: {
something: '='
},
template: '<div ng-transclude></div>',
link: function($scope, elem, attr, ctrl) {
console.debug($scope);
}
};
})
my-wrapper-directive encapsulates both previous directives and accepts a parameter which should be used as ng-model value of the input field:
.directive('myWrapperDirective', function() {
return {
restrict: 'E',
transclude: false,
replace: true,
scope: {
myDirectiveVar: '='
},
template: '<div><my-isolated-scope-directive><my-input ng-model="myDirectiveVar"></my-input></my-isolated-scope-directive></div>',
link: function($scope, elem, attr, ctrl) {
console.debug($scope);
}
};
});
Any suggestions and hints on what I am missing are appreciated. Can I maybe somehow link ng-model to a service-instance without making my directive dependant on that service?
I wouldn't do it like this as it is old notation using scopes... I would use controllerAs and bindToController
script:
var app = angular.module('plunker', []);
app.controller('MainCtrl', function() {
this.model = { name: 'World' };
this.name = "Felipe";
});
app.directive('myInput', function() {
return {
restrict: 'E',
replace: true,
// controllerAs: 'app',
require: 'ngModel',
template: '<input class="some">',
controller: function(){
}
};
})
.directive('myIsolatedScopeDirective', function() {
return {
restrict: 'E',
transclude: true,
controllerAs: 'app1',
bindToController: {
something: '='
},
template: '<div ng-transclude></div>',
controller: function(){
}
};
})
.directive('myWrapperDirective', function() {
return {
restrict: 'E',
transclude: false,
controllerAs: 'app2',
bindToController: {
myDirectiveVar: '='
},
template: '<div><my-isolated-scope-directive>'+
'<my-input ng-model="app2.myDirectiveVar"></my-input>'+
'</my-isolated-scope-directive></div>',
controller: function(){
}
};
});
index:
<!doctype html>
<html ng-app="plunker" >
<head>
<meta charset="utf-8">
<title>AngularJS Plunker</title>
<link rel="stylesheet" href="style.css">
<script>document.write("<base href=\"" + document.location + "\" />");</script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.js"></script>
<script src="app.js"></script>
</head>
<body ng-controller="MainCtrl as main">
This scope value
<input ng-model="main.model.name">
<my-isolated-scope-directive>
<my-input ng-model="main.model.name"></my-input>
</my-isolated-scope-directive>
<my-wrapper-directive my-directive-var="main.model.name">
</my-wrapper-directive>
</body>
</html>
See the plunker:
http://plnkr.co/edit/VD0wXO1jivQc3JvfQFTh?p=preview
UPDATE
yes, good point, so if you want to use controllerAs, you need angular 1.2 as minimum, for bindToController you need angular 1.3
I am wrapping the HTML "select" element with my own directive. This directive should create a Dropdown menu representing it's options.
I tag the select element with my custom "arBootstrapSelect" directive/attribute.
This directive appends a Dropdown menu with the options inside it repeated by ng-repeat.
The "option" elements in the "select" element are tagged with "arBootstrapSelectOption". They should have a "content" attribute, representing a dynamic directive. This dynamic directive should be compiled and shown in the Dropdown menu.
Basiclly, each option(tagged by "arBootstrapSelectOption") compiles it's "content" attribute using the $compile service and injects it into a list living in the arBootstrapSelect directive. After that arBootstrapSelect should show the compiled options using ng-repeat. Hope it's not too complicated.
I am getting Error Link
HTML:
<select ar-bootstrap-select class="form-control">
<option ar-bootstrap-select-option value={{$index}} ng-repeat="c in countries" content="<ar-country country-id='{{$index}}'></ar-country>">
</option>
</select>
<div class="dropdown">
<button class="btn btn-default dropdown-toggle" type="button" id="dropdownMenu1" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
Dropdown
<span class="caret"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenu1">
<li ng-repeat="option in arBootstrapSelectOptions">
{{option[1]}}
</li>
</ul>
</div>
My directives:
(function () {
'use strict';
var app = angular.module('mainApp');
app.directive('arBootstrapSelect', function ($interval, $compile) {
$scope.arBootstrapSelectOptions = [];
function link(scope, element, attrs) {
//element.hide();
}
return {
restrict: 'A',
link: link,
scope: true
}
});
app.directive('arBootstrapSelectOption', function ($compile) {
function link(scope, element, attrs) {
scope.arBootstrapSelectOptions.push([attrs.value, $compile(attrs.content)(scope)]);
}
return {
scope: true,
restrict: 'A',
link: link,
}
});
})();
This works, but it's ugly and slow:
(function () {
'use strict';
var app = angular.module('mainApp');
app.directive('arBootstrapSelect', function ($interval, $compile) {
//$scope.arBootstrapSelectOptions = [];
function link(scope, element, attrs) {
//element.hide();
scope.$on('arBootstrapSelectNewItem', function (event, data) {
var test = $('<li></li>').append(data);
element.parent().find('.dropdown-menu').append(test);
});
}
return {
restrict: 'A',
link: link,
scope: true,
transclude: true
}
});
app.directive('arBootstrapSelectOption', function ($compile) {
function link(scope, element, attrs) {
//scope.arBootstrapSelectOptions.push([attrs.value, $compile(attrs.content)(scope)]);
scope.$emit('arBootstrapSelectNewItem', $compile(attrs.content)(scope));
}
return {
scope: true,
restrict: 'A',
link: link
}
});
})();
I don't fully understand your specific example, so I'll answer at a conceptual level.
It seems that at a high level all you want to is to specify a user-provided template for ng-repeat (e.g. <ar-country country-id='{{$index}}'>) and place it within a directive-provided template (e.g. within <li>).
It would be easier and more user-friendly to provide the template as content, rather than as an attribute. Then, all you'd need is to transclude the content. (I'm avoiding here the use of <option> since it is a directive on its own and I don't understand exactly what you are trying to do - I'll use <my-option> instead):
<my-option ng-repeat="c in countries">
<ar-country country-id='{{$index}}'></ar-country>
<my-option>
The directive would look like so:
.directive("myOption", function(){
return {
scope: true,
transclude: true,
template: '<li ng-transclude></li>'
}
});
This will create conceptually the following HTML:
<my-option>
<li><ar-country country-id="0"></ar-country></li>
</my-option>
<my-option>
<li><ar-country country-id="1"></ar-country></li>
</my-option>
..
(The directive arCountry will also be compiled and linked, and may generate its own content)
Additionally, if the parent needs to register each child, it's better to use require to communicate rather than $emit/$on. So, the parent needs to define a controller with a function to register children. And, again, if you need to supply an additional template, then transclude the content:
<my-select>
<my-option ng-repeat="c in countries>
<ar-country country-id='{{$index}}'></ar-country>
</my-option>
</my-select>
.directive("mySelect", function(){
return {
scope: true,
transclude: true,
template: '<h1>heading</h1><div ng-transclude></div>',
controller: function(){
// this can be called by the child
this.registerChild = function(childElement, childController){
// store the children, if needed
}
}
}
});
To make use of this.registerChild we'd need to make myOption use require:
.directive("myOption", function(){
return {
scope: true,
transclude: true,
template: '<li ng-transclude></li>'
require: ['myOption', '^mySelect'],
controller: function(){
// you can define some functions here
},
link: function(scope, element, attrs, ctrls){
var me = ctrls[0],
selectCtrl = ctrls[1];
selectCtrl.registerChild(element, me);
}
});
I am trying to use a Bootstrap Popover in an AngularJS ngRepeat directive. The problem is the title attribute isn't being interpolated. How can I resolve this issue?
View
<div ng-app="app" id="ng-app">
<ul ng-controller="Main">
<li ng-repeat="i in items" data-toggle="popover" title="{{ i.title }}" <!-- Title isn't interpolated so Popover doesn't render a title -->
data-content="Events: {{ day.events.length }}"
init-popover>{{ i.data }}</li>
</ul>
Code
var app = angular.module('app', []);
app.controller('Main', ['$scope', function($scope){
$scope.items = [
{ title: 'Title 1', data: 'lorem' },
{ title: 'Title 2', data: 'ipsum' },
];
}]);
app.directive('initPopover', function() {
return function(scope, element, attrs) {
if (scope.$last){
$('[data-toggle="popover"]').popover({container: 'ul'});
}
};
});
FIDDLE
You can make it work but it's pretty ugly, here is my solution
var app = angular.module('app', []);
app.controller('Main', ['$scope', function($scope){
$scope.items = [
{ title: 'Title 1', data: 'Test in ngRepeat' },
{ title: 'Title 2', data: 'Test in ngRepeat' },
];
}]);
app.directive('initPopover', function() {
return function(scope, element, attrs) {
if (scope.$last) {
attrs.$observe('title', function () {
$('[data-toggle="popover"]').popover({container: 'ul'});
});
}
};
});
You shouldn't use title="{{i.title}}", but ng-attr-title="{{i.title}}"...
I had the same problem. I'm using bootstrap v3.3.6 and Angular v1.5.0
Adding attribute data-original-title="{{someTitle}}" fixed the problem.
<button type="button" class="btn btn-lg btn-danger" data-toggle="popover" title="{{someTitle}}" data-original-title="{{someTitle}}" data-content="Some Content">Click to toggle popover</button>
Also, as bootstrap requires, call .popover() in directive
module.directive('myDir', ['jQuery', function (jQuery) {
return {
link: function () {
jQuery('[data-toggle="popover"]').popover();
}
};
}]);
Add $timeout to directive will solve your problem.
$timeout will gets call when last element of ng-repeat rendered on UI & run $digest cycle.
Directive
app.directive('initPopover', function($timeout) {
return function(scope, element, attrs) {
if (scope.$last) {
$timeout(function() {
angular.element('[data-toggle="popover"]').popover({
container: 'ul'
});
});
}
};
});
Working Fiddle
Hope this could help you. Thanks.
Consider using ui-bootstrap for native Angular directives instead of Bootstrap's jQuery version. They're a lot easier to work with in an Angular project.
Edit: If you can't do that, here's a solution:
<li ng-repeat="i in items" data-toggle="popover" data-ng-attr-popover_title="{{ i.title }}"
data-content="Popover Content"
init-popover>{{ i.data }}</li>
With a small change to the directive:
app.directive('initPopover', function() {
return function(scope, element, attrs) {
if (scope.$last){
$('[data-toggle="popover"]').popover({ container: 'ul', title: attrs.popoverTitle });
}
};
ng-attr-popup_title will create an HTML attribute called popupTitle on the element with the Angular syntax inside {{}} parsed correctly. Then, in the directive, we can just examine that attribute to set the popover title.
Fiddle: here
Just to add to the knowledge base here. Here's the angular way, in case anyone needs. I based my solution on Mark Coleman's PopOver tutorial, and of course what has been contributed by Nate Barbettini.
My situation was I have a nested ng-repeat over items in items, so I need to have a title for my popup that is based on an array stored in a scope, while the content of the popup is from another array stored in the array of the scope.
I added ng-attr-title to my HTML element, like so:
<div pop-over items='week.items' ng-attr-title="{{'Week number:'+ week.week}}"> Total sales this week : {{week.sales}}</div>
Then, in my directive, I followed mostly what Mark Coleman did.
angular.module('vegadirectives', [])
.directive('popOver', function ($compile, $templateCache) {
var itemsTemplate = "<ul class='popper'><li ng-repeat='thing in items track by $index'>";
itemsTemplate+=" <i class='fa fa-square' ng-class=";
itemsTemplate+="{purple:thing.color==='purple','orange':thing.color==='orange','white':thing.color==='white','pink':thing.color==='pink','red':thing.color==='red','green':thing.color==='green','yellow':thing.color==='yellow'}";
itemsTemplate+="></i> {{thing.model}} - {{thing.qty}}</li></ul>";
var getTemplate = function () {
var template=itemsTemplate;
return template;
}
return {
restrict: "A",
transclude: true,
template: "<span ng-transclude></span>",
link: function (scope, element, attrs) {
var popOverContent;
if (scope.items) {
var html = getTemplate();
popOverContent = $compile(html)(scope);
var options = {
trigger:'click',
content: popOverContent,
placement: "right",
html: true,
title:scope.title
};
if (scope.$last) {
}
$(element).popover(options);
}
},
scope: {
items: '=',
title:'#'
}
};
});
Explanation:
To fill up my popover with a list of items, with some fancy color square to indicate different types of items, I created a chunk of HTML code. This chunk named itemsTemplate will be used by $compile to create the popover content with scope. I think the usage is $compile(htmlstring)(scope).
Next comes the standard stuff for options settings for the popover.
For the interpolation of scope with title, I specified the option title: with scope.title. Then, in the scope: option way down at the end, the two scope elements are specified with '=' and '#'.
So, the title:scope.title refers or binds to scope: {items:'=', title:'#' }, and the title:'#' is the attributes of the title in the HTML, which is ng-attrs-title={{week.week}}, where week is from the outer ng-repeat loop.
Hope it helps.
After finding this answer at https://stackoverflow.com/a/17864868/2379762, I tried data-original-title={{i.title}}, and it works.
http://jsfiddle.net/f1tjj8x5/
<li ng-repeat="i in items" data-toggle="popover" data-original-title="{{ i.title }}"
data-content="Popover Content"
init-popover>{{ i.data }}</li>
Inside angularJS directive I'm trying to iterate over array and based on values I would like to create nested list of directives.
Current version of directive
Directive type
.directive("type", function($compile, $log){
return{
restrict: "E",
replace: true,
transclude: true,
scope: {
type: '='
},
template: "<div></div>",
link: function(scope, element, attrs){
if (angular.isArray(scope.type)){
angular.forEach(scope.type, function(value, index){
$log.error(value);
element.append("<type type='scope.type['"+index+"]'></type>");
});
} else if (angular.isObject(scope.type)){
element.append("OBJECT")
} else {
element.append("<div>{{scope.type}}</div>")
}
$compile(element.contents())(scope)
}
};
})
I also tried to use above directive with next version of link function:
if (angular.isArray(scope.type)) {
element.append("<div ng-repeat='element in scope.type'><type type='element'></type></div>");
} else if (angular.isObject(scope.type)) {
element.append("OBJECT")
} else {
element.append("<div>{{scope.type}}</div>")
}
$compile(element.contents())(scope)
}
None of provided codes solve my issue.
Below you will find example explaining on specific case:
Let's say that I had next object in the scope.type = [null,"int"]. Now I would like to use <type type='type'><type> and as a result of first evaluation I want to have sth like:
<type type='type[0]'></type><type type='type[1]'></type>
Further evaluation of those values should lead to some simpler form but right now it is not important.
How I can achieve sth like this?
Edit
I tried even to exctract part of the code responsible for iteration to the seperate directive but it still does not work. Code:
Update link function in type directive:
link: function(scope, element, attrs) {
if (angular.isArray(scope.type)) {
element.append("<typeb type='scope.type'></typeb>")
} else if (angular.isObject(scope.type)) {
element.append("OBJECT")
} else {
element.append("<div>{{scope.type}}</div>")
}
$compile(element.contents())(scope)
}
New directive:
.directive("typeb", function($compile, $log){
return{
restrict: "E",
replace: true,
transclude: true,
scope: {
type: '='
},
template: "<div ng-repeat='t in type'>{{t}}</div>",
};
})
Problem still occurs but generated html contains only next pieces as a result of typeb directive:
<!-- ngRepeat: t in type -->
The problem you are getting is <!-- ngRepeat: t in type --> this is because your type didn't contains any value, when it is inside typeb directive
Your directive shouldn't be use scope.variable on view.
Scope variable will be directly accessible by their name like
{{type}} or <typeb type='type'></typeb>
Change your link code to below.
Directive link
link: function(scope, element, attrs) {
if (angular.isArray(scope.type)) {
element.append("<typeb type='type'></typeb>")
} else if (angular.isObject(scope.type)) {
element.append("OBJECT")
} else {
element.append("<div>{{type}}</div>")
}
$compile(element.contents())(scope)
}
Thanks.
Use an ng-repeat in the template
<your-directive attr="item.someattr" ng-repeat="item in items"></your-directive>
.directive("type", function($compile, $log){
return{
restrict: "E",
replace: true,
transclude: true,
scope: {
type: '='
},
template: "NG REPEAT HERE",
...
})
New to Angular, just trying to get some harmony with Zurb Foundation 4. A case in point; I am trying to make use of the http://foundation.zurb.com/docs/components/reveal.html component.
Straight-forward approach seemed to be to wrap as directives:
directive('modal', function() {
return {
template: '<div ng-transclude id="notice" class="reveal-modal">' +
'<a close-modal></a>' +
'</div>',
restrict: 'E',
transclude: true,
replace: true,
scope: {
'done': '#',
},
transclude: true,
link: function(SCOPE, element, attrs, ctrl) {
SCOPE.$watch('done', function (a) {
// close-modal
});
}
}
}).
directive('closeModal', function() {
return {
template: '<a ng-transclude href="#" class="close-reveal-modal">x</a>',
restrict: 'A',
transclude: true,
replace: true
}
}).
directive('showModal', function() {
return {
template: '<a ng-transclude class="reveal-link" data-reveal-id="notice" href="#"></a>',
restrict: 'A',
transclude: true,
replace: true,
}
});
This works fine up to a point, for example, I can use the modal to show different notices from a template:
<modal done="">
<div ng-include src="'partials/notices/' + notice + '.html'"></div>
</modal>
<select ng-model="notice" ng-options="n for n in ['notice-1', 'notice-2']">
<option value="">(blank)</option>
</select>
<a show-modal>show modal</a>
However, where it gets sticky is if I want to trigger close-modal/ show-modal from a controller/ on a certain event (e.g. within $watch). I'm assuming my directive needs a controller to trigger click, but would be good Angular practise?
This question is very old and I don't know if it works with Reveal. But I've wrapped the dropbox library into Angular only by calling the .foundation() method on the .run() method of angular:
app.run(function ($rootScope) {
$rootScope.$on('$viewContentLoaded', function () {
$(document).foundation();
});
});
That works for me. I guess that you can also create a directive to handle user interaction.
Controllers should not be triggering UI events directly neither manipulate UI elements directly. All that code is supposed to go in Directives.
What you can do is:
Bind a bool in Directive Scope to the Parent scope and add a watch on that. I think you already did that. OR
Do a scope.$broadcast on the controller and add scope.$watch on the directive to close then.