I have inherited a mess of a Angular project. I've never really messed with Angular too much but know MVC well enough to feel like I can learn. My question is I have a property of a JSON object that I want to return a different views for. (one is an archived state and one is a non-archived state) As they both have different view templates, how would I return the non-archive template if the json.status == 'archived'
I have the following as my current StateProvider's templateURL property.
templateUrl: appConfig.viewPath + 'non-archived.html?v=' + appConfig.version
should I just return multiple template urls here? Or do I have to create a whole new url path?
Thanks!
I've gone down this road a few times, I don't think I've found the optimal way yet, but I've learned a few things.
It really all depends on when you have access to your json-object. You can pass a function to templateUrl, and send in a service.. (A service that returns your current json-object could be great, but how would you update it? Probably when you change route right? Then you have a egg-hen problem. You can't decide route until you have the json-object, but you don't have the json-object until you change route.)
But IF you have access to the json-object you could do something like this:
templateUrl: function(){
var tpl = (json.status == 'archived') ? 'archived.html?v=' : 'non-archived.html?v=';
return appConfig.viewPath + tpl + appConfig.version
}
But my guess is that you don't have access to the json-object until after the route has loaded.
Then I'd say the easiest way (but maybe not so pretty) is to have just one template. $scope.json = json in the controller:
<div ng-if="json.status == 'archived'">
<h1>ARCHIVED</h1>
...
</div>
<div ng-if="json.status != 'archived'">
<h1>NOT ARCHIVED</h1>
...
</div>
Or if you think that is too cheap, declare two routes. The whole "create a whole new url path" is not as painful as you might think. It'll be considerably less complex than trying to wedge out a value from a route before it has loaded.
1: Try this. send json.status in $stateParams and apply condition inside stateProvider :
$stateProvider.state('home', {
templateProvider: ['$stateParams', 'restService' , function ($stateParams, restService) {
restService.getJson().then(function(json) {
if (status.status == 'archived') {
return '<div ng-include="first.html"></div>';
} else {
return '<div ng-include="second.html"></div>';
}
})
}]
});
2 : or simply in view you can try this:
<div ng-if="json.status == 'archived'">
<h1>ARCHIVED</h1>
...
</div>
<div ng-if="json.status != 'archived'">
<h1>NOT ARCHIVED</h1>
...
</div>
Related
I'm trying to allow for hiding of certain sections of the project I'm working on via user toggle. It saves in the database and gets pulled when the page is loaded in the constructor using the following code
this.http.get(`api/section/get/${this.id}`, this.id).subscribe(res => {
this.section = res.json()[0];
this.sect = res.json();
console.log(this.section);
this.hideIntro = this.sect[0].hideIntro;
this.hideMainVideo = this.sect[0].hideMainVideo;
this.hideHandout = this.sect[0].hideHandout;
this.hideQuiz = this.sect[0].hideQuiz;
console.log("Hide Intro = " + this.hideIntro);
console.log("Hide Main = " + this.hideMainVideo);
console.log("Hide Handout = " + this.hideHandout);
console.log("Hide Quiz = " + this.hideQuiz);
});
The HTML is as follows...
<div class="row classMainBackground col-md-12" *ngIf="!hideIntro">
...content...
</div>
For some reason, no matter what I do, whether I change it to *ngIf="hideIntro == false" or even use [hidden]="hideIntro", it is not working.
Even the console logs in the .ts file show up correctly. Is there a reason why this is not working for me? I've used it in other positions and it works fine there...
Does it have something to do with assigning it in the constructor or something?
Thanks in advance!
Angular change detection runs in response to use interaction with the component. If values are updated outside of that event handling (such as after an HTTP request), you need to manually tell the component that it has changed.
constructor(private changeDetector: ChangeDetectorRef){}
this.http.get(`api/section/get/${this.id}`, this.id).subscribe(res => {
[...]
this.changeDetector.markForCheck();
})
More in depth reading: https://blog.thoughtram.io/angular/2016/02/22/angular-2-change-detection-explained.html
I ended up solving the problem by using {{!section.hideIntro}} in the HTML instead of trying to define a new variable to pass that boolean to.
I believe the answer was a combination of what #Vlad274 and #ConnorsFan were mentioning.
the HTML was returning an [object object] for {{hideIntro}} and it seems like there's a delay between the assigning the new value data from the GET response before the DOM actually loads.
Grabbing the data right from the GET respone variable ended up doing the trick.
I have been fighting with ES6 trying to come up with, what should be, a pretty straightforward operation. I want to call JSON API data for Bitcoin from one of the three following websites:
https://cryptowat.ch
https://coinmarketcap.com/
https://www.cryptocompare.com/
All three sites API endpoints go straight to the price I want and I think this may be the problem. There is no array of data, just the specific price. In my example using #3 above, the only object is "USD". That being said, I think I'm overthinking the process as getting into APIs with much more data and arrays of data -- I have accomplished using ReactJS.
Trying to reach a single endpoint that shows up as the "State" in the React DOM Inspector as "USD" and is pulling in the correct price, I cannot get the price to render on the page even though ReactJS is seeing it and capturing it.
My code:
var BitcoinApp = React.createClass({
getInitialState: function() {
return {
"USD": []
}
},
componentDidMount: function() {
var th = this;
this.serverRequest =
axios.get(this.props.source)
.then(function(result) {
th.setState({
USD: result.data.USD
});
})
},
componentWillUnmount: function() {
this.serverRequest.abort();
},
render: function() {
return (
<span>
{this.state.USD.map(function(Data) {
return (
<div key={Data.USD} className="testbtc">
<p>{Data.USD}</p>
</div>
);
})}
</span>
)
}
});
ReactDOM.render(<BitcoinApp source="https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=USD&e=Coinbase" />, document.querySelector("#btcPrice"));
I will mention that I have done a lot of research into this and have found a lot of answers -- all different! Everyone knows the ReactJS docs are severely outdated so finding the right path with ReactJS is difficult to say the least. Also, I'm using "axios" to "GET" the API data as I've read that "fetch" isn't globally supported yet? Is this still the case in 2017?
Using the above method, I can see this in the Inspector:
But when I go over to the "Console" portion of the inspector, I'm told that "this.state.USD.map is not a function".
I feel like I'm right on the cusp of solving this task, but I think I'm getting something wrong with the mapping of the promise.
the problem is that:
th.setState({
USD: result.data.USD
});
is seting not iterable object. I mean that this.state.USD.map is not a function means that USD is not an array (and you can see this in console).
Try this to see what happens:
th.setState({
USD: [result.data.USD]
});
However tho, you wrote:
There is no array of data, just the specific price.
then I think the best solution is to change just the render method and initial state:
render: function() {
return (
<span>
<div className="testbtc">
<p>{this.state.USD}</p>
</div>
</span>
)
}
getInitialState: function() {
return {
"USD": "",
}
},
Hi I am just beginning with angular and I am struggling to find the answer to what I'm sure is quite a simple thing to do.
I am currently getting the values of some input boxes and pushing them into my scope. This is creating one long 'array' eg:
['data-1','data-2','data-3']
I would like to format my data in the following way instead
$scope.data = [
{
'header1': 'data1-1',
'header1': 'data1-2',
'header1': 'data1-3'
},
{
'header1': 'data2-1',
'header1': 'data2-2',
'header1': 'data2-3'
}
]
This is my function as it currently is.
$scope.createRow = function(){
angular.forEach(angular.element("input"), function(value, key){
$scope.td.push($(value).val());
});
}
Any help or pointers would be greatly appreciated as I am just getting my head round the angular way
Doing this isn't hard... but before I give you a gun to shoot yourself in the foot, just to say that I think it would be beneficial to explain WHY you want structure in that other format you are mentioning. You seem to have lots of data repetition and that's always a red flag.
Now for the code, you just need to create object before pushing it to the array like:
$scope.createRow = function(){
angular.forEach(angular.element("input"), function(value, key){
var obj = {
"header1": val + "-1",
"header2": val + "-2"
};
$scope.td.push(obj);
});
}
EDIT:
OK, so you are trying to add new row to the table. First of all, you shouldn't be doing angular.forEach, but rather those input elements in HTML should bind to existing scope object, like:
// obviously use better names than Input1Value
// I am here just giving you example
$scope.bindData = {
Input1Value: null,
Input2Value: null
};
// in HTML you will do
// <input ng-model="bindData.Input1Value" />
// <input ng-model="bindData.Input2Value" />
Now that you've eliminated that nasty angular.forEach you need to have some kind of event handler, for example when user clicks the button you want to add this object to the array to which table is data bound. Just be sure to clone the $scope.bindData object when you add it to array.
$scope.createRow = function(){
var newRowData = $scope.cloneObject($scope.bindData);
$scope.td.push(newRowData);
}
// http://heyjavascript.com/4-creative-ways-to-clone-objects/
// https://stackoverflow.com/questions/728360/most-elegant-way-to-clone-a-javascript-object
$scope.cloneObject = function(objToClone) {
var newObj = (JSON.parse(JSON.stringify(objToClone)));
}
To close this answer off - keep in mind, if you ever find yourself directly referencing HTML DOM elements in Javascript with AngularJS - you are doing something wrong. It's a nasty habit to eliminate, especially if you are coming from jQuery background (and how doesn't?), where everything is $("#OhHiThere_ElementWithThisId).
Obviously the main thread on this topic on StackOverflow is this one:
“Thinking in AngularJS” if I have a jQuery background?
However I find that it's too theoretical, so Google around and you may find better overviews like:
jQuery vs. AngularJS: A Comparison and Migration Walkthrough
I have put the question at the bottom as the only way I could explain my problem was with an example so with out the example it might not make sense but feel free to skip down to the bottom and just read the question.
I will use this example to try give some idea of what I do understand and where my understanding falls down.
I want to build a page where I can browse through a collection of items which I would set up like this:
angular.module('App')
.config(['$stateProvider', function ($stateProvider) {
$stateProvider
.state('browse', {
url: '/browse',
templateUrl: 'app/browse/browse.html',
controller: 'BrowseCtrl',
title: 'Browse',
mainClass: 'browse'
});
}]);
Each item is pulled through and place on this page using ng-repeat and then calling an api:
$scope.items = [];
$http.get('/api/items').success(function(items) {
$scope.items = items;
socket.syncUpdates('Item', $scope.items);
$scope.totalItems = $scope.items.length;
$scope.$watch('currentPage + itemsPerPage', function() {
var begin = (($scope.currentPage - 1) * $scope.itemsPerPage),
end = begin + $scope.itemsPerPage;
$scope.filteredItems = $scope.items.slice(begin, end);
});
});
This then accesses the api and repeats out the items. So far so good. Heres an example of the API setup. Worth mentioning I am using the Angular-fullstack generator which plugs in to Mongo DB using Express & Sockets.io
Item.find({}).remove(function() {
Item.create({
"image_url" : "../../../assets/images/test.jpg",
"title" : "Test Item",
"created_on" : new Date(2014, 9, 23, 3, 24, 56, 2),
"creator" : {
"profile_img" : "../../../assets/images/stephanie-walters.jpg",
"username" : "StephW",
"url" : "/stephanie-walters",
"first_name" : "Stephanie",
"last_name" : "Walters",
}
}
Ok now this is where things start to get unclear for me.
I now need to create the item pages, so that when I click on an item I get access to the content of that item. Short of creating every single page for every entry I would very much like to be able to create a page template that ui-router is able to attach content to when the correct url structure is met.
Thats probably not clear so let me try be a bit clearer. Lets say if we follow that JSON above I want to go to 'Stephanie Walters' profile I am going to need three things.Firstly a profile template, secondly I need the content for the profile in an api call and lastly a dynamic url that can take that api content and put it in to the page template.
Perhaps something similar to:
.state('profile.username', {
url: '/:username',
templateUrl: '/partials/profile.username.html',
controller: 'profileUsernameCtrl'
})
But I don't exactly understand how to get the take a variable like username from the item JSON(above) and then use that to build a URL /:username that connects to a template page profile.username.html and further still fill that page with the users content that is stored in another API call.
To "build a url" so to speak, you need to use the ui-sref directive.
Given a state like so:
.state('profile.username', {
url: '/:username',
templateUrl: '/partials/profile.username.html',
controller: 'profileUsernameCtrl'
})
to create a link to this state use:
<a ui-sref="profile.username({username: user.name})">{{ user.name }}</a>
where user is an attribute on the scope where that link is displayed.
For more complex URLs you just add additional parameters like so:
.state('browse.item', {
url: '/:username/:itemId'
})
To get the parameters you use the $stateParams service in your controller like so:
.controller('MyController', function($scope, $stateParams) {
$scope.username = $stateParams.username;
$scope.itemId = $stateParams.itemId;
})
I'm trying to use $sanitize provider and the ng-bind-htm-unsafe directive to allow my controller to inject HTML into a DIV.
However, I can't get it to work.
<div ng-bind-html-unsafe="{{preview_data.preview.embed.html}}"></div>
I discovered that it is because it was removed from AngularJS (thanks).
But without ng-bind-html-unsafe, I get this error:
http://errors.angularjs.org/undefined/$sce/unsafe
Instead of declaring a function in your scope, as suggested by Alex, you can convert it to a simple filter :
angular.module('myApp')
.filter('to_trusted', ['$sce', function($sce){
return function(text) {
return $sce.trustAsHtml(text);
};
}]);
Then you can use it like this :
<div ng-bind-html="preview_data.preview.embed.html | to_trusted"></div>
And here is a working example : http://jsfiddle.net/leeroy/6j4Lg/1/
You indicated that you're using Angular 1.2.0... as one of the other comments indicated, ng-bind-html-unsafe has been deprecated.
Instead, you'll want to do something like this:
<div ng-bind-html="preview_data.preview.embed.htmlSafe"></div>
In your controller, inject the $sce service, and mark the HTML as "trusted":
myApp.controller('myCtrl', ['$scope', '$sce', function($scope, $sce) {
// ...
$scope.preview_data.preview.embed.htmlSafe =
$sce.trustAsHtml(preview_data.preview.embed.html);
}
Note that you'll want to be using 1.2.0-rc3 or newer. (They fixed a bug in rc3 that prevented "watchers" from working properly on trusted HTML.)
You need to make sure that sanitize.js is loaded. For example, load it from https://ajax.googleapis.com/ajax/libs/angularjs/[LAST_VERSION]/angular-sanitize.min.js
you need to include ngSanitize module on your app
eg: var app = angular.module('myApp', ['ngSanitize']);
you just need to bind with ng-bind-html the original html content. No need to do anything else in your controller. The parsing and conversion is automatically done by the ngBindHtml directive. (Read the How does it work section on this: $sce). So, in your case <div ng-bind-html="preview_data.preview.embed.html"></div> would do the work.
For me, the simplest and most flexible solution is:
<div ng-bind-html="to_trusted(preview_data.preview.embed.html)"></div>
And add function to your controller:
$scope.to_trusted = function(html_code) {
return $sce.trustAsHtml(html_code);
}
Don't forget add $sce to your controller's initialization.
The best solution to this in my opinion is this:
Create a custom filter which can be in a common.module.js file for example - used through out your app:
var app = angular.module('common.module', []);
// html filter (render text as html)
app.filter('html', ['$sce', function ($sce) {
return function (text) {
return $sce.trustAsHtml(text);
};
}])
Usage:
<span ng-bind-html="yourDataValue | html"></span>
Now - I don't see why the directive ng-bind-html does not trustAsHtml as part of its function - seems a bit daft to me that it doesn't
Anyway - that's the way I do it - 67% of the time, it works ever time.
You can create your own simple unsafe html binding, of course if you use user input it could be a security risk.
App.directive('simpleHtml', function() {
return function(scope, element, attr) {
scope.$watch(attr.simpleHtml, function (value) {
element.html(scope.$eval(attr.simpleHtml));
})
};
})
You do not need to use {{ }} inside of ng-bind-html-unsafe:
<div ng-bind-html-unsafe="preview_data.preview.embed.html"></div>
Here's an example: http://plnkr.co/edit/R7JmGIo4xcJoBc1v4iki?p=preview
The {{ }} operator is essentially just a shorthand for ng-bind, so what you were trying amounts to a binding inside a binding, which doesn't work.
I've had a similar problem. Still couldn't get content from my markdown files hosted on github.
After setting up a whitelist (with added github domain) to the $sceDelegateProvider in app.js it worked like a charm.
Description: Using a whitelist instead of wrapping as trusted if you load content from a different urls.
Docs: $sceDelegateProvider and ngInclude (for fetching, compiling and including external HTML fragment)
Strict Contextual Escaping can be disabled entirely, allowing you to inject html using ng-html-bind. This is an unsafe option, but helpful when testing.
Example from the AngularJS documentation on $sce:
angular.module('myAppWithSceDisabledmyApp', []).config(function($sceProvider) {
// Completely disable SCE. For demonstration purposes only!
// Do not use in new projects.
$sceProvider.enabled(false);
});
Attaching the above config section to your app will allow you inject html into ng-html-bind, but as the doc remarks:
SCE gives you a lot of security benefits for little coding overhead.
It will be much harder to take an SCE disabled application and either
secure it on your own or enable SCE at a later stage. It might make
sense to disable SCE for cases where you have a lot of existing code
that was written before SCE was introduced and you're migrating them a
module at a time.
You can use filter like this
angular.module('app').filter('trustAs', ['$sce',
function($sce) {
return function (input, type) {
if (typeof input === "string") {
return $sce.trustAs(type || 'html', input);
}
console.log("trustAs filter. Error. input isn't a string");
return "";
};
}
]);
usage
<div ng-bind-html="myData | trustAs"></div>
it can be used for other resource types, for example source link for iframes and other types declared here