Javascript-форум (https://javascript.ru/forum/)
-   Общие вопросы Javascript (https://javascript.ru/forum/misc/)
-   -   Генерация таблицы из JSON (https://javascript.ru/forum/misc/57171-generaciya-tablicy-iz-json.html)

Lemme 22.07.2015 18:37

Генерация таблицы из JSON
 
В общем то, решил попробовать написать что-то.

Берем JSON, отображаем его в виде таблицы с постраничной навигацией..
Хотелось бы советов, критики и.т.д. Как можно больше, если, конечно не лень будет смотреть=).

class Table{
	constructor(config){
		this.config = config;
		this.currentPage = 1;
		this.init();
	}
	splitByPages(data){
		let headers 	= data.shift(), //заголовок таблицы (id == Идентификатор и.т.д)
			container 	= document.querySelector(this.config.output.container), // сюда помещаются все данные
			pages		= [], // матрица страниц
			numPages	= Math.ceil(data.length / this.config.perPage); // подсчет кол-ва необходимых страниц

		// разбиваем все данные по страницам.
		// массив имеет следующий вид: pages[номер страницы] === [[], [], [].....]

		for (let i = 0; i < numPages; i++){
			pages[i] = [];
			for (let j = 0; j < this.config.perPage; j++){
				if (data.length === 0) break;
				pages[i].push(data.shift());
			}
		}

		// записываем информацию о сраницах в localStorage
		localStorage.setItem("pages", JSON.stringify(pages));

		// создаем начальную сруктуру таблицы
		let table 	= document.createElement('table'),
			thead 	= document.createElement('thead'),
			tr 		= document.createElement('tr');

		// генерируем заголовок таблицы
		for (let item of headers){
			let td = document.createElement('td');
			td.setAttribute('data-field', item.field);
			td.innerHTML = item.title;
			tr.appendChild(td);
		}

		thead.appendChild(tr);
		table.appendChild(thead);

		container.appendChild(table);
	}
	// переключение между страницами (т.е генерация таблицы на основе массива pages из localStorage)
	switchPage(){
		// если данных в localStorage по какой-то причине нет, то останавливаем работу метода.
		if (!localStorage.getItem('pages')) return;

		let table = document.querySelector(this.config.output.container + ' table'), // таблица
			pages = JSON.parse(localStorage.getItem('pages')), // массив со страницами
			tbody = document.querySelector(this.config.output.container + ' table tbody'); // элемент tbody в таблице

		// если tbody не найден, то создаем его. В обратном случае обнуляем.
		if (!tbody){
			tbody = document.createElement('tbody');
		}
		else {
			tbody.innerHTML = '';
		}

		// генерируем содержимое элемента tbody
		pages[this.currentPage - 1].forEach((row)=>{
			let tr	= document.createElement('tr');

			row.forEach((cell)=>{
				let td = document.createElement('td');
				td.innerHTML = cell;
				tr.appendChild(td);
			});

			tbody.appendChild(tr);
		});
		table.appendChild(tbody);
	}
	pagination(){
		// если данных в localStorage по какой-то причине нет, то останавливаем работу метода.
		if (!localStorage.getItem('pages')) return;

		let numPages 	= JSON.parse(localStorage.getItem('pages')).length, // кол-во страниц
			container 	= document.querySelector(this.config.output.container), // сюда помещаются все данные
			items 		= document.createElement('ul'); // контейнер пунктов меню

		// генерируем необходимое кол-во элементов.
		for (let i = 0; i < numPages; i++){
			let item = document.createElement('li');
			item.innerHTML = i+1;
			items.appendChild(item);
		}

		// вешаем обработчик клика на элементы пагинации
		items.addEventListener('click', (e)=>{
			if (e.target.tagName !== 'LI') return false;

			this.currentPage = +e.target.innerHTML; // обновляем текущую страницу
			this.switchPage(); // перерисовываем таблицу (tbody)
		});

		// добавляем пагинацию в контейнер
		container.appendChild(items);
	}
	init(){
		// запрос к серверу
		fetch(this.config.url)
		.then((response)=>{
			if (response.status !== 200){
				console.error('Something went wrong, response status: '+response.status);
				return;
			}
			response.json().then((response)=>{
				this.splitByPages(response); // разделяем данные по страницам
				this.switchPage(); // так как изначально this.currentPage = 1, то отрисовываем первую страницу
				this.pagination(); // генерируем пагинацию.
			});
		})
		.catch((err)=>{
			console.error(err);
		});
	}
}


Вызывается таким способом.
<div class="dataContainer data"></div>
<script>
	new Table({
		url: 'http://thethz.com/dataset.php?type=small',
		perPage: 50,
		output:{
			container: '.dataContainer'
		}
	});
</script>


Многое не сделано (активный пункт навигации, нормальная навигация по страницам, т.е, если много пунктов, то они все отображаются, но это не главное).

А вот так вот это дело работает:

http://jsfiddle.net/1sbLt00y/

tysonfury2015 22.07.2015 18:42

Цитата:

Сообщение от Lemme
class Table{

Никто не осмелится критиковать. Все по науке сделано, а академики в отпуске.

tsigel 22.07.2015 18:47

Методы длинные, хороший метод - 3-4 строки, дробите мельче)

Lemme 22.07.2015 18:56

Цитата:

Сообщение от tysonfury2015 (Сообщение 380979)
Никто не осмелится критиковать. Все по науке сделано, а академики в отпуске.

Какая разница как я его назвал, ведь дело не в этом.

Lemme 22.07.2015 18:57

Цитата:

Сообщение от tsigel (Сообщение 380980)
Методы длинные, хороший метод - 3-4 строки, дробите мельче)

Длинные, но разбиты по смыслу ;)

tsigel 22.07.2015 18:59

Почему вдруг splitByPages создает header таблицы? Название метода должно отображать его суть и содержимое не должно выходить за рамки.
У вас должны быть отдельные методы назначения обработчиков, снятия обработчиков. Методы отвечающие за создание дома хоршо бы тоже сделать отдельно.

tsigel 22.07.2015 19:03

Где jsDoc? :)

tysonfury2015 22.07.2015 19:06

Цитата:

Сообщение от Lemme
Какая разница как я его назвал, ведь дело не в этом.

Да, дело не в этом. Дело в том, что ты нахерачил тут детсадовскую парашу, заправил ее ESXXX соусом, и вывалил сюда. С какой целью? Чтобы все удивились, какой ты крутой спец? Ну молодец, хуля, че тут еще сказать. Подожди, щас заваляться еще пара тройка нулевых пидорочков-хипстерочков, и вы будете сосать хвалить друг друга.

tsigel 22.07.2015 19:11

я бы писал что-то типа такого (отрывок):

createPagination() {
     this.createPaginationDom();
     this.setPaginationHandlers();
  }

   setPage(index) {
      if (this.hasPage(index)) {
         this.currentPage = index;
         this.refrashBody();
      }
   }


switchPage - заменил бы на setPage и дать ей на вход номер страницы, тогда этот метод будет заодно служить публичным АПИ по переключению страницы из js

Lemme 22.07.2015 19:19

tysonfury2015, нет, я хочу лишь показать то, что я хреновый, и спросить людей, как стать лучше, а ты, друг, можешь идти туда, откуда вылез, со своим трешом.

рони 22.07.2015 19:19

Lemme,
tmsTable - плагин для отображения таблиц

github
habrahabr

Lemme 22.07.2015 19:24

tsigel, да,спасибо,я думал об этом.

Lemme 22.07.2015 19:24

рони, дело не в плагине, дело в велосипеде=) Мне это вообще не нужно, лишь для обучения.

tysonfury2015 22.07.2015 20:44

Цитата:

Сообщение от Lemme
я думал

/0

devote 23.07.2015 14:06

tysonfury2015,
Уважаемый, будьте вежливее. Вас никто тут не унижает и не оскорбляет, прошу и вас так же поступать к другим участникам форума.

tysonfury2015 23.07.2015 14:31

devote,
Я не выдержал просто. Зачем тут держать этих клоунов? Они бесят меня по жизни. Он пару дней назад не мог понять, что такое область видимости, с асинхронными запросами запутался в 2-х соснах, хотел чтобы у него из синхронного вызова result=asyncQuery() вернулся результат. Посмотрите его темы. там такой тупняк, что финиш просто. Ниже его уровня -- некуда. Но вворачивать умные словечки в свой поток бреда он не забывает, однако. И вот не прошло и тысячи лет, как он тут пишет
fetch(this.config.url)
        .then((response)=>{
            if (response.status !== 200){
                console.error('Something went wrong, response status: '+response.status);
                return;
            }
            response.json().then((response)=>{
                this.splitByPages(response); // разделяем данные по страницам
                this.switchPage(); // так как изначально this.currentPage = 1, то отрисовываем первую страницу
                this.pagination(); // генерируем пагинацию.
            });
        })
        .catch((err)=>{
            console.error(err);
        });


Нахрена это нужно на форуме? Тут что цирк чтоли? Если бы он пришел и сказал: "поцаны, помогите разобраться, че это за фигня, как это все называется, что тут вообще происходит, где почитать букварь для первоклассника" -- я бы и слова не сказал. Это еще, спасибо, что он про моноиды в категориях эндофункторов пока не прочитал. Нахрен это нужно? Почему это существо еще не в бане? Вы хотите превратить форум в цирк?

Lemme 23.07.2015 15:06

tysonfury2015, может я в чем то заблуждаюсь, но, форум для того и нужен, чтоб общаться, делиться опытом. И да, такие существа как вы тоже нужны, ибо без вас будет все тускло и однообразно. И да, почему ко мне столько внимания, фанат?? А на счет асинхронности, да, было дело, я этого не знал, но теперь знаю, и чья это заслуга? Людей, которые мне объяснили, ибо я понятия не имел что и как там происходит. А существа, как "Вы" ничего из себя не представляют, лишь срут в темах. И уж поверьте, я стараюсь усвоить все, что мне советуют. А если я написал сценарий для цирка, над которым нужно смеяться, то да, можете посмеяться и пройти мимо если нечего сказать по делу, как и поступает 30% сообщества, другие 50%, если есть возможность и желание, они помогут разобраться, а оставшиеся 20%, те, в которые входите вы, они срут в темах, а по делу ни слова. Пустые слова.

tysonfury2015 23.07.2015 15:15

Цитата:

Сообщение от Lemme
они помогут разобраться

В чем тебе помогать разбитаться? В коде, в котором ты ни строчки не понимаешь, и никогда не поймешь? Или в том, как вывалить код погламурней и под умного закосить? Так по-таким вопросам лучше сразу к ксении собчак, тут люди, в основном, делами занимаются.

devote 23.07.2015 15:40

Цитата:

Сообщение от tysonfury2015
Нахрен это нужно?

Начнем с того что этот форум не является форумом для избранных

Цитата:

Сообщение от tysonfury2015
Почему это существо еще не в бане?

За что человека банить? Просто потому что человек еще учится? Так нельзя. А вот за оскорбление и унижение, спокойно можно получить бан.

Цитата:

Сообщение от tysonfury2015
Вы хотите превратить форум в цирк?

На данный момент цирк устраиваете здесь вы, высказывая свои недовольства и плача о том как плохо жить в интернете.

Цитата:

Сообщение от Lemme
но, форум для того и нужен, чтоб общаться, делиться опытом.

Верно, именно для этого он и существует. А не для того что бы только профи постили посты.

Цитата:

Сообщение от tysonfury2015
В чем тебе помогать разбитаться? В коде, в котором ты ни строчки не понимаешь, и никогда не поймешь?

Откуда такие познания о будущем человека?

Цитата:

Сообщение от tysonfury2015
тут люди, в основном, делами занимаются.

тут люди помогают другим людям а так же получают опыт не имея его.

tysonfury2015 23.07.2015 15:48

Цитата:

Сообщение от devote
Откуда такие познания о будущем человека?

Из опыта. Я таких людей насквозь вижу. Если бы он что-то из себя представлял, он бы тупил, да, но тупил бы совсем в другом ключе, видно было бы творческий поиск. И на него бы тут так-же собак спускали, только не я, а разного рода нулевые гламурные кисы, вроде erolast, либо энтерпрайзное быдло, с шаблонами вместо мозга, которые тут тоже имеются, не буду показывать пальцем.

Lemme 23.07.2015 18:29

tysonfury2015, надеюсь, когда нибудь ты отойдешь от той "хрени", которую ты употребляешь, ибо такую ересь можно "нести" только в нетрезвом состоянии.

По делу:

tsigel, попробовал учесть все ваши замечания, кроме методов в 3-4 строки =). Ну и немного изменил хранилище данных (точнее его наполнение), ибо столкнулся с проблемой его переполнения.

class Tysonfury2015_idi_mimo{
	/**
	 * @constructor
	 * @param  {Object} config{url: 'path/to/json', perPage: Integer, output: { container: 'element class/id'}}
	 */
	constructor(config){
		this.config = config;
		this.init();
		this.storageToken = 'Data__' + Math.round(new Date().getTime() / 1000);
	}
	/**
	 * Получет данные в формате JSON по адресу this.config.url
	 * 
	 * @param  {Callback}
	 */
	getData(success){
		fetch(this.config.url)
			.then((response)=>{
				if (response.status !== 200){
					console.error('Response error! Status - '+response.status);
					return;
				}
				response.json().then((response)=>{
					success(response);
				});
			})
		.catch((error)=>{
			console.error(error);
		});
	}
	/**
	 * Рабиваем массив с данными по страницам 
	 * 
	 * @param  {Array}
	 */
	splitDataByPages(data){
		let	pages		= [],
			header  	= data.shift(),
			numPages	= Math.ceil(data.length / this.config.perPage);


		for (let i = 0; i < numPages; i++){
			pages[i] = [];
			for(let j = 0; j < this.config.perPage; j++){
				if (data.length === 0) break;
				pages[i].push(data.shift());
			}
		}

		this.putDataToStorage({header, pages});
	}
	/**
	 * Добавляет данные в sessionStorage
	 * 
	 * @param data
	 */
	putDataToStorage(data){
		sessionStorage.setItem(this.storageToken, JSON.stringify(data));
	}
	/**
	 * Возвращает данные из sessionStorage
	 * 
	 * @return data
	 */
	getDataFromStorage(){
		return JSON.parse(sessionStorage.getItem(this.storageToken));
	}
	/**
	 * Удаляет все данные из sessionStorage , которые относятся к этому классу
	 */
	clearStorage(){
		Object.keys(sessionStorage)
		.filter((key)=>{
			return /^Data__\d+$/.test(key);
		})
		.forEach((key)=>{
			sessionStorage.removeItem(key);
		});
	}
	/**
	 * Генерирует основную структуру таблицы
	 * Таблица (table), заголовок(thead)
	 */
	makeTable(){
		let table	= document.createElement('table'),
			thead	= document.createElement('thead'),
			tr		= document.createElement('tr'),
			header	= this.getDataFromStorage().header;

		for (let item of header){
			let td = document.createElement('td');
			td.innerHTML = item.title;
			tr.appendChild(td);
		}

		thead.appendChild(tr);
		table.appendChild(thead);

		this.table = table;
	}
	/**
	 * Генерирует таблицу (tbody) по странице num
	 * 
	 * @param {Number} num - текущая страница
	 */
	setPage(num = 1){
		num = num < 1 || typeof num !== 'number' ? 1 : num >= this.getDataFromStorage().pages.length ? this.getDataFromStorage().pages.length : num;

		let page 	= this.getDataFromStorage().pages[num - 1],
			tbody 	= document.querySelector(this.config.output.container+' table tbody');
			
		if (!tbody){
			tbody = document.createElement('tbody');
		}
		else {
			tbody.innerHTML = '';
		}
		page.forEach((row)=>{
			let tr = document.createElement('tr');
			row.forEach((cell)=>{
				let td = document.createElement('td');
				td.innerHTML = cell;
				tr.appendChild(td);
			});
			tbody.appendChild(tr);
		});
		this.table.appendChild(tbody);
	}
	/**
	 * Генерирует структуру постраничной навигации
	 */
	makePagination(){
		let numPages = this.getDataFromStorage().pages.length;

		if (numPages <= 1) return;

		let items = document.createElement('ul');

		for (let i = 0; i < numPages; i++){
			let item = document.createElement('li');
			item.innerHTML = i+1;
			items.appendChild(item);
		}

		items.firstChild.classList.add('active');
		this.pagination = items;
	}
	/**
	 * Устанавливает слушателей событий
	 */
	setListeners(){
		if (this.pagination){
			this.pagination.addEventListener('click', (e)=>{
				if (e.target.tagName !== 'LI' || e.target.classList.contains('active')) return false;

				document.querySelector(this.config.output.container+' ul .active').classList.remove('active');
				e.target.classList.add('active');

				this.setPage(+e.target.innerHTML);
			});
		}
	}
	/**
	 *  Последовательно вызывает методоы необходимые для генерации, обработки таблицы и навигации по ней
	 */
	init(){
		this.clearStorage();
		this.getData((data)=>{
			this.splitDataByPages(data);
			this.makeTable();
			this.setPage(1);
			this.makePagination();
			
			let container = document.querySelector(this.config.output.container);
			container.appendChild(this.table);
			if (this.pagination){
				container.appendChild(this.pagination);
			}
			
			this.setListeners();
		});
	}
}


Если я Вас правильно понял и улучшил код, то, если не сложно, подскажите, что еще можно исправить/улучшить?

tsigel 23.07.2015 18:55

Lemme,
Судя по коду вы не используете ide (по крайней мере при reformatCode не осталось бы выравненных присвоений). Это не плохо но с ide удонее. Мне нравится этот гайд по стилю кода.

Стало лучше. Мне не ясно зачем тут localStorage? потому как если данные тянутся с сервера то их должно быть не много. И они должны тянуться постронично. На сколько я понимаю это просто для себя. Ок.

clearStorage - удалить ключи можно в 1 проход.
getData - зачем колбек если у вас есть промис? Возвращайте промис, красиво же!

Вы много используете красивых итераторов, но по данным бежите обчсным фором. Почему? В коде должно быть единообразие по максимому. Допустимы сильные отличия в пользу производительности там где она нужна.

по возможности действия метода должны нести описательный характер, например в setPage вы проверяете валидность индекса, но намного лучше это вынести в отдельный метод это в 10 раз повысит читабельность метода

Я на практике убедился что много однострочных методов - это хорошо, и условия тоже хорошо бывыносить. Это позволяет не вникать в скобки и прочее, а сразу понять суть проверки по названию метода.

nerv_ 23.07.2015 19:02

Lemme, тебя не смущает, что твоя таблица (Table) работает со страницами?

splitByPages
switchPage
pagination

:)

Это из серии

car.cookBorsch();
rabbit.fly();
window.swim()

Lemme 23.07.2015 19:26

Цитата:

Судя по коду вы не используете ide (по крайней мере при reformatCode не осталось бы выравненных присвоений). Это не плохо но с ide удонее. Мне нравится этот гайд по стилю кода.
Нет, ide не использую, использую sublime text. Посмотрел гайд, очень ко многому придется привыкать, но, думаю, постепенно буду соблюдать=)

Цитата:

Стало лучше. Мне не ясно зачем тут localStorage? потому как если данные тянутся с сервера то их должно быть не много. И они должны тянуться постронично. На сколько я понимаю это просто для себя. Ок.
Задумка в том, чтоб получить данные сервера, а ковырять уже на клиенте, localStorage для того, что б дальше работать с данными.

Цитата:

clearStorage - удалить ключи можно в 1 проход.
так?
for (let key in sessionStorage){
	if (/^Data__\d+$/.test(key)){
		sessionStorage.removeItem(key);
	}
}

Цитата:

getData - зачем колбек если у вас есть промис? Возвращайте промис, красиво же!
А вот этого, к сожалению, не понял :(
Цитата:

Вы много используете красивых итераторов, но по данным бежите обчсным фором. Почему? В коде должно быть единообразие по максимому. Допустимы сильные отличия в пользу производительности там где она нужна.
Потому, что тут циклы работают с цифрами.

по возможности действия метода должны нести описательный характер, например в setPage вы проверяете валидность индекса, но намного лучше это вынести в отдельный метод это в 10 раз повысит читабельность метода

Я на практике убедился что много однострочных методов - это хорошо, и условия тоже хорошо бывыносить. Это позволяет не вникать в скобки и прочее, а сразу понять суть проверки по названию метода.

Возьму на заметку, спасибо.

Lemme 23.07.2015 19:27

Цитата:

Сообщение от nerv_ (Сообщение 381203)
Lemme, тебя не смущает, что твоя таблица (Table) работает со страницами?

splitByPages
switchPage
pagination

:)

Это из серии

car.cookBorsch();
rabbit.fly();
window.swim()

То все мелочи;)

tsigel 23.07.2015 19:55

Цитата:

Сообщение от Lemme
Потому, что тут циклы работают с цифрами.

Это отмазка. Вы автор кода и вы создаете формат, можете написать так чтобы не было цифр и были массивы.

Цитата:

Сообщение от Lemme
так?
for (let key in sessionStorage){
    if (/^Data__\d+$/.test(key)){
        sessionStorage.removeItem(key);
    }
}

Например, или так:
var reg = /^Data__\d+$/;
Object.keys(sessionStorage).forEach((key) => {
   if (reg.test(key)) {
      sessionStorage.removeItem(key);
   }
});

Вынесение объявления рег выражения позволит не создавать его при итерации каждого имени.

Методы не просто по возможности должны носить описательный характер. Всякому дроблению должен быть конец :)
Не знаю правильно или нет, но мне очень нравится разделять методы которые должны выполнять логику от тех что несут реализацию. И по возможности их не мешать.

Ну на примере того же setPage (мне нравится typescript пример будет на нем)

public setPage(index:number):void {
   if (this.hasPage(index)) {
      this.changeCurrentPage(index);
      this.render();
   }
}

public render():void {
   this.clear();
   this.createBody();
}

private changeCurrentPage(index:number):void {
   this.currentPage = index;
}


Конечно многие скажут что удалять тело таблицы и потом его создавать - накладная операция, а таблица должна быть быстра, так что тут это не совсем удачный пример, НО метод clear - нужная штука, он может быть переиспользован в удалении таблице или изменении данных от сервера и прочее, и отдельный метод на создание данных тоже нужен. дробление методов позволяет увеличить переиспользуемость написанного кода.

tsigel 23.07.2015 19:58

Lemme,
Оптимизации и красивости кода должны гармонировать, не стоит писать лишний проход по массиву если его можно не писать и код будет красив, но при этом если массив заведомо мал, а метод используется редко и за один проход выглядит не красиво - не надо пытаться свалить все в 1 кучу (ну то есть не надо пытаться отфильтровать и отмапить и отсортировать в 1 проход массив из 5 элементов, профита вы не увидите).

Lemme 23.07.2015 20:15

tsigel, спасибо за помощь=)

tsigel 23.07.2015 20:25

Lemme,
Перебирая объекты не забывайте про hasOwnProperty
Цитата:

Сообщение от Lemme
А вот этого, к сожалению, не понял

Зачем getData принимает коллбек? пусть возвращает промис!

getData() {
   var promise = fetch(this.config.url);
   promise.catch(...);
   returm promise.then(...);
}

init() {
  this.getData().then((data) => {
    ...
  });
}

Lemme 23.07.2015 20:39

Цитата:

Сообщение от tsigel (Сообщение 381252)
Lemme,
Перебирая объекты не забывайте про hasOwnProperty


Зачем getData принимает коллбек? пусть возвращает промис!

getData() {
   var promise = fetch(this.config.url);
   promise.catch(...);
   returm promise.then(...);
}

init() {
  this.getData().then((data) => {
    ...
  });
}

А вот это красиво =) :thanks:

tsigel 23.07.2015 20:43

Lemme,
Дробление может быть очень хорошим, правда бывает и избытычным, но так вроде не плохо

...
promise.catch(this.onGetDataError.bind(this));
...


биндить соответстввенно надо если нужен контекст, в противном случае лучше сделать статичный метод.

Lemme 23.07.2015 21:25

В 171 аргумент не нужен, я просто забыл его убрать, когда не было навигации, я так проверял переход между страницам.

num = num < 1 || typeof num !== 'number' ? 1 : num >= this.getDataFromStorage().pages.length ? this.getDataFromStorage().pages.length : num;

Почему ничего? Может я в чем то заблуждаюсь, но:

function test(num = 1){
	var numPages = 10;

	if (num < 1 || typeof num !== 'number') {
		num = 1;
	}
	else if (num >= numPages){
		num = numPages;
	}
	return num;
}

console.log(test(5)) // 5
console.log(test(100)) // 10
console.log(test('smth')) // 1

Lemme 23.07.2015 21:55

Цитата:

Сообщение от Rise (Сообщение 381274)
Lemme, 159-я строка то где у вас находится посмотрите и что она принимает в качестве аргумента.

Если рассуждать так, то да, ничего 107 строка не делает, но я лишь решил перестраховаться, вдруг, потом захочется несколько изменить пагинацию. Например слушать изменение хэша.

т.е
<li><a href="#page=10">10</a></li>


В любом случае, спасибо за подсказку =)


Часовой пояс GMT +3, время: 09:46.