Showing posts with label html. Show all posts
Showing posts with label html. Show all posts

Thursday, March 5, 2009

Cascaded HTML SELECT elements using JQuery

Many web applications at least from which i worked in demanded the existance of many select boxes stacked over each other. These HTML select boxes are used for filtering data underneath them.

But the idea is that in some cases, these ones are dependent on each other. One case can be a filter for cars that starts with types, then makes then trims and so on. In some of these filters the amount of overall filters data are not so large that they can all be retrieved with page once and then JS manipulation can be used to propagate between them.

I used to make this myself until i found myself fed up of doing things that should be generic. I looked for a plugin for JQUERY which i use as a JS framework in my application. Lucky me. i found one that does this cascading on two levels and then you can repeat on others until all get cascaded together.

The solution i found was that blog post
the solution proposed here was very nice but i found that i need to modify it somehow to be much more elegent and complete from my opinion. You may disagree with me in some modifications and it's your right to do so. Take the code and modify it as you wish

Before looking at my code, you can have a look at the post in order to be familiar with my solution.

Here is the code

cascadeLists: function(parentId, childId, callbackFunction){
$("body").append('');
var childOptions = $('#' + childId + ' option');
$('#' + parentId + childId).html(childOptions);

var parent = $('#' + parentId)[0];
if(parent.options[parent.selectedIndex].value == '')
$('#' + childId).html(childOptions.filter('[value=""]').clone());

$('#' + parentId).change(function(){
var parent = $('#' + parentId)[0];

var selectedOptionValue = '';
if (parent.selectedIndex > -1)
selectedOptionValue = parent.options[parent.selectedIndex].value;

if (selectedOptionValue == '')
$('#' + childId).html($('#' + parentId + childId + ' option[value=""]').clone());
else {
var childs = $('#' + parentId + childId + ' option[parent="'+selectedOptionValue+'"]');
if(childs.size() == 0)
$('#' + childId).html($('#' + parentId + childId + ' option[value=""]').clone());
else
$('#' + childId).html($('#' + parentId + childId + ' option[parent="' + selectedOptionValue + '"]').clone());
}

$('#' + childId).trigger("change");

if(callbackFunction != null) callbackFunction(selectedOptionValue);
});
}

In order to use this function, you will do as this example

<select id="parent"> <option value="1">xxx</option> <option value="2">xxx</option></select>

<select id="child"> <option parent="1" value="1">xxx</option> <option parent="1" value="2">xxx</option> <option parent="2" value="3">xxx</option></select>


just as your normal select boxes, but with additional IDs for each one of them and for the select box that have parent, each option state in its parent attribute the value of its parent

One Question will be: What if i want to add dummy option such as --select-- for both selects and whenever i choose this option in parent, child should be also this dummy one. Don't worry code handles this for you. If available this will be done. As for the dummy one, don't add to it a parent and let its value = ''

Now, just call the function and pass parameters:
  • parent select id
  • child select id
  • callbackFunction (optional) if you wish certain function to be called on any change to parent select, this function is called and pass to this function, the value of the option which was selected. Use this value to make any extra manipulation you want to do in the HTML page

Modification made are:
  • this method depend on option sent to it in order to show empty option on child, this isn't very nice in all cases, especially when dealing with internationalizations, in this case the data added here will have different text from a language to another. That's why i give you the ability to add empty option at the top of options and use it instead of generating one myself. that way you can choose what to write in this without changing in the JS Code The new logic is if there is an empty value option in child
  • Another option is needed is to call certain function whenever a change occurs on parent level and this function takes the new value, this way you can do any changes that was waiting such change like loading in Ajax some changes in page
  • One last change is that i see adding a parent attribute will be better than using sub_ and parent value

Wednesday, February 11, 2009

HTML Scrapping using Javascript ((for google gadgets))

Some friends at my company were working on doing some google gadgets. A large sum of gadgets were depending on data gathered from other websites which lack of any XML or RSS service providing this data in a direct way.

Since this is a problem we will face every now and then, we started thinking about a more generic solution to use in any gadget depending on such source of data.

The solution we reached was one of these three
  1. using a scrapping service such as Dapper or Yahoo pipes to do the scrapping on behalf of us and returns a well formed XML file to use in any gadget
  2. create a google app engine that we call and it scrape the data and returns XML to us
  3. using JS for scrapping HTML pages 
the first and second solutions may seam the same and actually they are except that Dapper isn't that reliable as it sometimes fails due to extra load on it while google app engine was proven to survive under high request rates

Anyway, i liked the third solution and said to myself lets give it a try and see if it will be performant enough or not. I thought scrapping html using JS is an easy matter that can be done easily in any google gadget but i was proven not to be like that at all. I will summerize the trials i made here starting from those who failed to the last solution that worked.

  1. Depending on Google Api method "_IG_FetchXmlContent". This way failed easily because it was expecting XML document and was faced with HTML Page. It gave me parse error on Doctype line. The result is FAILURE
  2. Depending on Google Api method "_IG_FetchContent". This way gave us the html as it is and it was time to parse it using DOM Parsers built already inside browsers. I tried doing so using Firefox browser but also got parse error because this is not a XML document but HTML one and parsers available only expects XML. The result is FAILURE
  3. Repeating step 2 again but after using a regular expression to take only inner HTML of body tag. DOM Parser failed on one of the comments lines present in the HTML page which may appear in
    may pages so this isn't a generic solution to be accepted. The result is FAILURE
  4. Using Regular expression to get body inner html and then add this to a hidden div then using normal JS methods for traversing DOM nodes considering this Div as my root. The result is SUCCESS
Since, the fourth trial was successful i made a generic method that anyone can use in his gadget. this simple method will just get html and scrape based on your scrapping function. To understand what i mean, have a look at the function definition first

scrapeHTMLBody = function(url, dataHolderId, scrapeFunction){}

as in this definition we see that the function needs some parameters
  • url to retrieve html from
  • dataHolderId the id of the hidden div that the retrieved html will be added to it
  • scrapeFunction a function that takes the hidden div as a root element and use JS to get data desired "every one should write his according to what he wants to retrieve"
and this is the implementation of it

scrapeHTMLBody = function(url, dataHolderId, scrapeFunction){
_IG_FetchContent(url, function(responseText){ operate(responseText, dataHolderId, scrapeFunction); });
}


operate = function(responseText, dataHolderId, scrapeFunction){
  var body = /<body.*?>((.|\n|\r)*)<\/body>/.exec(responseText);
  var bodyData = body[1];
  _gel(dataHolderId).innerHTML = bodyData;
  scrapeFunction(dataHolderId);
}


these two functions are used to get html page then retrieve body inner html then call the scrape function passing to it the if of the hidden div containing the html body
it is your responsibility now to write the scrapping function desired based that this div is the root of your DOM tree

this is an example of a scrapping function i defined

scrape = function(dataHolderId){
  var elements = _gel(dataHolderId).getElementsByClassName('main');

  var noktas = [];
  var num = elements.length;
  for(i=0 ; i<num ; i+=2) noktas.push(elements[i].childNodes[0].innerHTML);

  for(i=0 ; i<noktas.length ; i++){
    var e = document.createElement('p');
    e.innerHTML = noktas[i];
    document.body.appendChild(e);
  }
}


That's it, i think you are ready now to use these two functions in any gadget whose data source should be scrapped
This method should be better as here all processing is made on client machine rather than any other servers