Goal: create an isolate-scoped directive that, when applied to an input element with an existing ng-model directive, constrains the value of the model arbitrarily, reflecting the changes back to the parent scope. Do all of this with "controller-as" naming.
For example, this template represents a possible use of the aforementioned directive, assuming the containing controller is outerCtrl:
<input ng-model="outerCtrl.value" my-directive>
Where the following represents a portion of the definition for myDirective (absent the functional pieces, which are represented as comments and are the actual question at hand):
angular.module('app').directive('myDirective', [function() {
return {
restrict: 'A',
scope: {},
require: 'ngModel',
controllerAs: 'myCtrl',
bindToController: true,
controller: controller: [function() {
var vm = this;
vm.magicValue = 'foo';
}],
// ----------
// Insert appropriate compile, link, etc. to ensure that if
// the user ever types the "magic value" in the input, then
// the parent model (outerCtrl.value) is set with the magic
// value; otherwise, the value set on the parent model should
// be "null" or "undefined." And, of course, if there is ever
// a programmatic setting of outerCtrl.value, that should be
// reflected down to the input while obeying the same rule -
// i.e., if some value other than the magic value is set, it
// should be ignored.
//
// Of critical importance: there may be other directives
// operating on the same ng-model within the isolate scope
// (or, rather, as child scopes of the isolate scope).
// These other directives should work in concert with
// myDirective. For example, one such directive might be a
// "typeahead" directive that manipulates the model based on
// selection from a dropdown list. This behavior can't be
// trampled by myDirective.
// ----------
};
}]);
My primary motivation in using isolate scope is to ensure that the directive is reusable. And my motivation in not using a wrapper div or other element to contain an input in the directive's template is to make it easier to use the directive on any existing input element, thereby also taking advantage of ng-model validation that might already be working in the parent scope.
Is this possible? More importantly, should I even be attempting this, or is it a misuse of ng-model/isolate scopes/directives per the intentions of the framework designers?
Related
When building angular directives, I've found there is more than one way to pass a value from the parent scope to the child scope. Some ways I'm aware of are:
Don't use an isolate scope at all, and the child will simply have
access to the parent scope (you can mount a pretty good argument that this is bad).
Use the attributes parameter of a link function.
Use an isolate scope and bind to the attribute (e.g. param: '=')
The codepen here: https://codepen.io/ariscol/pen/WEKzMe shows two similar directives, one done with link and one done with 2-way binding in an isolate scope. Furthermore, it shows how they differ as far as 1-time binding compared to 2-way binding. For reference, here are the two directives:
app.directive("contactWidgetWithScope", function() {
return {
restrict: 'E',
template: '<div ng-bind="contact.name"></div>'
+ '<div ng-bind="contact.title"></div>'
+ '<div ng-bind="contact.phone"></div>',
scope: {
contact: '='
}
};
});
app.directive("contactWidgetWithLink", function() {
return {
restrict: 'E',
template: '<div ng-bind="name"></div>'
+ '<div ng-bind="title"></div>'
+ '<div ng-bind="phone"></div>',
scope: {},
link: function(scope, elem, attrs) {
scope.name = attrs.contactname;
scope.title = attrs.contacttitle;
scope.phone = attrs.contactphone;
}
};
});
Now, if I were trying to decide which way was "better", I might consider how I was going to use this directive. If I was going to have a thousand contacts, and I wanted to use this directive to list all one thousand contacts on a page, in an ng-repeat, for example, I imagine that I would have significantly better performance with link, as it won't add any watchers. On the other hand, if I wanted this directive to be incorporated into a page header, and I wanted the contact details to be updated as you clicked on any given contact in a list, I would want 2-way binding, so that any change to some "selectedContact" property in a parent scope would be automatically reflected in this directive. Are those the proper considerations? Are there others?
To add to my confusion, it is simple to add an observer to a linked attribute and achieve a 1-way binding such that a change in the value of the attribute will be reflected in the child. Would doing this have more or less of a performance impact? Conversely, I imagine you could do a 1-time binding on the value of the scope version and thereby eliminate the performance impact, e.g.: <contact-widget-with-scope contact="::vm.contact">. That should work, right? Seems like that option gives you a lot of flexibility, because it means the person who invokes the directive can decide if they want to pay the performance price to get the benefit of 2-way binding or not. Are these considerations accurate? Are there other things I ought to consider when deciding how to make values available to my directives?
What I think I want to do is completely isolate each step of a wizard into a custom element directive.
I think this would allow me to completely encapsulate the detail of each page of the wizard. For example:
<custom-wizard-tag>
<enter-name-page page="1" name-placeholder="name"/>
<enter-address-page page="2" name-placeholder="name" address-placeholder="address" last-page/>
</custom-wizard-tag>
So far, so good. Each of the elements above has its own directive, and each of these specifies a templateUrl and a controller (templateUrl could be supplied as an attribute, of course).
I want each page of the wizard to 'inherit' some behaviour. The UI components would contain the buttons, which would need to query the outer scope, for example to determine whether it is possible to move forward, backward and so on. We would also need to call member functions on the parent scope in order to actually move the wizard forwards and backwards, and to check whether the current page number matches 'ours'.
I'm new to this so bear with me...
I read the documentation on directive, and thought I could use scope: { onNext: '&onNext' } in order to 'inherit' the onNext function from the previous scope (which is assumed to be one which is 'wizard-like'). However, this is not what angular seems to do. It seems to want map the inner scope's onNext via an attribute called on-next, thus breaking encapsulation, because now the UI elements must reference functions in the parent scope - which is exactly what I wanted to avoid.
Am I barking up the wrong tree, or is there an idiomatic way to do this. A day of web searching has not got me far, but I could be using the wrong search terms.
Thanks for your patience.
scope: { onNext: '&onNext' }
won't do any inherintance, you would have to define onNext in the template (the template scope) the same way you do with the page property: <enter-name-page page="1"
If you have a function onNext defined in you customWizardTag directive either in link function or in its controller, you'll have to put it in the controller, because the controller can be passed to the child directive. Then you'll be able to pass the parent directive's controller in the link functions of somethingPage directives.
.directive('parentDirective, function() {
return {
controller: someControllerThatHasOnNext,
}
})
.directive('childDirective', function() {
return {
require: '^^parentDirective',
link: function(scope, element, attrs, theParentDirectivesController){
theParentDirectivesController.onNext();
}
}
})
If this is what you wanted
Trying to write a angular model that allows for a two way binding, I.E. where it has a ng-model variable, and if the controller updates it, it updates for the directive, if the directive updates it, it updates for the controller.
I have called the controller scope variable ng-model, and the directive model bindModel. In this case it sends an array of data, but that should not make a difference to how the binding works (I think at least).
So first the mapping, this is how the initial directive looks (link comes later).
return {
link: link,
scope: { cvSkills: '=',
bindModel:'=ngModel'},
restrict: 'E'
}
My understanding is (and I am uncertain about the exact scope at this moment) the directive will be aware of cvSkills (called cvSkills internally, and used to provide intial data), and bindModel which should pick up whats in ng-model).
The call in html is:
<skilltree cv-skills="cvskills" ng-model="CV._skillSet"></skilltree>
So the variables aren't actually (quite) in the directive yet. But there is an awareness of them, so I created two watchers (ignoring the cvskills one for now)
function link(scope,element, attr){
//var selected = ["55c8a069cca746f65c9836a3"];
var selected = [];
scope.$watch('bindModel', function(bindModel){
if (bindModel.length >> 0) {
console.log("Setting " + bindModel[0].skill)
selected = bindModel;
}
})
scope.$watch('cvSkills', function(cvSkills) {
So once the scope.$watch sees an update to bindModel, it picks it up and stores it to selected (same happens separately with cvSkills). In cvskills I then do updates to selected. So it will add additional data to selected if the user clicks buttons. All works and nothing special. My question is now. How do I then update bindModel (or ngModel) when there are updates to selected so that the controllers scope picks it up. Basically, how do I get the updates to propagate to ng-model="CV._skillSet"
And is this the right way to do it. I.E. make the scope aware, than pick up changes with scope.$watch and manually update the variable, or is the a more "direct" way of doing it?
=================== Fix using ngModel ===================
As per the article, if you add require: "ngModel" you get a fourth option to the function (and skip having a binding between ngModel and bindModel).
return {
link: link,
require: 'ngModel',
scope: { cvSkills: '='},
restrict: 'E'
}
Once you have done that, ngModel.viewValue will contain the data from ngModel= , in my case I am updating this in the code (which may be a bad idea). then call ngModel.setViewValue. It is probably safeish if you set the variable and then store it straight away (as follows)
function link(scope,element, attr, ngModel){
ngModel.$render = function() {
console.log("ngRender got called " + ngModel.$viewValue );
} ;
....
ngModel.$viewValue.push(newValue);
ngModel.$setViewValue(ngModel.$viewValue)
================= If you don't care about viewValue ==================
You can use modelValue instead of viewValue, and any updates to modelValue is propagated straight through.
function link(scope,element, attr, ngModel){
ngModel.$render = function() {
console.log("ngRender got called " + ngModel.$modelValue );
} ;
....
ngModel.$modelValue.push(newValue);
Using the require attribute of the directive you can have the controller API from ngModel directive. So you can update values.
Take a look at this very simple demo here : https://blog.hadrien.eu/2015/01/16/transformation-avec-ngmodel/
For more information here is the documentation : https://docs.angularjs.org/#!/api/ng/type/ngModel.NgModelController
I have the following set up as a directive in my angular project:
{
restrict: "AE",
replace: true,
template: template,
require: "ngModel",
scope: {
chosen: "=ngModel",
choices: "=choices",
placeholder: "#placeholder"
}
}
I have everything working internally for my directive, the missing piece right now is that when I select a value inside of it, the parent scope containing my directive isn't receiving any kind of update. Despite me making assigments to chosen from anywhere inside of my directive.
As the title states, what's the simplest way for me to assign a value chosen inside of my directive, to it's parent's scope?
Ideally I'd like the solution to:
Not require me to use the link function - I feel like this can be done declaratively
Not require my directive to guess anything about it's parent scope
As a followup question, is there any reason to use ngModel in this circumstance? Is it or could it be beneficial? Or could I just as easily get away with recycling a name attribute which contains the parent scope's desired return value?
I am attempting to create a custom tag similar to the following:
<mytag type="Big" />
where type is an attribute that gets bound to the component. in such a way that it sets the text in a label, as shown below:
<label>{{type}}</label>
... (other components)...
As the documentation says, I have a controller that sets a default type:
$scope.type = "Small";
so that if I use my tag without the attribute type still gets set.
I am attempting to do binding using a directive:
angular.module('TestPage',[])
.directive('mytag',function() {
return {
restrict: 'E',
templateUrl: 'component.html',
scope: {
type: '='
}
}
});
Note that I do have the appropriate ng-app settings in my component template (ng-app="TestPage").
My problem is that the binding to type does not appear to be actually binding anything.
I have read the documentation about how to bind a variable to components using directive. According to the documentation, you can do such bindings inside a scope. Scopes apparently can contain an "object-hash" (whatever that is!) which creates something called an "isolate scope" (???). Such scopes can represent "local properties" in the following ways:
# or #attr - bind a local scope property to the DOM attribute. The result is always a string
since DOM attributes are strings. If no attr name is specified then the local name and
attribute name are same. Given and widget definition of scope: { localName:'#myAttr' }, then widget scope property localName will reflect the interpolated value of hello {{name}}. As the name attribute changes so will the localName property on the widget scope. The name is read from the parent scope (not component scope).
Huh??? What has all this to do with the proper syntax for binding?
= or =expression - set up bi-directional binding between a local scope property and the parent
scope property. If no attr name is specified then the local name and attribute name are same.
Given and widget definition of scope: { localModel:'=myAttr' }, then widget scope property localName will reflect the value of parentModel on the parent scope. Any changes to parentModel will be reflected in localModel and any changes in localModel will reflect in parentModel.
Excuse me? What is being said here???
& or &attr - provides a way to execute an expression in the context of the parent scope. If no
attr name is specified then the local name and attribute name are same. Given
and widget definition of scope: { localFn:'increment()' },
then isolate scope property localFn will point to a function wrapper for the increment() expression. Often it's desirable to pass data from the isolate scope via an expression and to the parent scope, this can be done by passing a map of local variable names and values into the expression wrapper fn. For example, if the expression is increment(amount) then we can specify the amount value by calling the localFn as localFn({amount: 22}).
Now I'm totally confused! You have widget tags and some kind of related function that I have to write iin order to do the bind??? All I want is to bind a value to a label tag!
I have copied the above text from the documentation (http://docs.angularjs.org/guide/directive) to make a point: that this doco reads like the old UNIX documentation: really useful to those who already know the system, but not so helpful to beginners who are trying to develop real expertise. With all the tutorials that show how to do simple tasks in AngularJS (great for toy apps but not so good for the kinds of client- side applications I want to build), why aren't there any for the more advanced stuff???
Okay, time for me to be more constructive.
Can someone please provide some nice, simple examples of how to do the various bindings that this documentation is trying so hard to describe??? Examples that show the proper syntax for these scope statements and descriptions (in plain English) of exactly how they go back to the attribute being added to the custom tag???
Thank you for your patience and thanks in advance for any assistance.
I struggled a bit with this documentation too when first getting into angular, but I will make an attempt try to clarify things for you. First, when using this scope property, it creates an "isolated scope." All this means is that it won't inherit any properties from parent scopes, and so you don't have to worry about any collisions within the scope.
Now, the '#' notation means that the evaluated value in the attribute will automatically get bound into your scope for the directive. So, <my-directive foo="bar" /> would end up with the scope having a property called foo that holds the string "bar". You could also do something like <my-directive foo="{{bar}}" And then the evaluated value of {{bar}} will be bound to the scope. Since attributes are always strings, you will always end up with a string for this property in the scope when using this notation.
The '=' notation basically provides a mechanism for passing an object into your directive. It always pulls this from the parent scope of the directive, so this attribute will never have the {{}}. So, if you have <my-directive foo="bar" /> it will bind whatever is in $scope.bar into your directive in the foo property of your directive's scope. Any change's you make to foo within your scope will be refelected in bar in the parent scope, and vice versa.
I haven't used the '&' notation nearly as much as the other too, so I don't know it as well as those two. From what I understand, it allows you to evaluate expressions from the context of the parent scope. So if you have something like <my-directive foo="doStuff()" />, whenever you call scope.foo() within your directive, it will call the doStuff function in the directive's parent scope. I'm sure there's a lot more you can do with this, but I'm not as familiar with it all. Maybe someone else can explain this one in more detail.
If just the symbol is set in the scope, it will use the same name as the attribute to bind to the directives scope. For example:
scope: {
foo1: '#',
foo2: '=',
foo3: '&'
}
When including the directive, there would need to be the attributes foo1, foo2, and foo3. If you want a property in your scope different than the attribute name, you can specify that after the symbol. So, the example above would be
scope: {
foo1: '#bar1',
foo2: '=bar2',
foo3: '&bar3'
}
When including the directive, there would need to be the attributes bar1, bar2, and bar3, and these would get bound in the scope under properties foo1, foo2, and foo3 respectively.
I hope this helps. Feel free to ask questions with which I can clarify my answer.
Youre pretty close..
app.directive('mytag',function() {
return {
restrict: 'E',
template: '<div>' +
'<input ng-model="controltype"/>' +
'<button ng-click="controlfunc()">Parent Func</button>' +
'<p>{{controlval}}</p>' +
'</div>',
scope: {
/* make typeattribute="whatever" bind two-ways (=)
$scope.whatever from the parent to $scope.controltype
on this directive's scope */
controltype: '=typeattribute',
/* reference a function from the parent through
funcattribute="somefunc()" and stick it our
directive's scope in $scope.controlfunc */
controlfunc: '&funcattribute',
/* pass a string value into the directive */
controlval: '#valattribute'
},
controller: function($scope) {
}
};
});
<div ng-controller="ParentCtrl">
<!-- your directive -->
<mytag typeattribute="parenttype" funcattribute="parentFn()" valattribute="Wee, I'm a value"></mytag>
<!-- write out your scope value -->
{{parenttype}}
</div>
app.controller('ParentCtrl', function($scope){
$scope.parenttype = 'FOO';
$scope.parentFn = function() {
$scope.parenttype += '!!!!';
}
});
The magic is mostly in the scope: declaration in your directive definition. having any scope: {} in there will "isolate" the scope from the parent, meaning it gets it's own scope... without that, it would use the parent's scope. The rest of the magic is in the scope's properties: scope: { 'internalScopeProperty' : '=externalAttributeName' }... where the = represents a two way binding scenario. If you change that = to a # you'll see it just allows you to pass a string as an attribute to the directive. The & is for executing functions from the parent scope's context.
I hope that helps.
EDIT: Here is a working PLNKR