Manually applying the ngModel directive - javascript

My directive needs to use ngModel.
I need to do this dynamically from within another directive as I want to do some funky stuff with scopes and abstract this away from the person writing the HTML.
My first thought was to use the $set function provided by the attrs argument in the link function, that works to modify the HTML but the directive itself does not get compiled. We can then combine this with the $compile provider, and it works.
attrs.$set('ngModel', someVar);
$compile(element)(scope);
The problem is that this creates infinite recursion if I do not (and I can not) replace the elements tag as the directive gets reapplied and recompiled indefinitely.
However I can fiddle with the priorities and get that to work:
module.directive('input', [
'$compile',
function($compile) {
return {
restrict: 'E',
scope: {},
priority: 100, // Set this high enough to perform other directives
terminal: true, // Make sure this is the last directive parsed
link: function(scope, element, attrs) {
var key = 'example';
attrs.$set('ngModel', key);
$compile(element, null, 100)(scope);
}
};
}
]);
This works fine, but it just feels wrong:
I now have to ensure that all other directives on the element are
capable of being recompiled as they will all get compiled twice.
I have to make sure that nobody uses a higher priority.
So this got me thinking why can't I just inject the ngModelDirective and force compile it against my element?
module.directive('input', [
'ngModelDirective',
function(ngModel) {
return {
restrict: 'E',
scope: {},
priority: 100, // Set this high enough to perform other directives
terminal: true, // Make sure this is the last directive parsed
require: '?^form',
link: function(scope, element, attrs, formCtrl) {
var key = 'example';
attrs.$set('ngModel', key);
var ngModelFactory = ngModel[0];
var ngModelLink = ngModelFactory.compile(element);
ngModelLink.call(this, scope, element, attrs, [ngModelFactory.controller, formCtrl]);
}
};
}
]);
See: https://github.com/angular/angular.js/blob/v1.2.x/src/ng/directive/input.js#L1356
No errors thrown, but nothing happens. It seems this isn't enough to hook it up, so my question is can anyone elaborate on to what I need to do link the ngModelDirective to my custom directive without forcing a recompile?

ngModel seems a bad fit for what you are trying to do. But you don't need it anyway. You can two-way-bind some variable and pass the name into the model directive scope:
app.directive("myDirective", function() {
// ...
scope: {
myModel = "=",
modelName = "myModel"
// ...
}
// ...
});
app.directive("ngModelDirective", function() {
// ...
// ...
transclude: true,
link: function(scope, element, attrs) {
var modelName = scope.modelName;
console.assert(modelName, '`modelName` must be set when using `ngModelDirective`.');
// TODO: Check if `scope[modelName]` is actually bound
doSomethingFancyWith(scope, modelName);
}
});
Template example:
<myDirective ngModelDirective my-model="..." />
Note that doSomethingFancyWith can read and write the model variable, with bindings to the outside world.

I don't think it is possible without a re-compile.
The ngModel is designed to be a kind of collaborator between other directives in the same element and also parent form diretives. For example, during complilation:
other directives (e.g. input, required or ng-change) may add its own $parser or $formatter to ngModel.
ngModel will add itself to a parent form directive if exists.
Therefore, if the ngModel is somehow added after the complication process is ended already, the above two actions will be missing.
Edit: In case the value to be assigned to ng-model attribute is known at the compile time, it is possible and will be something like this:
app.directive('myNgModel', function($compile) {
return {
restrict: 'A',
replace: false,
priority: 1000,
terminal: true, // these terminal and priority will stop all other directive from being compiled.
link: function (scope, element, attrs) {
var key = 'example';
attrs.$set('ngModel', key);
attrs.$set('myNgModel', null); // remove itself to avoid a recusion
$compile(element)(scope); // start compiling other directives
}
};
});
Here is the plunker with example: http://plnkr.co/edit/S2ZkiVIyq2bOK04vAnFO?p=preview

I've managed to do it. It's not the prettiest thing but it works and I can hook up my input directive to work using the native inputDirective so that it can use things like require or validate specific input types.
To build this against another standard directive that implements specific ngModel functionality such as ngChange just replace the injected inputDirective with the correct directive e.g., ngChangeDirective.
module.directive('input', function() {
return {
restrict: 'E',
scope: {},
require: '?ngModel',
priority: -1,
link: function(scope, element, attrs, ngModel) {
var key = 'example.property';
if (ngModel === undefined) {
attrs.$set('ngModel', key);
angular.injector(['ng']).invoke([
'inputDirective',
'ngModelDirective',
'$controller',
'$exceptionHandler',
'$parse',
'$animate',
function(inputDirective, ngModelDirective, $controller, $exceptionHandler, $parse, $animate) {
var ngModelFactory = ngModelDirective[0];
var ngModelLink = ngModelFactory.compile(scope); // Get the ngModel linkage function against this scope
ngModel = $controller(ngModelFactory.controller, {
$scope: scope,
$exceptionHandler: $exceptionHandler,
$attrs: attrs,
$element: element,
$parse: $parse,
$animate: $animate
}); // Call the ngModel controller and bootstrap it's arguments
// Call the inputDirective linkage function to set up the ngModel against this input
inputDirective[0].link(scope, element, attrs, ngModel);
element.data('$ngModelController', ngModel); // Allow additional directives to require ngModel on this element.
}
]);
}
}
};
});
NOTE: This will not work for ngOptions as it specifies terminal: true.

Related

how can I pass 'scope' argument outside the directive

how can I pass 'scope' argument outside the directive?
i need use it in some other component..
my code:
(function () {
angular.module('dmv.shared.components').
directive('doImportPackage', ['Package', function (Package) {
return {
restrict: 'A',
scope: {
onStart: '<',
onFinish: '<',
onError: '<'},
link: function (scope, element, attributes) {
}
tnx !!
You can do this via a controller. Since AngularJS works in 2-way data binding principle, these variables you assigned will already be updated from where you referenced, and you can use them with other directives too. For example, I assume that you use your directive as follows:
<do-import-package
on-start="myCtrl.onStart"
on-finish="myCtrl.onFinish"
on-error="myCtrl.onError">
</do-import-package>
You have following corresponding variables in myCtrl controllor:
this.onStart = some value;
this.onFinish = some value;
this.onErrod = some value;
Under normal conditions, you can bind other directive's attributes to these values and they will be updated in 2-way. For example, if you use the following directive, both directives should be updated with the same values.
<other-directive
on-start="myCtrl.onStart"
on-finish="myCtrl.onFinish"
on-error="myCtrl.onError">
</other-directive>

What is the scope of ngModel directive in AngularJS?

I think that ngModel directive should not create new scope as it needs to make changes in the variables of parent scope.
Please correct me if i am wrong .
And also looking at the source of ngModel directive scope is not defined so it should not create a new scope for directive.
var ngModelDirective = ['$rootScope', function($rootScope) {
return {
restrict: 'A',
require: ['ngModel', '^?form', '^?ngModelOptions'],
controller: NgModelController,
// Prelink needs to run before any input directive
// so that we can set the NgModelOptions in NgModelController
// before anyone else uses it.
priority: 1,
compile: function ngModelCompile(element) {
// Setup initial state of the control
element.addClass(PRISTINE_CLASS).addClass(UNTOUCHED_CLASS).addClass(VALID_CLASS);
return {
pre: function ngModelPreLink(scope, element, attr, ctrls) {
var modelCtrl = ctrls[0],
formCtrl = ctrls[1] || modelCtrl.$$parentForm;
modelCtrl.$$setOptions(ctrls[2] && ctrls[2].$options);
// notify others, especially parent forms
formCtrl.$addControl(modelCtrl);
attr.$observe('name', function(newValue) {
if (modelCtrl.$name !== newValue) {
modelCtrl.$$parentForm.$$renameControl(modelCtrl, newValue);
}
});
scope.$on('$destroy', function() {
modelCtrl.$$parentForm.$removeControl(modelCtrl);
});
},
post: function ngModelPostLink(scope, element, attr, ctrls) {
var modelCtrl = ctrls[0];
if (modelCtrl.$options && modelCtrl.$options.updateOn) {
element.on(modelCtrl.$options.updateOn, function(ev) {
modelCtrl.$$debounceViewValueCommit(ev && ev.type);
});
}
element.on('blur', function() {
if (modelCtrl.$touched) return;
if ($rootScope.$$phase) {
scope.$evalAsync(modelCtrl.$setTouched);
} else {
scope.$apply(modelCtrl.$setTouched);
}
});
}
};
}
};
}];
Also I don't understand why ngModel directive requires ngModel itself.
require: ['ngModel', '^?form', '^?ngModelOptions']
Can't it be ignored and written like
require: ['^?form', '^?ngModelOptions']
If not then please explain why ?
ngModel doesn't create an isolated scope. The reason ngModel is listed in the require array is so that its controller (NgModelController) will be injected into the link function. Notice the ctrls argument that is passed into the ngModelPostLink function. Because ngModel is listed in the array, ctrls[0] will be an instance of the NgModelController. ctrls[1] is the form controller, etc.

What is the point of controller.$viewValue/controller.$modelValue?

I'm unclear what the relation is between scope.ngModel and controller.$viewValue/controller.$modelValue/controller.$setViewValue() is, and specifically, what the point of the latter three is. For example, see this jsfiddle:
<input type="text" ng-model="foo" my-directive>
and:
myApp.directive('myDirective', function($timeout) {
return {
require: 'ngModel',
restrict: 'A',
scope: { ngModel: '=' },
link: function (scope, element, attrs, controller) {
function log() {
console.log(scope.ngModel);
console.log(controller.$viewValue);
console.log(controller.$modelValue);
}
log();
controller.$setViewValue("boorb");
log();
scope.$watch('ngModel', function (val) {
console.log("val is now", val);
});
$timeout(function () {
log();
}, 2000);
}
}
});
With the controller being:
function MyCtrl($scope, $timeout) {
$scope.foo = 'ahha';
$timeout(function () {
$scope.foo = "good";
}, 1000);
}
The output is:
(index):45 ahha
(index):46 NaN
(index):47 NaN
(index):45 ahha
(index):46 boorb
(index):47 boorb
(index):53 val is now ahha
(index):53 val is now good
(index):45 good
(index):46 boorb
(index):47 boorb
controller.$viewValue did not start out as the value of the foo variable. Further, controller.$setViewValue("boorb") didn't influence scope.ngModel at all, nor was the update reflected in the HTML. Thus it seems there is no relation between scope.ngModel and controller.$viewValue. It seems that with anything I'd want to do, I would just use scope.ngModel, and watch those values. What is ever the point of using controller.$viewValue and controller.$modelValue or keeping them up to date with scope.ngModel?
scope: { ngModel: '=' }, creates an isolated scope for the directive, which means that changes to foo in the directive will no longer be reflected in the parent scope of MyCtrl.
Also, changes made by $setViewValue() will not get reflected in the DOM until controller.$render() is called, which tells Angular to update the DOM in the next digest cycle.
But to answer the question, NgModelController and its methods are really only necessary if you need to create some extra-special-custom-fancy data-binding directives. For normal data input and validation, you shouldn't ever need to use it. From the documentation (emphasis mine):
[NgModelController] contains services for data-binding, validation, CSS updates, and value formatting and parsing. It purposefully does not contain any logic which deals with DOM rendering or listening to DOM events. Such DOM related logic should be provided by other directives which make use of NgModelController for data-binding to control elements. Angular provides this DOM logic for most input elements.
The confusion here is coming from sticking a directive onto an existing directive, namely ngInput.
Instead, consider a fresh directive:
<my-directive ng-model="ugh">Sup</my-directive>
With:
$rootScope.ugh = 40;
And:
.directive('myDirective', function () {
return {
require: "ngModel",
// element-only directive
restrict: "E",
// template turns the directive into one input tag
// 'inner' is on the scope of the *directive*
template: "<input type='text' ng-model='inner'/>",
// the directive will have its own isolated scope
scope: { },
link: function (scope, element, attrs, ngModelCtrl) {
// formatter goes from modelValue (i.e. $rootScope.ugh) to
// view value (in this case, the string of twice the model
// value + '-'
ngModelCtrl.$formatters.push(function (modelValue) {
return ('' + (modelValue * 2)) + '-';
});
// render does what is necessary to display the view value
// in this case, sets the scope.inner so that the inner
// <input> can render it
ngModelCtrl.$render = function () {
scope.inner = ngModelCtrl.$viewValue;
};
// changes on the inner should trigger changes in the view value
scope.$watch('inner', function (newValue) {
ngModelCtrl.$setViewValue(newValue);
});
// when the view value changes, it gets parsed back into a model
// value via the parsers, which then sets the $modelValue, which
// then sets the underlying model ($rootScope.ugh)
ngModelCtrl.$parsers.push(function (viewValue) {
var sub = viewValue.substr(0, viewValue.length-1);
return parseInt(sub)/2;
});
}
};
})
Try it on Plunker.
Note that typeof ugh stays "number", even though the directive's view value is of a different type.

Custom validators with AngularJs

I'm writing my own custom AngularJs validators which look like this:
.directive('float', function ($log) {
return {
restrict: 'A',
require: 'ngModel',
scope: {float: '='},
link: function ($scope, ele, attrs, ctrl) {
var settings = $scope.float || {};
ctrl.$validators.float = function(value) {
var valid = isTheInputValidFunction( settings );
ctrl.$setValidity('float', valid);
return valid;
};
}
};
});
I'm using the validators like so:
<input type="text"ng-model="someVar" name="preis" float="{precision: 5, scale: 2}">
However, as soon as I attach multiple validators, I get the following error:
Multiple directives [...] asking for new/isolated scope
This is, because all my validators get a settings-object which has to be passed into the scope scope: {float: '='}.
I know that I can use var settings = JSON.parse(attrs.float); in the directives, but it doesn't look right.
So my question is:
How do you correctly implement custom validators in AngularJs?
It really depends on whether you expect the settings to change.
If you think it will be constant, like in the example you've shown, then simply parsing once the value will be enough. The appropriate service to use in such a case is $parse:
link: function ($scope, ele, attrs, ctrl) {
var settings = $parse(attrs.float)($scope);
// …
}
If you think it may be used with a variable, you should watch its content:
link: function ($scope, ele, attrs, ctrl) {
var settings = undefined;
$scope.$watch(attrs.float, function (newSettings) {
settings = newSettings;
});
// …
}
Perhaps it is because you are calling $setValidity. I beleive the whole point of the $validators pipeline was to do it for you. Just return a boolean.
ctrl.$validators.float = function(value) {
return isTheInputValidFunction( settings );
};

passing defined object to another custom directive angularjs

so I have this custom directives that you could see below.
myApp.directive('myDirective', function (testService) {
return {
restrict:'EA',
link:function (scope, element, attr) {
//defined the object
var object = new object();
testService.setObject(object);
}
}
});
myApp.directive('mySecondDirective', function (testService) {
return {
restrict:'EA',
link:function (scope, element, attr) {
//call the variable from previous custom directive
console.log(testService.getobject()); -> always return undefined
}
}
});
and this is the html structure where I used the directives above.
<my-directive></my-directive>
<my-second-directive></my-second-directive>
there I want to retreive the object that contains new object() from previous custom directive, but it always return an undefined I wonder how could I do this without using require nor isolated scope as well.. could you guys help me ?
UPDATE
I create a service to provide the facility to set and retreive the object and apparently it returned undefined because I set the custom direcitve this way
<my-second-directive></my-second-directive>
<my-directive></my-directive>
and this is the service
define(
['services/services'],
function(services)
{
'use strict';
services.factory('testService', [function() {
var me = this;
var testObject = '';
return {
setObject: function(object) {
me.testObject = object;
},
getObject: function() {
return me.testObject;
}
}
}
]);
}
);
the thing is that I actually set the html markup like I already mentioned above which is
<my-second-directive></my-second-directive>
<my-directive></my-directive>
so could you give me some advice on how should I do this ?
note* that the passing object actually worked I prefer using services because it will easy to mantain latter. The question is how do I make the object accessible from another directive even though the initiate install of the object (set the object) in the directive that I defined at the html markup, as the last position of the html it self ?
UPDATE this is the PLUNKER that I've been made for you to understand the question it self
You could achieve this by firing a custom event and then listening for it in the second directive. Here's an updated plunker: http://plnkr.co/edit/512gi6mfepyc04JKfiep?p=info
Broadcast the event from the first directive:
app.directive('myDirective', function(testService) {
return {
restrict: 'EA',
link: function(scope, elm, attr) {
var object = {};
testService.setObject(object);
console.log('setting object');
scope.$broadcast('objectSet');
}
}
});
... and then listen for it on the second:
app.directive('mySecondDirective', function(testService) {
return {
restrict: 'EA',
link: function(scope, elm, attr) {
scope.$on('objectSet', function(){
console.log('retrieving object', testService.getObject());
})
}
}
});
You could also pass data along with the event, if you wanted to emit a specific piece of data to be picked up by the second directive.
1). Scope. Since you don't want to use controllers and isolated scope, then you can simply set this object as scope property.
myApp.directive('myDirective', function() {
return {
restrict: 'EA',
link: function(scope, element, attr) {
var object = {};
object.test = 21;
// set object as scope property
scope.object = object;
}
}
});
myApp.directive('mySecondDirective', function() {
return {
priority: 1, // priority is important here
restrict: 'EA',
link: function(scope, element, attr) {
console.log('Retrieve: ', scope.object);
}
}
});
Just make sure you are also defining priority on the second directive (only if both directive a applied to the same element) to make sure it's evaluated in proper turn (should be bigger then the one of myDirective, if omitted it's 0).
2). Service. Another simple solution is to use service. You can inject custom service into both directives and use it storage for you shared object.
Expanding from what #gmartellino said.
Anything that you wanted to do after listening to the event in second directive, can have a callBack method and use it.
app.directive('mySecondDirective', function(testService) {
return {
restrict: 'EA',
link: function(scope, elm, attr) {
// what if I created like this ?
// define the test variable
var test;
scope.$on('objectSet', function(){
//set the variable
test = testService.getObject();
console.log('retrieving object : ', testService.getObject());
//Anything that you wanted to do
//after listening to the event
//Write a callBack method and call it
codeToExecuteAsCallBack();
})
//then use this method to make a call back from the event
//and outside the event too.
var codeToExecuteAsCallBack = function(){
console.log(test);
}
codeToExecuteAsCallBack();
}
}
});
Updated plnkr link

Categories

Resources