I currently have an underscore.js template that I would also like to use with angular and still be able to use with underscore. I was wondering if it's possible to change the interpolation start and end symbols for a particular scope using a directive, like this:
angular.directive('underscoreTemplate', function ($parse, $compile, $interpolateProvider, $interpolate) {
return {
restrict: "E",
replace: false,
link: function (scope, element, attrs) {
$interpolateProvider.startSymbol("<%=").endSymbol("%>");
var parsedExp = $interpolate(element.html());
// Then replace element contents with interpolated contents
}
}
})
But this spits out the error
Error: Unknown provider: $interpolateProviderProvider <- $interpolateProvider <- underscoreTemplateDirective
Is $interpolateProvider only available for module configuration? Would a better solution be to simply using string replace to change <%= to {{ and %> to }}?
Also, I noticed that element.html() escapes the < in <%= and > in %>. Is there a way to prevent this automatic escaping?
Ok, you have a couple issues here, but I found a solution for you.
Demo
http://jsfiddle.net/colllin/zxwf2/
Issue 1
Your < and > characters are being converted to < and >, so when you call element.html(), you won't even find an instance of < or > in that string.
Issue 2
Since the $interpolate service has already been "provided" by the $interpolateProvider, it doesn't look like you can edit the startSymbol and endSymbol. However, you can convert your custom startSymbol and endSymbol to the angular start/end symbols dynamically in your linking function.
Solution
myApp.directive('underscoreTemplate', function ($parse, $compile, $interpolate) {
return {
restrict: "A",
link: function(scope, element, attrs) {
var startSym = $interpolate.startSymbol();
var endSym = $interpolate.endSymbol();
var rawExp = element.html();
var transformedExp = rawExp.replace(/<%=/g, startSym).replace(/<%-/g, startSym).replace(/%>/g, endSym);
var parsedExp = $interpolate(transformedExp);
scope.$watch(parsedExp, function(newValue) {
element.html(newValue);
});
}
}
});
Alternatives
I'm not sure how, but I'm sure there's a way to instantiate your own custom $interpolate service using the $interpolateProvider (after configuring it for underscore tags).
Related
I have a custom directive which I am decorating a <select ng-options=""> with as such...
<select custom ng-model="selected" ng-options="o for o in values">
with custom as my directive and values being a simple array. Here is my implementation...
<select custom ng-model="selected" ng-options="o for o in values">
<option value selected>uhh?</option>
</select>
app.directive('custom', [function() {
return {
scope: {},
link: function(scope, elem, attrs) {
// how can I get my array of values here?
}
}
}])
app.controller('ctrl', ['$scope', function($scope) {
$scope.values = ['er', 'um', 'eh'];
}])
Within my link I can see it as such
console.log(attrs.ngOptions);
Which, in this case, logs out the literal "o for o in values". Can I somehow parse or compile this within my link to get the array? I see I can grab it if I do something like scope.$parent.values, but this seems unnecessary and I would need to know the name of "values". I can probably get it through some hacky feeling string manipulation to target it, but I am hoping there is a more intuitive way.
hacky e.g.
var array = attrs.ngOptions.split(' ').pop(); // "values"
console.log(scope.$parent[array]);
Side note - constricted to AngularJS 1.2.x for this example
JSFiddle Link - example
As of Angular v1.4, neither select nor ngOptions directives provide an API to get the array of items that results in <option>s, so we are only left with 2 choices - 1) pass the values array explicitly to custom directive as an attribute value, or 2) derive it from the micro-syntax of ng-options.
With #1 - the approach is straightforward.
With #2 - we would need to parse the microsyntax, for example with RegExp. This is fragile, since the micro-syntax may change in the future.
We could use Angular's own regex expression (see src v1.4.3) to parse this syntax:
var NG_OPTIONS_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s+([\s\S]+?))?(?:\s+disable\s+when\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w]*)|(?:\(\s*([\$\w][\$\w]*)\s*,\s*([\$\w][\$\w]*)\s*\)))\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?$/;
(the 8th group matches the items)
Or, we could make it a simpler regex, perhaps even stabler, for example:
/^.*\s+in\s+(\S+)[\s\S]*$/
At any rate, the directive would look like so:
app.directive('custom', function($parse) {
return {
scope: {},
link: function(scope, elem, attrs) {
var optionsExp = attrs.ngOptions;
var match = optionsExp.match(NG_OPTIONS_REGEXP);
var valuesExp = match[8];
var valuesFn = $parse(valuesExp);
var values = valuesFn(scope.$parent);
// or simpler:
// var values = $parse(match[8])(scope.$parent);
}
}
})
Look the fiddle below.
JsFiddle
<div ng-app="app" ng-controller="ctrl">
<select custom ng-model="selected" ng-options="o for o in values">
<option value selected>uhh?</option>
</select>
</div>
var app = angular.module('app', []);
app.directive('custom', [function(){
return {
link: function(scope, elem, attrs) {
console.log(scope.values)
}
}
}])
app.controller('ctrl', ['$scope', function($scope){
$scope.values = ['er', 'um', 'eh'];
}])
Yeah, This is weird issue.
I am adding code to existing code base where there is a directive
<my-progress ng-progress="processingReport" msg="someString"></my-progress>
The problem is that msg needs to be dereferenced string.
I have a scope variable as $scope.myStatus but providing msg={{myStatus}} is not producing anything
Is there a way to dereference the value of $scope.myStatus so that msg only receives its value?
UPDATE
The directive looks like
.directive('myProgress', function($compile) {
return {
restrict: 'E',
link: function(scope, element, attrs){
var msg = attrs.msg?attrs.msg: "{{"+attrs.ngMsg+"}}";
var template = '<p ng-if="'+ attrs.ngProgress +'"><img src="img/busy-20.gif" alt=""> '+ msg +'</p>';
var el = $compile(template)(scope);
element.replaceWith(el);
}
};
})
UPDATE 1
As per #charlietfl recommendation, the following worked well
In Controller
$scope.runningStatus = {progressStatus: 'Not Started'};
In HTML
<my-progress ng-progress="processingReport" ng-msg="runningStatus.progressStatus"></my-progress>
Based on directive code you should be able to use :
ng-msg="myStatus"
Which would get added into the template as an expression
Note the line in directive:
var msg = attrs.msg?attrs.msg: "{{"+attrs.ngMsg+"}}";
which looks for one or the other attribute and treats them differently
The directive is kind of a hack and should be rewritten, but as is it should accept an interpolated expression:
<my-progress ng-progress="processingReport" msg="{{myStatus}}"></my-progress>
Don't forget the double quotes " around {{myStatus}}
If it still doesn't work, could you test that your scope is okay by adding this test element just after the directive one:
<div>{{myStatus}}</div>
I can't seem to wrap my mind around how I would pull this off. I have a directive which looks like the following:
.directive('seqWidget', ['Sequence', function(Sequence){
return {
restrict: 'E',
scope: {
placeholder: '#',
option: '#'
},
template: '<fieldset><legend data-ng-transclude></legend><input type="text" placeholder = {{placeholder}} autofocus data-ng-model="index" data-ng-change="retrieve({{option}});"/>{{output}}</fieldset>',
replace: true,
transclude: true,
link: function ($scope, $elem, $attr){
$scope.retrieve = function(key){
//var option = $attr.option;
console.log(key);
}
}
}
}]);
My HTML is as such:
<seq-widget placeholder="Index 0-400" option="accurate">Which index number would you like to gain accuracy on?</seq-widget>
I have tried several other ways of accomplishing a dynamic way of changing my function call based on an attribute value. I would use the '&' prefix but I'd like for this function to be triggered anytime the input is changed. Is there a practical way to achieve what I am trying to do? Or do I need to use jQuery to say something like $('input').on('change', function(){}); in my link function?
You do not have to pass option it is already in the scope, while you set up a text binding option: '#'.
So just do:-
$scope.retrieve = function(key){
console.log($scope.option);
}
It will also work if you remove interpolation, you do not have to interpolate scope variables in an expression.
data-ng-change="retrieve(option);"
What I'm trying to do:
I am trying to dynamically update a scope with AngularJS in a directive, based on the ngModel.
A little back story:
I noticed Angular is treating my ngModel strings as a string instead of an object. So if I have this:
ng-model="formdata.reports.first_name"
If I try to pull the ngModel in a directive, and assign something to it, I end up with $scope["formdata.reports.first_name"]. It treats it as a string instead of a nested object.
What I am doing now:
I figured the only way to get this to work would be to split the ngModel string into an array, so I am now working with:
models = ["formdata", "reports", "first_name"];
This works pretty good, and I am able to use dynamic values on a static length now, like this:
$scope[models[0]][models[1]][models[2]] = "Bob";
The question:
How do I make the length of the dynamic scope dynamic? I want this to be scalable for 100 nested objects if needed, or even just 1.
UPDATE:
I was able to make this semi-dynamic using if statements, but how would I use a for loop so I didn't have a "max"?
if (models[0]) {
if (models[1]) {
if (models[2]) {
if (models[3]) {
$scope[models[0]][models[1]][models[2]][models[3]] = "Bob";
} else {
$scope[models[0]][models[1]][models[2]] = "Bob";
}
} else {
$scope[models[0]][models[1]] = "Bob";
}
} else {
$scope[models[0]] = "Bob";
}
}
This is an answer to
I noticed Angular is treating my ngModel strings as a string instead of an object
Add the require property to your directive then add a fourth ctrl argument to your link function
app.directive('myDirective', function() {
return {
require: 'ngModel',
link: function(scope, element, attributes, ctrl) {
// Now you have access to ngModelController for whatever you passed in with the ng-model="" attribute
ctrl.$setViewValue('x');
}
};
});
Demonstration: http://plnkr.co/edit/Fcl4cUXpdE5w6fHMGUgC
Dynamic pathing:
var obj = $scope;
for (var i = 0; i<models.length-1; i++) {
obj = obj[models[i]];
}
obj[models[models.length-1]] = 'Bob';
Obviously no checks are made, so if the path is wrong it will fail with an error. I find your original problem with angular suspicious, perhaps you could explore a bit in that direction before you resort to this workaround.
I am pretty new to angularJs. My problem is:
My model data are kept in a database and if a project is reopened the data is displayed. With save the data are stored again in the database. I have a problem with displaying the number with
since the value is stored as a string and must be parsed to an integer.
Here is the html:
<numeric-input numericbinding="item.showInt" ng-model="item.operandvalue" ></numeric-input>
Depending on the format of the item value (bool, int, string), the operandvalue is displayed in different ways. This is directed with the value of numericbinding.
I tried to use the following directive to parse the string to an int:
.directive('numericInput', function () {
return {
restrict: 'E',
require: 'ngModel',
scope: {
model: '=ngModel',
numericbinding: '='
},
template: '<input id="integerType" type="number" ng-model="intValue" ng-hide="!numericbinding" >',
link: function (scope) {
//avoid that string and bool is parsed, showInt is false
if (scope.numericbinding === false || scope.numericbinding === undefined) {
return;
}
scope.intValue = parseInt(scope.model, 10);
}
};
})
This way the number is displayed correctly. But if I want to store my data again for the bound item.operandvalue I get a Nullpointer in the backend. I understand that I need to convert the number back to a string. I tried multiple ways,e.g.
ng-change: But it never got in the given function.
Then I wanted to use ngModelController with formatter and parser as described in AngularJS - Formatting ng-model before template is rendered in custom directive
.directive('numericInput', function () {
return {
restrict: 'E',
require: 'ngModel',
scope: {
model: '=ngModel',
numericbinding: '='
},
template: '<input id="integerType" type="number" ng-model="item.operandvalue" ng-hide="!numericbinding" >',
link: function (scope, ngModelController) {
//avoid that string and bool is parsed, showInt is false
if (scope.numericbinding === false || scope.numericbinding === undefined) {
//console.log('not a number - return');
return;
}
ngModelController.$formatters.unshift(function(valueFromModel) {
return parseInt(valueFromModel, 10);
});
ngModelController.$parsers.push(function(valueFromInput) {
return valueFromInput.toString();
});
}
};
})
However, the valueFromModel is undefined. The number is not displayed at all. Also scope.item.operandvalue is undefined. In Chrome the error is: TypeError: Cannot read property 'operandvalue' of undefined.
I think that I should keep the item.operandvalue, which is data bound, as a string and display the int value with a different name. In my first way it seems that item.operandvalue gets a number and the data binding does not work any longer.
How can I fix my problem? Many thanks for any help!
You could use two fields: backing_field and binding_field. When the db is read you write to the backing_field. This could then be used to update the binding_field, e.g. via $watch. You can use two watches to keep them in sync. In case one changes, you convert its value and write it to the other. You might now argue that his will create a loop, but AngularJS detects that nothing changes and will not fire again. Using this, you can update the backing_field, when the binding_field changes due to user interaction and update the binding_field through the backing_field when you get new data from the database.