What is wrong with my directive scope definitions? - angularjs

I have several <select> fields in my form. I want to load the options dynamically, so I use ng-repeat to iterate through a list of options that can travel with the data object.
To make this feature more reusable, I break off this segment of code and create a directive:
javascript:
angular.module( "ngFamilyTree" )
.directive( "selectInput", function(){
return {
restrict: "E",
templateUrl: "templates/directives/selectInput.html",
scope: {
element: "=",
options: "=",
change: "="
}
};
} );
templates/directives/selectInput.html:
<select ng-model="element" class="form-control">
<option ng-repeat="(text, value) in options" value="{{value}}">{{text}}</option>
</select>
In the primary form, I then use the following directive elements at various places:
<select-input element="docCntl.document.type" options="docCntl.document.typeOptions"></select-input>
<select-input element="docCntl.document.category" options="docCntl.categoryOptions" change="docCntl.document.updateSubCategoryOptions()"></select-input>
<select-input element="docCntl.document.subcategory" options="docCntl.subCategoryOptions"></select-input>
What I find peculiar is that the first instance, where I set the element to "docCntl.document.type" works perfectly. Every time I change the value of the select, the value of the corresponding element changes in the model object. However, the second and third items do not change the model value.
Also, I have tried this with and without the "change" attribute. The goal with this is to be able to set an update function that changes the available options for the subcategory as the category changes.
Note: I intentionally have stored the category options and sub-category options in docCntl and not in docCntl.document; this is why they look different from the options that are available for the type selector.
Note 2: The Category and Sub Category are properly loaded with the correct options on the initial page load.

Have you read this? You can use the ng-options directive to achieve the same result.
You can use something like this in your markup -
<select ng-options="item as item.label for item in items track by item.id" ng-model="selected"></select>
The corresponding script in your controller should be something like this -
$scope.items = [{
id: 1,
label: 'aLabel',
subItem: { name: 'aSubItem' }
}, {
id: 2,
label: 'bLabel',
subItem: { name: 'bSubItem' }
}];
Hope this helps :)

Try this solution http://plnkr.co/edit/wdKObUItDMqLyVx1gObS?p=preview
Add parent scope object the directive. You can use root scope but that is not a good practice. Note the addition of parentScope, link and cascade.
ngFamilyTree.directive("selectInput", function() {
return {
restrict: "E",
templateUrl: "templates/directives/selectInput.html",
scope: {
element: "=",
options: "=",
change: "=",
parentScope: "="
},
link: function(scope) {
scope.cascade = function(val) {
if (typeof scope.change === 'function') {
scope.change(scope.parentScope, scope.element);
}
};
}
};
});
2.Change the directive template to call cascade function
<select ng-model="element" class="form-control" ng-change="cascade()">
<option ng-repeat="(text, value) in options" value="{{value}}">{{text}}</option>
</select>
In the controller, make updateSubCategoryOptions function stateless and pass a way to access the master sub-category list and the currently selected category
$scope.docCntl.document.updateSubCategoryOptions = function(docCntlIn, category){docCntlIn.subCategoryOptions=docCntlIn.categorySubCategories[category];}

Related

How can I use interpolation to specify element directives?

I want to create a view in angular.js where I add a dynamic set of templates, each wrapped up in a directive. The directive names correspond to some string property from a set of objects. I need a way add the directives without knowing in advance which ones will be needed.
This project uses Angular 1.5 with webpack.
Here's a boiled down version of the code:
set of objects:
$scope.items = [
{ name: "a", id: 1 },
{ name: "b", id: 2 }
]
directives:
angular.module('myAmazingModule')
.directive('aDetails', () => ({
scope: false,
restrict: 'E',
controller: 'myRavishingController',
template: require("./a.html")
}))
.directive('bDetails',() => ({
scope: false,
restrict: 'E',
controller: 'myRavishingController',
template: require("./b.html")
}));
view:
<li ng-repeat="item in items">
<div>
<{{item.name}}-details/>
</div>
</li>
so that eventually the rendered view will look like this:
<li ng-repeat="item in items">
<div>
<a-details/>
</div>
<div>
<b-details/>
</div>
</li>
How do I do this?
I do not mind other approaches, as long as I can inline the details templates, rather then separately fetching them over http.
Use ng-include:
<li ng-repeat="item in items">
<div ng-controller="myRavishingController"
ng-include="'./'+item.name+'.html'">
</div>
</li>
I want to inline it to avoid the http call.
Avoid http calls by loading templates directly into the template cache with one of two ways:
in a script tag,
or by consuming the $templateCache service directly.
For more information, see
AngularJS $templateCache Service API Reference
You can add any html with directives like this:
const el = $compile(myHtmlWithDirectives)($scope);
$element.append(el);
But usually this is not the best way, I will just give a bit more detailed answer with use of ng-include (which actully calls $compile for you):
Add templates e.g. in module.run: [You can also add templates in html, but when they are required in multiple places, i prefer add them directly]
app.module('myModule').run($templateCache => {
$templateCache.put('tplA', '<a-details></a-details>'); // or webpack require
$templateCache.put('tplB', '<b-details></b-details>');
$templateCache.put('anotherTemplate', '<input ng-model="item.x">');
})
Your model now is:
$scope.items = [
{ name: "a", template: 'tplA' },
{ name: "b", template: 'tplB' },
{ name: "c", template: 'anotherTemplate', x: 'editableField' }
]
And html:
<li ng-repeat="item in items">
<div ng-include="item.template">
</div>
</li>
In order to use dynamic directives, you can create a custom directive like I did in this plunkr:
https://plnkr.co/edit/n9c0ws?p=preview
Here is the code of the desired directive:
app.directive('myProxy', function($compile) {
return {
template: '<div>Never Shown</div>',
scope: {
type: '=',
arg1: '=',
arg2: '='
},
replace: true,
controllerAs: '$ctrl',
link: function($scope, element, attrs, controller, transcludeFn) {
var childScope = null;
$scope.disable = () => {
// remove the inside
$scope.changeView('<div></div>');
};
$scope.changeView = function(html) {
// if we already had instanciated a directive
// then remove it, will trigger all $destroy of children
// directives and remove
// the $watch bindings
if(childScope)
childScope.$destroy();
console.log(html);
// create a new scope for the new directive
childScope = $scope.$new();
element.html(html);
$compile(element.contents())(childScope);
};
$scope.disable();
},
// controller is called first
controller: function($scope) {
var refreshData = () => {
this.arg1 = $scope.arg1;
this.arg2 = $scope.arg2;
};
// if the target-directive type is changed, then we have to
// change the template
$scope.$watch('type', function() {
this.type = $scope.type;
refreshData();
var html = "<div " + this.type + " ";
html += 'data-arg1="$ctrl.arg1" ';
html += 'data-arg2="$ctrl.arg2"';
html += "></div>";
$scope.changeView(html);
});
// if one of the argument of the target-directive is changed, just change
// the value of $ctrl.argX, they will be updated via $digest
$scope.$watchGroup(['arg1', 'arg2'], function() {
refreshData();
});
}
};
});
The general idea is:
we want data-type to be able to specify the name of the directive to display
the other declared arguments will be passed to the targeted directives.
firstly in the link, we declare a function able to create a subdirective via $compile . 'link' is called after controller, so in controller you have to call it in an async way (in the $watch)
secondly, in the controller:
if the type of the directive changes, we rewrite the html to invoke the target-directive
if the other arguments are updated, we just update $ctrl.argX and angularjs will trigger $watch in the children and update the views correctly.
This implementation is OK if your target directives all share the same arguments. I didn't go further.
If you want to make a more dynamic version of it, I think you could set scope: true and have to use the attrs to find the arguments to pass to the target-directive.
Plus, you should use templates like https://www.npmjs.com/package/gulp-angular-templatecache to transform your templates in code that you can concatenate into your javascript application. It will be way faster.

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

AngularJS Dropdown Directive Set Selected Item Through Two Way Binding

I have an angularjs dropdown directive. I want to be able to pass the id of the item I want to be selected as an attribute of the directive. Something like this:
<dropdown selected-item-id="ctrl.selectedItemId"></dropdown>
I implemented this and it's not working. If I display the value of itemId on the directive code, I can see the right value, but the dropdown selection does not update. Here's the relevant code:
(function () {
'use strict';
var dropdown = function ($state, service) {
return {
restrict: 'E',
replace: true,
templateUrl: '/dropdown.html',
scope: {
selectedItemId:"="
},
link: function (scope, elem, attr) {
service
.getItems()
.then(function (items) {
scope.items = items;
});
}
};
};
dropdown.$inject = ['$state', 'service'];
angular.module('app').directive('dropdown', dropdown);
})();
<select class="form-control"
ng-model="selectedItemId"
ng-options="item.id as item.name for item in items">
<option value="" selected disabled>Select an item</option>
</select>
Like I said, if I display the selectedItemId on the directive template (e.g. as one of the options) I see the right id value, however the dropdown selection doesn't change.
Any thoughts?
EDIT:
I had a typo (happened when typing the code, the actual code on my editor is correct) on the dropdown's property, item-id to selected-item-id
You are not binding selected value to item-id as you think according to your html code. You are binding selected value to selected-item-id.
Try changing your html to this:
<dropdown selected-item-id="ctrl.selectedItemId"></dropdown>
Looks like you might be having a race condition on the $digest cycle. If you call $apply() from your service callback on the first version of your code it should work. However, you will have the side effect that from time to time angular will complain about an $apply being already in progress so the second version of your code should do the trick.
I'm not sure why it wasn't working like I had it, but I made some changes an now it works ok. Here's what I did (the explanation is included as comments on the source code):
//Here I use the selectedItem bidirectional binding on the .then of my service call
//to get the id of the items that's supposed to be selected.
//Then, I filter the array of items using that id, so I get the actual object that matches the id.
//Finally, I set the scope object "selectedItem" to that object I just got.
//The "selectedItem" object is what's bound to the select via ng-model so that does the selection.
(function () {
'use strict';
var dropdown = function ($state, service) {
return {
restrict: 'E',
replace: true,
templateUrl: '/dropdown.html',
scope: {
selectedItemId:"="
},
link: function (scope, elem, attr) {
service
.getItems()
.then(function (items) {
scope.items = items;
var selectedItem = $filter('filter')(items, { id: scope.selectedItemId })[0];
scope.selectedItem = selectedItem;
});
}
};
};
dropdown.$inject = ['$state', 'service'];
angular.module('app').directive('dropdown', dropdown);
})();
<!--Here I replaced the ng-model to use an object rather than an id and did the same on the ng-options -->
<select class="form-control"
ng-model="selectedItem"
ng-options="item as item.name for item in items">
<option value="" selected disabled>Select an item</option>
</select>

Angularjs ng-repeat race condition in setting dropdown value

I had the problem of getting resource data from an API, loading that into a dropdown select, and setting the selected value of the dropdown. Basically it was trying to set the value of the dropdown before it was populated. I have two different ways to do this, but was wondering if anyone had a "better" way, or a "better practice" way. Here are my two ways.
Option 1: Directive attached to ng-repeat element
Controller
$scope.users = User.query();
$scope.dude={
name: "Dude",
id: 3
}
HTML
<select id="userSelect" ng-show="users.length">
<option ng-repeat="user in users" choose dude="dude">{{user.username}}</option>
</select>
Directive
.directive('choose', function() {
return {
restrict: 'A',
link: function(scope, element, attrs) {
if (scope.user) {
if (scope.user.id === scope.dude.id) {
$("#userSelect").val(element.val());
}
}
}
}
});
Option 2: Watch for the users length to change (the call is returned, and the dropdown is populated)
Controller
$scope.users = User.query();
$scope.dude={
name: "Dude",
id: 3
}
$scope.$watch('users.length', function() {
$("#userSelect").val($scope.dude.id);
});
HTML
<select id="userSelect" ng-show="users.length">
<option ng-repeat="user in users" value="{{user.id}}>{{user.username}}</option>
</select>
Any opinions on which one is better practice? Or if there is any other better way?
So, promises are your friend for this sort of thing. I'm going to use $http instead of resources, because I'm more familiar with it, but I'm pretty sure recent version of
resources return promises (or can).
Also.. no jquery in your controllers. Use directives like ng-model to change input values.
Also using ng-options to populate the options for a select is more powerful than using ng-repeat on an "option" element.
Here's what a lot of my code looks like (except that I'm using jsonp here instead of just get). http://jsfiddle.net/HB7LU/142/
CONTROLLER:
function MyCtrl($scope, $http) {
// The id we want to select when the data loads:
var desiredId = 3;
// Overly complicated $http request so that I can load with jsonp:
// You could normally use just $http.get()
$scope.users = $http.jsonp('http://www.json-generator.com/j/geku?callback=JSON_CALLBACK').then(function(d) { return d.data.result; });
// Use the returned promise to set the selection when the data loads:
// I'm using the "underscore" library function "findWhere" to set my
// model to the object I want selected:
$scope.users.then(function(d) {
$scope.uservalue = _.findWhere(d,{id:desiredId});
});
}
HTML:
<div ng-controller="MyCtrl">
{{uservalue | json}}
<select ng-model="uservalue" ng-show="users.length" ng-options="user.name for user in users">
</select>
</div>

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