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

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

Related

directive's scope inside ng-repeat angularjs

I'm trying to understand directive's scope inside ng-repeat, still wondering if it comes from ng-repeat but it looks like.
Here is my code
directive.js
myApp.directive('dirSample', function () {
return {
template: '<input type="text" ng-model="name" />',
replace: true,
restrict: 'AE'
}
});
mainController.js
angular.controller('mainController',function($scope){
$scope.name = 'name'
});
index.htm
<div ng-repeat="i in [1, 2, 3, 4]">
<dir-sample></dir-sample>
</div>
<dir-sample></dir-sample>
<dir-sample></dir-sample>
When i make a change in one of the last two directives (which are not inside ng-repeat) it works well, changes on one are reflected on the other.
Problem :
1 - if i change an input value of a directive generated by ng-repeat , changes are not reflected anywhere else.
2 - if i change value of input on one of the two last directives , the directives inside ng-repeat change too, but if touch ( change input value ) of any directive , changes will not be reflected on that directive but will keep being reflected on the other directives.
Can someone please explain why the scope has that behavior ?
Thanks.
Binding primitives is tricky, as is explained here: Understanding scopes. It has to with how Javascript works. Your 'name' variable will get shadowed once it is altered within the ng-repeat block. The suggested fix (from the link above):
This issue with primitives can be easily avoided by following the
"best practice" of always have a '.' in your ng-models
They also provide a link to a video explaining exactly this problem: AngularJS MTV Meetup
So a fix looks like this:
app.controller('mainController',function($scope){
$scope.attr= {}
$scope.attr.name = 'name'
});
app.directive('dirSample', function () {
return {
template: '<input type="text" ng-model="attr.name" />',
replace: true,
restrict: 'AE'
}
});

Custom Directive : Dropdown selected value not binding

I'm trying to create a custom directive for a drop down control in AngularJS 1.4.4. I can handle the selected event, but i can not get the binding for what is selected in the drop down list.
I want to call this from Html markup the following way.
<my-dropdown-list source="myList" destination="mySelection" />
The angular js custom directive is here.
(function() {
var directive = function($compile) {
return {
restrict: 'E',
scope: {
model: '=source',
selectedValues: '=destination'
},
controller: function($scope) {
$scope.onSelChange = function() {
alert('called');
console.log($scope.selectedItem.Code, $scope.selectedItem.Name);
};
// $scope.selectedItem is always undefined here.
},
link: function ($scope, $elem) {
var rowHtml =
'<select ng-options="item as item.Name for item in model" ng-model="selectedItem" ng-change="onSelChange()"></select>';
$elem.html(rowHtml);
$compile($elem.contents())($scope.$new());
}
};
};
my.directive('myDropdownList', directive);
})();
I'm new to Angular, so this may be something small that i missed here, but i can't seem to get a value for 'selectedItem'
I find out this in AgularJS document
Note that the value of a select directive used without ngOptions is
always a string. When the model needs to be bound to a non-string
value, you must either explictly convert it using a directive (see
example below) or use ngOptions to specify the set of options. This is
because an option element can only be bound to string values at
present.
Link: https://docs.angularjs.org/api/ng/directive/select
You should use ngRepeat to generate the list like this post:
Angularjs: select not updating when ng-model is updated

ng-repeat render order reversed by ng-if

I've just come across this and am wondering if it's a bug or expected behaviour? This is just a small example to show the issue. The code below is used in both examples:
<body ng-app="app" ng-controller="AppCtrl">
<item-directive ng-repeat="item in ::items" item="item"></item-directive>
</body>
angular
.module('app', [])
.controller('AppCtrl', AppCtrl)
.directive('itemDirective', itemDirective)
.factory('model', model);
function AppCtrl($scope, model) {
$scope.items = model.getItems();
}
function itemDirective() {
return {
restrict: 'E',
replace: true,
scope: {
item: '='
},
templateUrl: 'item-directive.html'
};
}
function model() {
return {
getItems: getItems
}
function getItems() {
return [
{
type: 'test',
title: 'test 1'
},
{
type: 'test',
title: 'test 2'
},
{
type: 'test',
title: 'test 3'
}
];
}
}
The first example has this item-directive.html which gets rendered in the correct order as expected
<div>
<span>{{::item.title}}</span>
</div>
Plunkr without ng-if
But the second example - which has the below item-directive.html - incorporates an ng-if, which is causing the list to get rendered in reverse order?
<div ng-if="item.type == 'test'">
<span>{{::item.title}}</span>
</div>
Plunkr with ng-if
-------- UPDATE ----------
I've just noticed (which relates to the issue noted in #squiroid's answer) that the isolate scope isn't actually working in this example. It appears to be, but item is being made available to the item-directive scope (or rather the scope it ends up with) by the ng-repeat, not the isolate scope. If you try to set any other values on the isolate scope, even though they show up on the scope passed to the directive's link and controller functions (as can be seen in the console output for the plnkr), they're not available to the template. Unless you remove replace.
Plunkr showing broken isolate scope
Plunkr showing fixed isolate scope when replace:false
--- UPDATE 2 ---
I've updated both of the examples to show the issue persisting once the the isolate scope is removed
Plunkr without ng-if and no isolate scope
Plunkr with ng-if and no isolate scope
And also a new version showing the change from templateUrl to template - as suggested by #Manube - that shows the behaviour working as expected
Plunkr with ng-if and no isolate scope using template instead of templateUrl
Using ng-if on a root element of a directive with replace: true creates a broken scope
ISSUE
This is happening with the combination of replace:'true' and ng-if on the root element.
Make sure the contents of your html in the templateUrl has exactly one root element.
If you place ng-if on span
<div >
<span ng-if="item.type == 'test'">{{::item.title}}</span>
</div>
Now why it is happening,it is happening because The ngIf directive removes or recreates a portion of the DOM tree based on an {expression}. If the expression assigned to ngIf evaluates to a false value then the element is removed from the DOM, otherwise a clone of the element is reinserted into the DOM.
which may lead to no root element on the templateUrl while rendering and thus leads to unwanted behaviour.
It is to do with templateUrl, which is asynchronous;
If you replace templateUrl by
template:'<div ng-if="item.type === \'test\'"><span>{{::item.title}}</span></div>'
it will work as expected: see plunker with template instead of templateUrl
the test <div ng-if="item.type === 'test'"> will execute when scope is ready and the templateUrl has been fetched.
As the way the template is fetched is asynchronous, whichever template comes back first executes the test, and displays the item.
Now the question is: why is it always the last template that comes back first?
second one showing in reverse order due to custom directive defined with item-directive name but its not rendering into DOM due to replace=true is used.
for more ref you can refer to this link

Binding data in a custom directive - AngularJS

I have a custom directive, and its purpose is to present a widget and bind it to a variable.
Every variable has different data type, so different widgets will be presented depending on the data type.
My problem is that I can pass the data of the variable, but I can't manage to bind the widget to it.
To simplify the problem, my widget is just a simple text input.
When I try to $compile the widget, Angular uses the value of the variable instead of binding to it.
HTML:
<body ng-app="app" ng-controller="myCtrl">
<input type="text" ng-model="resource.name"></div>
<div custom-widget widget-type="widget" bind-to="resource"></div>
</body>
Javascript:
angular.module('app', [])
.directive('customWidget', function($compile) {
return {
replace: true,
template: '<div></div>',
controller: function($scope) {
},
scope: {
bindTo: "=bindTo",
widgetType: "=widgetType"
},
link: function(scope, iElem, iAttrs) {
var html = '<div>' + scope.widgetType.label + ':<input ng-bind="' + scope.bindTo[scope.widgetType.id] + '" /></div>';
iElem.replaceWith($compile(html)(scope));
}
};
})
.controller('myCtrl', function($scope) {
$scope.widget = {
id: 'name',
label: 'Text input',
type: 'text'
};
$scope.resource = {
name: 'John'
};
});
Plunker demo: http://plnkr.co/edit/qhUdNhjSN7NlP4xRVcEA?p=preview
I'm still new to AngularJS and my approach may not be the best, so any different ideas are of course appreciated!
Since you're using an isolate scope one issue is that resource is on the parents scope and not visible within the directive. And I think you're looking for ng-model rather than ng-bind.
Also, since you want to bind to namein resource, we need to tie that in somehow.
So here's one approach to your template html (note the addition of $parent to get around the scope issue and the addition of .name(which you could add programatically using a variable if you preferred, or specify it as part of the attribute))
var html = '<div>' + scope.widgetType.label + ':<input ng-model="' + '$parent.' + iAttrs.bindTo +'.name'+ '" /></div>';
Updated plunker
Well, when you have a isolated scope within your directive and use the "=" operator you already have two-way data binding.
My suggestion would be to use the "template" more like a view so the operations are clearer.
I would change your directive to the following:
Using ng-model instead of ng-bing mainly because as the Documentation reveals:
The ngModel directive binds an input,select, textarea (or custom form control) to a property on the scope using NgModelController, which is created and exposed by this directive. [...]
Changed directive:
angular.module('app', [])
.directive('customWidget', function($compile) {
return {
replace: true,
template: '<div> {{widgetType.label}} <input ng-model="bindTo[widgetType.id]" /></div>',
scope: {
bindTo: "=bindTo",
widgetType: "=widgetType"
}
};
});
EDIT:
Ops forgot the Updated Plunker

AngularJS - Isolate scope binding to parent scope

If I have an html element in my "parent" page like so:
<div ng-include"'Go-To-Child-Page.html'" />
Any my child/include page is like so:
<some-directive two-way-binding="$parent.SomeParentScope"></some-directive>
Why is this not working for my directive? Or better yet, how do I make it work?
app.directive ('someDirective', function(){
return {
retrict: 'E',
replace: true,
scope: {
myBinding : "=twoWayBinding", <- this is what is not working
},
template: '<select ng-model="myBinding" ng-options="myType.Description for myType in myTypes"></select>'
};
}
Edit Update:
Why did I post this question?
After completing a very lengthy form, I immediately noticed how I had quite a number of similar controls that the coder in me said I should abstract out. One of those was the select control. Two scenarios were involved with this control:
(1) Where the user had to choose a filter before the select control was populated; and
(2) Where the code pre-defined the filter for the select control.
The solutions for both those scenarios are shown below. I hope this helps everyone because I truly enjoy using Angular and the directive functionality it provides to create "Html-magic" is amazing.
You seem to be doing a lot of things unnecessarily, but that might be because I'm misunderstanding your goal.
I've fixed your plnkr here: http://plnkr.co/edit/FqpzC5p1Ogm4attYiArV?p=preview
The basic changes that were necessary seem to be:
Pass the selected filter (Rear/Front) into your directive
Replace ngOptions with ngRepeat and a filter
There's really no need for your directive to have a controller (and generally most directive's should use a linker function). I stripped out some bits to make it simpler, but you can still wire up $scope.filterTypes as you were (pulling available Types from $scope.myTypes) and it'll still work the same.
Update
Since you didn't spell out all of your requirements, I may be missing some, but this implementation is what I gathered you are looking for:
http://plnkr.co/edit/pHgJ84Z35q5jxCpq3FHC?p=preview
It's got dynamic filtering, it's not unnecessarily using a controller, it's got two-way binding. The only problem is that it's referencing the "Description" field (as your original was). You can work that in to be configurable in HTML if you like.
Scenario 1 : Let the user filter:
Filter:
<input type="radio" ng-model="choice.type" value="Rear"> Rear
<input type="radio" ng-model="choice.type" value="Front"> Front
<br>
Select:
<name-value-select selected-item="selected.item" choice="choice.type" options="axleTypes"></name-value-select>
Scenario 2 : pre-filter in code:
<name-value-select preselected-filter="Front" selected-item="selected.item" options="axleTypes"></name-value-select>
The directive for both scenarios:
.directive('nameValueSelect', function () {
return {
replace: true,
restrict: "E",
scope: {
selectedItem: "=",
choice: "=",
options: "=",
preselectedFilter: "#"
},
controller: function ($scope) {
$scope.$watch('choice', function (selectedType) {
$scope.selectedItem = ''; // clear selection
$scope.filterTypes = [];
angular.forEach($scope.options, function (type) {
if ($scope.preselectedFilter !== undefined) {
selectedType = $scope.preselectedFilter;
}
if (type.Type === selectedType) {
this.push(type);
}
}, $scope.filterTypes);
$scope.filterTypes.sort(function (a, b) {
return a.Description.localeCompare(b.Description);
});
});
},
template: '<select ng-model="selectedItem" ng-options="o.Description for o in filterTypes"><option value="" selected="selected">Please Select </option></select>'
};
});
And the proverbial obligatory plunker:
http://plnkr.co/edit/tnXgPKADfr5Okvj8oV2S

Resources