I have a component (a directive with isolate scope) that maps a list of objects to a list of inputs that allow to modify one property of that objects.
But I want to make that component universal.
So it should accept path to deeply nested property of each object in a list that should be bound to input.
For example we have a list of people, each of those have name spoken in different languages:
var people = [
{
age: 31,
multilang_attributes: {
en: {name: 'John'},
ru: {name: 'Иван'}
}
},
{
age: 29,
multilang_attributes: {
en: {name: 'Peter'},
ru: {name: 'Пётр'}
}
},
];
I want to apply my universal component to that list of people like the following:
<input-list
items="people",
item-model="multilang_attributes[locale].name"
></input-list>
I tried to create scope property with & that would allow me to execute expression in parent scope and then pass that expression to ngModel, but that does not work. You can look at this attempt in plunkr.
How that task can be approached in angular?
One option would be to $parse the accessor-string and use a getter/setter for the model. For this:
change the directive-html to:
item-accessor="'multilang_attributes.' + app.locale + '.name'"
This will make something like multilang_attributes.en.name available.
change the directive-code to:
app.directive('inputList', function () {
return {
restrict: 'E',
scope: {
items: '=',
itemAccessor: '='
},
template: '<ul><li ng-repeat="item in items"><input ng-model="getModel(item)" ng-model-options="{ getterSetter: true }" /></li></ul>',
controller: function ($scope, $parse) {
$scope.getModel = function(item) {
return function (newValue) {
var getter = $parse($scope.itemAccessor);
if (arguments.length > 0) {
getter.assign(item, newValue);
}
return getter(item);
};
};
}
};
});
External demo: http://plnkr.co/edit/VzFrBxNcsA5BarIVr6oG?p=preview
var app = angular.module('TestApp', [])
app.controller('AppCtrl', function AppController() {
this.locales = ['en', 'fr']
this.locale = 'en';
this.people = [
{
age: 31,
multilang_attributes: {
en: {name: 'John (en)'},
fr: {name: 'John (fr)'}
}
},
{
age: 29,
multilang_attributes: {
en: {name: 'Fred (en)'},
fr: {name: 'Fred (fr)'}
}
},
];
});
app.directive('inputList', function () {
return {
restrict: 'E',
scope: {
items: '=',
itemAccessor: '='
},
template: '<ul><li ng-repeat="item in items"><input ng-model="getModel(item)" ng-model-options="{ getterSetter: true }" /></li></ul>',
controller: function ($scope, $parse) {
$scope.getModel = function(item) {
return function (newValue) {
var getter = $parse($scope.itemAccessor);
if (arguments.length > 0) {
getter.assign(item, newValue);
}
return getter(item);
};
};
}
};
});
<!DOCTYPE html>
<html>
<head>
<script data-require="angular.js#1.4.7" data-semver="1.4.7" src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.7/angular.js"></script>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" integrity="sha512-dTfge/zgoMYpP7QbHy4gWMEGsbsdZeCXz7irItjcC3sPUFtf0kuFbDz/ixG7ArTxmDjLXDmezHubeNikyKGVyQ==" crossorigin="anonymous">
</head>
<body ng-app="TestApp" ng-controller="AppCtrl as app">
<div class="container">
<select ng-model="app.locale" ng-options="v as v for v in app.locales"></select>
<hr>
<input-list
items="app.people"
item-accessor="'multilang_attributes.' + app.locale + '.name'"
></input-list>
<hr>
<pre>{{ app.people|json }}</pre>
</div>
</body>
</html>
note that you should propably use proper injection-syntax and controllerAs/bindToController.
Related
I ran into a situation in AngularJS where I want to use a template that appends to the same template when data is changed instead of replacing the existing template. Directive can be something like this:
<my-template data="myData"></my-template>
template.html can be like:
<p>ID: {{data.id}}, Name: {{data.name}}</p>
The data will contain single data, so when I change data I want above template to append instead of replacing it. So the output will be like this:
ID: 1, Name: John
ID: 2, Name: Michael
ID: 3, Name: Abraham
I also want to handle onClick when the user tap on any name above.
How can I achieve this?
Ok i got it ! try this
angular.module('myApp', [])
.controller('myController', function($scope, $interval) {
var count = 0;
var addPerson = function() {
count++
$scope.person = {
name: 'person' + count,
age: 20 + count
};
};
var timer = $interval(addPerson, 1000);
})
.directive('myDirective', function($compile) {
return {
restrict: "A",
transclude: true,
scope: "=",
link: function(scope, element, attrs) {
scope.handleClick = function(event) {
console.log(event.target.innerText)
}
scope.$watch(function() {
return scope.person;
}, function(obj) {
var elementToRender = angular.element('<div ng-click="handleClick($event)">' + obj.name + '</div>');
function renderDiv() {
return elementToRender
}
element.append(renderDiv());
$compile(elementToRender)(scope);
}, false);
}
};
});
template
<div ng-app="myApp" ng-controller="myController">
<div my-directive data="{{person}}"></div>
</div>
I was trying to create a custom directive with options passed as attributes..
I've noticed that currency filter works with numbers only and it don't work if it is used with some string property only. And if i try to use this filter with property having string it don't generate any result.. is it so?
and how can i use currency filter with string value if i wants to?
var app = angular.module('myApp', []);
app.controller('defaultCtrl', ['$scope', function($scope) {
$scope.products = [
{
name: "Apples",
category: "Fruits",
price: 1.20,
expiry: 10
}, {
name: "Bananas",
category: "Fruits",
price: 2.42,
expiry: 7
}, {
name: "Pears",
category: "Fruits",
price: 2.02,
expiry: 6
}
];
}]);
app.directive('unorderedList', function() {
return function(scope, element, attrs) {
var data = scope[attrs["unorderedList"]];
if(angular.isArray(data)) {
var listElem = angular.element('<ul>');
var propertyExpression = attrs["listProperty"];
element.append(listElem);
for(var i = 0; i < data.length; i++) {
listElem.append(angular.element('<li>').text(scope.$eval(propertyExpression, data[i])));
}
}
}
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="myApp">
<div id="wrapper" ng-controller="defaultCtrl">
<div class="panel panel-default">
<div class="panel-heading">
<h3>Products</h3>
</div>
<div class="panel-body">
<div unordered-list="products" list-property="price | currency"></div>
<div unordered-list="products" list-property="name | currency"></div>
</div>
</div>
</div>
</div>
The source code of that filter (currency) stand as
currencyFilter.$inject = ['$locale'];
function currencyFilter($locale)
{
var formats = $locale.NUMBER_FORMATS;
return function(amount, currencySymbol, fractionSize)
{
if (isUndefined(currencySymbol))
{
currencySymbol = formats.CURRENCY_SYM;
}
if (isUndefined(fractionSize))
{
fractionSize = formats.PATTERNS[1].maxFrac;
}
// if null or undefined pass it through
return (amount == null)
? amount
: formatNumber(amount, formats.PATTERNS[1], formats.GROUP_SEP, formats.DECIMAL_SEP, fractionSize).replace(/\u00A4/g, currencySymbol);
};
}
Now the code for formatNumber starts as follow
function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) {
if (!(isString(number) || isNumber(number)) || isNaN(number)) return '';
and angular.isNumber states that
Determines if a reference is a Number.
This includes the "special" numbers NaN, +Infinity and -Infinity
Thus, formatNumber(...) with your apples and bananas simply return '' (empty string)
Since list-property is not defined in the scope with the # it won't get evaluated.
So as you put it there : YES you need scope.$eval.
However you can change it to :
list-property="{{price | currency}}"
And remove the scope.$eval in your directive. This should work.
Why is that moo object parameter comes as [object Object] string?
var App = angular.module('myApp', [
'controllers']);
var ctrls = angular.module('controllers', []);
ctrls.controller('myController', ['$scope', function ($scope) {
$scope.moos = [
{'id':1, 'name': 'foo'}, {'id':2, 'name': 'boo'}
];
$scope.proxy = {
setter: function (moo) {
if(!arguments.length) return;
console.log(moo);
$scope.moo = moo;
}
};
}]);
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0-beta.5/angular.js"></script>
<div ng-app="myApp" ng-controller="myController">
<p>{{moo.name}}</p>
<select ng-model="proxy.setter" ng-model-options="{ getterSetter: true }">
<option ng-selected="moo.id===m.id" ng-value="m" ng-repeat="m in moos">{{m.name}}</option>
</select>
</div>
ng-value will put a string into the value of the option. And a javascript object converted into a string is [object Object].
But you can put m.id in your ng-value to retrieves it in your javascript code. Here an example :
var App = angular.module('myApp', [
'controllers']);
var ctrls = angular.module('controllers', []);
ctrls.controller('myController', ['$scope', function ($scope) {
$scope.moos = [
{'id':1, 'name': 'foo'}, {'id':2, 'name': 'boo'}
];
$scope.proxy = {
setter: function (id) {
if(!arguments.length) return;
var moo = $scope.moos.filter(function(moo) {
return moo.id == id;
})[0];
console.log(moo);
$scope.moo = moo;
}
};
}]);
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0-beta.5/angular.js"></script>
<div ng-app="myApp" ng-controller="myController">
<p>{{moo.name}}</p>
<select ng-model="proxy.setter" ng-model-options="{ getterSetter: true }">
<option ng-selected="moo.id===m.id" ng-value="m.id" ng-repeat="m in moos">{{m.name}}</option>
</select>
</div>
So I have this filter directive:
app.directive('filter', function(){
return {
restrict: 'E',
transclude: true,
scope: {
callFunc: '&'
},
template:
' <div>' +
' <div ng-transclude></div>' +
' </div>',
controller: function($scope, $element, $attrs){
this.getData = function() {
$scope.callFunc()
}
}
}
});
app.directive('positions', function(){
return {
require: '^filter',
scope: {
selectedPos: '='
},
template:
' Positions: {{selectedPos}}' +
' <ul>' +
' <li ng-repeat="pos in positions">' +
' {{pos.name}}</a>' +
' </li>' +
' </ul>',
controller: function($scope, $element, $attrs){
$scope.positions = [
{name: '1'},
{name: '2'},
{name: '3'},
{name: '4'},
{name: '5'}
];
$scope.selectedPos = $scope.positions[0].name;
$scope.setPosition = function(pos){
$scope.selectedPos = pos.name;
};
},
link: function(scope, element, attrs, filterCtrl) {
scope.posRegData = function() {
filterCtrl.getData();
}
}
}
})
And the controller:
app.controller('keyCtrl', ['$scope', function($scope) {
var key = this;
key.callFunc = function() {
key.value = key.selectedPos;
console.log(key.selectedPos)
}
}]);
The main question is why the key.selectedPos in the controller get's the right value only on the second click?
Here is a plunker replicating my issue.
One way of doing it is to send a param when calling callFunc()
Then, I update the func in the ctrl: key.callFunc = function(filterParams), but also, I am updating the passed method call-func="key.callFunc(filterParams)
Then in filter directive I change getData method to:
this.getData = function(val) {
$scope.callFunc({filterParams: val})
}
In positions directive I pass the value that I need:
scope.posRegData = function() {
filterCtrl.getData({position: scope.selectedPos});
}
Finally, in keyCtrl I get the value like this:
key.callFunc = function(filterParams) {
key.value = filterParams.position;
console.log(filterPrams.position)
}
Here is a plunker demonstrating this attempt.
The question in this case is if this is a good way of doing it, keeping in mind it's within a very large application.
That's because how isolated scopes work. The parent scope (the controller in your case) will be updated when the digest cycle runs, which is after your ng-click function called the callFunc. So you can put your callFunc code in a $timeout and it will work (but will cause another digest cycle).
Another solution will be to put the value in an object, so when you change the object the controller (which have the reference) will see the update immediately:
In the controller:
key.selectedPos = { value: {}};
key.callFunc = function() {
key.value = key.selectedPos.value;
console.log(key.selectedPos.value)
}
In the directive:
$scope.selectedPos.value = $scope.positions[0].name;
$scope.setPosition = function(pos){
$scope.selectedPos.value = pos.name;
};
See this plunker.
I'm trying to send an event when an item gets selected, from directive to controller using $emit. I've two update functions for organizations and another for people. My directive should specify which event should emit.
Here is my update functions:
// For organization
$scope.updateOrgs = function(selectedVal) {
}
// For people
$scope.updatepeople = function(selectedVal, type) {
}
When it is people my directive should raise an emit event for updatepeople (), if it was org it should raise updateorg().
My directive looks like:
.directive('search', function ($timeout) {
return {
restrict: 'AEC',
scope: {
model: '=',
searchobj: '#',
},
link: function (scope, elem, attrs, index) {
scope.handleSelection = function (selectedItem) {
scope.model = selectedItem;
scope.searchModel="";
scope.current = 0;
scope.selected = true;
$timeout(function () {
scope.onSelectupdate();
}, 200);
};
scope.Delete = function (index) {
scope.selectedIndex = index;
scope.delete({ index: index });
};
scope.Search = function (searchitem,event,searchobj) {
// alert('item entered'+name)
scope.searching = searchitem;
scope.searchobject = searchobj;
scope.onSearch({ searchitem: searchitem , searchobj:searchobj});
};
scope.current = 0;
scope.selected = true;
scope.isCurrent = function (index) {
return scope.current == index;
};
scope.setCurrent = function (index) {
scope.current = index;
};
},
controller: ['$scope','$element','$rootScope','SearchOrg', function($scope,$element,$rootScope,SearchOrg) {
$scope.searchItem = function(filter,searchobj){
//alert('search'+searchobj);
SearchOrg().fetch({'filter': filter, 'searchType': searchobj}).$promise.then(function(value){
$scope.searchData = value.data;
console.info($scope.searchData);
},
function(err) {
});
}
}],
templateUrl: TAPPLENT_CONFIG.HTML_ENDPOINT[0] + 'home/genericsearch.html'
}
});;
HTML snippet
<search searchobj=“tei-org” selectedItems=“arrayofIds” search-id=”someidtoIdentify”/>
How can I do this both functions are in different controllers, and also I need to send parameters from directive to the controller using $emit?
Working with $scope.$emit and $scope.$on
I'm guessing that your other controllers are not parents, so look at the second option using $broadcast.
var app = angular.module('app', []);
app.controller('firstController', function($scope) {
$scope.selectedOrgs = []
$scope.$on('updateorgs', function(evt, data) {
$scope.selectedOrgs.push(data);
});
});
app.controller('secondController', function($scope) {
$scope.selectedPeople = []
$scope.$on('updatepeople', function(evt, data) {
$scope.selectedPeople.push(data);
});
});
app.directive('someDirective', function($rootScope) {
return {
scope: {},
link: function(scope) {
scope.options = [{
id: 1,
label: 'org a',
type: 'org'
}, {
id: 2,
label: 'org b',
type: 'org'
}, {
id: 3,
label: 'person a',
type: 'person'
}, {
id: 4,
label: 'person b',
type: 'person'
}];
scope.changed = function() {
if (scope.selected) {
var updatetype = scope.selected.type;
if (updatetype === 'person') {
$rootScope.$broadcast('updatepeople', scope.selected);
} else if (updatetype === 'org') {
$rootScope.$broadcast('updateorgs', scope.selected);
}
}
};
},
template: '<select ng-change="changed()" ng-model="selected" ng-options="option.label for option in options"><option value="">Select</option></select>'
};
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.15/angular.min.js"></script>
<div ng-app='app'>
<some-directive></some-directive>
<div ng-controller='firstController'>
<div>ORGS:</div>
<div>
{{ selectedOrgs }}
</div>
</div>
<div ng-controller='secondController'>
<div>PEOPLE:</div>
<div>
{{ selectedPeople }}
</div>
</div>
</div>