Evaluate an expression from within a JSON file - javascript

I have an Angular app where data is loaded in through JSON files. For the various objects, one of the properties is a "Description". In my app I pop it in my html via {{item.Description}}. My problem is that the string in the JSON file has values that need to be adjusted based on a variable. For example, "The value is 160 (+20 per var)". I would like this description to read out 160 plus 20 times the value of the provided variable.
Unfortunately I can't just put {{160+(20*var)}} in the description, because it just prints out the expression as string.
Is there anyway to create that binding in angular so it updates dynamically based on the other variable?
Update
As per request I'm adding as much code as I can.
In my file's head I'm including a JSON file with:
<script src="path/to/file.json"></script>
Then, I have my controller:
app.controller('itemController', function(){
this.items = Items //Items is declared in the JSON file as the JSON object.
});
Then in my HTML I call:
<div ng-controller="itemController as ctrl">
<span class="description" ng-repeat="item in ctrl.items">
{{item.Description}}
</span>
</div>
The problem is, that item.Description has expressions I would like to evaluate. I would normally just do {{160+(20*ctrl.var)}}, but since that expression is contained in the item.Description string, Angular doesn't evaluate it normally.

It appears you can do this by replacing {{item.Description}} with {{$eval(item.Description)}}, which will evaluate a string as an Angular expression. See the Angular docs for expressions, or a StackOverflow post about $eval.
Edit: OP has clarified that item.Description may contain mixed Angular expressions and other text, for example "The value is {{85 + 22 * ctrl.var}}". Fortunately the Angular docs for $compile contain an example that solves this exact problem! Here is a brief demo.
angular.module('app', [])
.directive('compile', function($compile) {
// directive factory creates a link function
return function(scope, element, attrs) {
scope.$watch(
function(scope) {
// watch the 'compile' expression for changes
return scope.$eval(attrs.compile);
},
function(value) {
// when the 'compile' expression changes
// assign it into the current DOM
element.html(value);
// compile the new DOM and link it to the current
// scope.
// NOTE: we only compile .childNodes so that
// we don't get into infinite loop compiling ourselves
$compile(element.contents())(scope);
}
);
};
})
.controller('itemController', function() {
this.var = 5;
this.items = [
{Description: "What's 1+1? It's {{1+1}}"},
{Description: "The value is {{85+22*ctrl.var}}"},
{Description: "He{{'llo'}} World!"}
];
});
body {
font-family: sans-serif;
}
#main > span {
background-color: #ddd;
padding: 8px 16px;
margin: 8px;
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<body ng-app="app" class="ng-scope">
<h1>Compile Test!</h1>
<div ng-controller="itemController as ctrl" id="main">
<span class="description ng-scope" ng-repeat="item in ctrl.items" compile="item.Description"></span>
</div>
</body>

Related

Different way of parsing through value part of key-value pair in AngularJS

I have a key-value pair that looks something like this ($ctrl.displayData)-
143:"/this/is/a/very/long/fil22↵/this/is/a/very/long/file/path.php↵anotherone.php↵newfilel123.php"
It saves file names and when I display it with an ng-repeat, file names are displayed in newlines (just as I want).
For display I use-
<div>
<div id="outputDiv" ng-click="$ctrl.deleteRow(displayData)" ng-repeat="displayData in $ctrl.displayData">{{displayData}}</div>
</div>
The function deleteRow() is pretty basic as of now-
ctrl.deleteRow = function(index){
console.log(index);
}
But when I loop through using ng-repeat, the entire {{displayData}} gets printed in just one iteration, so if I were to call a function like deleteRow() on click of any one file name, it just returns the entire set of file names each time (and not the particular file name that I have clicked on).
Is there a way of looping through $ctrl.displayData in such a way that on clicking any particular file name, the function is called only for that file name.
could you use javascript split?
// <body ng-app='myApp' binds to the app being created below.
var app = angular.module('myApp', []);
// Register MyController object to this app
app.controller('MyController', ['$scope', MyController]);
// We define a simple controller constructor.
function MyController($scope) {
$scope.files = "/this/is/a/very/long/fil22 \n /this/is/a/very/long/file/path.php \n anotherone.php \n newfilel123.php"
$scope.split = function(s) {
return s.split('\n');
}
$scope.doStuff = function(d) {
console.log(d);
}
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="myApp">
<div ng-controller='MyController'>
<p ng-repeat="d in split(files)">
<span ng-click="doStuff(d)">{{d}}</span>
</p>
</div>
</div>
I handled it inside the JS controller (using split there).
ctrl.arrayList = new Array();
for(var i in ctrl.displayData) {
ctrl.arrayList = ctrl.displayData[i].split('\n');
}
return ctrl.arrayList;
After this I use an ng-repeat on the array ctrl.arrayList for display inside the HTML file.

Issues with calling function from template

In one case I have a problem with running a function on the Controller from the template. The value becomes a string containing the function signature, not the value that should be returned from the function.
When I use {{ getSomeObject(d) }} in my template markup it works fine, and it prints the object values, meaning that the function got called on the Controller.
I have tried with and without the {{ }}.
Pseudo code:
<div class"xyz" data-lav-fact="getSomeObject(d)"> <!-- Does not work here -->
{{ getSomeObject(d) }} <!-- Works here -->
</div>
And of course the function is added to the scope in the Controller:
$scope.getSomeObject = function(data) {
return { key: "test" };
};
This works in other parts of the application and I don't know what wrong in this case.
Does anyone know what typically can be wrong here?
Since you are trying to set an attribute with a $scope function, you'll need to {{ interpolate }} and use ngAttr attribute bindings. Here is a simple example that shows this in action. Examine the difference between the elements logged out. As you dig, you'll see your { key: 'test' } value being set
<div id="without" data-lav-fact="getSomeObject()">without</div>
<div id="with" ng-attr-data-lav-fact="{{ getSomeObject() }}">with</div>
app.controller('ctrl', ['$scope', function($scope) {
$scope.getSomeObject = function() {
return { key: 'test' };
}
var w = angular.element(document.getElementById('with'));
var wo = angular.element(document.getElementById('without'));
console.log(w[0].attributes); // has value
console.log(wo[0].attributes); // does not have value
}]);
JSFiddle Link

Does AngularJS store a value in the $error.maxlength object?

I've got a UI page setup through Angular, and I'm trying to take advantage of the built in ng-maxlength validator on an input element. Long story short, I know about $scope.form.$error and how that object has a maxlength property in the case that the validation fails. But I want to display an error message specific to the character length that was violated, and I don't see anywhere that the length that I specified was stored on this object. Does anyone know if it's possible to access this, so I don't have to write out a separate error message for each input that has the max length violated?
EDIT: To answer your question, yes angular does store a boolean value in the $error object that is accessible to your via the key(s) that are set in the object. In the case of the code I provided below and in th jsFiddle, we are setting the key for angular, and the value of either true or false.
Be mindful when setting the value as it is reversed. ex. $setValidity( true ), flips the $error to false.
Ok, here is what I think you were looking for...
In Angularjs v1.2.13 you will not have access to ng-message or the $validator pipeline,
which is why are are using $formatters and $parsers.
In this case, I am using named inputs, but perhaps in your case you need dynamic input names?
Plus, if you are using inputs but no form, then getting the error message to display would have to be done with a separate custom directive.
If so, then please look here for dynamically named input fields for some help.
dynamic input name in Angularjs link
Let me know if this works; I'll make changes as needed to HOOK YOU UP!
In case you don't know, you can write over Angular's maxlength for each individual input.
If you changed 'maxlength' in the updateValidity() function in the directive below, to something like 'butter', then $scope.form.inputname.$error would be something like
$scope.formname.inputname.$error { butter: true }
if you also used ng-maxlength="true", then it would be
$scope.formname.inputname.$error { butter: true, maxlength: true }
Another example if you used ng-maxlength, and capitalized the 'maxlength' in the directive to 'Maxlength'
Then you would get
$scope.formname.inputname.$error { maxlength: true(angular maxlength), Maxlength: true(your maxlength)
And of course if you name it the same, then yours writes over angulars
$scope.formname.inputname.$error { maxlength: true };
The point is YOU can add your own names to the angular $error object; you can write over Angular's; and you can just use what Angular gives you when you use Angular's directives: like ng-required="true", or ng-maxlength="true"
Link to YOUR angularjs version on jsFiddle
jsFiddle LInk
<div ng-app="myApp">
<form name="myForm">
<div ng-controller="MyCtrl">
<br>
<label>Input #1</label>
<br>
<input ng-model="field.myName" name='myName' my-custom-length="8" />
<span ng-show="myForm.myName.$error.maxlength">
Max length exceeded by {{ myForm.myName.maxlength }}
</span>
<br>
<br>
<label>Input #2</label>
<br>
<input ng-model="field.myEmail" name='myEmail' my-custom-length="3" />
<span ng-show="myForm.myEmail.$error.maxlength">
Max length exceeded by {{ myForm.myEmail.maxlength }}
</span>
</div>
</form>
</div>
var app = angular.module('myApp', []);
app.controller('MyCtrl', function ($scope) {
$scope.field = {};
});
app.directive("myCustomLength", function () {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, element, attrs, ctrl) {
if (!ctrl) { return } // ignore if no ngModel controller
ctrl.$formatters.push(validateInput);
ctrl.$parsers.unshift(validateInput);
function validateInput(value) {
if (!value) {
updateValidity(false);
return;
}
inputLength(value);
var state = value.length > attrs.myCustomLength;
updateValidity(state);
}
function inputLength(value) {
ctrl.maxlength = null;
var length = value.length > attrs.myCustomLength;
if (length) {
ctrl.maxlength = (value.length - attrs.myCustomLength).toString();
}
}
function updateValidity(state) {
ctrl.$setValidity('maxlength', !state);
}
} // end link
} // end return
});
CSS Here if you need it.
input.ng-invalid {
border: 3px solid red !important;
}

AngularJS : Compile directives inside HTML returned by an API

So I have access to a REST API that I am hitting, that returns the following pre-generated HTML:
<p class="p">
<sup id="John.3.16" class="v">16</sup>
<span class="wj">“For </span>
<span class="wj">God so loved </span>
<span class="wj">the world,</span>
<span class="wj">that he gave his only Son, that whoever believes in him should not </span>
<span class="wj">perish but have eternal life.</span>
</p>
This has presented an interesting new challenge for me in my learning of AngularJS. I have no control over the HTML that is returned from the API, since it's not an API that I built.
What I'm trying to do (and this could be the completely wrong approach) is to build a class directive on the "v" class, so that I can add an ng-click attribute to the verse number and pass the verse information on to another part of my application.
Below is the code I currently have, which doesn't seem to do anything, though I thought it would.
var app = angular.module('ProjectTimothy');
app.filter("sanitize", ['$sce', function($sce) {
return function(htmlCode){
return $sce.trustAsHtml(htmlCode);
}
}]);
app.controller("timothy.ctrl.search", ['$scope', '$http', function($scope, $http){
$scope.url = "http://apiendpoint.com/";
$scope.copyright = "";
$scope.search = function() {
// Make the request to the API for the verse that was entered
// Had to modify some defaults in $http to get post to work with JSON data
// but this part is working well now
$http.post($scope.url, { "query" : $scope.searchwords, "version": "eng-ESV"})
.success(function(data, status) {
// For now I'm just grabbing parts of the object that I know exists
$scope.copyright = data.response.search.result.passages[0].copyright;
$scope.result = data.response.search.result.passages[0].text;
})
.error(function(data, status) {
$scope.data = data || "Request failed";
$scope.status = status;
});
};
}]);
app.directive("v", ['$compile', function($compile) {
return {
restrict: 'C',
transclude: true,
link: function(scope, element, attrs) {
element.html("<ng-transclude></ng-transclude>").show();
$compile(element.contents())(scope);
},
scope: { id:'#' },
/*template: "<ng-transclude></ng-transclude>",*/
replace: false
};
}]);
HTML Template that is being populated with the HTML returned by API:
<div class="bible_verse_search_container" ng-controller="timothy.ctrl.search">
<div class="input-group">
<input type="text" class="form-control" placeholder="Bible Verse To Read (i.e. John 11:35)" ng-model="searchwords">
<span class="input-group-btn">
<button class="btn btn-default" type="button" ng-click="search()">Search</button>
</span>
</div>
<div class="well" ng-show="copyright" ng-bind-html="copyright | sanitize"></div>
<div ng-bind-html="result | sanitize"></div>
</div>
So What I was hoping would happen would be that the HTML is populated into the bottom div that binds the html, and then somehow $compile would be called to convert the "v" class sup's into directives that I can modify. Again, I'm pretty new to Angular, so there may be a super easy way to do this like most other things in Anguler that I just haven't found yet.
Really, the end goal is that each verse number is converted into a directive of its own to be able to make it clickable and access the id attribute that it has so that I can send that information with some user content back to my own API.
This feels like a lot of information, so let me know if anything is unclear. I'll be working on it, so if I figure it out first, I'll be sure to update with an answer.
IN PROGRESS
Checked out this question: https://stackoverflow.com/a/21067137/1507210
Now I'm wondering if it would make more sense to try and convert the section where the verse is displayed into a directive, and then making the search controller populate a scope variable with the HTML from the server, and then use that as the template for the directive... think think think
I think your second approach--convert the section where the verse is displayed into a directive--would be a nice way of doing it.
You could replace this:
<div ng-bind-html="result | sanitize"></div>
with a directive like this:
<verse-display verse-html="{{result}}"></verse-display>
The directive definition would look like this:
app.directive('verseDisplay', ['$compile', function($compile) {
function handleClickOnVerse(e) {
var verseNumber = e.target.id;
// do what you want with the verse number here
}
return {
restrict: 'E',
scope: {
verseHtml: '#'
},
replace: true,
transclude: false,
template: '<div ng-bind-html="verseHtml | sanitize"></div>',
link: function(scope, element, attrs) {
$compile(element.contents())(scope);
element.on('click', '.v', handleClickOnVerse);
}
};
}]);
So you could apply your own click handler to the element.
Here's a fiddle. (Open the console to see the verse number getting logged out.)
Probably the most unwisest of things I have posted here, but it's pretty cool code. I do not know if I recommend actually running this, but here's a jsfiddle
So one of the reasons I call this unwise is because the injected code will run any directives you have not just the one you wanted. There may be many other security risks beyond that as well. But it works fantastically. If you trust the HTML you are retrieving then go for it.
Check out the fiddle for the rest of the code:
function unwiseCompile($compile) {
return function (scope, element, attrs) {
var compileWatch = scope.$watch(
function (scope) { return scope.$eval(attrs.unwiseCompile); },
function (unwise) {
element.html(unwise);
$compile(element.contents())(scope);
// for better performance, compile once then un-watch
if (scope.onlyOnce) {
// un-watch
compileWatch();
}
});
};
}

$digest rendering ng-repeat as a comment

I'm writing a test for a directive, when executing the test the template (which is loaded correctly) is rendered just as <!-- ng-repeat="foo in bar" -->
For starters the relevant parts of the code:
Test
...
beforeEach(inject(function ($compile, $rootScope, $templateCache) {
var scope = $rootScope;
scope.prop = [ 'element0', 'element1', 'element2' ];
// Template loading, in the real code this is done with html2js, in this example
// I'm gonna load just a string (already checked the problem persists)
var template = '<strong ng-repeat="foo in bar"> <p> {{ foo }} </p> </strong>';
$templateCache.put('/path/to/template', [200, template, {}]);
el = angular.element('<directive-name bar="prop"> </directive-name>');
$compile(el)(scope);
scope.$digest(); // <--- here is the problem
isolateScope = el.isolateScope();
// Here I obtain just the ng-repeat text as a comment
console.log(el); // <--- ng-repeat="foo in bar" -->
}));
...
Directive
The directive is fairly simple and it's not the problem (outside the test everything works just fine):
app.directive('directiveName', function () {
return {
restrict : 'E',
replace : true,
scope : {
bar : '='
},
templateUrl : '/path/to/template', // Not used in this question, but still...
});
A few more details:
The directive, outside the test, works fine
If I change the template to something far more simple like: <h3> {{ bar[0] }} </h3> the test works just fine
The rootScope is loaded correctly
The isolateScope results as undefined
If you have a look at the generated output that angular creates for ng-repeat you will find comments in your html. For example:
<!-- ngRepeat: foo in bars -->
These comments are created by the compile function - see the angular sources:
document.createComment(' ' + directiveName + ': '+templateAttrs[directiveName]+' ')
What you get if you call console.log(el); is that created comment. You may check this if you change the output in this way: console.log(el[0].parentNode). You will see that there are a lot of childNodes:
If you use the directive outside of a test you will not be aware of this problem, because your element directive-name will be replaced by the complete created DocumentFragment. Another way to solve the problem is using a wrapping element for your directive template:
<div><strong ng-repeat="foo in bar"> <p> {{ foo }} </p> </strong></div>
In this case you have access to the div element.

Categories

Resources