Posts Tagged jQuery
Tutorial: dashboard web 2.0 dla leniwych…
W moim pierwszym tutorialu napiszę jak wydajnie korzystać z tego co daje Ci cakePHP i jQuery. Aktualnie cake opiera się na współpracy z innym frameworkiem javascript – prototype. Ale już w wersji 1.3 core cake’a ma współpracować z jquery. Już nie mogę się doczekać. Ten tutorial oparłem na wersji 1.3.0-beta dlatego, że przy okazji jego pisania chciałem rzucić okiem na kolejną wersję tego świetnego frameworka php. Jednak nie będę korzystał z żadnych helperów jquery, zatem większość z tych rad powinna pasować do wersji 1.2 (w razie problemów – chętnie służę pomocą).
Jest to mój pierwszy tutorial, więc proszę o wskazówki, komentarze i odrobinę wyrozumiałości.
Efekt jaki chcemy osiągnąć można obejrzeć jako demo oraz źródła na świetnym hostingu, który gorąco wszystkim webdeveloperom i ich klientom polecam: vipserv.org.
W tym tekście zakładam, ze znasz podstawy cakePHP i javascript. Czyli przynajmniej zrobiłeś sławny blog tutorial , wiesz jak mniej więcej używać narzędzia bake, oraz potrafisz przynajmniej zniknąć element na stronie i zmienić jego kolor przy pomocy czystego javascript’u.
Zacznijmy od przygotowania przykładowej aplikacji. Wypiekamy (cake bake) aplikację złożoną z dwóch modeli Thing i Item (każda tabela składa się z id i pola tekstowego name).
Następnie tworzymy dla niej zbiorczy widok (tzw. dashboard, metoda all())
// /app/controllers/items_controller.php
class ItemsController extends AppController {
//...
function all() {}
}
// /app/views/items/all.ctp
<?php echo $this->requestAction(
array(
"controller"=> "items",
"action"=> "index"),
array("return")
); ?>
<?php echo $this->requestAction(
array(
"controller"=> "things",
"action"=> "index"),
array("return")
); ?>
Na razie nie przejmujcie się kwestią requestAction i wydajności. Zazwyczaj efekty requestAction należy umieszczać w cache’owanych elementach, ale w naszym przypadku już niedługo zrezygnujemy z samego requestAction.
Zróbmy z tego widoku zakładki modyfikując widok all.ctp w następujący sposób:
<script type="text/javascript">
$(document).ready(function() {
$("#tabs").tabs();
});
</script>
<div id="tabs">
<ul>
<li><a href="#tabs-1">Items</a></li>
<li><a href="#tabs-2">Things</a></li>
</ul>
<div id="tabs-1">
<?php echo $this->requestAction(
array(
"controller"=> "items",
"action"=> "index"),
array("return")
); ?>
</div>
<div id="tabs-2">
<?php echo $this->requestAction(
array(
"controller"=> "things",
"action"=> "index"),
array("return")
); ?>
</div>
</div>
Żeby wypieczone widoki były “otoczone” ramką z zakładki – trzeba w nich dodać taki fragment na samym końcu plików index.ctp:
<div style="float: none; clear: both;"> </div>
Dodajmy więcej elementów, żeby zobaczyć jak zachowuje się pagination…
insert into items(`name`) values ('Item 4'), ('Item 5'), ('Item 6'), ('Item 7')
i zmieńmy parametry stronicowania, żeby efekty stronicowania zobaczyć szybko
class ItemsController extends AppController {
//...
var $paginate = array(
'limit'=> 5
);
//...
Widzimy, że w naszym zbiorczym widoku link ciągle prowadzi do items/index. Wolelibyśmy, żeby stronicowanie odbyło się w zakładkach.
Można oczywiście próbować rozwiązania via php. Oprogramować obsługę parametrów w metodzie all(), zmienić cel linków stronicowania i sortowania w widokach. I ogólnie dużo napracować się nad tym, żeby nasz kod wyglądał źle
. Już za chwilę moc javascript i w szczególności jQuery przyjdzie nam z pomocą. Zanim to nastąpi – zamieńmy nasze zakładki na Ajaxowe zmieniając widok all.ctp:
// views/items/all.ctp
<script type="text/javascript">
$(document).ready(function() {
$("#tabs").tabs();
});
</script>
<div id="tabs">
<ul>
<li>
<?php echo $html->link(
"Items",
array(
"controller"=> "items",
"action"=> "index"
)
);
?>
</li>
<li>
<?php echo $html->link(
"Things",
array(
"controller"=> "things",
"action"=> "index"
)
);
?>
</li>
</ul>
</div>
…prościzna, prawda? Kwestię requestAction też już mamy z głowy. Jednak pojawił się problem – wnętrze zakładek pojawiło się w powtórzonym layoucie.
Szybko naprawimy to w app_controller.php przy pomocy RequestHanlder:
// /app/app_controller.php
var $components = array("RequestHandler");
function beforeFilter(){
if($this->RequestHandler->isAjax()){
$this->layout = "ajax";
}
}
Wrócmy do kwestii stronicowania (i sortowania przy okazji). Chcemy, aby kolejne strony ładowały się w danej zakładce. Wykorzystajmy do tego potęgę jQuery.
Dodajemy taki prosty skrypt
// views/items/all.ctp
$('#tabs a[href*=/sort:],#tabs a[href*=/page:]').live('click', function(){
$('#tabs>div:visible').load($(this).attr('href'));
return false;
});
Jeśli interesuje Cię co się w tym fragmencie dzieje, zapraszam na do innego wpisu. W dużym skrócie: łapiemy linki, które mają w adresie “sort:” i “page:” i zmuszamy je aby przeładowały zawartość zakładki, zamiast całej strony.
Kolejną kwestią jest usuwanie elementów. Jeśli nie używasz na co dzień selektorów jQuery – może Cię kusić, aby linkom odpowiedzialnym za usuwanie elementów dodawać atrybut onclick, a w nim wywołanie funkcji, która sprawi, że całość odbędzie się ajaxem. Nie jest to złe podejście, ale jeśli trochę poznasz selektory jQuery – będziesz wiedział, że to rozwiązanie nie jest sexy
Zróbmy to od razu porządnie, przy pomocy tylko kilku linii kodu.
W widokach index.ctp (dla items i things) znajdź fragment odpowiadający za linki. Dodajmy dla linku “delete” klasę “tab-update”,
dzięku temu łatwo je złapiemy w przeglądarce:
<td class="actions">
<?php echo $this->Html->link(__('View', true), array('action' => 'view', $item['Item']['id'])); ?>
<?php echo $this->Html->link(__('Edit', true), array('action' => 'edit', $item['Item']['id'])); ?>
<?php echo $this->Html->link(
__('Delete', true),
array('action' => 'delete', $item['Item']['id']),
array("class"=> "tab-update"),
sprintf(__('Are you sure you want to delete # %s?', true),
$item['Item']['id'])
); ?>
</td>
Metoda tutaj zastosowana nie będzie współgrała z confirm message cake’a (czwarty parametr metody link()), więc proponuję to na razie usunąć, bo chwilowo nie mam pomysłu jak to naprawić (spodziewam się w finalnej wersji 1.3 rozwiązania tej kwestii, więc pozwalam sobie teraz nie zaprzątać nią głowy).
Używamy podobnego triku jak przy pagination, a właściwie dopisujemy jedynie nowy selektor “.tab-update” do funkjcji, którą już mamy:
jQuery('#tabs a[href*=/sort:],#tabs a[href*=/page:],.tab-update').live('click', function(){
jQuery('#tabs>div:visible').load(jQuery(this).attr('href'));
return false;
});
Jednak pojawia się problem z redirectami. To dlatego, że dispatcher nie wie jaki to controller, żeby to naprawić wystarczy w metodzie delete zamienić
// /app/controllers/items_controller.php
$this->redirect(array('action'=>'index'));
na
// /app/controllers/items_controller.php
$this->redirect(array("controller"=> "items", 'action' => 'index'));
jeśli chcesz mieć bardziej estetyczny kod, to zamiast explicite podawać “items” możesz zmienić na $this->name,
Uważny obserwator (czyli Twój klient) zauważy, że już nie widać informacji, że element został usunięty. Zostawmy sobie tą drobnostkę na koniec.
Teraz warto by poprawić działanie przycisku “Add Item”. Teraz przenosi nas do nowego widoku, a potem do /items/index. W przypadku pierwszej zakładki nie ma problemu – wystarczyłoby zmienić przekierowanie na adres /items/all. Ale w kwestii kolejnych zakładek – natkniemy się na problem włączenia odpowiedniej zakładki. Dlatego tu również pomocny będzie Ajax. Do tego wykorzystamy fajny plugin do jQuery – Boxy.
Myślę, że teraz jest najwyższa pora aby pozbyć się powtórzeń w widokach add i edit. Szkoda życia na edycję dwóch niemal identycznych widoków. Aby to zrobić – rzuć okiem tutaj. W dalszej części kod będzie po przeprowadzeniu takiego refaktoringu.
Od razu zrobimy to tak aby wszystkie linki, które chcemy, wywoływały się ajaxem w oknie dialogowym. Dodaj klasę “display-in-box” dla linków edit i add (w widokach index.ctp items i things), a następnie w widoku /items/all dodaj taki kod:
// /app/views/items/all.ctp
$("a.display-in-box").live(
"click",
function(){
Boxy.load(
this.href,
{
title: (this.title)? this.title: this.href,
modal: true,
}
);
return false;
}
);
Przypisze on wszystkim linkom klast “display-in-box” zachowanie polegające na tym, że po kliknięciu otworzy okno dialogowe Boxy, załaduje do niego ajaxem zawartość z pod adresu na który ten link wskazywał (href), ustawi tytuł boxu taki jak title linku (lub w wypadku braku – wywołany adres) oraz zapobiegnie przeładowaniu strony po kliknięciu linku.
Wygląda to teraz ładnie, działa poprawnie, ale po dodaniu, lub edycji elementu lądujemy nie tam gdzie się spodziewamy.
Pierwszym problemem jest to, że kliknięcie submit skutkuje przeładowaniem strony (i w następstwie redirectem nie tam gdzie chcemy). Przekonajmy formularz, żeby wysłał się również ajaxem
jQuery("body").ajaxComplete(
function( event, xhr, options) {
if(Boxy.isModalVisible()){
$(".boxy-wrapper form").live(
"submit",
function() {
var form = this;
jQuery.ajax({
type: "post",
url: this.action,
data: jQuery(this).serialize(),
success: function(data, status, xhr) {
if(data != ":ajaxRedirect:") {
Boxy.get(form).setContent(data);
}else{
Boxy.get(form).hideAndUnload();
}
},
});
return false;
}
);
}
}
);
Ten fragment wymaga konkretnego wyjasnienia. W linii 1 podpinamy naszą funkcję (linia 2) pod zdarzenie ajaxComplete, a to znaczy, że przy każdym odpaleniu jakiegokolwiek ajaxa na naszej stronie przez jQuery zostanie ona uruchomiona (z odpowiednimi parametrami jak w linni 2).
W linni 3 sprawdzamy czy istnieje jakiekolwiek okno dialogowe otwarte na tej stronie (modal to typ okna, które pojawia się z takim szarym tłem przykrywającym resztę strony). Jeśli tak, to w lini 4 łapiemy wszystkie (w naszym wypadku zawsze jeden) formularze, które znajdują się w oknie dialogowym i podpinamy się w nich pod zdarzenie “submit” (4i5). Teraz przy każdej próbie wysłania naszego formularza odpali się nasza funkcja (linia 6).
Przy pomocy jQuery odpalamy wywołanie ajaxowe, przekazując odpowiednie parametry:
sposób przesłania danych (8), endpoint wywołania (9), dane do wysłania (10) (zostawiamy brudna robotę jQuery), a następnie definiujemy zachowanie w przypadku, gdy wywołanie ajaxa się powiedzie (11).
Zanim opiszę działanie następnego bloku – trochę teorii. Wywołanie ajaxa się powiedzie zawsze, kiedy uda sie wysłać zapytanie pod wskazany adres, a w odpowiedzi dostaniemy jakiś ciąg tekstowy. Metody zapisywania elementów w cakePHP korzystają z mechanizmów przekierowań, więc niezaleźnie od tego, czy nasz zapis zakończy się poprawnie, czy wyskoczą błędy walidacji – zapytanie z punktu widzenia Javascript zakończy się sukcesem. Dlatego należy zastosować mały hack, żeby przesłać klientowi informacje “Zapis się udał, zamknij okno”. Proponuję podejście nadpisania działania funkcji redirect w AppController:
// /app/app_controller.php
function redirect($url, $status = null, $exit = true) {
if(!$this->RequestHandler->isAjax()){
parent::redirect($url, $status, $exit);
}else{
exit(":ajaxRedirect:");
}
}
która w przypadku zwykłych wywołań działa normalnie, a przypadku wywołań ajaxowych – zwraca ustalony ciąg znaków, będący umówionym hasłem
Gdy już mamy ten fragment łatwiej zrozumieć co się dzieje w liniach 12-16. Gdy zwrócone dane nie są umówionym hasłem – ładujemy je do naszego otwartego okna dialogowego (13) (zobacz to w działaniu dodając walidację do modelu i wysyłając dane nie spełniające wymagań walidacji), a gdy hasło się zgadza – wiemy, że zapis się powiódł i zamykamy okno (15).
Linia 19 zabezpiecza nas przed wysłaniem formularza ponownie przez przeglądarkę.
Niestety, z powodu nowej metody AppController::redirect – usuwanie elementów się popsuło:
zajmiemy się tym później. Teraz mamy na tapecie inną kwestię: po prawidłowym zapisaniu elementu nie widzimy żadnego efektu na stronie.
Odnajdźmy fragment, w którym podpięliśmy okno dialogowe pod linki klasy “display-in-box”. Na początku naszej funkcji “złapmy” element klasy .tab-container (element, w kŧórym są zakładki) w którym został kliknięty link, który otworzył okno dialogowe, a następnie sprawmy, żeby po zamknięciu tegoż okna została odświeżona aktualnie aktywna (wybrana) zakładka w złapanym tab-containerze. Cała funkcja będzie wyglądać tak:
$("a.display-in-box").live(
"click",
function(){
var tabContainer = $(this).parents(".ui-tabs");
Boxy.load(
this.href,
{
title: (this.title)? this.title: this.href,
modal: true,
fixed: false,
afterHide: function() {
tabContainer.tabs(
"load",
tabContainer.tabs("option", "selected")
);
}
}
);
return false;
}
);
Naprawmy szybko popsute usuwanie. Znajdźmy teraz fragment, w którym podpięliśmy ajaxa pod linki “delete” (czyli te klasy “tab-update”). I rozbudujmy wywołanie ajaxowe, na identycznej zasadzie jak przy okazji ładowania do boxy formularza z błędami walidacji.
Żeby coś z tym zrobić, musimy przy obsłudze linków sortowania i stronicowania w zakładkach trochę pogrzebać
jQuery('#tabs a[href*=/sort:],#tabs a[href*=/page:],.tab-update').live('click', function(){
jQuery.ajax({
url: jQuery(this).attr('href'),
success: function(response, status, xhr){
if(response != ":ajaxRedirect"){
jQuery('#tabs>div:visible').html(response);
}
}
});
return false;
});
A teraz po prostu w wypadku otrzymania “:ajaxRedirect:” – odświeżmy zawartość zakładki, zamiast ładować response, dopisując blok else:
jQuery('#tabs a[href*=/sort:],#tabs a[href*=/page:],.tab-update').live('click', function(){
jQuery.ajax({
url: jQuery(this).attr('href'),
success: function(response, status, xhr){
if(response != ":ajaxRedirect:"){
jQuery('#tabs>div:visible').html(response);
}else {
$("#tabs").tabs(
"load",
$("#tabs").tabs("option", "selected")
);
}
}
});
return false;
});
Kolejnymi elementami są flashMessage. Nie działają teraz dobrze – po pierwsze nie dostajemy odpowiedniego feedbacku. Po drugie, po usunięcie elementu i kliknięciu “view” przy innym – w widoku szczegółów widzimy “Thing deleted” – spóźniony feedback jest gorszy niż żaden, bo wprowadza zamieszanie. Najprosztszym sposobem, aby to poprawić jest skopiowanie layoutu ajax:
/cake/libs/views/layouts/ajax.ctp do /app/views/layouts/ajax.ctp i dodanie kodu odpowiadającego za wyświetlenie wiadomości:
<?php echo $this->Session->flash(); ?> <?php echo $content_for_layout; ?>
Jako wisienkę na torcie można dodać ficzer, żeby flash zniknął po chwili – zachęcam do własnych ekpserymentów, dzielenia się tym na swoich blogach linkując do tego tutoriala, oraz do komentarzy.
Jeśli uważasz, że autor tego wpisu odwalił niezłą robotę i czegoś Cię nauczył, albo jeśli uważasz zawartość tego bloga za interesującą nie wahaj się subskrybowac kanał rss, dodać jego adres w sygnaturkach swoich profili, dodać link do niego na własnym blogu i napisać co nieco od siebie w komentarzach pod postem.
CakePHP, pagination, jQuery i zakładki (tabs)
Jakoś tak wyszło, że więcej bawię się ostatnio jQuery. Pojawił się kolejny problem:
przerobić Pagination cake’a na taki, żeby działał po ajaxie w zakładkach jQuery (jquery.ui.tabs):
Z pomocą przyszedł chankov.net z takim oto skryptem:
$('a[href*=/sort:],a[href*=/page:]').livequery('click', function(){
$('#content').load($(this).attr('href'));
return false;
});
Niestety nie ma chyba kontroli nad tym jakie id jquery.ui nada div’om, które są zawartością dla danej zakładki. Jednak i na to jest sposób (dzięki pluginowi livequery), nalezy zamiast
$('#content').load($(this).attr('href'));
Użyć nieco bardziej skomplikowanego selektora. Zakładając, że #content to div przechowujący zakładki (wywołałeś jQuery(“#content”).tabs()), to nalezy zmienić powyższą linię na
jQuery('#contents>div:visible').load(jQuery(this).attr('href'));
Dzięki selektorowi ‘#contents>div:visible’ znajdziesz w elemencie o id #contents pierwszego div’a (>div), który jest widzialny (:visible), a to właśnie jest element, którego zawartość należy uaktualnić nowymi danymi…
Jak wyczyścić cache dla zakładek (tabs) w jQuery?
Notka ku pamięci (żeby następnym razem nie szukać godzinę rozwiązania pt JQuery.tabs.disableCache).
Jeśli używasz zakładek (jQuery.UI.tabs()) z włączonym keszowaniem (co jest dobrym pomysłem, ze względu na czas odpowiedzi) to możesz w końcu dojść do momentu, że należy wyczyścić cache dla konkretnej zakładki
(dlatego, ze na przykład został wykonany ajaxowy submit formularza).
Nie znajdziesz niestety funkcji “clearCache”, którą ja bezskutecznie poszukiwałem (zakładałem, że najpierw muszę wyczyścić cache dla tej zakładki, a później obsłużyć załadowanie nowych elementów do zakładki). Okazuje się, że można to zrobić równocześnie za pomocą metody “load”:
jQuery(“#idKonteneraZakladek”).tabs(“load”, [id_zakłaki]);






