How to "bookmark" page or content fetched using AJAX?
It looks like it can be easy if we just add the details to the "anchor", and then, use the routing or even in PHP code or Ruby on Rails's route.rb, to catch that part, and then show the content or page accordingly? (show the whole page or partial content)
Then it can be very simple? It looks like that's how facebook does it. What are other good ways to do it?
Update: There is now the HTML5 History API (pushState, popState) which deprecates the HTML4 hashchange functionality. History.js provides cross-browser compatibility and an optional hashchange fallback for HTML4 browsers.
To store the history of a page, the most popular and full featured/supported way is using hashchanges. This means that say you go from yoursite/page.html#page1 to yoursite/page.html#page2 you can track that change, and because we are using hashes it can be picked up by bookmarks and back and forward buttons.
You can find a great way to bind to hash changes using the jQuery History project
http://www.balupton.com/projects/jquery-history
There is also a full featured AJAX extension for it, allowing you to easily integrate Ajax requests to your states/hashes to transform your website into a full featured Web 2.0 Application:
http://www.balupton.com/projects/jquery-ajaxy
They both provide great documentation on their demo pages to explain what is happening and what is going on.
Here is an example of using jQuery History (as taken from the demo site):
// Bind a handler for ALL hash/state changes
$.History.bind(function(state){
// Update the current element to indicate which state we are now on
$current.text('Our current state is: ['+state+']');
// Update the page"s title with our current state on the end
document.title = document_title + ' | ' + state;
});
// Bind a handler for state: apricots
$.History.bind('/apricots',function(state){
// Update Menu
updateMenu(state);
// Show apricots tab, hide the other tabs
$tabs.hide();
$apricots.stop(true,true).fadeIn(200);
});
And an example of jQuery Ajaxy (as taken from the demo site):
'page': {
selector: '.ajaxy-page',
matches: /^\/pages\/?/,
request: function(){
// Log what is happening
window.console.debug('$.Ajaxy.configure.Controllers.page.request', [this,arguments]);
// Adjust Menu
$menu.children('.active').removeClass('active');
// Hide Content
$content.stop(true,true).fadeOut(400);
// Return true
return true;
},
response: function(){
// Prepare
var Ajaxy = $.Ajaxy; var data = this.State.Response.data; var state = this.state;
// Log what is happening
window.console.debug('$.Ajaxy.configure.Controllers.page.response', [this,arguments], data, state);
// Adjust Menu
$menu.children(':has(a[href*="'+state+'"])').addClass('active').siblings('.active').removeClass('active');
// Show Content
var Action = this;
$content.html(data.content).fadeIn(400,function(){
Action.documentReady($content);
});
// Return true
return true;
And if you ever want to get the querystring params (so yoursite/page.html#page1?a.b=1&a.c=2) you can just use:
$.History.bind(function(state){
var params = state.queryStringToJSON(); // would give you back {a:{b:1,c:2}}
}
So check out those demo links to see them in action, and for all installation and usage details.
If you use jquery, you can do that in a simple manner. just use ajaxify plugin. it can manage bookmarking of ajax pages and many other things.
Check this, something may help you:
How to change URL from javascript: http://doet.habrahabr.ru/blog/15736/
How to pack the app state into url: http://habrahabr.ru/blogs/javascript/92505/
An approach description: http://habrahabr.ru/blogs/webstandards/92300/
Note: all articles are in Russian, so either Google Translate them, or just review the code and guess the details.
Take a look to the Single Page Interface Manifesto
I tried many packages. The jQuery History plugin seems to be most complete:
http://github.com/tkyk/jquery-history-plugin
Related
Ok, this idea might seem quite a bit crazy and it kindo' is (at least for me at my level).
I have a fairly standarad rails app (some content pages, a blog, a news block, some authentication). And I want to make it into a single page app.
What I want to accomplish is:
All the pages are fetched through AJAX like when using turbolinks, except that the AJAX returns only the view part (the yield part in the layout) withought the layout itself, which stays the same (less data in the responces, quicker render and load time).
The pages are mostly just static html with AngularJS markup so not much to process.
All the actual data is loaded separately through JSON and populated in the view.
Also the url and the page title get changed accordingly.
I've been thinking about this concept for quite a while and I just can't seem to come up with a solution. At this point I've got to some ideas on how this actualy might be done along with some problems I can't pass. Any ideas or solutions are greatly appreciated. Or might be I've just gone crazy and 3 small requests to load a page are worse then I big that needs all the rendering done on server side.
So, here's my idea and known problems.
When user first visits the app, the view template with angular markup is rendered regularly and the second request comes from the Angular Resource.
Then on ngClick on any link that adress is sent to ngInclude of the content wrapper.
How do I bind that onClick on any link and how can I exclude certain links from that bind (e.g. links to external authentication services)?
How do I tell the server not to render the layout if the request is comming from Angular? I though about adding a parameter to the request, but there might be a better idea.
When ngInclude gets the requested template, it fires the ngInit functions of the controllers (usually a single one) in that template and gets the data from the server as JSON (along with the proper page title).
Angular populates the template with the received data, sets the browser url to the url of the link and sets the page title to what it just got.
How do I change the page title and the page url? The title can be changed using jQuery, but is there a way through Angular itself?
Again, I keep thinking about some kind of animation to make this change more fancy.
Profit!
So. What do you guys think?
OK, in case enyone ever finds this idea worth thinking about.
The key can be solved as follows.
Server-side decision of whether to render the view or not.
Use a param in the ngInclude and set the layout: false in the controller if that param is present.
Have not found an easier way.
Client-side binding all links except those that have a particular class no-ajax
Here's a directive that does it.
App.directive('allClicks', function($parse) {
return {
restrict: 'A',
transclude: true,
replace: true,
link: function(scope, element, attrs) {
var $a = element.find('a').not($('a.no-ajax')),
fn = $parse(attrs['allLinks']);
$a.on('click', function(event) {
event.preventDefault();
scope.$apply(function() {
var $this = angular.element(event.target);
fn(scope, {
$event: event,
$href: $this.attr('href'),
$link: $this
});
});
});
}
};
})
And then use it on some wrapper div or body tag like <body ng-controller="WrapperCtrl" all-links="ajaxLink($href)"> and then in your content div do <div id="content" ng-include="current_page_template">
In your angular controller set the current_page template to the document.URL and implement that ajaxLink function.
$scope.ajaxLink = function(path) {
$scope.current_page_template = path+"?nolayout=true";
}
And then when you get your JSON with your data from the server don't forget to use history.pushState to set the url line and document.title = to setr the title.
I am using the following directive to create a ckEditor view. There are other lines to the directive to save the data but these are not included as saving always works for me.
app.directive('ckEditor', [function () {
return {
require: '?ngModel',
link: function ($scope, elm, attr, ngModel) {
var ck = ck = CKEDITOR.replace(elm[0]);
ngModel.$render = function (value) {
ck.setData(ngModel.$modelValue);
setTimeout(function () {
ck.setData(ngModel.$modelValue);
}, 1000);
}; }
};
}])
The window appears but almost always the first time around it is empty. Then after clicking the [SOURCE] button to show the source and clicking it again the window is populated with data.
I'm very sure that the ck.setData works as I tried a ck.getData and then logged the output to the console. However it seems like ck.setData does not make the data visible at the start.
Is there some way to force the view window contents to appear?
You can call render on the model at any time and it will simply do whatever you've told it to do. In your case, calling ngModel.$render() will grab the $modelValue and pass it to ck.setData(). Angular will automatically call $render whenever it needs to during its digest cycle (i.e. whenever it notices that the model has been updated). However, I have noticed that there are times when Angular doesn't update properly, especially in instances where the $modelValue is set prior to the directive being compiled.
So, you can simply call ngModel.$render() when your modal object is set. The only problem with that is you have to have access to the ngModel object to do that, which you don't have in your controller. My suggestion would be to do the following:
In your controller:
$scope.editRow = function (row, entityType) {
$scope.modal.data = row;
$scope.modal.visible = true;
...
...
// trigger event after $scope.modal is set
$scope.$emit('modalObjectSet', $scope.modal); //passing $scope.modal is optional
}
In your directive:
ngModel.$render = function (value) {
ck.setData(ngModel.$modelValue);
};
scope.$on('modalObjectSet', function(e, modalData){
// force a call to render
ngModel.$render();
});
Its not a particularly clean solution, but it should allow you to call $render whenever you need to. I hope that helps.
UPDATE: (after your update)
I wasn't aware that your controllers were nested. This can get really icky in Angular, but I'll try to provide a few possible solutions (given that I'm not able to see all your code and project layout). Scope events (as noted here) are specific to the nesting of the scope and only emit events to child scopes. Because of that, I would suggest trying one of the three following solutions (listed in order of my personal preference):
1) Reorganize your code to have a cleaner layout (less nesting of controllers) so that your scopes are direct decendants (rather than sibling controllers).
2) I'm going to assume that 1) wasn't possible. Next I would try to use the $scope.$broadcast() function. The specs for that are listed here as well. The difference between $emit and $broadcast is that $emit only sends event to child $scopes, while $broadcast will send events to both parent and child scopes.
3) Forget using $scope events in angular and just use generic javascript events (using a framework such as jQuery or even just roll your own as in the example here)
There's a fairly simple answer to the question. I checked the DOM and found out the data was getting loaded in fact all of the time. However it was not displaying in the Chrome browser. So the problem is more of a display issue with ckEditor. Strange solution seems to be to do a resize of the ckEditor window which then makes the text visible.
This is a strange issue with ckeditor when your ckeditor is hidden by default. Trying to show the editor has a 30% chance of the editor being uneditable and the editor data is cleared. If you are trying to hide/show your editor, use a css trick like position:absolute;left-9999px; to hide the editor and just return it back by css. This way, the ckeditor is not being removed in the DOM but is just positioned elsewhere.
Use this java script code that is very simple and effective.Note editor1 is my textarea id
<script>
$(function () {
CKEDITOR.timestamp= new Date();
CKEDITOR.replace('editor1');
});
</script>
Second way In controller ,when your query is fetch data from database then use th
is code after .success(function().
$http.get(url).success(function(){
CKEDITOR.replace('editor1');
});
I know, that this thread is dead for a year, but I got the same problem and I found another (still ugly) solution to this problem:
instance.setData(html, function(){
instance.setData(html);
});
In MVC4 applications, I would like to update a panel using AJAX but using jQuery methods instead using AjaxExtensions from MVC.
But my problem is the updatePanelId.
I've seen several people use this to update it when has success:
success: function (response) {
var $target = $("#target");
var $newHtml = response;
$target.replaceWith($newHtml);
}
But when I do this, it forces me to use in every partial view that includes the id="target" at the root level of my razor view, and I guess that's not a good practice; I said this because I've realized when I use AjaxExtensions it doesn't happens, replace the update and it does not remove the panelId. But using jQuery it does.
Any idea to port the AjaxExtensions feature to jQuery?
You can use just:
$("#target").html(response); // it will just update content of the $("#target") container
Use jQuery's .load function. This will load the contents of the URL you specify into the target element. You can optionally specify a selector after the URL in load to only grab part of the target page.
$(function() {
$("#target").load("/MyURL");
});
JavaScript same origin policy applies to this.
I'm confused as to how to accomplish this. I have a page which, has a popup filter, which has some input elements and an "Apply" button (not a submit). When the button is clicked, two jquery .get() calls are made, which load a graph, a DataTables grid, photos, and miscellaneous info into four separate tabs. Inside the graph, if one clicks on a particular element, the user is taken to another page where the data is drilled down to a finer level. All this works well.
The problem is if the user decides to go back to the original page, but with the ajax generated graph/grid/photos etc. Originally I thought that I would store a session variable with the filter variables used to form the original query, and on returning to the page, if the session var was found, the original ajax call would be made again, re-populating the tabs.
The problem that I find with this method is that Coldfusion doesn't recognize that the session variable has been set when returning to the page using the browser's back button. If I dump out the session var at both the original and the second page, I can see the newly set var at the second page, and I can see it if I go to the original page through the navigation menu, but NOT if I use the back button.
SO.... from reading posts on here about ajax browser history plugins, it seems that there are various jquery plugins which help with this, including BBQ. The problem that I see with this approach is that it requires the use of anchor elements to trigger it, and then modifies the query string using the anchors' href attributes. I suppose that I could modify the page to include a hidden anchor.
My question, at long last is: is an ajax history plugin like BBQ the best way to accomplish this, or is there a way to make Coldfusion see the newly created session var when returning to the page via the back button? Or, should I consider re-architecting the page so that the ajax calls are replaced by a form submission back to the page instead?
Thanks in advance, as always.
EDIT: some code to help clarify things:
Here's the button that makes the original ajax calls:
<button id="applyFilter">APPLY</button>
and part of the js called on #applyFilter, wrapped in $(document).ready():
$('#applyFilter').click(function(){
// fill in the Photos tab
$.get('tracking/listPhotos.cfm',
{
id: id,
randParam: Math.random()
},
function(response){
$('#tabs-photos').html(response);
}
);
});
Finally, when the user calls the drill-down on the ajax generated graph, it uses the MaintAction form which has been populated with the needed variables:
function DrillDown() {
//get the necessary variables and populate the form inputs
document.MaintAction.action = "index.cfm?file=somepage.cfm&Config=someConfig";
document.MaintAction.submit();
}
and that takes us to the new page, from which we'd like to return to the first page but with the ajax-loaded photos.
The best bet is to use the BBQ method. For this, you don't have to actually include the anchor tags in your page; in fact, doing so would cause problems. This page: http://ajaxpatterns.org/Unique_URLs explains how the underlying process works. I'm sure a jQuery plugin would make the actual implementation much easier.
Regarding your other question, about how this could be done with session variables - I've actually done something similar to that, prior to learning about the BBQ method. This was specifically to save the state of a jqGrid component, but it could be easily changed to support any particular Ajax state. Basically, what I did was keep a session variable around for each instance of each component that stored the last parameters passed to the server via AJAX requests. Then, on the client side, the first thing I did was run a synchronous XHR request back to the server to fetch the state from that session variable. Using the callback method for that synchronous request, I then set up the components on my page using those saved parameters. This worked for me, but if I had to do it again I would definitely go with the BBQ method because it is much simpler to deal with and also allows more than one level of history.
Some example code based on your update:
$('#applyFilter').click(function(){
var id = $("#filterid").val(); // assumes the below id value is stored in some input on the page with the id "filterid"
// fill in the Photos tab
$.get('tracking/listPhotos.cfm',
{
id: id // I'm assuming this is what you need to remember when the page is returned to via a back-button...
//randParam: Math.random() - I assume this is to prevent caching? See below
},
function(response){
$('#tabs-photos').html(response);
}
);
});
/* fixes stupid caching behavior, primarily in IE */
$.ajaxSetup({ cache: false });
$.ajax({
async: false,
url: 'tracking/listPhotosSessionKeeper.cfm',
success: function (data, textStatus, XMLHttpRequest)
{
if (data.length)
{
$("#filterid").val(data);
$('#applyFilter').trigger('click');
}
}
});
This is what you need on the client-side to fetch the state of the photo list. On the server side, you'll need to add this modification to tracking/listPhotos.cfm:
<cfset session.lastUsedPhotoFilterID = URL.id>
And add this new one-line file, tracking/listPhotosSessionKeeper.cfm:
<cfif IsDefined("session.lastUsedPhotoFilterID")><cfoutput>#session.lastUsedPhotoFilterID#</cfoutput></cfif>
Together these changes will keep track of the last ID used by the user, and will load it up each time the page is rendered (whether via a back button, or simply by the user revisiting the page).
Firstly I am very new to all forms of javascript, particularly anything remotely AJAX. That said, over the course of the last day I have managed to code a script that dynamically refreshes a single div and replaces it with the contents of a div on another page.
The problem however is that several of my other scripts do not work in the ajax refreshed content. The most important of which being "colorbox".
I have spent several hours this evening researching this and am seeing lot's of stuff regarding .load, .live... updating the DOM on refresh etc...etc... But to be quite honest most of it is going over my head currently and I wouldn't know where to begin in terms of integrating it with the code I currently have.
My Ajax refresh code is as follows (My apologies if I haven't used best practice, it was my first attempt):-
$(function() {
$(".artist li.artist").removeClass("artist").addClass("current_page_item");
$("#rightcolumnwrapper").append("<img src='http://www.mywebsite.com/wp-content/images/ajax-loader.gif' id='ajax-loader' style='position:absolute;top:400px;left:190px;right:0px;margin-left:auto;margin-right:auto;width:100px;' />");
var $rightcolumn = $("#rightcolumn"),
siteURL = "http://" + top.location.host.toString(),
hash = window.location.hash,
$ajaxSpinner = $("#ajax-loader"),
$el, $allLinks = $("a");
$ajaxSpinner.hide();
$('a:urlInternal').live('click', function(e) {
$el = $(this);
if ((!$el.hasClass("comment-reply-link")) && ($el.attr("id") != 'cancel-comment-reply-link')) {
var path = $(this).attr('href').replace(siteURL, '');
$.address.value(path);
$(".current_page_item").removeClass("current_page_item");
$allLinks.removeClass("current_link");
$el.addClass("current_link").parent().addClass("current_page_item");
return false;
}
e.preventDefault();
});
$.address.change(function(event) {
$ajaxSpinner.fadeIn();
$rightcolumn.animate({ opacity: "0.1" })
.load(siteURL + event.value + ' #rightcolumn', function() {
$ajaxSpinner.fadeOut();
$rightcolumn.animate({ opacity: "1" });
});
});});
I was hoping someone might be kind enough to show me the sort of modifications I would need to make to the above code in order to have the colorbox load when the contents of #rightcolumn have been refreshed.
There is also a second part to this question. My links to the pictures themselves are now also being effected by the hashtag due to the above code which will in turn prevent the images themselves from loading correctly in the colorbox I should imagine. How can I prevent these images from being effected and just have them keep the standard URL. I only want the above code to effect my internal navigation links if at all possible.
Many thanks guys. I look forward to your replies.
That's a lot of code to review so I'll focus first on the conceptual side of things. Maybe that you will give you some clues...
It sounds like when you load content via Ajax the DOM is changed. No worries, that's kind of what we expect. However, scripts loaded before the Ajax calls may have difficulty if they are bound to elements that weren't there at page load time or are no longer there.
JQuery's live function is one solution to that. Instead of binding to a specific element (or collection of elements) at particular point in time, live lets you specify a binding to an element (or collection) of elements without regard to when they show up in the DOM (if ever).
ColorBox, however, in its default "vanilla" use abstracts that all away and, I believe, uses classic DOM binding - meaning the elements must be present at bind time. (Since you don't show your call to ColorBox I can't see how your using it.)
You may want to consider re-initalizing ColorBox after each content load by Ajax to be certain the binding happens the way you need it to.
Use $('selector').delegate() it watches the DOM of 'selector' and .live() is deprecated.
Use this to watch your elements AND fire the colorbox initilization. This way the colorbox is not dependent on the DOM element, but the other way around.
$("body").delegate("a[rel='lightbox']", "click", function (event) {
event.preventDefault();
$.colorbox({href: $(this).attr("href"),
transition: "fade",
innerHeight: '515px',
innerWidth: '579px',
overlayClose: true,
iframe: true,
opacity: 0.3});});
This should basically solve your problem and is cross browser tested.
The a[rel='lightbox'] in the delegate closure is the reference to what ever link you're clicking to fire the colorbox, whether it has been loaded with the initial DOM or with an AJAX request and has been added to the DOM in a live fashion. ie: any tag like this:
<a rel='lightbox' href="http://some.website.com">Launch Colorbox</a>