This post was originally published on Coding Glamour.

Wanneer het op 'normale' backendcode aankomt is refactoring en het opnieuw schrijven van delen code welhaast een routineklus geworden. JavaScript is daarin wat lastiger; scoping is minder transparant, dus je breekt eenvoudig bestaande code die wellicht niets te maken heeft met jouw deel; en de flexibiliteit kan tegen je gaan werken. Vandaar een artikel over de wondere wereld van het herschrijven van de JavaScript voor de footer op funda!
http://www.100procentjan.nl/tweakers/viafunda.png Eisen

  • Zoeken in het aanbod op funda, funda in business en funda landelijk
  • Afhankelijk van de categorie staat er een textbox, een lijst met provincies, of een lijst met regio's
  • Backend code herschrijven we niet
Huidige situatie
De huidige code voor de footer is als volgt. Key element hierin is de 'SoortAanbod' enum die bij ons in code staat:

public enum SoortAanbod
{
    Koop: 10,
    Huur: 11,
    // etc.
}

De dropdownlist met categorie-selectie is een directe mapping van deze enum:

<select class="type-via-funda" id="dropSubType">
    <optgroup label="Woningaanbod">
        <option value="10" selected="selected">Koopwoningen</option>
        <option value="11">Huurwoningen</option>
        <option value="12">Nieuwbouwprojecten</option>
    </optgroup>
</select>

In de JavaScript wordt er weer met deze id's gewerkt voor het tonen van de verschillende input-mogelijkheden (vanaf regel 86):

var cs = $('#dropSubType').val();
switch (cs) {
    case '10':
    case '11':
        $('#ppcplaats').show();
        // toon <input type=text/>
}

De input-elementen staan altijd op de pagina, en worden met 'display: none' onzichtbaar gemaakt als ze niet mogelijk zijn qua selectie.

<p>
    <!-- textbox -->
    <input type="text" title="Plaatsnaam -of- postcode" placeholder="Plaatsnaam -of- postcode" value="" class="input-via-funda" id="ppcplaats" autocomplete="off" style="display: block; ">
    <!-- landen voor europees aanbod -->
    <select class="select-via-funda" id="land" name="land" style="display: none; "></select>
    <!-- regio's voor horeca -->
    <select class="select-via-funda" id="horegio" name="horegio" style="display: none; "></select>
    <!-- provincies -->
    <select class="select-via-funda" id="provincie" name="provincie" style="display: none; "></select>
    <!-- regio's voor landelijk aanbod -->
    <select class="select-via-funda" id="landelijk" name="landelijk" style="display: none; "></select>
</p>


Huidige situatie op de server
Ja, dit is bad-practice; maar dit artikel gaat over het herschrijven van JavaScript, we houden de werking dus intact.
Bij elke aanpassing in tekst, dropdown-waarde, etc. wordt een hidden field bijgewerkt:

<input type="hidden" value="0" class="viaSearchKey" name="viaSearch" id="viaSearch" />

In dit hidden element moet de waarde als volgt worden opgemaakt:

SoortAanbod | Waarde
bv. 10|Amsterdam  voor koopaanbod in Amsterdam
of    23|Utrecht  voor horeca in Utrecht


Herbouwen
We gebruiken jQuery als onze core-library voor al onze JavaScript. Veel van onze componenten ontwikkelen we daarom ook als jQuery plugin: door de best-practices hierin te gebruiken worden veel problemen m.b.t. scoping opgelost.

De basis van de plugin:

// functie die direct wordt aangeroepen
(function ($) {
    // $ === jQuery op dit moment; snellere syntax
    
    // we gaan jQuery.protoype uitbreiden. Deze is gemapt op $.fn
    $.fn.extend({
        // nieuwe plugin heet 'viaFunda'
        // 'opts' argument voor het meegeven van options
        viaFunda: function (opts){
            // this is een jQuery object hier
            // we gaan voor elk element in de set de plugin uitvoeren
            return this.each(function () {                            
                // magic!
            });
        }
    });
})(jQuery);


Allereerst hebben we een lijst met alle controls nodig, door deze strong te mappen is de kans op fouten kleiner:

return this.each(function () {        
    // in options zetten we de 'standaard' waardes
    var options = {
        controls: {
            type: $('#dropSubType'),
            vrijeInvoer: $('#ppcplaats'),
            landen: $('#land'),
            horecaRegios: $('#horegio'),
            provincies: $('#provincie'),
            landelijkRegios: $('#landelijk'),
            magicElement: $('#viaSearch'),
            form: $(this).closest('form')
        }
    };
    
    // options kunnen overschreven worden via 'opts' argument
    // dus gaan we ze samenvoegen, waar 'opts' voorrang heeft
    $.extend(true, options, opts);
    // options bevat nu ook de waardes uit 'opts'
}


Ook willen we af van de gehardcode waardes van de enum in code, dus maken we de enum na. JavaScript kent geen 'enum' velden, maar eigenlijk zijn ze sowieso zwaar overrated:

var typeEnum = {
    wonen: {
        koop: 10,
        huur: 11,
        nieuwbouw: 12,
        recreatie: 13,
        europe: 14
    },
    business: {
        kantoor: 20,
        bedrijfshal: 21,
        winkel: 22,
        horeca: 23,
        bouwgrond: 24,
        overige: 25
    },
    landelijk: {
        woningen: 111,
        agrarischeBedrijven: 205,
        losseGrond: 102
    }
};


Events afvangen
Het belangrijkste event dat we willen afvangen, is het moment dat iemand de waarde in de dropdown verandert.

// events die we willen afhandelen
var initEvents = function () {
    // bind de functie aan het 'change' event
    // roep deze meteen aan om de initial weergave goed te zetten
    options.controls.type.change(typeDropdownChanged).change();
};
// event - dropdownlist is veranderd
var typeDropdownChanged = function (ev) {
    // pak de waarde uit de type-dropdownlist
    var selectedType = Number(options.controls.type.val());
    
    // het nieuwe control dat we gaan tonen
    var ctrl;
    
    // hee, dat lijkt verdomd veel op C#
    switch(selectedType) {
        case typeEnum.wonen.europe:
            ctrl = options.controls.landen;
            break;
            
        case typeEnum.business.overige:
        case typeEnum.business.bouwgrond:
            ctrl = options.controls.provincies;
            break;
        
        case typeEnum.business.horeca:
            ctrl = options.controls.horecaRegios;
            break;
            
        case typeEnum.landelijk.agrarischeBedrijven:
        case typeEnum.landelijk.woningen:
        case typeEnum.landelijk.losseGrond:
            ctrl = options.controls.landelijkRegios;
            break;
            
        default:
            ctrl = options.controls.vrijeInvoer;
            break;
    }
    
    // hier gaan we echt switchen
    switchType(ctrl);
};


Goede control tonen:

// ctrl is het control dat getoond moet worden
var switchType = function (ctrl) {
    // hide alle controls
    options.controls.vrijeInvoer.hide();
    options.controls.landen.hide();
    options.controls.horecaRegios.hide();
    options.controls.provincies.hide();
    options.controls.landelijkRegios.hide();
    
    // en toon degene die echt zichtbaar moet worden
    ctrl.show();
};


Hidden element setten
Nu wordt weliswaar het juiste element zichtbaar, maar de waarde is nog niet zichtbaar. Daarvoor kunnen we inhaken op het 'submit' event van ons formulier:

var initEvents = function () {
    options.controls.type.change(typeDropdownChanged).change();
    
    // code die uitgevoerd wordt 'on submit'
    options.controls.form.submit(submitForm);
};
// event - form wordt gesubmit
var submitForm = function (ev) {
    // hier gaan we magicElement setten
    // key begint met de waarde van de 'type' dropdown
    var key = options.controls.type.val();
    key += '|'; // pipe toevoegen
    
    // zoek op welk input element visible is
    var visibleElement;
    // itereer over alle selectie-inputs en kijk welke zichtbaar is
    $([ options.controls.vrijeInvoer, options.controls.landen, options.controls.horecaRegios, options.controls.provincies, options.controls.landelijkRegios ]).each(function (ix, ele) {
        if(ele.is(':visible')) {
            visibleElement = ele;
        }
    });
    // voeg de waarde toe aan de key
    key += visibleElement.val();
    
    // en doe some magic
    options.controls.magicElement.val(key);
};


Et voila
Werking is hetzelfde gebleven, maar de code gebruikt nu geen global vars, magic numbers, etc. De logica voor de autosuggest is uit deze plugin gehaald, want die heeft hier niets mee te maken. Wel hadden we nog een event nodig waaraan gebruikers van onze plugin konden subscriben, om zo dingen te veranderen aan de autosuggest-plugin. Je gebruikt de code ongeveer als volgt:

$(document).ready(function () {
    // roep de plugin aan
    $('#ftr-extra').viaFunda({
        // event dat gevuurd wordt
        events: {
            // als het type verandert
            onTypeChanged: function (aanbod) {
                // geef dit door aan de zoekbox
                $('#ppcplaats').zoekboxfrontend('soortaanbod', aanbod);
            }
        }
    });
    // roep de autocomplete plugin aan
    $('#ppcplaats').zoekboxfrontend({
        /* instellingen */
    });
});


Hele plugin
De hele plugin, inclusief het 'onTypeChanged' event, is hier te vinden.