Unable to access scope property in directive - angularjs

I'm building a combobox plugin in Angular, and I'm at a point where I'm getting all sorts of weird behaviors I can't figure out. Here's one of them:
I have one directive (called linksEdit) that contains within it comboboxes. In the link function of the linksEdit directive, I set the following array:
scope.levels = [
{ 'id': 'link', 'value': 'Link'},
{ 'id': 'affiliate', 'value': 'Affiliate'},
{ 'id': 'partner', 'value': 'Partner'},
];
In that directive, I have the combobox directive, passing the levels value:
<combobox data="levels" value="cb_value" search="data.level" strict></combobox>
And combobox does have an isolate scope:
scope: {
'data': '=data',
'search': '=search',
'value': '=value',
}
I have code in the combobox that sets the default value of the combobox based on what data.level is. The linksEdit directive is called once (with the new attribute set) and then is being repeated with ng-repeat (to show existing data). I added two console.logs at the beginning of my combobox directive:
console.log(scope);
console.log(scope.data);
For the first instance of linksEdit (not in the repeat), scope.data has the data I expect. In the ng-repeat, scope shows my data property, but scope.data comes up as undefined. I'm not sure why they're behaving so differently.

Related

Angular.js: Data binding not working with directives controller option

Why is angular's data binding not working when I specify a controller in the directives controller option? $scope.emailInvalid.text normally should get mapped to type.text but in my case, nothing get's displayed.
JS:
.directive('alert', function () {
return {
template: '<div>{{type.text}}</div>',
restrict: 'E',
scope: {
type: '='
},
controller: function ($scope) {
$scope.emailInvalid = {
text: 'Text Alert Two'
};
}
};
});
HTML:
<alert type="emailInvalid"></alert>
When I define a separate controller and pass it with ng-controller in the HTML, everything works like expected.
Here is a plunker.
Since you want to display type.text you need to define
$scope.type = {
text: 'Text Alert Two'
};
in your directive controller. By doing so, you don't event have to pass the object to the directive.
PLUNKR
OK, I found a solution:
The problem was that angular fails at mapping $scope.emailInvalid to $scope.type. What I have done in my example with <alert type="emailInvalid"></alert>, is passing an object emailInvalid to the directive. Angular is looking for this object in the scopes model of where I used the directive. Obviously this object doesn't exist and angular can't map anything.
The part I was missing is, that the controller I defined with the directives controller option is defined in a different scope than the controller I used with ng-controller.
To work around this problem I'm now passing a string instead of an object and use switch/case to map the alert type.
plunker

Angular transcluded directive with two-way binding to controller not updating

I am using this https://github.com/blackgate/bg-splitter for creating horizontal and vertical pane splitters. (The full code is in the plunkr I have created)
Since I started using it, I have an issue with the two-way binding of a controller and directive.
The directive has two variables, listData and the selectedOption:
template: '<select ng-model="selectedOption" ng-options="l.name for l in listData" class="form-control"></select>',
scope: {
listData: "=",
selectedOption: "="
},
The controller has these variables and has a watch function to watch for changes:
$scope.listdata = [{id : 1, name : "listitem1"},{id : 2, name : "listitem2"},{id : 3, name : "listitem3"}];
$scope.selectedOption = null;
$scope.$watch('selectedOption', function() {
console.log('updating selected choice');
console.log($scope.selectedOption);
}, true);
And the directive is being used like:
<dropdown list-data="listdata" selected-option="listItem"></dropdown>
Without the paneSplitter the dropdown is working. For some reason, when the bound variable is updated in the dropdown directive, it doesn't get updated in the controller. This is probably a scope issue, but I can't seem to figure it out myself. Any help is greatly appreciated. Please see the following plunkr with the full code:
http://plnkr.co/edit/UnJaPV8LYm3unILEU3Lq
Remember the famous quote: "If you are not using a .(dot) in your models you are doing it wrong?"
If you change the watch to this:
$scope.$watch('data.listItem', function() {
console.log('updating selected choice');
console.log($scope.data.listItem);
}, true);
and in change the Html to this
<dropdown list-data="listdata" selected-option="data.listItem"></dropdown>
Here a Plunker

Angular: How do I dynamically add ng-hide to a template that was loaded via templateUrl?

I'm trying to build a directive that reduces boilerplate for text fields, where the server-side declarations of things like field visibility can be passed in via a model.
I want to load the HTML for a general field from a templateUrl, transform the DOM of that (adding in various attributes and directives to this template) according to the model.
I've got it binding the proper ng-model to the nested input field, but when I try to apply an ng-hide to the top-level element, it shows up in the DOM but does no take effect.
If it were working properly, the code (so far) should be hiding the field, but it is not.
The code is at http://jsbin.com/AHoLAnUg/1/edit, and is reproduced below:
angular.module("directives", []).
directive('tuTextField',
function() {
return {
restrict: 'E',
replace: true,
compile: function(ele, attr) {
var element = jQuery(ele);
var input = jQuery(element.children('input')[0]);
// These work:
element.attr('id', attr.id);
element.attr('class', attr['class']);
// this fails: (I've tried element.attr() as well)
attr.$set('ngHide', attr.model + ".invisible['" + attr.field + "']");
// but this WORKS:
input.attr("ng-model", attr.model + ".fields." + attr.field);
},
templateUrl: '/AHoLAnUg/1.css'
};
}).
controller('v', [ '$scope', function(scope) {
scope.state = {
fields: {
name: "Tony"
},
invisible: {
name: true
},
readonly: {
name: true
},
validations: {
name: {
pattern: "^[a-zA-Z]",
message: "Must begin with a letter"
}
}
};
}]);
You shouldn't manipulate the root element of directive because compile function is called after $compile service finish its job, but you CAN manipulate child elements since they'll be compiled after their parent.
This is an example for directive execution order:
jsFiddle
That's why ngHide in your example won't take effect but ngModel will.
Try wrapping your template with another and manipulate them as you want.

Calling a directive attribute as a nested directives attribute value

EDIT: It turns out that this actually works, it just doesn't show the content behind the 'model' attribute in the inspector. I didn't notice it because there was no content on that particular data point i was using. Facepalm thanks to all who helped.
So I'm trying to make my form structure simpler, by creating a pretty verbose nested directive structure. I'm using pretty basic angular code to achieve this, but for some reason, using an attribute from the parent as the value for a child directive's attribute doesn't work (code below to explain, had to change some proprietary words but its essentially the same). What am I doing wrong?
Parent HTML directive call:
<field-label project-for="projectName" project-model="data.product.projectName">Project Name</field-label>
Directive code:
app.directive("fieldLabel", function() {
return {
restrict: "E",
transclude: true,
scope: { model: '=projectModel', for: '#projectFor' },
templateUrl: 'views/products/label.html',
};
});
EDIT: By Request, the other directive in use here:
app.directive("projectOtherView", function() {
return {
restrict: "E",
scope: { field: '=projectOtherViewModel' },
templateUrl: 'views/products/XXX.html',
};
});
Template HTML
<div class="col-sm-2 text-right">
<label for="{{for}}" class="control-label" ng-transclude></label>
<project-other-view project-other-view-model="model"></project-other-view>
</div>
The 'for' works fine but the 'model' only passes through itself, not what it should be(the model name I passed through at the beginning).
As per my comment. The fact that it doesn't display anything other than what you've defined, in this case the word 'model' is fine, it is still wired up to the parent model. So if you were to put the expression {{field}} into your child template, you would get access to the two way binding in the model. So your model hierarchy looks something like this:
level 1: data.product.projectName (binding to next level: project-model="data.product.projectName")
level 2: projectModel (binding to next level: model: '=projectModel')
level 3: model (binding to next level: project-other-view-model="model")
level 4: projectOtherViewModel (binding to next level: field: '=projectOtherViewModel' )
Remember hyphens are removed and CamelCased so 'project-model' in a template becomes 'projectModel' in the javascript.
See http://plnkr.co/edit/MsAHkTU3KLXWhpplWAPs?p=preview for a working example.
Can you create a jsfiddle that replicates your problem? I have a feeling your problem is related to this

Angularjs + kendoui dropdownlist

I have this directive
angular.module('xxx', [
])
.directive('qnDropdown', [
'$parse',
function($parse) {
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, elem, attr, ngModel) {
scope.$watch(attr.qnDropdown, function(source) {
var model = $parse(attr.ngModel);
elem.kendoDropDownList({
dataTextField: "Name",
dataValueField: "ID",
value: attr.value,
select: function(e) {
var item = this.dataItem(e.item.index());
scope.$apply(function() {
model.assign(scope, item.value);
});
},
//template: '<strong>${ data.Name }</strong><p>${ data.ID }</p>',
dataSource: source
});
});
}
};
}]);
Input field is
<input qn:dropdown="locations" ng:model="installation.LocationID" value="{{installation.LocationID}}" />
EVerything works fine but initial value for kendoDropDownList is not filled (value: attr.value).
I suppose I am doing something at wrong place or time but not sure what?
You probably need to use $observe:
Use $observe to observe the value changes of attributes that contain interpolation (e.g. src="{{bar}}"). Not only is this very efficient but it's also the only way to easily get the actual value because during the linking phase the interpolation hasn't been evaluated yet and so the value is at this time set to undefined. -- docs, see section Attributes.
Here's an example where I used $observe recently. See also #asgoth's answer there, where he uses $watch, but he also created an isolate scope.
I'm still not clear on when we need to use $observe vs when we can use $watch.
Are you sure {{installation.LocationID}} has a value you expect? I was able to copy-paste your code with some tweaks for my situation and the dropdownlist is working wonderfully (thank you for doing the hard work for me!). I'm populating value on the input field and when the directive executes, attr.value has it and Kendo shows it as expected. Perhaps this was an Angular issue a couple versions ago?
I had the same problem, the attr.value was empty. The problem was related to an $http async call being made to get the data. The scope data was not yet populated when the dropdownlist was being defined in the directive.
I fixed this by watching attr.ngModel instead of attr.qnDropdown in the link function of the directive. This way the dropdownlist gets defined when the scope data is populated.

Resources