Namespace’y javascript w widokach cakePHP

Czasem zdarza się tak, że w danym widoku potrzebuję bardzo specyficzną funkcję javascript. Na przykład w widoku invoices/add funkcja count_gros_value, która na podstawie wpisanej wartości w polu netto (net) i stawki var wyliczy wartość brutto (gros).

Załóżmy, że nie wiem jak napisać taką funkcję, żeby była elastyczna, po prostu we wnętrzu mam zaszyte id pól formularza:

function count_gros_value(){
   $("#Invoice[gros]").val( 
      parseFloat($("#Invoice[net]").val()) * 
      parseFloat($("#Invoice[vat]").val())
   );
}
// wywołanie
count_gros_value();

(nie sprawdzałem tego kodu, służy jako przykład i nie mam pojęcia czy dobrze działa).

Oprócz wielkiej nieelastyczności kodu (o czym teraz pisał nie będę) istnieje problem polegający na tym, że nie wiem czy jakiś inny programista nie zdefiniował funkcji count_gros_value gdzieś w layoucie. Oczywiście dostanę error jeśli tak jest, więc poprawię ją na my_count_gros_value() (sic!). Ok, wszystko działa. Jednak przychodzi inny programista i odczuwa potrzebę zdefiniowania funckji my_count_gros_value() w layoucie (no bo count_gros_value() już istnieje, a jego robi coś inaczej/lepiej więc trzeba ją zaimplementować). W ten sposób rozwala twój zaimplementowany fragment i nikt tego nie zauważa.

Jak tego uniknąć? Można zacząć nazywać takie “lokalne” funkcje invoices_index_count_gros_value() ale to zaczyna wyglądać nieestetycznie i aż się ciśnie na usta magiczne słowo “namespace”.

W Javascript nie ma przestrzeni nazw, ale możesz użyć obiektów:

var invoices = {
   index: {
      count_gros_value: function(){
         $("#Invoice[gros]").val( 
            parseFloat($("#Invoice[net]").val()) * 
            parseFloat($("#Invoice[vat]").val())
         );
      }
   }
}
// wywołanie
invoices.index.cunt_gros_value();

to jest już fajniejsze, ale ma dwa problemy:

  1. ciągłe definiowanie obiektów dla namespace’ów jest równie uciążliwe jak nazwa funkcji invoices_index_count…
  2. gdy ktoś przed nami zdefiniuje zmienną var invoices = “invoices”; to mu ją nadpiszemy

Dlatego proponuję taką funckję, która definiuje dowolnie głębokie obiekty udające namespace’y i do tego tworzy je wszytkie w zmiennej o pseudolosowej nazwie (w stylu: Namespace12883495048110.1425782083547854):

function Namespace(namespace_path){
	var w = window;

	window.namespaceFrameworkNamespaceName = 
		window.namespaceFrameworkNamespaceName || 
		"Namespace"+ new Date().getTime() + new Math.random();

	var arr  = (window.namespaceFrameworkNamespaceName + namespace_path).
						split(".");

	for(i in arr){
		w[ arr[i] ] = w[ arr[i] ] || {};
		w = w[ arr[i] ];
	}
	return w;
}

Ok, jakiś uparciuch mógłby się uprzeć, że ktoś może w aplikacji zmiennej ‘namespaceFrameworkNamespaceName’ – takiego problem nie rozwiązywałbym na poziomie kodu, ale personalnym (zwiałbym z projektu) ;)

tak wygląda definiowanie funkcji w namespace:

Namespace("app.users.index").check_form = function(){ 
	document.write("my_check_form()");
};

a tak jej wywołanie:

Namespace("app.users.index").check_form();
// lub gdy mamy zamiar wywołać wiele funkcji z danego namespace:
with(Namespace("app.users.index")){
 check_form();
}

Można by z tego zrobić tez fajne rozszerzenie jQuery,
$().nspc(“app.invoices.index”) (pisałem już o rozszerzaniu jQuery w tutorialu “longPolling”)

Share Button

Dług techniczny – przykład z życia wzięty

Można się spierać co jest długiem technicznym, a co nie jest. Chciałbym Wam pokazać przykład kodu, który świetnie nadaje się do zobrazowania problemu.

Problem:
Na jeden ze stron jest wyszukiwarka- formularz, w którym po wpisaniu danych w inputach i selectach przesyła te dane jako parametry “named”, np.:
example.com/controller/action/field1:value1/field2:value2
Potrzebny jest kawałek kodu javascript, który wygeneruje odpowiedni link i wywoła ten adres

Rozwiązanie 1:

function submitform()
{
    form = document.getElementById('car_form');
    name = form.elements["CarName"].value;
    registration_number = form.elements["CarRegistrationNumber"].value;
    entry = document.getElementById('CarEntry').value
    driver = document.getElementById('CarDriver').value
    linkForm = 'url('/cars/find/')?>';
    location = linkForm+'name:'+name+'/registration_number:'+registration_number+'/entry:'+entry+'/driver:'+driver;
    document.forms["car_form"].action = location;
}

Jest ono poprawne. Warto się zastanowić, czy został w tym przypadku zaciągnięty dług techniczny? Na pewno będą tacy, którzy od razu będą wiedzieć co z tym kodem jest nie tak. Inni będą to czuli w kościach. Resztę pocieszę mówiąc, że ta umiejętność przychodzi z czasem pod warunkiem, że chcesz się uczyć jak pisać dobry kod.

Ten formularz wisi sobie gdzieś w jakimś panelu admina w listingu samochodów. Jednak taka wyszukiwarka jest potrzebna też przy listingu kierowców. Oczywiście kopiujesz rozwiązanie i poprawiasz je do nowych warunków. Nie ma w tym nic złego pod warunkiem, że robisz to z odpowiednim nastawieniem: “kopiuję kod, żeby móc zauważyć części wspólne i przeprowadzić refactoring” (czasem łatwiej mieć dwa dublujące się elementy i na ich podstawie tworzyć uniwersalny element/funkcje niż wymyślać ją od zera).

No i zabierając się za refactoring – w tym wypadku chciałbym zbudować wspólny element (dla tych co raczej siedzą w RoR – partial), który wywołam sobie tam, gdzie potrzebuję – zaczynam widzieć, że teraz oto przyjdzie mi spłacić ów mistyczny dług techniczny. Na czym on polega? Otóż w moim przykładzie funkcja budująca url jest strasznie “sztywna”. Działa w tym i tylko w tym przypadku. Nie ma najmniejszych znamion uniwersalności. Dlatego teraz, zanim zacznę myśleć o elemencie, muszę tą funkcję przebudować na bardziej uniwersalną. Co to znaczy?

Powinna być w stanie złapać wszystkie inputy i selecty, które są istotne przy budowaniu url’a i iterując po nich sprytnie go zbudować. Mogła by wyglądać na przykład tak:

Rowziązanie 2:

function submitform(){
    var params = "";

    $("#car_form input[type!=submit][name!=_method], #car_form select").each(
    	    function(index,element) {
        	    params +=
        	    		  element.name.substr(
        	    				  element.name.lastIndexOf("[")
        	    		  ).slice(1, -1)+
        	    		  ":"+
        	    		  element.value+
        	    		  "/";
    	    }
    );
    window.location.href = 'url('/cars/find/')?>' + params;
    return false;
}

Teraz inputy i selecty są zbierane bardziej automatycznie (“#car_form input[type!=submit][name!=_method], #car_form select”), a nazwy parametrów są brane z parametru name tych inputów i selectów. Dzięki temu powinna działać dla każdego formularza. Mniejsza o szczegóły.

Jednak rozwiązanie 1 nie było złe. Było wręcz idealne (bo proste i zrozumiałe) tak długo, jak było potrzebne w jednym miejscu. Rozwiązanie 2 mogło zająć przynajmniej 4 razy tyle czasu co pierwsze. Dlatego dopóki nie było powtórzeń kodu – było dobre. Dług techniczny nie istniał. Jednak pojawił się w momencie, kiedy zacząłem refaktoring kodu.

Jak to możliwe, że dług techniczny pojawił się w momencie, kiedy zacząłem go “spłacać”?
Czy istniał cały czas i był ukryty, a ja go nagle odkryłem?

Odpowiem na to pytanie nie wprost: Dług techniczny jest tylko pojęciem umożliwiającym zrozumienie ludziom, kŧórzy nie są programistami, dlaczego nie wolno im zachęcać programistów do chodzenia na skróty. Uzmysławia też (mam nadzieję) niedoświadczonym programistom jakie są niebezpieczeństwa związane z chodzeniem na skróty.

Leniwy Dobry, doświadczony programista w ogóle nie musi zagłębiać się w szczegóły czy dług techniczny istnieje, czy nie. Po prostu tworzy dobry kod.

Share Button

Code snippet: ekstrakcja nazwy pola z inputów cakePHP przy pomocy jQuery

Natknąłem się na następujący problem:

Mam formularz wyszukiwania, który wygląda różnie w różnych widokach, jednak na jego podstawie jest budowany url tak samo dla każdego widoku. Na przykład:

// some view
echo $form->create("Model");
echo $form->input("field1");
echo $form->input("field2");
echo $form->input("field3");
echo $form->end("submit");

W zdarzeniu onsubmit chcę wygenerować taki url:
“/field1:value1/field2:value2/field3:value3”
Gdzie valueX to oczywiście wartość danego pola.

Pobranie interesujących nas pól:

$("form input[type!=submit][name!=_method], form select");

Tak można wytargać nazwę pola z “name” inputów (zakładając, że w zmiennej string mamy cały name):

// string.substr(
//   string.indexOf("[", string.indexOf("]"))
// ).slice(1, -1);
// nowa, lepsza, prostsza wersja:
string.substr(
   string.lastIndexOf("[")
).slice(1, -1)


Całość tworzenia linków:

var params = "";

$("form input[type!=submit][name!=_method], form select").each(
   function(index,element) {
      params +=
         element.name.substr(
            element.name.lastIndexOf("[")
         ).slice(1, -1)+
         ":"+
         element.value+
         "/";
   }
);

Przydaje się, jeśli pracujesz z parametrami “named”

Share Button