Angular directive: return object from collection - javascript

I try to implement a custom directive that list all available plans and allow user to choose one.
When plan is selected, the parent scope must be updated with selected object (it is linked with two way binding)
It should behave exactly the same as angular ng-options does, but I have hard time fighting the Javascript object replacement.
What I have right now works (with some clutter removed):
In HTML:
<choose-plan ng-model='plan' plans='plans' choose-plan-title='Premium plans' />
In Controller:
$scope.plan = {}
Plans.get (resource) ->
$scope.plans = resource.plans
return
It does not work with $scope.plan = undefined obviously, but I look for the solution that does.
In JS (Coffeescript) directive:
angular.module('tv-dashboard').directive 'choosePlan', [
'lodash'
(lodash) ->
'use strict'
restrict: 'E'
scope:
plan: '=ngModel'
plan_collection: '=plans'
title: '#choosePlanTitle'
link: (scope, element, attrs) ->
# HACK two way binding does not replace the value. Investigate
scope.choosePlan = (available_plan) ->
# scope.plan = available_plan # Does NOT update the parent scope binded ng-model
angular.copy available_plan, scope.plan
return
scope.isSelected = (available_plan) ->
return unless available_plan?
available_plan.id == scope.plan.id
presentPlans = (collection) ->
angular.copy(collection).map (resource_plan) ->
price_parts = resource_plan.interval_price.split '.'
resource_plan['integer_price'] = price_parts[0]
resource_plan['decimal_price'] = price_parts[1]
resource_plan
chooseDefaultPlan = (collection) ->
scope.choosePlan lodash.last collection
unWatchCollection = scope.$watch 'plan_collection', (collection) ->
return unless collection? && collection.length > 0
scope.plans = presentPlans collection
chooseDefaultPlan scope.plans
unWatchCollection()
return
return
templateUrl: 'form/choose_plan.html'
]
But if you take a look on the isSelected function, you notice that i have to compare objects using the id field. Comparison by == (=== in JS) does not return true.
Is there a way I can replace the parent scope plan without dancing around with angular.copy available_plan, scope.plan and forced to use the id field comparison?

You should use require: 'ngModel' to inject the model attached to the element or its parent element on which the directive is bound to. Here is a demo.

One way to solve this is to share an object and write into its properties
$scope.plan = {
// value: set in directive
}
scope.choosePlan = (available_plan) ->
scope.plan.value = available_plan
return
scope.isSelected = (available_plan) ->
return unless available_plan?
available_plan == scope.plan.value
Your code as jsfiddle: http://jsfiddle.net/ht6tmhfu/1/
Fixed code as jsfiddle: http://jsfiddle.net/ht6tmhfu/3/

Related

AngularJS - When should we avoid using {{}}? [duplicate]

In angular sometimes i have seen curly brackets but some times not.i search a lot but i couldn't find correct question
with curly bracket
ng-src="{{imageSrc}}
without curly bracket
ng-hide="imageSrc"
what i'm asking is why we cannot write ng-hide as
ng-hide="{{imageSrc}} // doesn't work anyway
why there is 2 different syntax for src and hide?
It simply depends on the way the directive you are using is "declared".
If the directive has the following declaration:
scope:{
ngHide: '='
}
then, you don't have to use double mustaches because the directive expects an object
If the directive is declared like the following :
scope:{
ngMin:'#'
}
then, it expects a value. If your value comes from a javascript variable, then you have to use curly braces to interpolate the string contained into your variable.
EDIT :
It has been a long time since I read angular source code.
I haven't found any source code to prove my point :
ngController which expects a string is declared like the following
var ngControllerDirective = [function() {
return {
restrict: 'A',
scope: true,
controller: '#',
priority: 500
};
}];
https://github.com/angular/angular.js/blob/master/src/ng/directive/ngController.js#L3
ngMaxLength
var maxlengthDirective = function() {
return {
restrict: 'A',
require: '?ngModel',
link: function(scope, elm, attr, ctrl) {
if (!ctrl) return;
var maxlength = -1;
attr.$observe('maxlength', function(value) {
var intVal = toInt(value);
maxlength = isNaN(intVal) ? -1 : intVal;
ctrl.$validate();
});
ctrl.$validators.maxlength = function(modelValue, viewValue) {
return (maxlength < 0) || ctrl.$isEmpty(viewValue) || (viewValue.length <= maxlength);
};
}
};
};
https://github.com/angular/angular.js/blob/master/src/ng/directive/validators.js#L186
Beacuse they mean two different things.
When you use this:
<span data-ng-bind="test">
This means that angular will go to the scope and get value from there with test as key. So value will be $scope.test. But attribte value will be "test"
When you use
ng-src="{{imageSrc}}
then value will be evaluated and placed to the attribute. So value willbe $scope.imageSrc and attribute value will be $scope.imageSrc.
But. Not all tags can wait for evaluation. They think that value {{}} is correct and will not be changed. This cause to bad request. To fix this problem ng-src was created.
You can't write because both have different meaning see this link
,It's all about expression and template argument.
https://docs.angularjs.org/api/ng/directive/ngSrc
ng-src=template
You can find it in argument
https://docs.angularjs.org/api/ng/directive/ngHide
ng-hide=expression
You can also find it in argument

angular when to use curly brackets

In angular sometimes i have seen curly brackets but some times not.i search a lot but i couldn't find correct question
with curly bracket
ng-src="{{imageSrc}}
without curly bracket
ng-hide="imageSrc"
what i'm asking is why we cannot write ng-hide as
ng-hide="{{imageSrc}} // doesn't work anyway
why there is 2 different syntax for src and hide?
It simply depends on the way the directive you are using is "declared".
If the directive has the following declaration:
scope:{
ngHide: '='
}
then, you don't have to use double mustaches because the directive expects an object
If the directive is declared like the following :
scope:{
ngMin:'#'
}
then, it expects a value. If your value comes from a javascript variable, then you have to use curly braces to interpolate the string contained into your variable.
EDIT :
It has been a long time since I read angular source code.
I haven't found any source code to prove my point :
ngController which expects a string is declared like the following
var ngControllerDirective = [function() {
return {
restrict: 'A',
scope: true,
controller: '#',
priority: 500
};
}];
https://github.com/angular/angular.js/blob/master/src/ng/directive/ngController.js#L3
ngMaxLength
var maxlengthDirective = function() {
return {
restrict: 'A',
require: '?ngModel',
link: function(scope, elm, attr, ctrl) {
if (!ctrl) return;
var maxlength = -1;
attr.$observe('maxlength', function(value) {
var intVal = toInt(value);
maxlength = isNaN(intVal) ? -1 : intVal;
ctrl.$validate();
});
ctrl.$validators.maxlength = function(modelValue, viewValue) {
return (maxlength < 0) || ctrl.$isEmpty(viewValue) || (viewValue.length <= maxlength);
};
}
};
};
https://github.com/angular/angular.js/blob/master/src/ng/directive/validators.js#L186
Beacuse they mean two different things.
When you use this:
<span data-ng-bind="test">
This means that angular will go to the scope and get value from there with test as key. So value will be $scope.test. But attribte value will be "test"
When you use
ng-src="{{imageSrc}}
then value will be evaluated and placed to the attribute. So value willbe $scope.imageSrc and attribute value will be $scope.imageSrc.
But. Not all tags can wait for evaluation. They think that value {{}} is correct and will not be changed. This cause to bad request. To fix this problem ng-src was created.
You can't write because both have different meaning see this link
,It's all about expression and template argument.
https://docs.angularjs.org/api/ng/directive/ngSrc
ng-src=template
You can find it in argument
https://docs.angularjs.org/api/ng/directive/ngHide
ng-hide=expression
You can also find it in argument

Passing format string to a directive and interpolating it

I want to pass a format string to a directive and make the directive interpolate it with an object. The problem is that if I use curly brackets Angular tries to interpolate the string before the directive is even created. If I escape the curly brackets, Angular isn't showing the values of the object (the string isn't interpolated correctly).
How can I pass a format string to a directive?
This is my demo code template:
<div test-directive item-text="{{ x }} - {{ y }}"></div>
Angular app/directive:
var app = angular.module('plunker', ["test-directive"]);
app.controller('MainCtrl', function($scope) {
$scope.name = 'World';
});
var dir = angular.module('test-directive', []);
dir.directive("testDirective", ['$interpolate', function($interpolate) {
return {
template: "<div>{{ text }}</div>",
link: function ($scope, element, attrs) {
var obj = {
x: 6,
y: 9
};
$scope.text = $interpolate(attrs.itemText)( obj );
}
}
}]);
Demo: http://plnkr.co/edit/vUVVuLVBptEmUcv3y7o4?p=preview
Edit:
Applying #Lucas's annswer to my original problem isn't solving the issue. For some reason the attr gets wiped, even if I'm not erasing it anywhere.
Please check the line 301: http://plnkr.co/edit/6bjW35D3W1dTGQz9kNSn?p=preview
Note that itemLabel isn't changed anywhere.
The way to do it is to change it as
$scope.text = $interpolate(element.attr(attrs.$attr.itemText))( obj );
http://plnkr.co/edit/ctPalH7Szqqnpit5t0On?p=preview
Not sure what the actual usecase is, but from your example, you don't seem to need interpolation (so you shouldn't use it and avoid the overhead).
What you (seem to) need, is to evaluate a expression in some context (e.g. obj).
One way of doing it, is to use Scope's $eval() method; e.g.:
<div test-directive item-text="x+' - '+y" ...
...
link: function postLink(scope, elem, attrs) {
var obj = {x: 6, y: 9};
scope.text = scope.$eval(attrs.itemText, obl);
}
Updated Plnkr
EDIT: Admitedly, this beats the purpose of simplicity, so here is another approach using a helper service that replaces the start-/end-symbols and interpolates: Demo

Data Binding to convert mm to ft and inches or vice versa using Angularjs and ng-model

I want to convert lenghts from mm to ft and inches or vice versa. User can input either mm or ft&in.The case is I always want to save data in mm to database but use angular to see it in both format.
I have already created a solution for it. http://plnkr.co/edit/thgx8vjsjxwfx6cLVVn1?p=preview
But it is using ng-change everytime to convert the values.
I was wondering if some angular expert has better idea of doing similar stuff. Please note that I am only planning to save single value $scope.lengthmm
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
// $scope.lengthtodb = 0;
$scope.mmToFtIn =function(){
toInch = $scope.lengthmm * 0.0393701;
toFt = toInch/12;
OutputFT = Math.floor(toFt);
OutputInch = (toFt - Math.floor(toFt))*12;
$scope.lengthft = OutputFT;
$scope.lengthin = OutputInch;
$scope.Result1 = OutputFT + "ft" + OutputInch + "Inches";
};
$scope.FtInTomm =function(){
tomm = (($scope.lengthft *12)+ $scope.lengthin)*25.4;
$scope.lengthmm = tomm;
$scope.Result2 = tomm;
};
});
In addition, as there will be lots of fields using the the same logic maybe method mmToFTIn needs to split in two methods to bind ft and inches separately. But I am really looking forward to see a smarted solution.
Formatting the Output onto the view is best done with filters.
JS:
app.filter('inchFilter', function() {
return function(input) {
return Math.floor(input * 0.0393701);
};
});
HTML:
<input name="mm" type="text" value="{{lengthmm | inchFilter}}">
Edit:
For a more complete and powerful solution I extended the plunker with a directive to now allow two-way-binding on the non-metric fields aswell.
app.directive('enhancedInput', function($parse){
return {
restrict: 'A',
require: 'ngModel',
link : function(scope, element, attr, ngModelCtrl){
ngModelCtrl.$parsers.unshift(scope.$eval(attr.fromMm));
ngModelCtrl.$formatters.unshift(scope.$eval(attr.toMm));
}
};
});
This is achieved by first "requiring" the ngModelController and then using its $parsers and $formatters to intercept communication in between model and view.
plunker
One option is to create a $watch on the millimeter variable and then update the inches model. This is probably a better option than ng-change because the $watch will fire any time the variable is changed within the scope, not just when the input to your text box changes.
I have created an example for you. While you state you only wish to use one variable, I believe using a second variable in this manner is really the better approach. Alternatively you could use an object or an array to maintain a single variable while storing two values.
For this example I defined the variables as follows
$scope.inches = 0;
$scope.mm = 0;
Then we just create a watch on both variables. To reiterate, this function gets called any time there is a change to the mm variable which allows you to ensure that the relationship between mm and inches is maintained.
$scope.$watch('mm', function(newVal, oldVal) {
$scope.inches = newVal / 25.4;
});
You could also create a custom filter as follows... this might even be a better solution if you only need to display mm in a formatted manner (as opposed to using it for calculations or something else).
angular.module('myFilters', []).filter('mmToInches', function() {
return (function (input) {
return (input / 25.4);
});
});
Then later on do...
Inches Filter : {{ mm | mmToInches }}
You can add arguments to filters to a single filter can do both conversions.
angular.module('myFilters', []).filter('convert', function() {
return (function (input, type) {
if (type == 'mm2inch')
{
return (input / 25.4);
} else if (type == 'inch2mm') {
return (input * 25.4);
}
});
});
Inches Filter : {{ mm | convert:'mm2inch' }}
MM Filter : {{ inches | convert:'inch2mm' }}
Here is an improvement I made to Christoph Hegemann's answer.
The directive only uses one attribute, which points to a scope object inchesShowFeet (you can call it anything you want).
<input type="text" ng-model="data.beamLength"
ng-enhanced-input="inchesShowFeet"
ng-model-options="{ debounce: 500 }" />
The directive itself, as he mentioned, uses the ng-model attribute's parsers
app.directive('ngEnhancedInput', function($parse){
return {
restrict: 'A',
require: 'ngModel',
link : function(scope, element, attr, ngModelCtrl){
ngModelCtrl.$parsers.unshift(scope.$eval(attr.ngEnhancedInput).store);
ngModelCtrl.$formatters.unshift(scope.$eval(attr.ngEnhancedInput).show);
}
};
});
The object, usually set in the controller, has a show and a store function. The name is funny, but it's the best I could think of. The one returns the value to show in the text box, the other one returns the value to store in the model.
$scope.inchesShowFeet = {
show: function(val){ return val / 12; },
store: function(val){ return val * 12; }
};
So, say I have 25 inches stored in my model. The show function get's called with a val of 25. It divides it by 12 and returns it, which get's displayed in the text box. When you type in 4, the store function get's called with a val of 4. It multiplies it by 12 and returns 48, which get's stored in the model.

AngularJS: 2-way binding of custom string serialization

Short Question:
How to create a <input type="text"> which contains a custom-format string serialization of an object in a way that editing the string updates the model and vice versa?
I think AngularJS’ directives are the way to go, but i can’t get it pinned down.
Long Question:
Prequel
I have a object which is my application’s “master model”. it can be serialized to a string of a specific format:
it has 2-3 attributes, whose serializations are joined by “;” (no trailing “;” if the third is missing)
attributes 2 and 3 are lists of objects, and serialized by joining those with “,”.
the serialization of the objects is just one of their string attributes, or two ow them with “x” between.
so i have a constructor (accepting a spec string), and a toString function. Following; the latter for clarity:
World.prototype.toString = function() {
var spec = [];
spec[0] = this.version;
spec[1] = this.layers.map(function(layer) {
var c = (layer.c > 1) ? layer.c + 'x' : '';
return c + layer.id; //e.g. 'T' or '2xT'
}).join(',');
//spec[2] in python: ','.join(option.id for option in options if option.checked)
var options = this.options.filter(function(option) {
return option.checked;
});
if (options.length > 0)
spec[2] = options.map(function(option) {
return option.id;
}).join(',');
return spec.join(';');
};
The directive i tried to use looks thusly, but the $watch only fires once.
angular.module('layersApp', []).directive('spec', function() {
return {
restrict: 'A',
link: function(scope, element, attrs) {
scope.$watch('world', function(val) {
element.val(val.toString());
console.log('object updated', element.val());
}, true);
element.blur(function(e) {
scope.world = new World(element.val());
});
}
};
});
Actual long question
What i want is an easy way to make this work,
<input type="text" data-ng-model="theWorld" spec>
where spec is the custom directive shown above, setting up the two-way binding
Outlook
it would be awesome if this could result in a generic “serialization” directive used like that:
<input type="text" data-serialization="string2Foo, foo2String" data-ng-model="foo">
Which would look up the object foo, and the functions string2Foo and foo2String to setup custom (de)serialization.
I think you can use of $parsers and $filters of ngModel controller.
Here is the simplest example of doing it.
http://plnkr.co/edit/13PJN2
It should be easy to add validation, too.
I tried to make it accept custom serializer from parent scope, but failed to do so. Not sure about it.

Categories

Resources