Building Custom jQuery and Developing a Knockout.js Table Application
Building a Custom jQuery Library
jQuery introduced a feature allowing developers to construct custom builds of the library, containing only the required components. This minimizes the file size, which is particularly beneficial for mobile and performance-critical applications. The process involves using the official jQuery build tool, Grunt.js, which relies on Node.js.
Environment Setup
To build a custom jQuery version, you must first set up a development environment with the following tools:
- Git: For cloning the jQuery source code repository.
- Node.js: Required to run the build tool.
- Grunt.js: The build tool itself, installed via Node Package Manager (NPM).
Cloning and Building jQuery
Clone the jQuery repository from GitHub:
git clone https://github.com/jquery/jquery.git
Navigate in to the cloned directory and install Grunt and its dependencies:
npm install -g grunt-cli
npm install
To build a complete jQuery version, execute:
grunt
This generates the standard jQuery files in the dist directory.
Creating a Custom Build
You can exclude specific modules to reduce the library size. For example, to build a core-only version, run:
grunt custom:-ajax,-css,-deprecated,-dimensions,-effects,-offset
This command produces a smaller jQuery file containing only essential functionalities.
Running Unit Tests with QUnit
jQuery uses QUnit for unit testing. To run the test suite, ensure you have a web server (like Apache with PHP) configured to serve the jQuery source directory. Access the test suite via:
http://localhost/test
Tests can be run for any jQuery version. To switch to a sttable branch (e.g., 1.8.3), use:
git checkout 1.8.3
npm install
grunt
Then, reload the test page to verify all tests pass.
Implementing an Infinite Scroller with jQuery
Infinite scrolling loads content dynamically as the user scrolls, enhancing user experience by progressively disclosing data. This technique is ideal for sequentially ordered content like news feeds or video lists.
Basic Page Setup
Start with a simple HTML structure and include necessary scripts: jQuery, JsRender for templating, and the imagesLoaded plugin.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Infinite Scroller</title>
<link rel="stylesheet" href="css/infinite-scroller.css">
</head>
<body>
<div id="videoContainer"></div>
<script src="img/jquery-1.9.0.min.js"></script>
<script src="img/jsrender.js"></script>
<script src="img/jquery.imagesloaded.min.js"></script>
<script src="img/infinite-scroller.js"></script>
</body>
</html>
Data Retrieval and Display
Use jQuery's $.getJSON() to fetch data from an API (e.g., YouTube). Define functions to retrieve user profile and video data.
$(function() {
let appData = {};
let startIndex = 1;
const fetchUser = () => {
return $.getJSON('https://gdata.youtube.com/feeds/api/users/tedtalksdirector?callback=?', {
v: 2,
alt: 'json'
}, (response) => {
appData.user = response.entry;
});
};
const fetchVideos = () => {
return $.getJSON('https://gdata.youtube.com/feeds/api/videos?callback=?', {
author: 'tedtalksdirector',
v: 2,
alt: 'jsonc',
'start-index': startIndex
}, (response) => {
appData.videos = response.data.items;
});
};
$.when(fetchUser(), fetchVideos()).done(() => {
startIndex += 25;
renderContent(true);
});
});
Templating with JsRender
Define templates for rendering user information and video lists.
<script id="userTemplate" type="text/x-jsrender">
<section>
<header>
<img src="{{>avatar}}" alt="{{>name}}">
<h1>{{>name}}</h1>
<p>{{>summary.substring(0,200)}}...</p>
</header>
<ul id="videoList"></ul>
</section>
</script>
<script id="videoTemplate" type="text/x-jsrender">
<li>
<article>
<a href="{{>link}}">
<img src="{{>thumbnail}}" alt="{{>title}}">
<h3>{{>title}}</h3>
</a>
<p>{{>description.substring(0,100)}}...</p>
</article>
</li>
</script>
Scroll Event Handling
Attach a scroll event listener to load more content when the user reaches the bottom.
$(window).on('scroll', function() {
if ($(window).scrollTop() + $(window).height() >= $(document).height()) {
$('<li class="loading">Loading more videos...</li>').appendTo('#videoList');
$.when(fetchVideos()).done(() => {
startIndex += 25;
renderContent(false);
$('.loading').remove();
});
}
});
Developing a Heatmap with jQuery
A heatmap visualizes user interaction data, such as click locations, on a webpage. This can be particularly useful for analyzing user behavior on responsive designs.
Client-Side Click Tracking
Capture click coordinates and viewport information, then send data to a server for storage.
$(function() {
const clickData = {
url: window.location.href,
clicks: []
};
const layouts = [];
// Parse CSS media queries to determine layout breakpoints
$('link[rel="stylesheet"]').each(function() {
const sheet = this.sheet;
if (sheet && sheet.cssRules) {
Array.from(sheet.cssRules).forEach(rule => {
if (rule.media && rule.media.length) {
const mediaText = rule.media[0];
layouts.push({
min: mediaText.includes('min-width') ? parseInt(mediaText.split('min-width:')[1]) : 0,
max: mediaText.includes('max-width') ? parseInt(mediaText.split('max-width:')[1]) : Infinity
});
}
});
}
});
$(document).on('click', function(e) {
const clickInfo = {
x: Math.round((e.pageX / $(document).width()) * 100),
y: Math.round((e.pageY / $(document).height()) * 100),
layout: determineLayout($(document).width())
};
clickData.clicks.push(clickInfo);
});
function determineLayout(width) {
for (let i = 0; i < layouts.length; i++) {
if (width >= layouts[i].min && width <= layouts[i].max) {
return i + 1;
}
}
return layouts.length + 1;
}
// Send data before page unload
$(window).on('beforeunload', function() {
$.ajax({
url: '/save-clicks',
method: 'POST',
data: JSON.stringify(clickData),
contentType: 'application/json',
async: false
});
});
});
Admin Console for Heatmap Visualization
Create an admin interface to display collected click data as an overlay on the target page.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Heatmap Console</title>
<link rel="stylesheet" href="css/console.css">
</head>
<body class="heatmap-console">
<header>
<h1>Heatmap Dashboard</h1>
<input type="text" id="pageUrl" placeholder="Enter page URL">
<button id="loadPage">Load</button>
<select id="layoutSelect"></select>
</header>
<section>
<iframe id="targetPage" scrolling="no"></iframe>
<canvas id="heatmapCanvas"></canvas>
</section>
<script src="img/jquery-1.9.0.min.js"></script>
<script src="img/console.js"></script>
</body>
</html>
Rendering the Heatmap on Canvas
Fetch stored click data and draw points on a canvas overlay.
$(function() {
const $iframe = $('#targetPage');
const $canvas = $('#heatmapCanvas');
const ctx = $canvas[0].getContext('2d');
$('#loadPage').on('click', function() {
const url = $('#pageUrl').val();
$iframe.attr('src', url).on('load', function() {
$.getJSON('/get-clicks', { url: url }, function(clicks) {
renderHeatmap(clicks);
});
});
});
function renderHeatmap(clicks) {
const width = $iframe.width();
const height = $iframe.height();
$canvas.attr({ width, height }).css({ width, height });
ctx.clearRect(0, 0, width, height);
ctx.fillStyle = 'rgba(255,0,0,0.5)';
clicks.forEach(click => {
const x = (click.x / 100) * width;
const y = (click.y / 100) * height;
ctx.beginPath();
ctx.arc(x, y, 8, 0, Math.PI * 2);
ctx.fill();
});
}
});
Creating a Sortable, Paginated Table with Knockout.js
Knockout.js facilitates building dynamic user interfaces with a clean separation of data (Model), presentation (View), and logic (ViewModel). Combined with jQuery, it enables efficient development of interactive applications.
Basic Table with Knockout Bindings
Define a ViewModel with an observable array and bind it to a table in the HTML.
<table>
<thead>
<tr>
<th data-bind="click: sortByColumn, css: sortOrder('name')">Name</th>
<th data-bind="click: sortByColumn, css: sortOrder('number')">Atomic Number</th>
<th data-bind="click: sortByColumn, css: sortOrder('symbol')">Symbol</th>
<th data-bind="click: sortByColumn, css: sortOrder('weight')">Atomic Weight</th>
<th data-bind="click: sortByColumn, css: sortOrder('discovered')">Discovered</th>
</tr>
</thead>
<tbody data-bind="foreach: displayedItems">
<tr>
<td data-bind="text: name"></td>
<td data-bind="text: number"></td>
<td data-bind="text: symbol"></td>
<td data-bind="text: weight"></td>
<td data-bind="text: discovered"></td>
</tr>
</tbody>
</table>
<select data-bind="value: pageSize, event: { change: resetPage }">
<option value="10">10</option>
<option value="30">30</option>
<option value="all">All</option>
</select>
<nav>
<a href="#" data-bind="click: previousPage, css: { disabled: currentPage() === 0 }">Previous</a>
<span data-bind="foreach: pageNumbers">
<a href="#" data-bind="text: $data, click: $parent.goToPage, css: { active: $data === $parent.currentPage() + 1 }"></a>
</span>
<a href="#" data-bind="click: nextPage, css: { disabled: isLastPage() }">Next</a>
</nav>
ViewModel Implementation
function TableViewModel(data) {
const self = this;
self.allItems = ko.observableArray(data);
self.displayedItems = ko.observableArray([]);
self.pageSize = ko.observable(10);
self.currentPage = ko.observable(0);
self.sortKey = ko.observable('name');
self.sortDirection = ko.observable('asc');
self.sortedItems = ko.computed(() => {
const key = self.sortKey();
const dir = self.sortDirection();
return self.allItems().slice().sort((a, b) => {
const valA = a[key];
const valB = b[key];
if (valA < valB) return dir === 'asc' ? -1 : 1;
if (valA > valB) return dir === 'asc' ? 1 : -1;
return 0;
});
});
self.totalPages = ko.computed(() => {
if (self.pageSize() === 'all') return 1;
return Math.ceil(self.sortedItems().length / self.pageSize());
});
self.pageNumbers = ko.computed(() => {
const pages = [];
for (let i = 1; i <= self.totalPages(); i++) pages.push(i);
return pages;
});
self.isLastPage = ko.computed(() => {
return self.currentPage() === self.totalPages() - 1;
});
ko.computed(() => {
const start = self.pageSize() === 'all' ? 0 : self.currentPage() * self.pageSize();
const end = self.pageSize() === 'all' ? self.sortedItems().length : start + self.pageSize();
self.displayedItems(self.sortedItems().slice(start, end));
});
self.sortByColumn = function(column) {
if (self.sortKey() === column) {
self.sortDirection(self.sortDirection() === 'asc' ? 'desc' : 'asc');
} else {
self.sortKey(column);
self.sortDirection('asc');
}
self.currentPage(0);
};
self.sortOrder = function(column) {
return ko.computed(() => {
return self.sortKey() === column ? self.sortDirection() : '';
});
};
self.previousPage = function() {
if (self.currentPage() > 0) self.currentPage(self.currentPage() - 1);
};
self.nextPage = function() {
if (!self.isLastPage()) self.currentPage(self.currentPage() + 1);
};
self.goToPage = function(pageNum) {
self.currentPage(pageNum - 1);
};
self.resetPage = function() {
self.currentPage(0);
};
}
const initialData = [
{ name: "Hydrogen", number: 1, symbol: "H", weight: 1.00794, discovered: 1766 },
// ... more data
];
ko.applyBindings(new TableViewModel(initialData));
Adding Filtering Capability
Extend the ViewModel to include filtering by element state.
self.filterStates = ko.observableArray(['All', 'Solid', 'Liquid', 'Gas', 'Unknown']);
self.selectedState = ko.observable('All');
self.filteredItems = ko.computed(() => {
const state = self.selectedState();
if (state === 'All') return self.allItems();
return self.allItems().filter(item => item.state === state);
});
// Update sortedItems to use filteredItems
self.sortedItems = ko.computed(() => {
const key = self.sortKey();
const dir = self.sortDirection();
return self.filteredItems().slice().sort((a, b) => {
const valA = a[key];
const valB = b[key];
if (valA < valB) return dir === 'asc' ? -1 : 1;
if (valA > valB) return dir === 'asc' ? 1 : -1;
return 0;
});
});
Add a select element for filtering in the HTML:
<select data-bind="options: filterStates, value: selectedState"></select>