ngModel, dot-rule and more than one level of scope hierarchy - javascript

Let's say I want to write HTML like:
<div my-directive my-start="topModel.start" my-end="topModel.end">
my-directive has a template that invokes other directives with ngModel, like so:
<div>
<input ng-model="myStart" />
<input ng-model="myEnd" />
</div>
I would like the inner inputs to transparently update topModel. It doesn't work this way, because there's no dot in the ng-model attribute and the value is set in local scope.
The only way I found so far is to watch both models in my-directive and translate, but it's a horrible abomination.
restrict: 'A',
scope: {
myStart: '=',
myEnd: '='
},
link: function(scope, el, attrs) {
scope.model = { start: scope.myStart, end: scope.myEnd };
scope.$watch("model.start", function(n) {
scope.myStart = n;
});
scope.$watch("model.end", function(n) {
scope.myEnd = n;
});
scope.$watch("myStart", function(n) {
scope.model.start = n;
});
scope.$watch("myEnd", function(n) {
scope.model.end = n;
});
}
How can I pass the bindings through my-directive to the inner directives without all this manual synchronization?
EDIT: See plunker at http://plnkr.co/edit/ppzVd7?p=preview - this one actually works
EDIT2: See another at http://plnkr.co/edit/Nccpqn?p=preview - this one shows how "direct access" without dot doesn't work, and with dot and $watches does.

When you define the scope property as you do on your directive, you will automatically get two properties, scope.myStart and scope.myEnd, with a bidirectional binding to topModel. When you map them over to scope.model you break that binding.
Here is a working example:
module.directive('myDirective', function () {
return {
scope: {
myStart: '=',
myEnd: '='
},
template: '<p><label>Start: <input type="text" ng-model="myStart" /></label></p>' +
'<p><label>End: <input type="text" ng-model="myEnd" /></label></p>',
link: function (scope, element, attrs) {
scope.$watch('myStart', function (val, old) {
console.log('Start changed from', old, 'to', val);
});
scope.$watch('myEnd', function (val, old) {
console.log('End changed from', old, 'to', val);
});
}
};
});
Plunker

With your 2nd example I finally understood your problem, though I still don't think it's comming from breaking the dot-rule. You're simply mixing scopes / model binding (see $parent.myStart).
Try:
(function (app, ng) {
'use strict';
app.controller('MyCtrl', ['$scope', function($scope) {
$scope.topModel = {
start: 'Initial Start',
end: 'Initial end'
};
}]);
app.directive('myDirective', function(){
return {
scope: { myStart: '=', myEnd: '=' },
template: '<div>\
<p>Start: <span special-editor-a ng-model="$parent.myStart"></span></p>\
<p>End: <span special-editor-b ng-model="myEnd"></span></p>\
</div>'
}
});
// with replace
app.directive('specialEditorA', function(){
return {
scope: true,
require: 'ngModel',
replace: true,
template: '<input type="text" />'
}
});
// without replace
app.directive('specialEditorB', function(){
return {
scope: { m: '=ngModel' },
require: 'ngModel',
template: '<input type="text" ng-model="m" />'
}
});
}(angular.module('app', []), angular));
demo: http://plnkr.co/edit/SfFBf5NgFhSOQiFf5Emc?p=preview

Related

Proper "Angular" way to pass behavior to directive?

When looking for information regarding Angular directives and passing behavior to directives, I get ended up being pointed in the direction of method binding on an isolate scope, i.e.
scope: {
something: '&'
}
The documentation for this functionality is a bit confusing, and I don't think it'll end up doing what I want.
I ended up coming up with this snippet (simplified for brevity), that works by passing a scope function in HomeCtrl, and the directive does it's work and calls the function. (Just incase it matters, the real code passes back a promise from the directive).
angular.module('app', []);
angular.module('app')
.directive('passingFunction',
function() {
var changeFn,
bump = function() {
console.log('bump() called');
internalValue++;
(changeFn || Function.prototype)(internalValue);
},
internalValue = 42;
return {
template: '<button ng-click="bump()">Click me!</button>',
scope: {
onChange: '<'
},
link: function(scope, element, attrs) {
if (angular.isFunction(scope.onChange)) {
changeFn = scope.onChange;
}
scope.bump = bump;
}
};
})
.controller('HomeCtrl',
function($scope) {
$scope.receive = function(value) {
console.log('receive() called');
$scope.receivedData = value;
};
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.4/angular.min.js"></script>
<div ng-app="app" ng-controller="HomeCtrl">
<passing-function on-change="receive"></passing-function>
<p>Data from directive: {{receivedData}}</p>
</div>
Is this a proper "Angular" way of achieving this? This seems to work.
What you need is to pass the function to the directive. I'll make a very small example.
On controller:
$scope.thisFn = thisFn(data) { console.log(data); };
In html:
<my-directive passed-fn="thisFn()"></my-directive>
On directive:
.directive('myDirective', [
() => {
return {
restrict: 'E',
scope: {
passFn: '&'
},
template: '<div id="myDiv" ng-click="passFn(data)"></div>',
link: (scope) => {
scope.data = "test";
}
}
}]);

Angularjs directive

I want to have an attribute directive a bit similar to ng-model. I just want to additionally bind an input fields value to a scope variable (just in one direction input field -> scope variable). So I have just tried this directive but I can not get the directive called anyway.
script:
.directive('passivemodel', function () {
return {
restrict: "A",
scope: {
passivemodel: '='
},
link: function (scope, element, attrs) {
scope.$watch('passivemodel', function(newPassivemodel, oldPassivemodel) {
console.log("passive model", newPassivemodel);
});
}
};
})
html:
<input data-passivemodel="keyword">
Edit:
Hmmm .. based on vilo20 answer I am running into a very strange behavior.
while this code is working very well:
<input data-ng-model="keyword" data-passivemodel="keyword">
this one does not (note the value of passivemodel):
<input data-ng-model="keyword" data-passivemodel="keyword2">. Sure I have defined the variable in the controller.
Controller:
.controller('SearchController', function($scope, $routeParams, $search) {
$scope.search = new $search();
$scope.keyword = "";
$scope.keyword2 = "";
})
Edit2: here is a fiddle http://jsfiddle.net/HB7LU/12107/
try this:
.directive('passivemodel', function () {
return {
restrict: "A",
scope: {
passivemodel: '='
},
link: function (scope, element, attrs) {
console.log("passive model", scope.passivemodel);
$scope.$watch('passivemodel', function(newPassivemodel, oldPassivemodel) {
//put your logic when passivemodel changed
});
}
};
})
Hope it helps
Edit: Here is a plunker http://plnkr.co/edit/T039I02ai5rBbiTAHfzv?p=preview
Use the scope attribute:
.directive('passivemodel', function () {
return {
restrict: "A",
scope: {
"passivemodel": "="
},
link: function (scope, element, attrs) {
console.log("access passivemodel: ", scope.passivemodel);
}
};
})
Finally it was as simple as that:
.directive('modelRed', [function(){
return {
require: 'ngModel',
restrict: 'A',
scope: {},
link: function (scope, element, attrs, ngModel) {
scope.$watch(function () {
return ngModel.$modelValue;
}, function(newValue) {
scope.$parent[attrs.modelRed] = newValue;
//console.log(attrs.modelRed, newValue, scope);
});
}
}
}])
And in the html:
<p><input type="text" data-ng-model="testInput" data-model-red="redInput">{{redInput}}</p>
Of course you have to define testInput and redInput in the $scope object.

AngularJs Directive with dynamic Controller and template

I want to create a directive that has dynamic view with dynamic controller. the controller and the template view is coming from the server.
The Directive
var DirectivesModule = angular.module('BPM.Directives', []);
(function () {
'use strict';
angular
.module('BPM.Directives')
.directive('bpmCompletedTask', bpmCompletedTask);
bpmCompletedTask.$inject = ['$window'];
function bpmCompletedTask ($window) {
// Usage:
// <bpmCompletedTask></bpmCompletedTask>
// Creates:
//
var directive = {
link: link,
restrict: 'E',
scope: {
type: '=',
taskdata: '=',
controllername:'#'
},
template: '<div ng-include="getContentUrl()"></div>',
controller: '#',
name: 'controllername'
};
return directive;
function link(scope, element, attrs) {
scope.getContentUrl = function () {
return '/app/views/TasksViews/' + scope.type + '.html';
}
scope.getControllerName = function ()
{
console.warn("Controller Name is " + scope.type);
return scope.type;
}
}
}
})();
Here how I'm trying to use the directive
<div ng-controller="WorkflowHistoryController as vm">
<h2>Workflow History</h2>
<h3>{{Id}}</h3>
<div ng-repeat="workflowStep in CompletedWorkflowSteps">
<bpm-completed-task controllername="workflowStep.WorkflowTaskType.DataMessageViewViewName" taskdata="workflowStep.WorkflowTaskOutcome.TaskOutcome" type="workflowStep.WorkflowTaskType.DataMessageViewViewName">
</bpm-completed-task>
</div>
</div>
The problem now is when the directive gets the controller name it get it as literal string not as a parameter.
Is it doable ?
if it's not doable, What is the best solution to create dynamic views with its controllers and display them dynamically inside ng-repeat?
Thanks,
Update 20 Jan I just updated my code in case if some one interested in it. All the Credit goes to #Meligy.
The First Directive:
(function () {
'use strict';
angular
.module('BPM.Directives')
.directive('bpmCompletedTask', bpmCompletedTask);
bpmCompletedTask.$inject = ['$compile', '$parse'];
function bpmCompletedTask ($compile, $parse) {
var directive = {
link: function (scope, elem, attrs) {
console.warn('in the first directive - before if');
if (!elem.attr('bpm-completed-task-inner'))
{
console.warn('in the first directive');
var name = $parse(elem.attr('controllername'))(scope);
console.warn('Controller Name : ' + name);
elem = elem.removeAttr('bpm-completed-task');
elem.attr('controllernameinner', name);
elem.attr('bpm-completed-task-inner', '');
$compile(elem)(scope);
}
},
restrict: 'A',
};
return directive;
}
})();
The Second Directive
angular
.module('BPM.Directives')
.directive('bpmCompletedTaskInner',['$compile', '$parse',
function ($window, $compile, $parse) {
console.warn('in the second directive');
return {
link: function (scope, elem, attrs) {
console.warn('in the second directive');
scope.getContentUrl = function () {
return '/app/views/TasksViews/' + scope.type + '.html';
}
},
restrict: 'A',
scope: {
type: '=',
taskdata: '=',
controllernameinner: '#'
},
template: '<div ng-include="getContentUrl()"></div>',
controller: '#',
name: 'controllernameinner'
};
}]);
The Html
<div ng-repeat="workflowStep in CompletedWorkflowSteps">
<div bpm-completed-task controllername="workflowStep.WorkflowTaskType.DataMessageViewViewName" taskdata="workflowStep.WorkflowTaskOutcome.TaskOutcome"
type="workflowStep.WorkflowTaskType.DataMessageViewViewName">
</div>
</div>
Update:
I got it working, but it's really ugly. Check:
http://jsfiddle.net/p6Hb4/13/
Your example has a lot of moving pieces, so this one is simple, but does what you want.
Basically you need a wrapper directive that takes the JS object and converts into a string property, then you can use هى your directive for everything else (template, scope, etc).
.
Update 2:
Code Inline:
var app = angular.module('myApp', []).
directive('communicatorInner', ["$parse", "$compile",
function($parse, $compile) {
return {
restrict: 'A',
template: "<input type='text' ng-model='message'/><input type='button' value='Send Message' ng-click='sendMsg()'><br/>",
scope: {
message: '='
},
controller: '#'
};
}
]).
directive('communicator', ['$compile', '$parse',
function($compile, $parse) {
return {
restrict: 'E',
link: function(scope, elem) {
if (!elem.attr('communicator-inner')) {
var name = $parse(elem.attr('controller-name'))(scope);
elem = elem.removeAttr('controller-name')
elem.attr('communicator-inner', name);
$compile(elem)(scope);
}
}
};
}
]).
controller("PhoneCtrl", function($scope) {
$scope.sendMsg = function() {
alert($scope.message + " : sending message via Phone Ctrl");
}
}).
controller("LandlineCtrl", function($scope) {
$scope.sendMsg = function() {
alert($scope.message + " : sending message via Land Line Ctrl ");
}
})
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.9/angular.min.js"></script>
<div ng-app="myApp">
<div ng-init="test = {p: 'PhoneCtrl', l: 'LandlineCtrl' }">
<communicator controller-name="test.p" message="'test1'"></communicator>
<communicator controller-name="test.l"></communicator>
</div>
</div>
.
Original (irrelevant now but can help other related issues)
Yes, it should work.
A test with Angular 1.3:
http://jsfiddle.net/p6Hb4/9/
Things to check:
Is the controller defined and added to the module? It will not work
If the controller is just a global function it won't work. It has to be added via the <myModule>.controller("<controllerName>", <functiion>) API
Does ng-controller work? Just adding it to the template
Similarly, does using ng-controller directly outside of the directive work?

Dynamic NG-Controller Name

I want to dynamically specify a controller based on a config that we load. Something like this:
<div ng-controller="{{config.controllerNameString}}>
...
</div>
How do I do this in angular? I thought this would be very easy, but I can seem to find a way of doing this.
What you want to do is have another directive run before anything else is called, get the controller name from some model remove the new directive and add the ng-controller directive, then re-compile the element.
That looks like this:
global.directive('dynamicCtrl', ['$compile', '$parse',function($compile, $parse) {
return {
restrict: 'A',
terminal: true,
priority: 100000,
link: function(scope, elem) {
var name = $parse(elem.attr('dynamic-ctrl'))(scope);
elem.removeAttr('dynamic-ctrl');
elem.attr('ng-controller', name);
$compile(elem)(scope);
}
};
}]);
Then you could use it in your template, like so:
<div dynamic-ctrl="'blankCtrl'">{{tyler}}</div>
with a controller like this:
global.controller('blankCtrl',['$scope',function(tyler){
tyler.tyler = 'tyler';
tyler.tyler = 'chameleon';
}]);
There's probably a way of interpolating the value ($interpolate) of the dynamic-ctrl instead of parsing it ($parse), but I couldn't get it to work for some reason.
I'm using it in ng-repeat, so this is improved code for loops and sub objects:
Template:
<div class="col-xs6 col-sm-5 col-md-4 col-lg-3" ng-repeat="box in boxes">
<div ng-include src="'/assets/js/view/box_campaign.html'" ng-dynamic-controller="box.type"></div>
</div>
Directive:
mainApp.directive('ngDynamicController', ['$compile', '$parse',function($compile, $parse) {
return {
scope: {
name: '=ngDynamicController'
},
restrict: 'A',
terminal: true,
priority: 100000,
link: function(scope, elem, attrs) {
elem.attr('ng-controller', scope.name);
elem.removeAttr('ng-dynamic-controller');
$compile(elem)(scope);
}
};
}]);
Personally the 2 current solutions here didn't work for me, as the name of the controller would not be known when first compiling the element but later on during another digest cycle. Therefore I ended up using:
myapp.directive('dynamicController', ['$controller', function($controller) {
return {
restrict: 'A',
scope: true,
link: function(scope, elem, attrs) {
attrs.$observe('dynamicController', function(name) {
if (name) {
elem.data('$Controller', $controller(name, {
$scope: scope,
$element: elem,
$attrs: attrs
}));
}
});
}
};
}]);

Angular $parser won't work in attribute directive with input template

I really need to leave it as a attribute, company rules.
Here is the jsfiddle:
http://jsfiddle.net/M3sZN/
I can't seem to get the console.logs to work for $parser when I type into the input box.
var module = angular.module("demo", []);
module.directive('lowercase', function() {
return {
require: 'ngModel',
restrict: 'A',
scope: {
ngModel: '='
},
link: function(scope, element, attr, ngModelCntrl) {
ngModelCntrl.$formatters.unshift(function (text) {
console.log('from formatters: ');
});
ngModelCntrl.$parsers.push(function (text) {
console.log('from parsers: ');
});
},
template: '<input ng-model="ngModel" type="text">',
};
});
function Demo($scope) {
$scope.data = 'Some Value';
}
And here is the HTML:
<div ng-app="demo" ng-init="" ng-controller="Demo">
<div lowercase ng-model="data"></div>
</div>
I have tried a lot but this includes the following:
Using require: '^ngModel', instead.
Removing the scope, though I need it for the rest of the directive I'm using.
Changing up the names to use myModel to pass in the model.
And a lot of other things that I have lost track of.
Edit
I was able to get this working like this: http://jsfiddle.net/M3sZN/2/
HTML:
<div ng-app="demo" ng-init="" ng-controller="Demo">
<input lowercase ng-model="data" type="text">
</div>
JS
var module = angular.module("demo", []);
module.directive('lowercase', function() {
return {
require: 'ngModel',
restrict: 'A',
link: function(scope, element, attr, ngModelCtr) {
ngModelCtr.$formatters.unshift(function (text) {
console.log('from formatters: ');
});
ngModelCtr.$parsers.push(function (text) {
console.log('from parsers: ' + text);
return text;
});
}
};
});
module.controller('Demo', Demo);
function Demo($scope) {
$scope.data = 'Some Value';
}
By removing the template and just putting the model directly onto the input field.
This is NOT ideal, since I would like to use a template since I have a ton of attributes that need to go on the input field. I would like to avoid the duplication each time I use this directive.

Categories