rails link_to not works when navigated to any entity - ruby-on-rails

I cannot navigate or post when there is a parameter on the url (when navigated to any entity)
xxx only works when i am at a url like
http://localhost:3000/xxx
but when i am at a url that has a entity parameter like
http://localhost:3000/xxx/12
non of the <a\> tags work and non of remote form posts work
When I attach to mouse click on Chrome, I saw that on a url like http://localhost:3000/xxx the jquery function below returns undefined (the situation which navigation works without problem), but when on a url like http://localhost:3000/xxx/12 function returns jQuery.fn.init[0]
The function (jquery excerpt) is this
if ( !( eventHandle = elemData.handle ) ) {
eventHandle = elemData.handle = function( e ) {
// Discard the second event of a jQuery.event.trigger() and
// when an event is called after a page has unloaded
return typeof jQuery !== "undefined" && jQuery.event.triggered !== e.type ?
jQuery.event.dispatch.apply( elem, arguments ) : undefined;
};
}

Related

hide browser back button after user gets logout [duplicate]

I am doing an online quiz application in PHP. I want to restrict the user from going back in an exam.
I have tried the following script, but it stops my timer.
What should I do?
The timer is stored in file cdtimer.js.
<script type="text/javascript">
window.history.forward();
function noBack()
{
window.history.forward();
}
</script>
<body onLoad="noBack();" onpageshow="if (event.persisted) noBack();" onUnload="">
I have the exam timer which takes a duration for the exam from a MySQL value. The timer starts accordingly, but it stops when I put the code in for disabling the back button. What is my problem?
There are numerous reasons why disabling the back button will not really work. Your best bet is to warn the user:
window.onbeforeunload = function() { return "Your work will be lost."; };
This page does list a number of ways you could try to disable the back button, but none are guaranteed:
http://www.irt.org/script/311.htm
It is generally a bad idea overriding the default behavior of the web browser. A client-side script does not have the sufficient privilege to do this for security reasons.
There are a few similar questions asked as well,
How can I prevent the backspace key from navigating back?
How can I prevent the browser's default history back action for the backspace button with JavaScript?
You can-not actually disable the browser back button. However, you can do magic using your logic to prevent the user from navigating back which will create an impression like it is disabled. Here is how - check out the following snippet.
(function (global) {
if(typeof (global) === "undefined") {
throw new Error("window is undefined");
}
var _hash = "!";
var noBackPlease = function () {
global.location.href += "#";
// Making sure we have the fruit available for juice (^__^)
global.setTimeout(function () {
global.location.href += "!";
}, 50);
};
global.onhashchange = function () {
if (global.location.hash !== _hash) {
global.location.hash = _hash;
}
};
global.onload = function () {
noBackPlease();
// Disables backspace on page except on input fields and textarea..
document.body.onkeydown = function (e) {
var elm = e.target.nodeName.toLowerCase();
if (e.which === 8 && (elm !== 'input' && elm !== 'textarea')) {
e.preventDefault();
}
// Stopping the event bubbling up the DOM tree...
e.stopPropagation();
};
}
})(window);
This is in pure JavaScript, so it would work in most of the browsers. It would also disable the backspace key, but that key will work normally inside input fields and textarea.
Recommended Setup:
Place this snippet in a separate script and include it on a page where you want this behavior. In the current setup it will execute the onload event of the DOM which is the ideal entry point for this code.
Working DEMO!
It was tested and verified in the following browsers,
Chrome.
Firefox.
Internet Explorer (8-11) and Edge.
Safari.
I came across this, needing a solution which worked correctly and "nicely" on a variety of browsers, including Mobile Safari (iOS 9 at time of posting). None of the solutions were quite right. I offer the following (tested on Internet Explorer 11, Firefox, Chrome, and Safari):
history.pushState(null, document.title, location.href);
window.addEventListener('popstate', function (event)
{
history.pushState(null, document.title, location.href);
});
Note the following:
history.forward() (my old solution) does not work on Mobile Safari --- it seems to do nothing (i.e., the user can still go back). history.pushState() does work on all of them.
the third argument to history.pushState() is a url. Solutions which pass a string like 'no-back-button' or 'pagename' seem to work OK, until you then try a Refresh/Reload on the page, at which point a "Page not found" error is generated when the browser tries to locate a page with that as its URL. (The browser is also likely to include that string in the address bar when on the page, which is ugly.) location.href should be used for the URL.
the second argument to history.pushState() is a title. Looking around the web most places say it is "not used", and all the solutions here pass null for that. However, in Mobile Safari at least, that puts the page's URL into the history dropdown the user can access. But when it adds an entry for a page visit normally, it puts in its title, which is preferable. So passing document.title for that results in the same behaviour.
<script>
window.location.hash = "no-back-button";
// Again because Google Chrome doesn't insert
// the first hash into the history
window.location.hash = "Again-No-back-button";
window.onhashchange = function(){
window.location.hash = "no-back-button";
}
</script>
For restricting the browser back event:
window.history.pushState(null, "", window.location.href);
window.onpopstate = function () {
window.history.pushState(null, "", window.location.href);
};
This code will disable the back button for modern browsers which support the HTML5 History API. Under normal circumstances, pushing the back button goes back one step, to the previous page. If you use history.pushState(), you start adding extra sub-steps to the current page. The way it works is, if you were to use history.pushState() three times, then start pushing the back button, the first three times it would navigate back in these sub-steps, and then the fourth time it would go back to the previous page.
If you combine this behaviour with an event listener on the popstate event, you can essentially set up an infinite loop of sub-states. So, you load the page, push a sub-state, then hit the back button, which pops a sub-state and also pushes another one, so if you push the back button again it will never run out of sub-states to push. If you feel that it's necessary to disable the back button, this will get you there.
history.pushState(null, null, 'no-back-button');
window.addEventListener('popstate', function(event) {
history.pushState(null, null, 'no-back-button');
});
How to block coming backwards functionality:
history.pushState(null, null, location.href);
window.onpopstate = function () {
history.go(1);
};
None of the most-upvoted answers worked for me in Chrome 79. It looks like Chrome changed its behavior with respect to the Back button after version 75. See here:
https://support.google.com/chrome/thread/8721521?hl=en
However, in that Google thread, the answer provided by Azrulmukmin Azmi at the very end did work. This is his solution.
<script>
history.pushState(null, document.title, location.href);
history.back();
history.forward();
window.onpopstate = function () {
history.go(1);
};
</script>
The problem with Chrome is that it doesn't trigger onpopstate event
unless you make browser action ( i.e. call history.back). That's why
I've added those to script.
I don't entirely understand what he wrote, but apparently an additional history.back() / history.forward() is now required for blocking Back in Chrome 75+.
React
For modal component in React project, the open or close of the modal, controlling browser back is a necessary action.
The stopBrowserBack: the stop of the browser back button functionality, also get a callback function. This callback function is what you want to do:
const stopBrowserBack = callback => {
window.history.pushState(null, "", window.location.href);
window.onpopstate = () => {
window.history.pushState(null, "", window.location.href);
callback();
};
};
The startBrowserBack: the revival of the browser back button functionality:
const startBrowserBack = () => {
window.onpopstate = undefined;
window.history.back();
};
The usage in your project:
handleOpenModal = () =>
this.setState(
{ modalOpen: true },
() => stopBrowserBack(this.handleCloseModal)
);
handleCloseModal = () =>
this.setState(
{ modalOpen: false },
startBrowserBack
);
This is the way I could it accomplish it.
Weirdly, changing window.location didn't work out fine in Google Chrome and Safari.
It happens that location.hash doesn't create an entry in the history for Chrome and Safari. So you will have to use the pushstate.
This is working for me in all browsers.
history.pushState({ page: 1 }, "title 1", "#nbb");
window.onhashchange = function (event) {
window.location.hash = "nbb";
};
history.pushState(null, null, document.URL);
window.addEventListener('popstate', function () {
history.pushState(null, null, document.URL);
});
This JavaScript code does not allow any user to go back (works in Chrome, Firefox, Internet Explorer, and Edge).
This article on jordanhollinger.com is the best option I feel. Similar to Razor's answer but a bit clearer. Code below; full credits to Jordan Hollinger:
Page before:
<a href="/page-of-no-return.htm#no-back>You can't go back from the next page</a>
Page of no return's JavaScript:
// It works without the History API, but will clutter up the history
var history_api = typeof history.pushState !== 'undefined'
// The previous page asks that it not be returned to
if ( location.hash == '#no-back' ) {
// Push "#no-back" onto the history, making it the most recent "page"
if ( history_api ) history.pushState(null, '', '#stay')
else location.hash = '#stay'
// When the back button is pressed, it will harmlessly change the url
// hash from "#stay" to "#no-back", which triggers this function
window.onhashchange = function() {
// User tried to go back; warn user, rinse and repeat
if ( location.hash == '#no-back' ) {
alert("You shall not pass!")
if ( history_api ) history.pushState(null, '', '#stay')
else location.hash = '#stay'
}
}
}
<html>
<head>
<title>Disable Back Button in Browser - Online Demo</title>
<style type="text/css">
body, input {
font-family: Calibri, Arial;
}
</style>
<script type="text/javascript">
window.history.forward();
function noBack() {
window.history.forward();
}
</script>
</head>
<body onload="noBack();" onpageshow="if (event.persisted) noBack();" onunload="">
<H2>Demo</H2>
<p>This page contains the code to avoid Back button.</p>
<p>Click here to Goto NoBack Page</p>
</body>
</html>
This code was tested with the latest Chrome and Firefox browsers.
<script type="text/javascript">
history.pushState(null, null, location.href);
history.back();
history.forward();
window.onpopstate = function () { history.go(1); };
</script>
Try it with ease:
history.pushState(null, null, document.title);
window.addEventListener('popstate', function () {
history.pushState(null, null, document.title);
});
You can just put a small script and then check. It won't allow you to visit previous page.
This is done in JavaScript.
<script type="text/javascript">
function preventbackbutton() { window.history.forward(); }
setTimeout("preventbackbutton()", 0);
window.onunload = function () { null };
</script>
The window.onunload function fires when you try to visit back or previous page through browser.
Very simple and clean function to break the back arrow without interfering with the page afterward.
Benefits:
Loads instantaneously and restores original hash, so the user isn't distracted by URL visibly changing.
The user can still exit by pressing back 10 times (that's a good thing), but not accidentally
No user interference like other solutions using onbeforeunload
It only runs once and doesn't interfere with further hash manipulations in case you use that to track state
Restores original hash, so almost invisible.
Uses setInterval, so it doesn't break slow browsers and always works.
Pure JavaScript, does not require HTML5 history, works everywhere.
Unobtrusive, simple, and plays well with other code.
Does not use unbeforeunload which interrupts user with modal dialog.
It just works without fuss.
Note: some of the other solutions use onbeforeunload. Please do not use onbeforeunload for this purpose, which pops up a dialog whenever users try to close the window, hit backarrow, etc. Modals like onbeforeunload are usually only appropriate in rare circumstances, such as when they've actually made changes on screen and haven't saved them, not for this purpose.
How It Works
Executes on page load
Saves your original hash (if one is in the URL).
Sequentially appends #/noop/{1..10} to the hash
Restores the original hash
That's it. No further messing around, no background event monitoring, nothing else.
Use It In One Second
To deploy, just add this anywhere on your page or in your JavaScript code:
<script>
/* Break back button */
window.onload = function(){
var i = 0;
var previous_hash = window.location.hash;
var x = setInterval(function(){
i++;
window.location.hash = "/noop/" + i;
if (i==10){
clearInterval(x);
window.location.hash = previous_hash;
}
}, 10);
}
</script>
In a modern browser this seems to work:
// https://developer.mozilla.org/en-US/docs/Web/API/History_API
let popHandler = () => {
if (confirm('Go back?')) {
window.history.back()
} else {
window.history.forward()
setTimeout(() => {
window.addEventListener('popstate', popHandler, {once: true})
}, 50) // delay needed since the above is an async operation for some reason
}
}
window.addEventListener('popstate', popHandler, {once: true})
window.history.pushState(null,null,null)
I had this problem with React (class component).
And I solved it easily:
componentDidMount() {
window.addEventListener("popstate", e => {
this.props.history.goForward();
}
}
I've used HashRouter from react-router-dom.
You simply cannot and should not do this. However, this might be helpful:
<script type = "text/javascript" >
history.pushState(null, null, 'pagename');
window.addEventListener('popstate', function(event) {
history.pushState(null, null, 'pagename');
});
</script>
This works in my Google Chrome and Firefox.
This seems to have worked for us in disabling the back button on the browser, as well as the backspace button taking you back.
history.pushState(null, null, $(location).attr('href'));
window.addEventListener('popstate', function () {
history.pushState(null, null, $(location).attr('href'));
});
Just run code snippet right away and try going back
history.pushState(null, null, window.location.href);
history.back();
window.onpopstate = () => history.forward();
<script src="~/main.js" type="text/javascript"></script>
<script type="text/javascript">
window.history.forward();
function noBack() {
window.history.forward();
}
</script>
Try this to prevent the backspace button in Internet Explorer which by default acts as "Back":
<script language="JavaScript">
$(document).ready(function() {
$(document).unbind('keydown').bind('keydown', function (event) {
var doPrevent = false;
if (event.keyCode === 8 ) {
var d = event.srcElement || event.target;
if ((d.tagName.toUpperCase() === 'INPUT' &&
(
d.type.toUpperCase() === 'TEXT' ||
d.type.toUpperCase() === 'PASSWORD' ||
d.type.toUpperCase() === 'FILE' ||
d.type.toUpperCase() === 'EMAIL' ||
d.type.toUpperCase() === 'SEARCH' ||
d.type.toUpperCase() === 'DATE' )
) ||
d.tagName.toUpperCase() === 'TEXTAREA') {
doPrevent = d.readOnly || d.disabled;
}
else {
doPrevent = true;
}
}
if (doPrevent) {
event.preventDefault();
}
try {
document.addEventListener('keydown', function (e) {
if ((e.keyCode === 13)) {
//alert('Enter keydown');
e.stopPropagation();
e.preventDefault();
}
}, true);
}
catch (err) {
}
});
});
</script>
It's basically assigning the window's "onbeforeunload" event along with the ongoing document 'mouseenter' / 'mouseleave' events so the alert only triggers when clicks are outside the document scope (which then could be either the back or forward button of the browser)
$(document).on('mouseenter', function(e) {
window.onbeforeunload = null;
}
);
$(document).on('mouseleave', function(e) {
window.onbeforeunload = function() { return "You work will be lost."; };
}
);
Just set location.hash="Something". On pressing the back button, the hash will get removed from the URL, but the page won't go back.
This method is good for preventing going back accidentally, but for security purposes you should design your backend for preventing reanswering.
Some of the solutions here will not prevent a back event from occurring - they let a back event happen (and data held about the page in the browsers memory is lost) and then they play a forward event to try and hide the fact that a back event just happened. Which is unsuccessful if the page held transient state.
I wrote this solution for React (when react router is not being used), which is based on vrfvr's answer.
It will truly stop the back button from doing anything unless the user confirms a popup:
const onHashChange = useCallback(() => {
const confirm = window.confirm(
'Warning - going back will cause you to loose unsaved data. Really go back?',
);
window.removeEventListener('hashchange', onHashChange);
if (confirm) {
setTimeout(() => {
window.history.go(-1);
}, 1);
} else {
window.location.hash = 'no-back';
setTimeout(() => {
window.addEventListener('hashchange', onHashChange);
}, 1);
}
}, []);
useEffect(() => {
window.location.hash = 'no-back';
setTimeout(() => {
window.addEventListener('hashchange', onHashChange);
}, 1);
return () => {
window.removeEventListener('hashchange', onHashChange);
};
}, []);
I create one HTML page (index.html). I also create a one (mechanism.js) inside a script folder / directory. Then, I lay all my content inside of (index.html) using form, table, span, and div tags as needed. Now, here's the trick that will make back / forward do nothing!
First, the fact that you have only one page! Second, the use of JavaScript with span / div tags to hide and display content on the same page when needed via regular links!
Inside 'index.html':
<td width="89px" align="right" valign="top" style="letter-spacing:1px;">
<small>
<b>
IN
</b>
</small>
[ <span id="inCountSPN">0</span> ]
</td>
Inside 'mechanism.js':
function DisplayInTrafficTable()
{
var itmsCNT = 0;
var dsplyIn = "";
for (i=0; i<inTraffic.length; i++)
{
dsplyIn += "<tr><td width='11'></td><td align='right'>" + (++itmsCNT) + "</td><td width='11'></td><td><b>" + inTraffic[i] + "</b></td><td width='11'></td><td>" + entryTimeArray[i] + "</td><td width='11'></td><td>" + entryDateArray[i] + "</td><td width='11'></td></tr>";
}
document.getElementById('inOutSPN').innerHTML =
"" +
"<table border='0' style='background:#fff;'><tr><th colspan='21' style='background:#feb;padding:11px;'><h3 style='margin-bottom:-1px;'>INCOMING TRAFFIC REPORT</h3>" +
DateStamp() +
" - <small><a href='#' style='letter-spacing:1px;' onclick='OpenPrintableIn();'>PRINT</a></small></th></tr><tr style='background:#eee;'><td></td><td><b>###</b></td><td></td><td><b>ID #</b></td><td></td><td width='79'><b>TYPE</b></td><td></td><td><b>FIRST</b></td><td></td><td><b>LAST</b></td><td></td><td><b>PLATE #</b></td><td></td><td><b>COMPANY</b></td><td></td><td><b>TIME</b></td><td></td><td><b>DATE</b></td><td></td><td><b>IN / OUT</b></td><td></td></tr>" +
dsplyIn.toUpperCase() +
"</table>" +
"";
return document.getElementById('inOutSPN').innerHTML;
}
It looks hairy, but note the function names and calls, embedded HTML, and the span tag id calls. This was to show how you can inject different HTML into same span tag on same page! How can Back/Forward affect this design? It cannot, because you are hiding objects and replacing others all on the same page!
How can we hide and display? Here goes:
Inside functions in ' mechanism.js ' as needed, use:
document.getElementById('textOverPic').style.display = "none"; //hide
document.getElementById('textOverPic').style.display = ""; //display
Inside ' index.html ' call functions through links:
<img src="images/someimage.jpg" alt="" />
<span class="textOverPic" id="textOverPic"></span>
and
Introduction
In my case this was a shopping order. So I disabled the button. When the user clicked back, the button was disabled still. When they clicked back one more time, and then clicked a page button to go forward. I knew their order was submitted and skipped to another page.
In the case when the page actually refreshed which would make the button (theoretically), available; I was then able to react in the page load that the order was already submitted and redirected then too.
This code is full javascript.
Put this on your home page or whatever you need when someon goes back it brings them to the page they were previously on.
<script type="text/javascript">
function preventBack() {
window.history.forward();
}
setTimeout("preventBack()", 0);
window.onunload = function () { null };
</script>

How to get query params for internal page link in JQuery Mobile

I'm looking for a clean way to do quite a trivial thing in JQuery Mobile.
When linking to an internal page to be loaded into the Dom, I want to read the query params.
So in the example below I want to access the id=test2 part of the url.
<div data-role="page" id="page1">
Go to page 2
</div>
<div data-role="page" id="page2">
Go back to page 1
</div>
I use pagecontainerbeforeshow to perform code on loading the page like this, but I don't know how to get to the query params.
$(document).ready(function(){
$( "body" ).on( "pagecontainerbeforeshow", function( event, ui ) {
console.log("How to get the Query Params?");
});
});
Here's a fiddle with this code: http://jsfiddle.net/wpgs06r1/6/
jQuery Mobile ignores (removes) querystring parameters in URL and shows URL with hash only. However, you can retrieve querystring only on pagecontainerbeforechange and when data.toPage is a string not an object. At this stage, full URL is stored in data.absUrl.
You may use $.mobile.path.parseUrl().search method to retrieve querystring, or you can use .split("?"), both should work properly.
$(document).on("pagecontainerbeforechange", function (e ,data) {
if (typeof data.toPage == "string") {
var url = $.mobile.path.parseUrl(data.absUrl),
querystring = url.search; /* retruns ?id=test1 */
/* or */
var url = data.absUrl,
querystring = url.split("?")[1]; /* retruns ?id=test1 */
}
});
Edit: If querystring comes after hash, $.mobile.path.parseUrl(url).search will return no value as it considers it a hash. Hence, use second method .split("?").
Another possible way is to utilize pagecontainerbeforetransition as it fires once and returns data.toPage object and data.absUrl string.
Custom function to process URL and retrieve querystring
function getParam(url) {
var parsed = $.mobile.path.parseUrl(url),
hash = parsed.hash.split("?");
return {
search: hash[1].split("=")[1]
};
}
Listen to pagecontainerbeforetransition; both .toPage and .absUrl should be defined and .toPage's ID is the page you want to utilize parameters at.
$(document).on("pagecontainerbeforetransition", function (e, data) {
if ($.type(data.toPage) !== "undefined" && $.type(data.absUrl) !== "undefined" && data.toPage[0].id == "pageID") {
var param = getParam(data.absUrl).search;
$(".selector", data.toPage).text("Retrieved: " + param);
}
});
Demo

External link in WinJS, iframe or not, doesnt matter

I work on a Windows 8 app, and from a page that I use link hystory for running back and forward through the app, I also have 3 or 4 links to external websites(eg: facebook or my site). I tried to run them in iframe, or also to make them open in the default browser like simple links. Both method resulted in an error in base.js that says it can't handle my error (!?) I searched a lot before asking here. I watched msdn sample that works just fine, but if i copy what I need in my app results in the same error. I I use it from another page where I dont have forward history, it works, but i really need it on the front page. Any ideeas? Thank you very much.
LE:
This is my items.js code: ( for the items.html page )
(function () {
"use strict";
var appViewState = Windows.UI.ViewManagement.ApplicationViewState;
var ui = WinJS.UI;
ui.Pages.define("/pages/items/items.html", {
// This function is called whenever a user navigates to this page. It
// populates the page elements with the app's data.
ready: function (element, options) {
var listView = element.querySelector(".itemslist").winControl;
listView.itemDataSource = Data.groups.dataSource;
listView.itemTemplate = element.querySelector(".itemtemplate");
listView.oniteminvoked = this._itemInvoked.bind(this);
this._initializeLayout(listView, Windows.UI.ViewManagement.ApplicationView.value);
listView.element.focus();
WinJS.Utilities.query("a").listen("click", this.linkClickEventHandler, false);
},
// This function updates the page layout in response to viewState changes.
updateLayout: function (element, viewState, lastViewState) {
/// <param name="element" domElement="true" />
var listView = element.querySelector(".itemslist").winControl;
if (lastViewState !== viewState) {
if (lastViewState === appViewState.snapped || viewState === appViewState.snapped) {
var handler = function (e) {
listView.removeEventListener("contentanimating", handler, false);
e.preventDefault();
}
listView.addEventListener("contentanimating", handler, false);
var firstVisible = listView.indexOfFirstVisible;
this._initializeLayout(listView, viewState);
if (firstVisible >= 0 && listView.itemDataSource.list.length > 0) {
listView.indexOfFirstVisible = firstVisible;
}
}
}
},
linkClickEventHandler: function (eventInfo) {
eventInfo.preventDefault();
var link = eventInfo.target;
WinJS.Navigation.navigate(link.href);
},
// This function updates the ListView with new layouts
_initializeLayout: function (listView, viewState) {
/// <param name="listView" value="WinJS.UI.ListView.prototype" />
if (viewState === appViewState.snapped) {
listView.layout = new ui.ListLayout();
} else {
listView.layout = new ui.GridLayout();
}
},
_itemInvoked: function (args) {
var groupKey = Data.groups.getAt(args.detail.itemIndex).key;
WinJS.Navigation.navigate("/pages/split/split.html", { groupKey: groupKey });
}
});
})();
And from items.html I have different types of links: some of them links to other application pages, from where I can return with history buttons back/forward and some of them are links to external page. Simple link.These links crashes my app with the error that I mentioned below. If I erase the next line:
WinJS.Utilities.query("a").listen("click", this.linkClickEventHandler, false);
from my js script, external links works, but I dont have anymore history buttons in my others's app pages.
You are trying to use the navigation framework to navigate to an external URI. It's usually meant to be used within the application's local context and pages that can contain 'fragments' to load up into your main nav control.
I wouldn't hook anchor tags with your function call, instead in your linkClickEventHandler I would do the following to only hook your internal links
WinJS.Utilities.query(".nav").listen("click", linkClickEventHandler, false);
in turn your internal links would be
click me
This approach only hooks the navigation framework into your internal links. Another approach is to inspect the 'this.href' in your handler and if it contains http:// or https:// then call window.open instead

Lift - Autocomplete with Ajax Submission

I would like to use an autocomplete with ajax. So my goal is to have:
When the user types something in the text field, some suggestions provided by the server appear (I have to find suggestions in a database)
When the user presses "enter", clicks somewhere else than in the autocomplete box, or when he/she selects a suggestion, the string in the textfield is sent to the server.
I first tried to use the autocomplete widget provided by lift but I faced three problems:
it is meant to be an extended select, that is to say you can originally only submit suggested values.
it is not meant to be used with ajax.
it gets buggy when combined with WiringUI.
So, my question is: How can I combine jquery autocomplete and interact with the server in lift. I think I should use some callbacks but I don't master them.
Thanks in advance.
UPDATE Here is a first implementation I tried but the callback doesn't work:
private def update_source(current: String, limit: Int) = {
val results = if (current.length == 0) Nil else /* generate list of results */
new JsCmd{def toJsCmd = if(results.nonEmpty) results.mkString("[\"", "\", \"", "\"]") else "[]" }
}
def render = {
val id = "my-autocomplete"
val cb = SHtml.ajaxCall(JsRaw("request"), update_source(_, 4))
val script = Script(new JsCmd{
def toJsCmd = "$(function() {"+
"$(\"#"+id+"\").autocomplete({ "+
"autocomplete: on, "+
"source: function(request, response) {"+
"response("+cb._2.toJsCmd + ");" +
"}"+
"})});"
})
<head><script charset="utf-8"> {script} </script></head> ++
<span id={id}> {SHtml.ajaxText(init, s=>{ /*set cell to value s*/; Noop}) } </span>
}
So my idea was:
to get the selected result via an SHtml.ajaxText field which would be wraped into an autocomplete field
to update the autocomplete suggestions using a javascript function
Here's what you need to do.
1) Make sure you are using Lift 2.5-SNAPSHOT (this is doable in earlier versions, but it's more difficult)
2) In the snippet you use to render the page, use SHtml.ajaxCall (in particular, you probably want this version: https://github.com/lift/framework/blob/master/web/webkit/src/main/scala/net/liftweb/http/SHtml.scala#L170) which will allow you to register a server side function that accepts your search term and return a JSON response containing the completions. You will also register some action to occur on the JSON response with the JsContext.
3) The ajaxCall above will return a JsExp object which will result in the ajax request when it's invoked. Embed it within a javascript function on the page using your snippet.
4) Wire them up with some client side JS.
Update - Some code to help you out. It can definitely be done more succinctly with Lift 2.5, but due to some inconsistencies in 2.4 I ended up rolling my own ajaxCall like function. S.fmapFunc registers the function on the server side and the function body makes a Lift ajax call from the client, then invokes the res function (which comes from jQuery autocomplete) on the JSON response.
My jQuery plugin to "activate" the text input
(function($) {
$.fn.initAssignment = function() {
return this.autocomplete({
autoFocus: true,
source: function(req, res) {
search(req.term, res);
},
select: function(event, ui) {
assign(ui.item.value, function(data){
eval(data);
});
event.preventDefault();
$(this).val("");
},
focus: function(event, ui) {
event.preventDefault();
}
});
}
})(jQuery);
My Scala code that results in the javascript search function:
def autoCompleteJs = JsRaw("""
function search(term, res) {
""" +
(S.fmapFunc(S.contextFuncBuilder(SFuncHolder({ terms: String =>
val _candidates =
if(terms != null && terms.trim() != "")
assigneeCandidates(terms)
else
Nil
JsonResponse(JArray(_candidates map { c => c.toJson }))
})))
({ name =>
"liftAjax.lift_ajaxHandler('" + name
})) +
"=' + encodeURIComponent(term), " +
"function(data){ res(data); }" +
", null, 'json');" +
"""
}
""")
Update 2 - To add the function above to your page, use a CssSelector transform similar to the one below. The >* means append to anything that already exists within the matched script element. I've got other functions I've defined on that page, and this adds the search function to them.
"script >*" #> autoCompleteJs
You can view source to verify that it exists on the page and can be called just like any other JS function.
With the help of Dave Whittaker, here is the solution I came with.
I had to change some behaviors to get:
the desired text (from autocomplete or not) in an ajaxText element
the possibility to have multiple autocomplete forms on same page
submit answer on ajaxText before blurring when something is selected in autocomplete suggestions.
Scala part
private def getSugggestions(current: String, limit: Int):List[String] = {
/* returns list of suggestions */
}
private def autoCompleteJs = AnonFunc("term, res",JsRaw(
(S.fmapFunc(S.contextFuncBuilder(SFuncHolder({ terms: String =>
val _candidates =
if(terms != null && terms.trim() != "")
getSugggestions(terms, 5)
else
Nil
JsonResponse(JArray(_candidates map { c => JString(c)/*.toJson*/ }))
})))
({ name =>
"liftAjax.lift_ajaxHandler('" + name
})) +
"=' + encodeURIComponent(term), " +
"function(data){ res(data); }" +
", null, 'json');"))
def xml = {
val id = "myId" //possibility to have multiple autocomplete fields on same page
Script(OnLoad(JsRaw("jQuery('#"+id+"').createAutocompleteField("+autoCompleteJs.toJsCmd+")"))) ++
SHtml.ajaxText(cell.get, s=>{ cell.set(s); SearchMenu.recomputeResults; Noop}, "id" -> id)
}
Script to insert into page header:
(function($) {
$.fn.createAutocompleteField = function(search) {
return this.autocomplete({
autoFocus: true,
source: function(req, res) {
search(req.term, res);
},
select: function(event, ui) {
$(this).val(ui.item.value);
$(this).blur();
},
focus: function(event, ui) {
event.preventDefault();
}
});
}
})(jQuery);
Note: I accepted Dave's answer, mine is just to provide a complete answer for my purpose

use ajax call in jquery autocomplete only if result is not found in locally built array

In my web app , I am showing rates of stocks.I am using jquery autocomplete to show options while entring stocks name. But I have built local copy of javascript array. I want to show the options from this local array , If search term is not found in local array then ajax call must be made to get the list from server side.
Thanks !!!
//Local array
var local_array=["option1","option2"];
//jqueryUI call of autocomplete function
$('#search_stock').autocomplete({
source:function(){
if(search term is found in local array)
{
show suggestion from local array.
}
else
{
make ajax call to show suggestions of stock names.
}
}
});
UPDATE
Here's the actual code
$(function() {
var cache = {'option1':'option1','option2':'option2'}, lastXhr;
$( "#stock_rates" ).autocomplete({
minLength: 2,
source: function( request, response ) {
var term = request.term;
if ( term in cache ) {
response( cache[ term ] );
return;
}
lastXhr = $.getJSON( "stock_rates.php", request, function( data, status, xhr ) {
cache[ term ] = data;
if ( xhr === lastXhr ) { response( data ); }
});
}
});
});
The example pages for jQuery UI autocomplete have an example of exactly this issue.
http://jqueryui.com/demos/autocomplete/#remote-with-cache. Click the 'View Source' link on that page to see the code for the example.
The key part is that 'source' takes arguments.
source: function(request, response){
You need to read request, either fetch the value from your cache, or do a request, and then call the response function and pass it the matched values.
Update
Your problem now is that the format that you are storing in your cache is wrong. The cache just stores data as it would be returned from your getJSON call, indexed by the search term. It is up to you do to do the prefix checking and such.
To continue the way you are trying now, you'll either need to populate the cache properly.
var cache = {
"o": ['option1', 'option2'],
"op": ['option1', 'option2'],
// ....
"option1": ['option1'],
"option2": ['option2']
};
Otherwise, you could store the data differently and put more logic in your 'source' function to do the prefix checking on a static array or something. That all really depends on the data you are caching though.
Use search event of autocomplete and check your condition in that event and based on that return true or false if you want to make a ajax call respectively.
Below is the sample code.
$('#search_stock').autocomplete({
search:function(event,ui){
if(search term is found in local array)
{
return false;
}
else
{
return true;
}
}
});

Resources