How can I pass a model into custom directive - angularjs

My goal is to pass the projectName model from my MainController to my custom contenteditable directive.
I have the following controller:
app.controller("MainController", function($scope){
$scope.projectName = "Hot Air Balloon";
});
Here is how I'm calling the directive:
<div class="column" ng-controller="MainController">
<h2 contenteditable name="myWidget" ng-model="projectName" strip-br="true"></h2>
<p>{{projectName}}</p>
</div>
I've gotten the contenteditable directive working by following this tutorial: https://docs.angularjs.org/api/ng/type/ngModel.NgModelController
If I understand the docs correctly then Angular is not going to use the model I want it to. Instead its going to create a new model w/ scope local to the contenteditable directive. I know that I can add an isolate scope to the directive but I don't know how to use the model passed to the isolate scope within the link function.
I have tried something like the following which didn't work ...
<h2 contenteditable item="projectName"></h2>
--- directive code ---
scope: {
item: '=item'
}
link: function(){
...
item.$setViewValue(...)
...
}
--- my original directive call --
<div class="column" ng-controller="MainController">
<h2 contenteditable name="myWidget" ng-model="projectName" strip-br="true"></h2>
<p>{{projectName}}</p>
</div>
--- my original controller and directive ---
app.controller("MainController", function($scope){
$scope.projectName = "LifeSeeds";
});
app.directive('contenteditable', function(){
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, element, attrs, ngModel){
console.log(ngModel);
if(!ngModel) return;
console.log(ngModel.$viewValue);
ngModel.$render = function(){
element.html(ngModel.$viewValue || '');
};
element.on('blur keyup change', function(){
scope.$apply(read);
});
read();
function read(){
var html = element.html();
if(attrs.stripBr && html == '<br>'){
html = '';
}
ngModel.$setViewValue(html);
}
}
};
});

You can use ng-model with your own directive. To make sure it is included, you can use the attribute require like this:
app.directive("myDirective", function(){
return {
require:"ngModel",
link: function(scope, element, attr, ngModel){
console.log(ngModel);
}
}
});
Then, you can code whatever behavior your want of ng-model within your directive.

Working solution: http://plnkr.co/edit/Lu1ZG9Lpx2sl8CYe8FCx?p=preview
I mentioned that I tried using an isolate scope in my original post.
<h2 contenteditable item="projectName"></h2>
This was actually the correct approach so ignore my full original example using the model argument of the directive's link function.
link: function(scope, element, attrs, ngModel)
The reason the isolate scope approach did not work was because $scope.projectName stored a primitive instead of an object. I did not understand some javascript basics. Mainly, I did not know that primitive types were passed to functions by value.
Primitives are passed by value in javascript. Consequently, changes made to primitive values within a function do NOT change the value of the variable passed to the function.
function changeX(x){
x = 5;
}
x = 4;
changeX(x);
console.log(x) // will log 4 ... Not 5
However, objects passed to functions in javascript are passed by reference so modifications to them within the function WILL be made to the variable passed into the function.
My problem was in how I declared the scope within the MainController.
I had:
$scope.projectName = "LifeSeeds";
That's a primitive. When I passed projectName to the directive I was passing a primitive.
<h2 contenteditable item="projectName"></h2>
Thus, changes to the editable element were being made to the value within the directive but not to the value stored in the MainController's scope. The solution is to store the value within an object in the MainController's scope.
// correct
$scope.project = {
html: "Editable Content"
};
// wrong
$scope.projectName = "Editable Content"

Related

Removing element from DOM via ng-if bound to attribute directive scope property

I assumed this would be straightforward, but it's seemingly not!
I'm trying to create a generic attribute directive that will call a method in one of my services and conditionally cause the element in which it is placed to not be added to the DOM if the service method returns false. Basically, ng-if, but an ng-if that internally calls a service method and acts on that
Link to Plunker
I have an element containing an attribute directive: e.g
<p ng-if="visible" my-directive>Hi</p>
I set visible to true in the myDirective directive. I was expecting the <p> element to be removed from the DOM when visible was falsy and added to the DOM when it's truthy. Instead, the ng-if never seems to spot that visible has been set to true in the directive's link function and, hence, the <p> element never displays.
I wasn't 100% sure it would work since the directive is removing the element on which it exists, bit of a catch 22 there.
I've spent far too long on this and have so far tried (unsucessfully):
Adding an ng-if attribute in the link function via these two methods
attr.ngIf = true;
element.attr('ng-if', true);
Changing the ng-if in the <p> to ng-show, thereby not removing the element (which I really want to do)
I'm wondering if it's something as simple as scope? Since the ng-if is bound to a property of the <p> element, is setting visible in the directive scope setting it on the same scope?
On the other hand, I may be drastically over-simplifying, I have a nasty feeling I may have to consider directive compilation and transclusion to get a solution for this.
Does anyone have any feel for where I might be going wrong?
tldr: apparently you want your directive to be self-contained and it should be able to remove and add itself to the DOM. This is possible and makes the most sense via isolated scope or manual manipulation of the DOM (see below).
General
When you do <p ng-if="visible" my-directive>Hi</p> angular looks for the visible on the current scope, which is the parent scope of the directive. When visible is defined, the directive is inserted in the DOM, e.g. taken from your plunker
<body ng-controller="MainCtrl">
<p my-directive="showMe" ng-if="visible">I should be shown</p>
</body>`<br>
app.controller('MainCtrl', function($scope) {
$scope.visible = 3;
});
would make the directive being shown. As you defined an isolated scope on your directive
app.directive('myDirective', function() {
return {
restrict: 'A',
scope: {
myDirective: '='
},
link: function(scope, element, attr, ctrl) {
scope.visible = (scope.myDirective == 'showMe') ? true : false;
}
}
});
scope.visible in the directive does not affect the visible taken into account for ngIf.
Child Scope
You could define a child scope to get access to the parent scope. If you do that, you can actually affect the right visible property, but you have to put it on an object so that the directive can follow the scope prototype chain.
<body ng-controller="MainCtrl">
<p my-directive ng-if="visibleDirectives.directive1">I should be shown</p>
</body>
The $timeouts are there for demonstration purposes. Initially the ngIf has to evaluate to true else the directive is not being created at all.
app.controller('MainCtrl', function($scope) {
$scope.visibleDirectives = { directive1 : true };
});
app.directive('myDirective', function($timeout) {
return {
restrict: 'A',
scope : true,
link: function(scope, element, attr, ctrl) {
console.log(scope);
$timeout(function() {
scope.visibleDirectives.directive1 = !scope.visibleDirectives.directive1;
$timeout(function() {
scope.visibleDirectives.directive1 = !scope.visibleDirectives.directive1;
}, 2000);
}, 2000);
}
}
});
Like this the directive has to know about the property that defines it's visibility beforehand (in this case scope.visibleDirectives.visible1), which is not very practical and prohibits several directives.
Isolated Scope
In your example you used an isolated scope. This allows reusing the directive. In order for the directive to be able to modify the appropriate property for ngIf you have to again give it the right reference.
<body ng-controller="MainCtrl">
<p my-directive="directive1" ng-if="directive1.visible">I should be shown</p>
</body>
Again you have to provide the property on an object so that the directive can follow the object reference to modify the right visible.
app.controller('MainCtrl', function($scope) {
$scope.directive1 = {
visible : true
};
});
app.directive('myDirective', function($timeout) {
return {
restrict: 'A',
scope : {
myDirective : '='
},
link: function(scope, element, attr, ctrl) {
$timeout(function() {
scope.myDirective.visible = !scope.myDirective.visible;
$timeout(function() {
scope.myDirective.visible = !scope.myDirective.visible;
}, 2000);
}, 2000);
}
}
});
In these cases the directive gets recreated everytime ngIf evaluates to true.
Manual manipulation of the DOM
You can also just manually remove and append the node of the directive without consulting angular.
<body ng-controller="MainCtrl">
<p my-directive>I should be shown</p>
</body>
In this case you don't need the angular version of setTimeout and can even use a setInterval as the Interval is created only once, but you have to clear it.
app.controller('MainCtrl', function($scope) { });
app.directive('myDirective', function() {
return {
restrict: 'A',
scope : { },
link: function(scope, element, attr, ctrl) {
var el = element[0];
var parent = el.parentNode;
var shouldBeShown = false;
var interval = setInterval(function() {
var children = parent.children;
var found = false;
for(var i = 0; i < children.length; i++) {
if(children[i] === el) {
found = true;
break;
}
}
if(shouldBeShown) {
if(!found)
parent.appendChild(el);
}
else {
if(found)
parent.removeChild(el);
}
shouldBeShown = !shouldBeShown;
}, 2000);
scope.$on('$destroy', function() {
clearInterval(interval);
});
}
};
});
If you want an element to be removed, use ng-show="visible" this will evaluate as a Boolean and show the element if it evaluates to true. Use "!visible" if you need to flip it.
Also, but adding the scope attribute to your directive you are adding an additional scope, think alternate timeline, that your controller scope that is tied to the page cannot see. That would explain why ng-show may not have worked for you before.

Changing HTML of an element dynamically through a directive

I'm trying to change the HTML of an element based on a variable that is passed as an attribute of a directive.
The content is supposed to be changing back to the 'This is the original content...'. How come it doesn't work?
HTML
<div ng-app="myApp" ng-controller="myCtrl">
<div data-loading-data='{{testObj}}'>
<p> This is the original content. changingVar is '{{testObj.changingVar}}'</p>
</div>
</div>
JS
var app = angular.module('myApp', []);
app.controller('myCtrl', function($scope, $timeout) {
$scope.testObj = {};
$scope.testObj.changingVar = false;
$timeout(function() {
console.log("time out is done ! the original content should be restored now (from somewhere inside the directive!)");
$scope.testObj.changingVar = true;
}, 5000);
});
app.directive('loadingData', function() {
return {
restrict: 'AE',
replace: 'false',
link: function(scope, elem, attrs) {
var originalHTML = elem[0].innerHTML;
elem.replaceWith("<p>This is modified content</p>");
// When testObj.changingVar will be true, I want the original content (which is stored in var 'originalHTML') to be set!
// How to do?
// ........................................... ?
// ........................................... ?
}
}
});
First answer was useful; sorry, I accidently saved the jsfiddle with some commented out parts. Updated now!
There was an answer which suggested using objects is useful (passed by reference) instead of passing a single variable (passed by value) - that was great to know.
I updated the jsFiddle again to illustrate what I am trying to do better:
https://jsfiddle.net/4xbLrg5e/6/
First things you are passing testObj means you don't want to use inherited scope with directive.
So,I am assuming you want to use isolated scope.
As per this,
Here is the changes,
HTML :
<div test-data='testObj'>
<p> This is the original content. changingVar is '{{testObj.changingVar}}'</p>
</div>
Directive JS :
There are some correction,
1. replace : false; // implicitly it is false and not 'false';
2. You should not replace directive with html until unless you dont want to refer it again.
You can put watch to properties of passed object if you want update the html as per data changes.
You should use isolated scope with directive as per above assumption.
This is nothing but passed by reference using =
app.directive('loadingData', function() {
return {
restrict: 'AE',
replace: false,
scope : {testData:'=testData'},
link: function(scope, elem, attrs) {
var originalHTML = elem[0].innerHTML;
elem.html("<p>This is modified content</p>");
scope.$watch(function(){
return scope.testData.changingVar;
},function(nVal){
console.log('1')
elem.html("<p>Here is the original content</p>");
})
}
}
});
Here is the updated fiddle

pass data from controller to directive's link?

In my controller :
myApp.controller('homeCtrl', function($scope, $rootScope, $state, 'red';
$rootScope.$on('new_story', function(event, data) {
$scope.cardObj = {key:'value'};
});
});
In my HTML :
<div clickmeee ></div>
<div id="feedContainer" card='{{cardObj}}'> </div>
In my directive :
myApp.directive('clickmeee', function($compile, $rootScope) {
return {
restrict: 'A',
scope: {
card: '#'
},
link: function(scope, element, attrs) {
element.bind('click', function() {   
scope.$watch('card', function(newVal, oldVal) {
alert(scope.card);
});       
});
}
};
});
How do I pass data from controller to this directive. I compile some html and prepend it to the div. All of that is sorted out but I need some data from object I am trying to pass.
Any help??
There are several problems in your code:
you define a scope attribute named 'card', but you use cardObj instead
you use a watch that is completely unnecessary. And worse: you create a new watch every time the element is clicked
you don't define any card attribute on your clickmeee element. Instead, you're placing it on another element, on which the directive is not applied
you're passing the attribute with '#'. That works, but the directive will receive a string, containing the JSONified object, rather than the object itself
you're not showming us where you emit an event that will initialize cardObj in the controller scope
Here is a plunkr showing a working version of your code.
Also, note that using bind('click') is a bad idea. You'd better have a template in your directive and use ng-click in the template, or simply not use a directive at all and just use ng-click directly on the div element.
Bad news. You are doing it wrong all the ways.
Firstly
card='{{cardObj}}' >
this one should be put in the
<div clickmeee ></div>
So you can take it as binded scope variable in your directive registration
Secondly
If you managed to use '#' syntax
card: '#'
it will turn your input to string, not a binded scope. Use '=' instead.
In the end
You dont need to use watch here:
scope.$watch('card', function(newVal, oldVal) {
alert(newVal);
});
since scope.card is binded via '=' connector. Just simple use alert(scope.card). (Need to warn you that alert an object is not a good idea)
I have tried your code here: plunker. Changed a litte bit by using cardObj as string for easier presentation. Does it match your work?
You should watch the card object:
myApp.directive('clickmeee', function() {
return {
restrict: 'A',
scope: {
card: '#'
},
link: function(scope, element, attrs) {
scope.$watch('card', function(value) {
console.log(value);
});
}
};
});
And:
<div clickmeee id="feedContainer" card='{{cardObj}}'> </div>
Whenever the controller changes the cardObj, the directive's watch on card is triggered:
$scope.$apply(function() {
$scope.cardObj = "test";
}

Acessing form from directive's link function

In my custom directive I'm declaring new form <div ng-form="inputForm"></div> with input in it.
How can I access to this form within link function? scope.inputForm is undefined:/
Edit: code sample
.directive('ifInput', ['$system', function ($system) {
return {
restrict: "E",
replace: true,
scope: {},
link: function (scope, element, attrs) {
scope.temp = function () {
console.log(scope.inputForm);
}
},
templateUrl: "template/if-input-pola/index.html"
};
}])
.run(["$templateCache", function ($templateCache) {
$templateCache.put("template/if-input-pola/index.html",
"<div ng-form=\"inputForm\" class=\"form-group ng-fadeInLeft\" ng-class=\"{'has-error': inputForm[kolumna.id].$invalid && inputForm.$dirty}\">" +
"{{inputForm|json}}"+ //here is correct object (with $error and so on)
"<a ng-click=\"temp()\">a</a>" + //here is undefined
"</div>"
);
}])
EDIT 2:
Problem was that I wan's using object like this ng-form="model.inputForm" - now everything is ok
You can just define ng-model e.g.
<input ng-model="myInput"> and
link: function (scope, element) {
console.log(scope.myInput);
}
According to the documentation, using ng-form="inputForm" should post the controller on the local scope. If you cannot see it, you're either looking at a nested scope with that property overwritten, or at an ancestor scope. Keep in mind, that some directives, such as ng-repeat or ng-if, create local scopes, so in the following code:
<div ng-controller="myController">
<div ng-if="someCondition">
<div ng-form="myInput">
...
</div>
</div>
</div>
myInput would NOT be visible on the scope managed by myController.
When accesing the scope property within a link function, it also depends where your linked directive is, as the property may not be assigned to the scope yet. Try $watching the property and you just might find out it's being assigned later.

scope change in a directive not a apply in view

I have a directive in a template.html, included by a ng-include, in this directive I change the scope , but it is not change in my view
Here is my html
<div ng-controller="myCtrl">
<div id="modal">
<div ng-show="showDIv">Somthing to controll</div>
</div>
<div ng-include src="template.html">
</div>
Here is my template
<a ng-support></a>
And here is my directive
app.directive('ngSupport', function(){
return {
restrict: 'A',
link: function(scope, elem, attr, ctrl) {
elem.bind('click', function(e) {
$("#modal").dialog({height:518,width:900,modal:true });
scope.showDiv = true;
scope.$apply();
});
}
};
});
When i change the scope in the directive it is not apply in the view, anyone could help please ?
ng-include creates a new scope so scope.showDiv only affects the local scope.
Depending on how you want to structure your application, you could try accessing scope.$parent.showDiv instead, but it is not really future proof as it will depend on the HTML nesting.
A better solution would be to have the showDiv property stored inside an object in the parent scope. For example scope.ui = {}, this way, when you set scope.ui.showDiv = true in your directive, it will look up the parent scope automatically (using prototype inheritance), instead of adding the property to the local scope.
Finally, another solution would be to refactor your code to make it less complex: I think using a ng-include just for adding one element is an overkill, you could put directly <a ng-support></a> inside your html, which would avoid the problem you have with an intermediary scope being generated.
Another option is to broadcast an event, and watch for it in the controller. Something like this.
app.directive('ngSupport', function($rootScope){
return {
restrict: 'A',
link: function(scope, elem, attr, ctrl) {
elem.bind('click', function(e) {
$("#modal").dialog({height:518,width:900,modal:true });
$rootScope.$broadcast('event:modal-clicked');
});
}
};
});
With this in your controller.
$scope.$on('event:modal-clicked', function() {
$scope.showDiv = true;
});

Resources