Skip to content

Commit

Permalink
feat(location-select): location-select component
Browse files Browse the repository at this point in the history
This commit adds the bh-location-select component.  This component is
used to select a village uuid by selecting a country, province, sector,
and finally village as needed.  It's useful in all pages that require
location IDs to be specified.

For extensive documentation, see the component code.

Closes #36.
  • Loading branch information
jniles committed Feb 16, 2016
1 parent e95d069 commit e5598b9
Show file tree
Hide file tree
Showing 10 changed files with 395 additions and 369 deletions.
32 changes: 18 additions & 14 deletions client/src/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1722,12 +1722,10 @@
},
"LOCATION": {
"ADD" : "Add",
"COUNTRY" : "Country",
"CURRENT" : "Current",
"DESCRIPTION" : "Location Manager Configuration is used to create location and to store it in the system, locations are very importants when you register a patient and so one. Use this module to",
"ET" : "and",
"INFO_CREATE" : "create",
"INFO_MODIFY" : "modify",
"INFO_CREATE" : "Create",
"INFO_MODIFY" : "Modify",
"LOCALITY" : "Locality",
"LOCATION" : "Location",
"MANAGER_CONFIGURATION" : "Location Manager Configuration",
Expand All @@ -1737,15 +1735,16 @@
"MANAGE_VILLAGE" : "Manage Village",
"NEW_FORM" : "New Form",
"ORIGIN" : "Origin",
"PROVINCE" : "Province",
"REGISTER" : "Register a Location",
"REGISTER" : "Register a new Location",
"REGISTERED" : "Registered Locations",
"REMOVE" : "remove",
"SECTOR" : "Sector",
"REMOVE" : "Remove",
"TITLE" : "Location Manager",
"VILLAGE" : "Village",
"VILLAGE_NAME" : "Village Name",
"VILLE" : "Township/Commune"
"VILLE" : "Township/Commune",
"COUNTRY" : "Country",
"PROVINCE" : "Province",
"SECTOR" : "Sector",
"VILLAGE" : "Village"
},
"NO_EXCHANGE": {
"CURRENT_DATE" : "Current Date",
Expand Down Expand Up @@ -2584,9 +2583,9 @@
"TITLE" : "Sector Manager"
},
"SELECT": {
"ACCOUNT" : "Select Account",
"ACCOUNT_REFERENTIIAL" : "Select the accounts referential",
"ACCOUNT_TYPE" : "Select Account Type",
"ACCOUNT" : "Select an Account",
"ACCOUNT_REFERENTIIAL" : "Select the Referential Account",
"ACCOUNT_TYPE" : "Select an Account Type",
"ALL" : "All",
"BALANCE_SECTION" : "Select Balance Sheet Section",
"CASH" : "Select a Cash",
Expand Down Expand Up @@ -2650,7 +2649,12 @@
"VALUE" : "Select a value",
"VILLAGE" : "Select a Village",
"WHOLE_YEAR_BUDGET" : "Select the annual budget",
"ZS" : "Select an health zone"
"ZS" : "Select an health zone",
"VILLAGE" : "Select a Village",
"SECTOR" : "Select a Sector",
"PROVINCE" : "Select a Province",
"COUNTRY" : "Select a Country",
"EMPTY" : "No matching records!"
},
"SERVICE": {
"COST_CENTER_SELECT" : "Cost center",
Expand Down
240 changes: 240 additions & 0 deletions client/src/js/directives/bhLocationSelect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
angular.module('bhima.directives')

/**
* Location Selection Component - bhLocationSelect
*/
.component('bhLocationSelect', {
templateUrl : 'partials/templates/bhLocationSelect.tmpl.html',
controller : LocationSelectController,
bindings: {
locationUuid: '=', // two-way binding
disable: '<', // one-way binding
validationTrigger: '<', // one-way binding
}
});

LocationSelectController.$inject = [ 'LocationService', '$scope' ];

/**
* Location Select Controller
*
* This component allows easy selection and validation of locations to be used
* throughout bhima.
*
* COMPONENT LIFECYCLE
*
* 1. On startup, all countries are downloaded and bound the view. If a
* location-uuid was provided, the location is immediately downloaded and
* selected in the view.
*
* 2. As the user changes each <select>, the dependent <select> will fire off
* an HTTP request to load fresh data from the server. It will also clear the
* previous selections from dependent selects.
*
* 3. When the user finally selects a village, the location-uuid is updated
* with the village's uuid.
*
* BINDINGS
*
* 1. [location-uuid] : A two-way bound location uuid. The parent controller
* should expect this ID to contain the selected location.
* 2. [disable] : A hook to allow an external controller to disable the entire
* component.
* 3. [validation-trigger] : A hook to trigger validation on the component.
* Will usually be ParentForm.$submitted.
*
* @constructor
* @example
* <bh-location-select
* location-uuid="ctrl.locationId"
* validation-trigger="ParentForm.$submitted">
* </bh-location-select>
*
*/
function LocationSelectController(Locations, $scope) {
var vm = this;

/** loading indicator */
vm.loading = false;

/** methods */
vm.loadVillages = loadVillages;
vm.loadSectors = loadSectors;
vm.loadProvinces = loadProvinces;
vm.updateLocationUuid = updateLocationUuid;

/** disabled bindings for individual <select>s */
vm.disabled = {
village: true,
sector: true,
province: true
};

/**
* <select> component messages to be translated
* if there is no data, indicate that to the user.
*/
var selectCountry = 'SELECT.COUNTRY';
var selectProvince = 'SELECT.PROVINCE';
var selectSector = 'SELECT.SECTOR';
var selectVillage = 'SELECT.VILLAGE';
var noData = 'SELECT.EMPTY';
vm.messages = {
country: selectCountry,
province: selectVillage,
sector: selectSector,
village: selectVillage,
};

// load the countries once, at startup
Locations.countries()
.then(function (countries) {
vm.countries = countries;

// if there are countries to select, show a "select a country" message
// however, if there isn't any data, show a "no data" message. This pattern
// is used throughout the component.
vm.messages.country = (countries.length > 0) ?
selectCountry :
noData;
});

/** load the provinces, based on the country selected */
function loadProvinces() {

// don't send an HTTP request if there is no country
if (!vm.country || !vm.country.uuid) { return; }

// allow the <select> to be selected
vm.disabled.province = false;

// load the provinces to bind to the view
Locations.provinces({ country : vm.country.uuid })
.then(function (provinces) {
vm.provinces = provinces;

// show the appropriate message to the user
vm.messages.province = (provinces.length > 0) ?
selectProvince :
noData;

// clear the dependent <select> elements
vm.sectors = [];
vm.villages = [];
});
}

/** load the sectors, based on the province selected */
function loadSectors() {

// don't send an HTTP request if there is no province
if (!vm.province || !vm.province.uuid) { return; }

// allow the <select> to be selected
vm.disabled.sector = false;

// fetch the sectors from the server
Locations.sectors({ province : vm.province.uuid })
.then(function (sectors) {
vm.sectors = sectors;

// show the appropriate message to the user
vm.messages.sector = (sectors.length > 0) ?
selectSector :
noData;

// clear the selected village
vm.villages = [];
});
}

/** load the villages, based on the sector selected */
function loadVillages() {

// don't send an HTTP request if there is no sector
if (!vm.sector || !vm.sector.uuid) { return; }

// allow the <select> to be selected
vm.disabled.village = false;

// fetch the villages from the server
Locations.villages({ sector : vm.sector.uuid })
.then(function (villages) {
vm.villages = villages;

// show the appropriate message to the user
vm.messages.village = (villages.length > 0) ?
selectVillage :
noData;
});
}

/** updates the exposed location uuid for the client to use */
function updateLocationUuid() {
vm.locationUuid = vm.village.uuid;
console.log('Location updated to:', vm.locationUuid);
}

/**
* If a location has been provided or changes, reload the datasource with the
* provided location uuid.
* @method loadLocation
* @private
*/
function loadLocation() {

// make sure we actually have an initial location (prevents needless firing
// during $scope churn).
if (!vm.locationUuid) { return; }

// if the location is already selected, do not reload all datasoures. This
// condition will occur when we manually called updateLocationUuid() from
// the village <select> element.
if (vm.village && vm.locationUuid === vm.village.uuid) { return; }

// download the location to the view via the LocationService
Locations.location(vm.locationUuid)
.then(function (initial) {

// bind initial data to each <select> elementin the view
vm.village = {
uuid : initial.villageUuid,
village : initial.village,
};

vm.sector = {
uuid : initial.sectorUuid,
sector : initial.sector,
};

vm.province = {
uuid : initial.provinceUuid,
province : initial.province,
};

vm.country = {
uuid : initial.countryUuid,
country : initial.country,
};

// refresh all data sources to allow a user to use the <select> elements.
loadVillages();
loadSectors();
loadProvinces();
});
}

/**
* Hook up a listener to the locationUuid to reload it if it is changed
* externally.
*
* Note - this will also fire when updated internally, however loadLocation()
* should detect it and prevent unnecessary HTTP requests.
*
* In general, $scope.$watch is more inefficient than exposing an API to the
* parent controller. However, this component favors minimal controller code
* over application efficiency. This could be optimized as the application
* evolves.
*/
$scope.$watch('vm.locationUuid', loadLocation);
}
Loading

0 comments on commit e5598b9

Please sign in to comment.