Object not assignable in directive - angularjs

In this plunk I have a directive that wraps a div. The div is shown when an ng-if condition is true (set with the click of a button).
The directive has a scope element css that is an object, where the object has an attribute width. Problem is that Angular complains when the directive is shown; see in the console the following error message when the button is clicked:
Expression '{ width: width}' in attribute 'css' used with directive
'modal' is non-assignable!
Note that this problem goes away when the $timeout in the directive is removed, but I cannot discard it.
Why does this happen and how to fix it (keeping the $timeout)?
HTML
<button ng-click="open()">Open modal</button>
<div modal ng-if="showModal" css="{ width: width}">
<p>some text in modal</p>
</div>
Javascript
angular.module("app", [])
.controller('ctl', function($scope) {
$scope.width = '200px';
$scope.open = function(){
$scope.showModal = true;
};
})
.directive("modal", function($timeout) {
var directive = {};
directive.restrict = 'EA';
directive.scope = { css: '=' };
directive.templateUrl = "modal.html";
directive.link = function (scope, element, attrs) {
$timeout(function(){
scope.css.height = '100%';
},100);
};
return directive;
});
Template
<style>
#modaldiv{
border:2px solid red;
}
</style>
<div id="modaldiv" ng-style="{'width': css.width,'height': css.height}">
Some content
</div>

The error appears since you are not passing a scope variable to your css attribute.
You can fix this by creating a variable that holds your css in ctrl and pass this variable to the css attribute.
Controller
$scope.css = {width: $scope.width};
HTML
<div modal ng-if="showModal" css="css">
<p>some text in modal</p>
</div>
Or alternatively create a local deep copy of css in the directive and manipulate the copy in your $timeout.
Directive
directive.link = function (scope, element, attrs) {
scope.cssCopy = angular.copy(scope.css);
$timeout(function(){
scope.cssCopy.width = '100%';
}, 100);
};
Template
<div id="modaldiv" ng-style="{'width': cssCopy.width,'height': cssCopy.height}">
Some content
</div>

Related

Why my angular directive doesn't work?

I have a directive that is triggered when clicking on a button. The function inside the directive simply has to change the property value of the field. So what I try to do is to change from 'popover-trigger="blur"' to 'popover-trigger="none"'.
Here is my plunkr: http://plnkr.co/edit/L81fQgi7j1dEtf1QAZJ2?p=preview
or the code is here:
var app = angular.module('ui.bootstrap.demo', ['ngAnimate', 'ui.bootstrap']);
app.controller('PopoverDemoCtrl', function ($scope) {
$scope.dynamicPopover = {
content: 'Hello, World!',
templateUrl: 'myPopoverTemplate.html',
title: 'Title'
};
$scope.label = "Please click";
$scope.message = "ON FOCUS trigger a tooltip";
$scope.htmlPopover = "myPopoverTemplate.html";
});
app.directive("changeTrigger", function($compile){
return{
restrict: 'A',
link: function(scope, elm, attrs)
{
elm.bind('click', function(){
var t = document.getElementsByClassName('f')[0].setAttribute('popover-trigger', 'none');
$compile(t);
console.log("Click works");
});
}
}
});
html
<div ng-controller="PopoverDemoCtrl">
<br><br><br>
<p>{{message}}</p>
<input class="f" type="text" value="Click me!" uib-popover-template="htmlPopover" popover-trigger="focus" popover-popup-close-delay="1000" popover-placement="right" required>
<test-directive></test-directive>
<script type="text/ng-template" id="myPopoverTemplate.html">
<div>
<p>Click the button to stop triggering tooltip!</p>
<button change-trigger><b style="color: red">Stop tooltip</b></button>
<div class="label label-success">page</div>
</div>
</script>
</div>
You can't reconfigure the angular-bootstrap Popup element by changing uib-popup-* parameters; but you can bind a scope variable to popup-enable attribute to be able to switch the popup on/off. Add:
<input ... uib-popover-template="htmlPopover" popover-enable="enable" ...>
and
$scope.enable = true;
The problem here is that your button and the input box have different scopes. But you can fix this by retrieving the scope of the field:
var t = document.getElementsByClassName('f')[0];
var scope_ = angular.element(t).scope();
Of course, you need to use $scope.$apply for the scope to correctly handle two-way data binding:
scope_.$apply(function () {
scope_.enable = false;
});
Working Plunkr.

how to test Bootstrap Modal in AngularJS using jasmine?

I am using bootstrap modal for displaying pop up's in angular app. It is working perfectly from front end but DOM elements inside pop up are not getting appended to the body
In the output, when I am displaying content of document, content of my html template is not getting appended to body tag. So I am unable to find/test any DOM element. It is not displaying any errors in console also.
Please any one can help me in resolving this issue by specifying how to test bootstrap modal in angular application using karma/jasmine.
angular.module("myApp").directive('popUp', ['$http', '$compile', function($http, $compile) {
return {
restrict: 'A',
replace: true,
scope: {
course: '=',
},
compile: function(element, cAtts){
var template,
$element,
loader;
var windowOpen = false;
loader = $http.get('components/popUp.view.html').success(function(data) {
template = data;
});
//return the Link function
return function(scope, element, lAtts) {
element.on('click', function(e) {
e.preventDefault();
$element = $( $compile(template)(scope) );
$element.modal({backdrop: 'static'});
windowOpen = true;
});
// if the template changes, we need to compile the current
// template again. just in case there are funky sticky things
scope.$watch('template', function(newValue, oldValue) {
if(newValue == undefined && oldValue == undefined) return;
if(windowOpen) return;
if(newValue == undefined){
//$(".modal").remove();
$element.remove();
$element = undefined;
return;
}
$element = $( $compile(template)(scope) );
});
};
}
}
}]);
My HTML Template:
<div id="{{handler}}" class="modal fade">
<div style="margin:-1px auto; width:90%;float:right;" class="modal-dialog">
<div style="padding-left: 25px; height:100vh;" class="modal-content">
<div class="modal-header navbar-static-top"><img src="content/assets/img/close.png" data-dismiss="modal" aria-hidden="true" ng-click="close()" title="Close" alt="Close" class="popUpClose close pull-left"/>
<div class="popUpTitle">
<div style="font-size:22px;">{{course.CourseName}}</div>
<div style="font-size:15px; margin-top:-5px;">{{course.CourseId}}</div><br/>
</div>
</div>
<div style="display:inline" class="modal-body">
<p>This is test Modal</p>
</div>
<div style="border:none;" class="modal-footer"> </div>
</div>
</div>
</div>
My Test File:
(function(){
"use strict";
describe("Testing Pop Up Directive Functionality", function(){
var $httpBackend, $scope, fakeData, $compile, $document;
var compileDirective, course, element, template;
beforeEach(module('myApp.testing'));
beforeEach(module('myApp'));
beforeEach(inject(function ($injector, _$templateCache_, _$httpBackend_, _fakeData_, _$document_) {
$httpBackend = _$httpBackend_;
fakeData = _fakeData_;
$document = _$document_;
angular.module('components/popUp.view.html')._runBlocks[0](_$templateCache_);
template = _$templateCache_.get('components/courses/course.attendance.popUp.view.html');
course = fakeData.fakeCourses.Courses[0];
compileDirective = function() {
inject(function(_$compile_, _$rootScope_) {
$compile = _$compile_;
$scope = _$rootScope_.$new();
$scope.course=course;
element = angular.element('<div manageattendance="" course="course"></div>');
element = $compile(element)($scope);
});
//$scope.$digest();
};
}));
it("Should display attendance of course if user is authorized", function(){
$httpBackend.expectGET('components/courses/course.attendance.popUp.view.html').respond(template);
compileDirective();
$httpBackend.flush();
element.trigger('click');
console.log($document.find('html').html());
});
});
})();
AngularJS : 1.3.11
Bootstrap : 3.3.2
Karma : 0.12.31
Jasmine-core : 2.3.4
karma-jasmine : 0.3.5
I am not sure what is the cause but by removing css class "fade" from modal div, html content from modal is getting appended to the body tag.

Controller $scope property does not change with expression in transcluded content

The problem is that when you have transcluded content which contains an expression as an attribute of ng-click for example, the change does not occur in the scope of the parent controller as you can see in this fiddle:
http://jsfiddle.net/vt7rmqya/
Nothing happens when you click hide box inside of transcluded content.
<div ng-controller="myCtrl">
<panel ng-show='showBox'>
<div class='box'>
{{name}}
<br>
<button ng-click='showBox = false'>hide box button inside of transcluded content</button>
</div>
</panel>
<br>
Here, the expression in ng-click has no effect on the $scope.showBox in the controller but you would think that it would because the scope of the transcluded content should be the same as the controller scope, right?
BaseApp = angular.module('BaseApp', []);
BaseApp.controller('myCtrl', function($scope) {
$scope.name = 'bill jones';
$scope.showBox = true;
});
BaseApp.directive('panel', function() {
return {
restrict: 'E',
template: '<div>header<br><div ng-transclude></div></div>',
transclude: true,
}
})
I realized that the solution is simply to set the ng-click attribute to a function in the controller instead of an expression like so:
BaseApp.controller('myCtrl', function($scope) {
$scope.name = 'bill jones';
$scope.showBox = true;
$scope.hideBox = function() {
$scope.showBox = false;
}
});
<panel ng-show='showBox'>
<div class='box'>
{{name}}
<br>
<button ng-click='hideBox()'>hide box button inside of transcluded content</button>
</div>
</panel>
Here's the working fiddle:
http://jsfiddle.net/75f2hgph/

Angular placing functions

I'm using the angular-google-maps library in my project. I have used a directive to load a custom google maps menu. The goal is to obviously reuse the directive. In the menu are a couple of buttons which when clicked should all carry out a function. I'm still trying to get my head around on how to do that, so here is my problem:
I would like to pan the map to its original position when the button "Home" is clicked. Normally that is just done with ng-click and the function is placed within the scope of the controller. With the directive I'm confused. Where should I place the "home()" function? Directive? Directive controller? Controller? I hope this makes any sense?!?!
HTML:
<div class="map_canvas">
<google-map center="map.center" zoom="map.zoom" draggable="true">
<marker ng-repeat="m in map.markers" coords="m" icon="m.icon" click="onMarkerClicked(m)">
<marker-label content="m.name" anchor="50 0" class="marker-labels"/>
<window ng-cloak coords="map.center" isIconVisibleOnClick="false" options="map.infowindows.options">
<p>This is an info window at {{ m.latitude | number:4 }}, {{ m.longitude | number:4 }}!</p>
<p class="muted">My marker will stay open when the window is popped up!</p>
</window>
</marker>
<map-custom-control position="google.maps.ControlPosition.TOP_CENTER" control-template="../templates/gmaps/main_menu.html" control-click=""></map-custom-control>
</google-map>
</div>
Template:
<div class="gmaps-menu">
<div class="gmaps-row">
<button type="button" class="btn btn-default"><img class="glyphicon-custom" src="../img/icons/glyphicons/glyphicons_020_home.png" ng-click="home()"></button>
<button type="button" class="btn btn-default"><img class="glyphicon-custom" src="../img/icons/glyphicons/glyphicons_349_fullscreen.png"></button>
<button type="button" class="btn btn-default"><img class="glyphicon-custom" src="../img/icons/glyphicons/glyphicons_096_vector_path_polygon.png"></button>
<button type="button" class="btn btn-default"><img class="glyphicon-custom" src="../img/icons/glyphicons/glyphicons_030_pencil.png"></button>
</div>
</div>
Directive:
AppDirectives.directive('mapCustomControl', ['$log', '$timeout', '$http', '$templateCache', 'google', 'GMapsLib' ,function ($log, $timeout, $http, $templateCache, google,GMapsLib) {
return {
restrict: 'E',
replace: true,
require: '^googleMap',
link: function(scope,element,attr,mapCtrl){
if (!angular.isDefined(attr.controlTemplate)) {
$log.error('map-custom-control: could not find a valid control-template property!');
return;
}
var templateUrl = attr.controlTemplate;
var position = google.maps.ControlPosition.TOP_CENTER;
if (angular.isDefined(attr.position)) {
var EVAL_IS_OK_WE_CONTROL_THE_INPUT = eval;
position = EVAL_IS_OK_WE_CONTROL_THE_INPUT(attr.position);
}
$timeout(function() {
var map = mapCtrl.getMap();
var controlDiv = document.createElement('div');
controlDiv.style.padding = '5px';
controlDiv.style.width = 'auto';
controlDiv.marginLeft = 'auto';
controlDiv.marginRight = 'auto';
$http.get(templateUrl, {cache: $templateCache})
.success(function(html) {
controlDiv.innerHTML = html;
})
.then(function (/*response*/) {
map.controls[position].push(controlDiv);
if (angular.isDefined(attr.controlClick)) {
google.maps.event.addDomListener(controlDiv, 'click', function() {
scope.$apply(attr.controlClick);
});
}
}
);
});
}
};
}]);
You can pass the scope function that has to be executed on the controller:
HTML
<div ng-app="app" ng-controller="sampleCtrl">
<maps-custom-control click-handler="alertMe()"></maps-custom-control>
</div>
JS
var app = angular.module('app', []);
app.directive('mapsCustomControl', function() {
return {
restrict: 'EA',
replace: true,
scope: {
clickHandler: '&'
},
template: '<div style="width: 100px; height:100px; background-color: red;" ng-click="clickHandler()"></div>'
};
});
app.controller('sampleCtrl', function ($scope) {
$scope.alertMe = function () {
window.alert('Refresh gMaps control');
};
});
Since we pass the alertMe function, this is the function that will get executed, I hope this makes sense?
Fiddle
A small remark on your code, it would be better if you get the template as follows:
app.directive('..', function() {
return {
template: '<div ng-include="getTemplate()"></div>',
link: function(scope, element, attr) {
scope.getTemplate = function() {
return this.attr.controlTemplate;
}
}
};
});
This way you don't need to do any strange ajax calls. Just add all the mark-up in your template and include it. don't make it necessary hard :-)

Share Directive Scope with Transcluded Elements

I am writing a reusable modal directive, but the transcluded elements create their own scope. Here is a JSFIDDLE of the issue. Here is how it works.
<button ng-click="show=!show">Show Modal</button>
<modal visible="show">
<button ng-click="show=!show">X</button>
</modal>
Notice that the button to show it works, but the X does not close it, because the inner button is transcluded and creates its own scope. Is there any way to link the transcluded scope to the current scope of the directive? Or just stop it from creating its own scope? Here is the directive.
.directive('modal', function($compile) {
return {
restrict: 'E',
template: "<div ng-style='bgstyling()' ng-transclude></div>",
transclude: true,
link: function(scope, elem, attrs) {
scope.$watch(attrs.visible, function(val) {
elem[0].style.visibility = val ? 'visible' : 'hidden';
});
scope.bgstyling = function() {
return {
'position': 'fixed',
'top': '0',
'left': '0',
'bottom': '0',
'right': '0',
'backgroundColor': attrs.bgColor || '#000',
'opacity': attrs.opacity || '0.85'
}
}
}
}
})
* UPDATE *
I think the answer might have something to do with the transclude function parameter of the link function. This is what I just tried, but still isn't quite working.
link: function(scope, elem, attrs, nullC, transclude) {
transclude(scope, function(clone) {
elem.append($compile(clone)(scope));
});
...
Making the controller responsible for updating the scope helps - the scope is shared after all and you probably want the logic for updating it in the same place.
.controller("testCtrl", function($scope) {
$scope.show = false;
$scope.toggle = function() {
$scope.show = !$scope.show;
};
})
And the template:
<div ng-app="test" ng-controller="testCtrl">
<button ng-click="toggle()">Show Modal</button>
<modal visible="show">
<button ng-click="toggle()">X</button>
</modal>
</div>
Check out this JSFiddle
The close button is specific to the Modal directive so you could just put the button in the directive html and avoid the scoping issue.
jsfiddle
...
template: "<div ng-style='bgstyling()'><button ng-click=\"show=!show\">X</button></div>",
...
You can also access the parent scope if necessary but I prefer to try and keep scopes as simple as possible.
Edit
You can still use transclude like in this jsfiddle
...
template: "
<div ng-style='bgstyling()'>
<button ng-click=\"show=!show\">X</button>
<div ng-transclude></div>
</div>
",
transclude: true,
...

Resources