Isolate ng-model in child directives - angularjs

I'm looking for the best solution to create a form, built from the small functional blocks.
What I actually do:
Main controller loads the object and passes it to directive:
$scope.context= {model object};
<form><employee-context ng-model="context"></form>
employeeContext directive has it owns template built with another directives (That's what I call small functional blocks):
<base-section ng-model="baseModel"></base-section>
<address-section ng-model="addressModel"></address-section>
<details-section ng-model="detailsModel"></details-section>
employeeContext link function:
link: function(scope, elem, attrs, ngModel) {
ngModel.$render = function() {
scope.model = ngModel.$modelValue; //this is main model
if (scope.model) {
scope.baseModel = {
name: scope.model.name,
photoKey: scope.model.photoKey
};
scope.addressModel = scope.model.addresses;
scope.detailsModel = scope.model.details;
}
};
scope.$watch('addressModel', function(newVal, oldVal) {
if (newVal !== oldVal){
scope.model.addresses = newVal;
ngModel.$setViewValue(scope.model);
}
});
scope.$watch('detailsModel', function(newVal, oldVal) {
if (newVal !== oldVal){
scope.model.details = newVal;
ngModel.$setViewValue(scope.model);
}
});
}
Above code is my solution to simplify building new context directives. Each time developer want to create new context, he must create similar directive, split the model and set his own html template. But this solution has some problems that I don't know how to resolve.
The problem:
I've expected that ng-model in each directive will be isolated from each other. But it is not.
It means that each change inside lower level directives are automatically propagated to main model. I'd like to avoid this behaviour and update main model only with calling ngModel.$setViewValue(updatedModel) (for example only after button click).
Example functional block code:
link: function(scope, elem, attrs, ngModel) {
ngModel.$render = function() {
scope.adr = ngModel.$modelValue;
};
scope.updateModel = function(){
_.each(scope.adr,function(m){
m.city = m.city.toUpperCase();
});
ngModel.$setViewValue(scope.adr);
};
}
I'm also confused why this watches in context link function are fired only once - at initialization. They don't react with later model changes.
My question is:
What is the best solution to create some isolated scopes with a model passed as ngModel and with the possibility to update this model only on demand.
Here is the plunkr http://plnkr.co/edit/vBUCwQHOT6GDDfJLNN5N?p=preview

Related

Angularjs controller function vs directive function

Lately I've been building some modules and in some of them I only used controllers (controller is set within an existing directive I already need to use to load template) to have this comunnication between services and the view, for example:
$scope.callFunction = function(data) {
factRequest = saveData(data);
};
I also noticed I could do this from within a directive, like this:
link:function(scope) {
scope.callFunction = function(data) {
factRequest.saveData(data);
}
}
//or..
link:function(scope, element, attr) {
attrValue = attr.myValue;
element.bind('click', function(attrValue) {
factRequest.saveData(attrValue);
});
}
//or even..
link:function(scope, element, attr) {
attrValue = attr.myValue;
element.bind('click', function(attrValue) {
factRequest.saveData(attrValue);
});
var elButton = element.fin('span'); //for example
elButton.bind('click', function(attrValue) {
factRequest.saveData(attrValue);
});
}
Considering a scenario where this a reusable object, for example, a product where it display on multiple pages and have a commom function, such as addFavorite, addCart, addWishList, etc.. And also considering performance.
What is the difference between those call methods? And what is the best option to use as a call Function?
To restate, you are calling a service method on a click event and want to know where the best place to put that logic is.
Let's look at each of your examples:
Controller
angular.module('myApp').controller('MyController', function($scope, factRequest) {
$scope.callFunction = function(data) {
factRequest.saveData(data);
};
});
First of all, whenever I find myself injecting $scope into a controller I question my approach. This is because adding variables to the current scope creates hidden dependencies if you are relying using those variables in a child controller -- and is unnecessary if you are not.
Instead, you should be using the controllerAs syntax and adding the function to the controller itself. Something like this:
angular.module('myApp').controller('MyController', function(factRequest) {
var vm = this;
vm.callFunction = function(data) {
factRequest.saveData(data);
};
});
...and you would access it in your template like this:
<div ng-controller="MyController as vm">
<input ng-model="vm.data">
<button ng-click="vm.callFunction(vm.data)">
Click Me!
</button>
</div>
This is a perfectly good approach utilizing native Angular directives.
Directive w/ Link Function
angular.module('myApp').directive('myDirective', function(factRequest) {
return {
link: function(scope) {
scope.callFunction = function(data) {
factRequest.saveData(data);
}
}
};
});
Again, I don't like this because you are adding the function to scope. If you have a directive and want to expose some functionality to the template, you should use a controller. For example:
angular.module('myApp').directive('myDirective', function() {
return {
controller: 'MyDirectiveController',
controllerAs: 'myDir',
template: '<input ng-model="myDir.data">' +
'<button ng-click="myDir.callFunction(myDir.data)">' +
'Click Me!' +
'</button>'
};
}).controller('MyDirectiveController', function(factRequest) {
var myDir = this;
myDir.callFunction = function(data) {
factRequest.saveData(data);
}
});
This is essentially the same as the first example, except that it is now a reusable component.
Directive w/ Click Event Handler
angular.module('myApp').directive('myDirective', function(factRequest) {
return {
link: function(scope, element, attr) {
element.on('click', function() {
factRequest.saveData(scope.$eval(attr.myValue));
});
}
};
});
Notice I took a few liberties here. For one thing, an event handler function gets the event object as its first argument, so trying to pass attr.myValue wouldn't work. Also, I call scope.$eval(), which is a best practice that enables the use of Angular expressions in the myValue attribute.
I like this approach best, because it doesn't rely on the use of other directives like ng-click. In other words, this directive is more self-contained.
One thing I should add is that Angular will not remove this event listener when the element is removed from the DOM. It is a best practice to clean up after your directive, like this:
angular.module('myApp').directive('myDirective', function(factRequest) {
return {
link: function(scope, element, attr) {
function onClick() {
factRequest.saveData(scope.$eval(attr.myValue));
}
element.on('click', onClick);
scope.$on('$destroy', function() {
element.off('click', onClick);
});
}
};
});
Conclusion
From a performance perspective, all of these approaches are roughly equivalent. The first two don't add any watchers themselves, but ng-click and ng-model do so its six of one, half a dozen of the other.

How to dynamically insert and remove one nested Angular custom directives?

I'm relatively new to creating custom Angular directives, and I'm trying to replace a page in my application which is a series of JQuery components which get nested inside one another.
I'm wanting to create a set of custom Angular directives, which I can nest within each other allowing the user to build up a kind of form, whilst allowing them to delete and re-add any directives nested within one another if they need to.
I'm not sure how dynamically insert one directive into another, based on a user's choice, and how to set this within the directive so that a given directive's child can be deleted, re-added and then recompiled.
So far the only (unsophisticated) method I have come up with is to 2-way bind to an attribute on the directive's isolate scope, and have this determine the inner content of the directive like so, however upon changing this attribute object on the parent scope, the directive's DOM doesn't recompile, so I'm convinced there is a much better way to do this.
I'm assuming I might need to use transclusion or the link function in some way, so any guidance on this is much appreciated!
Current attempt:
app.directive("testCustomMapperThings", function($compile) {
var testTemplate1 = '<div>THIS IS FIRST THE DYNAMICALLY COMPILED TEST TEMPLATE 1</div>';
var testTemplate2 = '<div>THIS IS SECOND THE DYNAMICALLY COMPILED TEST TEMPLATE 2</div>';
var getTemplate = function(contentType) {
var template = '';
switch(contentType) {
case 'testTemplate1':
template = testTemplate1;
break;
case 'testTemplate2':
template = testTemplate2;
break;
}
return template;
};
var linker = function(scope, element, attrs) {
//reads the scope's content attribute (2 way bound to this directive's isolate scope) and sets as DOM
element.html(getTemplate(scope.content.testContent)).show();
//compiles the 2 way bound DOM, recompiles directive on changes to attributes. todo: CHECK DIRECTIVE RECOMPILES ON CHANGES TO ATTRIBUTES
$compile(element.contents())(scope);
};
return {
restrict: "E", //can only be an element
link: linker, //link function
scope: { //isolate scope, 2 way bind to a 'content' attribute
content:'='
}
};
});
Use of this directive in the DOM, where I attempted to alter the $scope.content object but the directive's inner content didn't recompile:
<test-custom-mapper-things content="testContent"></test-custom-mapper-things>
<button ng-click="changeContent()">Click to change the test content and see if DOM recompiles</button>
Controller for the parent scope of the directive:
$scope.testContent = {
testContent : "testTemplate1"
};
$scope.changeContent = function() {
if($scope.testContent.testContent == 'testTemplate1') {
$scope.testContent.testContent = 'testTemplate2';
} else {
$scope.testContent.testContent = 'testTemplate1';
}
};
Use the $compile service and scope.$watch method.
Example:
Javascript:
angular.module('dynamicTemplate',[])
.directive("testCustomMapperThings",['$compile',function($compile) {
return {
resctrict: 'AE',
scope: {
active: '='
},
link: function (scope, el, attrs, ctrl) {
var t1 = "<div>I'm 1st Template</div>",
t2 = "<div>I'm 2nd Template</div>",
newTemplate;
function loadTemplate() {
el.html('');
switch(scope.active) {
case 'template1':
newTemplate = t1;
break;
case 'template2':
newTemplate = t2;
break;
}
el.append($compile(newTemplate)(scope));
}
loadTemplate();
scope.$watch('active', function(newVal, oldVal){
if(newVal === oldVal) return;
loadTemplate();
});
}
}
}])
.controller('templateController', function() {
var vm = this;
vm.option = true;
vm.templateOption = vm.option? 'template1' : 'template2';
vm.change = function() {
vm.option = !vm.option;
vm.templateOption = vm.option? 'template1' : 'template2';
}
});
HTML
<div ng-app="dynamicTemplate">
<div ng-controller="templateController as t">
<button ng-click="t.change()"></button>
<test-custom-mapper-things active="t.templateOption"></test-custom-mapper-things>
</div>
</div>
Codepen: http://codepen.io/gpincheiraa/pen/vLOXGz

Angular composite (super) directive not allowing 2 way binding on component (child) directives

I have a need to create a composite directive that incorporates separate fully functional directives. One of my component directives adds an element to the dom and that element binds to a value in the component directive's controller. When the composite directive adds the component directive in the compile function, it seems to work but the piece that has the 2 way binding in the component directive does not appear to get compiled and just renders the {{ctrl.value}} string on the screen. I realize this is a bit convoluted so I have included a plunk to help clarify the issue.
app.directive('compositeDirective', function($compile){
return {
compile: compileFunction
}
function compileFunction(element, attrs){
attrs.$set("component-directive", "");
element.removeAttr("composite-directive");
element.after("<div>Component value when added in composite directive: {{compCtrl.myValue}}</div>");
return { post: function(scope, element){
$compile(element)(scope);
}};
}
});
app.directive('componentDirective', function(){
return {
controller: "componentController as compCtrl",
link: link
};
function link(scope, element){
element.after("<div>Component value: {{compCtrl.myValue}}</div>");
}
});
app.controller('componentController', function(){
var vm = this;
vm.myValue = "Hello";
});
http://plnkr.co/edit/alO83j9Efz62VTKDOVgc
I don't think any compilation will happen as a result of changes in the link function, unless you call $compile manually, i.e.,
app.directive('componentDirective', function($compile){
return {
controller: "componentController as compCtrl",
link: link
};
function link(scope, element){
var elm = $compile("<div>Component value: {{compCtrl.myValue}}</div>")(scope);
element.append(elm);
}
});
Updated plunk: http://plnkr.co/edit/pIixQujs1y6mPMKT4zxK
You can also use a compile function instead of link: http://plnkr.co/edit/fjZMd4FIQ97oHSvetOgU
Also, make sure to use .append() instead of .after().

AngularJS - In a directive that changes the model value, why do I have to call $render?

I made a directive designed to be attached to an element using the ngModel directive. If the model's value matches something the value should then set to the previous value. In my example I'm looking for "foo", and setting it back to the previous if that's what's typed in.
My unit tests passed fine on this because they're only looking at the model value. However in practice the DOM isn't updated when the "put back" triggers. Our best guess here is that setting old == new prevents a dirty check from happening. I stepped through the $setViewValue method and it appears to be doing what it ought to. However it won't update the DOM (and what you see in the browser) until I explicitly call ngModel.$render() after setting the new value. It works fine, but I just want to see if there's a more appropriate way of doing this.
Code is below, here's a fiddle with the same.
angular.module('myDirective', [])
.directive('myDirective', function () {
return {
restrict: 'A',
terminal: true,
require: "?ngModel",
link: function (scope, element, attrs, ngModel) {
scope.$watch(attrs.ngModel, function (newValue, oldValue) {
//ngModel.$setViewValue(newValue + "!");
if (newValue == "foo")
{
ngModel.$setViewValue(oldValue);
/*
I Need this render call in order to update the input box; is that OK?
My best guess is that setting new = old prevents a dirty check which would trigger $render()
*/
ngModel.$render();
}
});
}
};
});
function x($scope) {
$scope.test = 'value here';
}
Our best guess here is that setting old == new prevents a dirty check from happening
A watcher listener is only called when the value of the expression it's listening to changes. But since you changed the model back to its previous value, it won't get called again because it's like the value hasn't changed at all. But, be careful: changing the value of a property inside a watcher monitoring that same property can lead to an infinite loop.
However it won't update the DOM (and what you see in the browser) until I explicitly call ngModel.$render() after setting the new value.
That's correct. $setViewValue sets the model value as if it was updated by the view, but you need to call $render to effectively render the view based on the (new) model value. Check out this discussion for more information.
Finally, I think you should approach your problem a different way. You could use the $parsers property of NgModelController to validate the user input, instead of using a watcher:
link: function (scope, element, attrs, ngModel) {
if (!ngModel) return;
ngModel.$parsers.unshift(function(viewValue) {
if(viewValue === 'foo') {
var currentValue = ngModel.$modelValue;
ngModel.$setViewValue(currentValue);
ngModel.$render();
return currentValue;
}
else
return viewValue;
});
}
I changed your jsFiddle script to use the code above.
angular.module('myDirective', [])
.directive('myDirective', function () {
return {
restrict: 'A',
terminal: true,
require: "?ngModel",
link: function (scope, element, attrs, ngModel) {
if (!ngModel) return;
ngModel.$parsers.unshift(function(viewValue) {
if(viewValue === 'foo') {
var currentValue = ngModel.$modelValue;
ngModel.$setViewValue(currentValue);
ngModel.$render();
return currentValue;
}
else
return viewValue;
});
}
};
});
function x($scope) {
$scope.test = 'value here';
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<h1>Foo Fighter</h1>
I hate "foo", just try and type it in the box.
<div ng-app="myDirective" ng-controller="x">
<input type="text" ng-model="test" my-directive>
<br />
model: {{test}}
</div>

How to write an Angular directive for input and contenteditable

my html is taking input in two form, input and contenteditable div . I want to write one directive that handles both, but I cannot find a way to figure out which tag has called the function (because Angular's JQLite doesnt provide a is() or get() function). The following code will be complete if I can figure out to evaluate IS_INPUT_TAG:
function funct() { return {
require: 'ngModel',
link: function(scope, element, attrs, ctrl) {
// view -> model
element.bind('input', function() {
scope.$apply(function() {
if(IS_INPUT_TAG)
ctrl.$setViewValue(element.val());
else
ctrl.$setViewValue(element.text());
scope.watchCallback(element.attr('data-ng-model'));
});
});
// model -> view
ctrl.$render = function() {
if(IS_INPUT_TAG)
element.val(ctrl.$viewValue);
else
element.text(ctrl.$viewValue);
};
}};
}
app.directive('input', funct);
app.directive('contenteditable', funct);
In your directive, you can make use of the element parameter of the linking function to identify the tag on which the directive is applied. You can then use that in your IF condition as follows:
ctrl.$render = function() {
var tagname = element["0"].tagName;
if(tagName === "INPUT")
element.val(ctrl.$viewValue);
else
element.text(ctrl.$viewValue);
};
After, this you can simply attach the directive to the input and the div tags as an attribute to the tags to identify the tag to which the directive is applied.

Resources