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.
Related
Hi i have created a directive and it does not pass back the correct message.
The directive is used to pass tool-tips back to the html page
this is what the html looks like
<info-text info-msg="Adding another applicant might help you to get approved."></info-text>
below is the directive
(function(){
angular.module('mainApp').directive('infoText', [function () {
return {
scope: { infoMessage: '&infoMsg' },
restrict: 'E',
replace: true,
template: '<p class="info-text"><i class="fa fa-info-circle"></i> {{infoText}}</p>',
link: function(scope, elem, attrs) {
$(elem).prev().hover(function(){
$(elem).addClass('info-hover');
}, function(){
$(elem).removeClass('info-hover');
});
}
};
}]);
}());
the message i get rendered on the page is as follows (it does send the glyphicon):
{{infoText}}
Any ideas,
thanks. Kieran.
You should not use & for this sort of binding, basically it is used for expression binding. I think one way binding (#) is efficient for what you are doing.
Also you should change directive template {{infoText}} to {{infoMessage}}
Markup
<info-text
info-msg="{{'Adding another applicant might help you to get approved.'}}"></info-text>
Directive
angular.module('mainApp').directive('infoText', [function () {
return {
scope: { infoMessage: '#infoMsg' },
restrict: 'E',
replace: true,
template: '<p class="info-text"><i class="fa fa-info-circle"></i> {{infoMessage}}</p>',
link: function(scope, elem, attrs) {
$(elem).prev().hover(function(){
$(elem).addClass('info-hover');
}, function(){
$(elem).removeClass('info-hover');
});
}
};
}]);
And making more cleaner and readable html you could place that string into some scope variable and pass that scope variable in info-msg attribute
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);
}
});
This is probably as basic as it gets. I have some simple markup that's suppose to represent a list:
<unordered-list>
<list-item></list-item>
<list-item></list-item>
</unordered-list>
I also have two directives defined for unordered-list and list-item:
.directive('unorderedList', function() {
return {
restrict: 'E',
replace: true,
template: '<ul></ul>'
};
})
.directive('listItem', function() {
return {
restrict: 'E',
replace: true,
template: '<li>Test</li>'
}
});
Yet when I run this (http://jsfiddle.net/esWUD/), the only thing rendered is the <ul>. No <li>Test</li> elements render within the <ul>.
Why not don't they render?
You'll need to use ngTransclude to allow directives to have sub-elements. Set transclude: true in unorderedList.
transclude: true,
template: '<ul ng-transclude></ul>'
Working fiddle.
I'm trying to inject 2 templates into an element and operate on them:
<div
ic-first="foo"
ic-second="bar"
ic-third="baz"
ic-fourth="qux"
>
</div>
icFirst should inject via a template an empty div as a child of its element. icSecond should inject a second div (with a bunch of content) as the second child of its element, so the resulting html would look like:
<div
ic-first="foo" // priority: 100
ic-second="bar" // priority: 50
ic-third="baz" // priority: 0
ic-fourth="qux" // priority: 0
>
<div id="foo"></div>
<div> <!-- a bunch of stuff from the templateUrl --> </div>
</div>
Both icFirst and icSecond will inject other elements into the newly created containers.
When I specify a directive template property on both directives, I get an error:
Error: Multiple directives [icFirst, icSecond] asking for template on: <div ic-first…
When I add transclude: true to both directives, icFirst executes just fine…but then the other directives on the same element are not executed. When I set transclude: 'element', the other directives execute but I get an error that the first child ($scope.firstObj) is undefined.
All four directives need access to each other's scope, so I'm doing most of my work in their controllers:
app.directive('icFirst', ['ic.config', function (icConfig) {
return {
restrict: 'A',
priority: 100,
template: '<div id="{{firstId}}"></div>',
replace: false,
transclude: 'element',
controller: function icFirst($scope, $element, $attrs) {
// …
$scope.firstId = $scope.opts.fooId;
$scope.firstElm = $element.children()[0];
$scope.firstObj = {}; // this is used by the other 3 directives
},
link: function(scope, elm, attrs) { … } // <- event binding
}
);
app.directive('icSecond', ['ic.config', function (icConfig) {
return {
restrict: 'A',
priority: 0,
templateUrl: 'views/foo.html',
replace: false,
transclude: 'element',
controller: function icSecond($scope, $element, $attrs) {
// …
$scope.secondElm = $element.children()[1];
$scope.secondObj = new Bar( $scope.firstObj );
// ^ is used by the remaining 2 directives & requires obj from icFirst
},
link: function(scope, elm, attrs) { … } // <- event binding
}
);
Note I have corrected the behaviour of replace: false to match the documented behaviour, as described in pull request #2433.
I tried instantiating $scope.firstObj in the controller, and setting it in the linkFn (hoping the transclusion would have completed by the time the linkFn executes), but I get the same problem. It appears first-child is actually a comment.
The only reason I can come up with that explains throwing this error is that the AngularJS team was trying to avoid needless overwrites/DOM manipulation:
Considering the actual behaviour of replace: false vs the documented behaviour, I think the actual is in fact the intended behaviour. If this is true, then allowing multiple templates/templateUrls to be used on the same element will cause subsequent templates to overwrite previous ones.
Since I already modified the source to match the documented behaviour, as a quick fix†, I modified the source again (/src/ng/compile.js:700) to remove the assertNoDuplicate check (which corresponds to angular.js:4624). Now I return the following 2 objects, and it works, and I can't find any negative repercussions:
// directive icFirst
return {
restrict: 'A',
priority: 100,
replace: false,
template: '<div id="{{firstId}}"></div>',
require: ["icFirst"],
controller: Controller,
link: postLink
};
// directive icSecond
return {
restrict: 'A',
require: ['icFirst'],
replace: false,
templateUrl: 'views/bar.html',
priority: 50,
controller: Controller,
link: postLink
};
† If made permanent, the check should probably be
if (directive.templateUrl && directive.replace)
(and similar for directive.template)