All, we're developing a webapp with AngularJS and we have a use case/requirement (that won't happen very often at all) where we will need to retrieve a complete HTML document from our static server. However, it appears that the $http object returns a raw HTML string as its 'data'. We are trying to avoid using an external library (like jQuery, for instance), but we need to parse that raw HTML string into a queriable DOM object. We could use an iframe and be done with it, but we're also trying to avoid using iframes for well-known reasons.
So, the question is: does AngularJS have a parser for HTML (as it does for JSON)? Or else, what's the most graceful way to handle this case?
P.S.: We tried sifting through Angular's API docs, but in all honesty they are hit-or-miss and unintuitive to navigate around.
If you need to get a DOM element from the string programmatically, parsing html into a DOM object is pretty simple.
Basically you'd use DOMParser.
var parser = new DOMParser();
var doc = parser.parseFromString('<div>some html</div>', 'text/html');
doc.firstChild; //this is what you're after.
From there if you wanted to query to get elements from it with vanilla JS (meaning, you don't want to use JQuery for some reason), you can do this:
//get all anchor tags with class "test" under a div.
var testAnchors = doc.firstChild.querySelectorAll('div a.test');
... but note: querySelectorAll is only supported in IE8 and higher.
EDIT: additional approach...
The "wrap" method:
var div = document.createElement('div');
div.innerHTML = '<div>some html</div>';
var result = div.childNodes;
... do note that this method will return HTMLUnknownElements if you put SVG or MathML into it. They'll "look" normal, but they won't work.
First of all, Angular uses jQuery as well. (jQuery lite)
From FAQ
Does Angular use the jQuery library?
Yes, Angular can use jQuery if it's present in your app when the
application is being bootstrapped. If jQuery is not present in your
script path, Angular falls back to its own implementation of the
subset of jQuery that we call jQLite.
However, I think you don't need full jQuery function here anyway.
That is what I did.
I use a directive and get the html as template and set replace as true.
jsFiddle
<myhtml></myhtml>
angular.module('myApp', []).directive('myhtml', function($http) {
return {
restrict: 'E',
scope: {},
template:'',
link: function(scope, element, attrs) {
$http.post('/echo/json/', data).success(function(re) {
element.html(re.html);
});
}
}
});
Edit note:
I just update the jsFiddle.
I include jquery on top for echoing the request in jsFiddle. In real, you don't need that.
However, it shows you that you can use jQuery in Angular.
If your html doesn't contain any angular tag, this example should work as your expectation .
Otherwise, you need to use compile instead of link.
Related
I am trying to bind the following json response in my html page.
The json is as follows:
{
"key":{
"text":"<p>For more information, please visit Support .</p>"
}
}
html page
<div ng-bind-html="message"></div>
Controller Code
$http({
method: 'GET',
url:'DAYS.json'
}).success(function(responsedata) {
$scope.message=responsedata.key.text;
}).error(function(responsedata){});
customizeWindow function inside controller
$scope.customizeWindow = function(url) {
window.open(url, "_blank", "toolbar=yes, scrollbars=yes, resizable=yes,top=70, left=190, width=970, height=460");
}
The ng-bind-html binds the html tags but it strips off the javascript and ng-click event.
i get only Support when i inspect element and the link does not work.
Please suggest me a solution.
This happens since angular Automatically uses $sce -> Strict Contextual Escaping. it allows you to ng-bind-html but it doesn't allow you to add possibly malicious code like JS.
What you are after is explicitly trusting that segment as HTML.
therefore:
angular.module('app', ["ngSanitize"]) // You have to include ngSanitize or it wouldn't work.
.controller('Ctrl', function ($scope, $sce){
$scope.htmlData = <p>For more information, please visit Support .</p> //Took from your example.
$scope.$watch("htmlData", function(newValue){
$scope.trustedData = $sce.trustAsHtml(newValuew);
});
});
HTML Usage:
<p ng-bind-html="trustedData"></p>
Angular Resource:
Strict Contextual Escaping (SCE) is a mode in which AngularJS requires
bindings in certain contexts to result in a value that is marked as
safe to use for that context. One example of such a context is binding
arbitrary html controlled by the user via ng-bind-html. We refer to
these contexts as privileged or SCE contexts.
As of version 1.2, Angular ships with SCE enabled by default.
read on : Angular on SCE - trustAsHtml method
ng-bind-html content is sanitized by default and its not meant to bring DOM into the page. You would use this method to bring content onto the page. Eg if you have a rich text editor - you want to feed it html content, however sanitized and thats when you use ng-bind-html.
For your purpose I would suggest to use templates or plain model binding.
Source of your json, what ever that is should not really know anything about the consumer side (your page) implementation/technology what if you move away from angularJS and start using Knockout, you will have to change server-side as well, because Knockout will have no idea about ng-click. Just pass back the content like http://www.google.com, 'For more information, please visit' and bind it.
{
"key":{
"textsource": {
source : 'http://www.google.com',
message : 'For more information, please visit '
}
}
}
<p>{{textsource.message}}<div ng-click="customizeWindow(textsource.source)\">Support</div> </p>
If you can't change server side, have a look at this example :
1) tell $sce your content is fine
$scope.message = $sce.trustAsHtml(json.key.text);
2) recompile your dynamicaly added content
$scope.init = function () {
var el = document.getElementById("dynamic");
$compile(el.childNodes[0])($scope);
};
NOTE: the recompile step is manual for now (press the recompile button). Follow this to make it work nicely.
I'm trying to sanitize HTML in the controller as I'm trying to update the document.title dynamically with the title of the post. (I know that for SEO purposes this isn't recommended but I need to use it here)
$scope.prevTitle = "dynamic title gets pulled in here &"
document.title = $scope.prevTitle
For this example, I've just used a random HTML entity. I've tried the parseAsHtml method from the official documentation but I'm having no luck. I tried the following:
document.title = $sce.parseAsHtml($scope.prevTitle)
But no luck. The documentation suggests it needs to be used within a function. Any suggestions on how I would acheive this?
A console log of the above ( console.log($sce.parseAsHtml($scope.prevTitle)) ) would return:
function (b,c){return e.getTrusted(a,d(b,c))}
$sanitize can be used as #acg pointed out. Alternatively, you can use it directly with the ng-bind-html directive where it automatically sanitizes the output variable before rendering the output.
The above point is not quite clear in the documentation, but there is a fairly extensive example in it with which you can play in pluncker.
Please also bear in mind that ngSanitize is an external module and you need to explicitly load angular-sanitize.js or include it in your js minification.
Use $sanitise and trustAsHtml instead
First of all inject 'ngSanitize' in your module
Now in your controller, just add
$scope.prevTitle = "dynamic title gets pulled in here &"
document.title = $sce.trustAsHtml($scope.prevTitle)
If you want to sanitize the html returned, I would think it would be as simple as using the $sanitize service:
document.title = $sanitize($sce.parseAsHtml($scope.prevTitle))
I'm looking to programmatically add ng-* HTML attributes to various DOM elements. I've had some success with using $compile(obj)($scope); but this secondary compile causes issues with a number of components.
I add the ng-* attributes via jQuery... and yes, I know, directives, but this won't work for me as the ng-* HTML attributes I'm adding are boilerplate actions based on DOM structures. That and directives seem clunky (to say the least) as compared to jQuery DOM manip.
So... is there any way I can add in these boilerplate ng-* HTML attributes BEFORE Angular runs so that I can avoid the re-$compile? What I'd really love is a way to do a pre-init hook on Angular, is there such a beast?
SOLUTION:
#ChrisMartin sent me on the right path to figure out an answer to this question (thanks Chris!). What I ended up doing is this...
First I created a file named "angular-defer-bootstrap.js" that is included before "angular.js" with the following code:
//# Set the window.name to signal Angular to delay bootstrapping until `angular.resumeBootstrap()` is called.
//# See: http://stackoverflow.com/a/21049890/235704 and https://docs.angularjs.org/guide/bootstrap
//# NOTE: This MUST be included BEFORE angular*.js
window.name = 'NG_DEFER_BOOTSTRAP! ' + window.name;
I then created the following function with jQuery to preform any pre-Angular bootstrap code:
//####################
//# Setup the jQuery onDocumentLoad event to handle the pseudo-ng-directive of ng-preinit
//####################
$(document).ready(function () {
var $this, $pre = $('[ng-preinit]');
//# If we have some [ng-preinit]'s to process
if ($pre.length > 0) {
//# Traverse the [ng-preinit] attributes, eval'ing/running each and removing them so Angular doesn't freak out
$pre.each(function() {
$this = $(this);
eval($this.attr('ng-preinit'));
$this.removeAttr('ng-preinit');
});
}
//# Let Angular know it can .resumeBootstrap and remove the flag from window.name
angular.resumeBootstrap();
window.name = window.name.replace('NG_DEFER_BOOTSTRAP! ', '');
});
This is then utilized by including a ng-preinit pseudo-Angular directive/HTML attribute:
<div class="row" ng-controller="IndexController" ng-init="init()" ng-preinit="globalScope.preinit()">
The rub here is that the eval'd code contained within the pseudo-Angular directive ng-preinit has the global scope, rather than the Angular controller's $scope.
With these few lines of code, I can now cleanly hook the "pre-init" (that is, pre-bootstrap) of Angular and do whatever I like without the need to re-$compile (and it's unintended consequences), which is exactly what I wanted!
This is explained in Angular's documentation on manual initialization.
If you need to have more control over the initialization process, you can use a manual bootstrapping method instead. Examples of when you'd need to do this include using script loaders or the need to perform an operation before Angular compiles a page.
So essentially I'm trying to build my own version of GitHub's tree slider. The relevant Javascript/JQuery code is:
// handles clicking a link to move through the tree
$('#slider a').click(function() {
history.pushState({ path: this.path }, '', this.href) // change the URL in the browser using HTML5 history module
$.get(this.href, function(data) {
$('#slider').slideTo(data) // handle the page transition, preventing full page reloads
})
return false
})
// binds hitting the back button in the browser to prevent full page reloads
$(window).bind('popstate', function() {
$('#slider').slideTo(location.pathname)
}
Ok, hopefully that's understandable. Now here's my interpretation of what's going on here, followed by my problem/issue:
The callback function for the GET request when navigating through the tree is the slideTo method, and an HTML string is passed in as an argument to that function. I'm assuming that slideTo is a function defined elsewhere in the script or in a custom library, as I can't find it in the JQuery documentation. So, for my purposes, I'm trying to build my own version of this function. But the argument passed into this function, "data", is just the string of HTML returned from the GET request. However, this isn't just a snippet of HTML that I can append to a div in the document, because if I perform the same GET request (e.g. by typing the url into a web browser) I would expect to see a whole webpage and not just a piece of one.
So, within this callback function that I am defining, I would need to parse the "data" argument into a DOM so that I can extract the relevant nodes and then perform the animated transition. However, this doesn't make sense to me. It generally seems like a Bad Idea. It doesn't make sense that the client would have to parse a whole string of HTML just to access part of the DOM. GitHub claims this method is faster than a full page reload. But if my interpretation is correct, the client still has to parse a full string of HTML whether navigating through the tree by clicking (and running the callback function) or by doing full page loads such as by typing the new URL in the browser. So I'm stuck with either parsing the returned HTML string into a DOM, or ideally only fetching part of an HTML document.
Is there a way to simply load the fetched document into a Javascript or JQuery DOM object so I can easily manipulate it? or even better, is there a way to fetch only an element with an arbitrary id without doing some crazy server-side stuff (which I already tried but ended up being too spaghetti code and difficult to maintain)?
I've also already tried simply parsing the data argument into a JQuery object, but that involved a roundabout solution that only seems to work half the time, using javascript methods to strip the HTML of unwanted things, like doctype declarations and head tags:
var d = document.createElement('html');
d.innerHTML = data;
body = div.getElementsByTagName("body")[0].innerHTML;
var newDOM = $(body);
// finally I have a JQuery DOM context that I can use,
// but for some reason it doesn't always seem to work quite right
How would you approach this problem? When I write this code myself and try to make it work on my own, I feel like no matter what I do, I'm doing something horribly inefficient and hacky.
Is there a way to easily return a JQuery DOM object with a GET request? or better, just return part of a document fetched with a GET request?
Just wrap it; jQuery will parse it.
$(data) // in your callback
Imagine you want to parse a <p> tag in your normal HTML web page. You probably would use something like:
var p = $('<p>');
Right? So you have to use the same approach to parse an entire HTML document and then, navigate through the DOM tree to get the specific elements you want. Therefore, you just need to say:
$.get(this.href, function(data) {
var html = $(data);
// (...) Navigating through the DOM tree
$('#slider').slideTo( HTMLportion );
});
Notice that it also works for XML documents, so if you need to download via AJAX a XML document from the server, parse the inner information and display it on the client-side, the method is exactly the same, ok?
I hope it helps you :)
P.S: Don't ever forget to put semicolons at the end of each JavaScript sentence. Probably, if you don't put them, the engine would work but it is better to be safe and write them always!
I'm trying to get a consistent, cross browser outerHtml for a jQuery element, particularly $("html").outerHtml(). For example if the page source is this:
<html><script src="blah.js"></script><div class="foo" id='bar'></p></div></html>
I want to be able to use $("html").outerHtml(); to get the HTML string including the element itself, which would be:
<html><script src="blah.js"></script><div class="foo" id="bar"><p></p></div></html>
I've been using Brandon Aaron's outerHtml plugin which looks something like this:
return $('<div>').append($("html").first().clone()).html();
However this seems to actually try to reload any externally referenced files in the document (scripts, stylesheets), which seems pretty excessive just to get the HTML source of a wrapper element. Can Javascript even put an HTML element inside a DIV element?
Locally, I get this error. Something to do with AJAX rules?
XMLHttpRequest cannot load file:///C:/demo.js?_=1311466511031. Origin null is not allowed by Access-Control-Allow-Origin.
Is there a better way to get outerHtml? I'd really like to avoid any network calls when doing this.
Wrote my own solution, which simply renders the wrapping element:
(function($){
$.fn.outerHtml = function() {
if (this.length == 0) return false;
var elem = this[0], name = elem.tagName.toLowerCase();
if (elem.outerHTML) return elem.outerHTML;
var attrs = $.map(elem.attributes, function(i) { return i.name+'="'+i.value+'"'; });
return "<"+name+(attrs.length > 0 ? " "+attrs.join(" ") : "")+">"+elem.innerHTML+"</"+name+">";
};
})(jQuery);
https://gist.github.com/1102076
jQuery outerHTML plugin from https://github.com/darlesson/jquery-outerhtml uses browser's native outerHTML when existent, as second option clone the nodes in a temporary document fragment and return its innerHTML or uses jQuery clone solution similar to Brandon Aaron's plugin.
This plugin might help you avoid the load of the reference files. In my tests in Chrome, Firefox and Internet Explorer the issue did not happen calling the code below in a page with external JavaScript files.
var outerHTML = $(document.documentElement).outerHTML();