A user object is available to the view which populates a form. One of the elements to display information is a drop-down. Two requests are made. One for the user information, and the other for a list of timezones. Both are resolved through a ui-router state like so:
.state('app.userprofile', {
url: '/userprofile',
component: 'user',
resolve: {
user: ['userService', function(userService) {
return userService.fetchUser();
}],
timezones: ['timezoneService', function(timezoneService){
return timezoneService.fetchUsaTimeZones();
}]
}
})
}]);
I have given a read of an article I found online after the select element failed to populate with the users timezone, but the select element still fails to display information.
Question
How do I populate the default select option with data from the user object but populate the options from the second response.
<label for="timezones">Time Zone</label>
<div>
<select name="timezones"
ng-init="userTimezone = $ctrl.user.business.timezone"
ng-change="userTimezone = userTimezone.abbr"
ng-model="userTimezone"
ng-options="item as item.abbr for item in $ctrl.timezones track by item.abbr" class="form-control">
<option value="">{{userTimezone}}</option>
</select>
<p>{{userTimezone}}</p>
</div>
//SECOND REQUEST FOR TIMEZONES
app.factory('timezoneService', ['$http', '$q', function($http, $q){
var factory = {};
factory.fetchUsaTimeZones = function() {
var deferred = $q.defer();
$http.get('../../p3sweb/assets/json/ustimezones.json')
.then(
function(response){
console.log(response.data.ustimezones)
deferred.resolve(response.data.ustimezones)
},
function(errResponse){
deferred.resolve(errResponse)
}
);
return deferred.promise;
}
return factory;
}])
{
"ustimezones": [
{
"value": "Hawaiian Standard Time",
"abbr": "HST",
"offset": -10,
"isdst": false,
"text": "(UTC-10:00) Hawaii",
"utc": [
"Etc/GMT+10",
"Pacific/Honolulu",
"Pacific/Johnston",
"Pacific/Rarotonga",
"Pacific/Tahiti"
]
},
{
"value": "Alaskan Standard Time",
"abbr": "AKDT",
"offset": -8,
"isdst": true,
"text": "(UTC-09:00) Alaska",
"utc": [
"America/Anchorage",
"America/Juneau",
"America/Nome",
"America/Sitka",
"America/Yakutat"
]
}
]
}
UPDATE
It was throwing an error when I had the value of ng-model as $ctrl.user.business.timezone so I have stored the it in a variable userTimezone through the ng-init directive. Updated the code
UPDATE 2
I have it semi-working. It updates all fields though it throws an inconsitent 405 error. Not going to lie, I'm in one of those 'how the hell is this working' situations.
<select name="timezones"
ng-init="userTimezone._abbr = {abbr: $ctrl.user.business.timezone}"
ng-change="$ctrl.user.business.timezone = userTimezone._abbr"
ng-model="userTimezone._abbr"
ng-options="zone.abbr as zone.text for zone in $ctrl.timezones track by zone.abbr" class="form-control">
<option value="">{{userTimezone._abbr}}</option>
</select>
<p>{{userTimezone._abbr}}</p>
You have complex objects as your options. Angular does equality comparison when checking the default value (which is set via the ng-model attribute), so with objects it's comparing object references (via the generated $$hashkey property). Because you have two different object references, once from the timezone list, once from the user, their hashkeys are different. Thus, they're treated as "not equal", so no default gets set.
If you extend your ng-options attribute to use track by, you can select a unique, primitive property where equality comparison makes more sense (such as the abbreviation). Angular will then use this property for equality/uniqueness comparison instead of the hashkey.
So you'd have something like
<select name="timezones" ng-model="$ctrl.user.business.timezone" ng-options="item as item.abbr for item in $ctrl.timezones track by item.abbr"></select>
Related
I'm new to Vue and trying to build a form that calls an API when an input is changed to dynamically build and set other inputs. It's purpose is to select a vehicle.
When you select a make, it populates the model and year select fields with appropriate values from the API via WebSocket. These fields may be filled automatically from the VIN (vehicle identification number) and any field in the process may change other fields.
This is causing a feedback loop where an input is changed by another input which in turn, calls the API which sends back another set of data and changes something else.
How can I ensure that data is only set once after a human changes a field?
My Vehicle selection code:
<template>
<div id="vehicleSelection">
<h1 class="title">{{ title }}</h1>
<b-field label="Make">
<b-select placeholder="Select a make" v-model="make" #input="setMake(make)" id="makeSelect">
<option
v-for="make in make_list"
:value="make"
:key="make">
{{ make }}
</option>
</b-select>
</b-field>
<b-field label="Model">
<b-select placeholder="Select a model" v-model="model" #input="setModel(model)" id="modelSelect">
<option
v-for="model in model_list"
:value="model"
:key="model">
{{ model }}
</option>
</b-select>
</b-field>
<b-field label="Year">
<b-select placeholder="Select a model year" v-model="year" #input="setYear(year)" id="yearSelect">
<option
v-for="year in year_list"
:value="year"
:key="year">
{{ year }}
</option>
</b-select>
</b-field>
</div>
</template>
<script>
import CommandClient from '../communication/command/CommandClient';
import MessageHandler from '../communication/handler/MessageHandler';
export default {
name: 'VehicleSelection',
methods: {
setMake(make) {
CommandClient.send(this.$socket, CommandClient.prepare('set_vehicle_make', ['make', make]));
},
setModel(model) {
CommandClient.send(this.$socket, CommandClient.prepare('set_vehicle_model', ['model', model]));
},
setYear(year) {
CommandClient.send(this.$socket, CommandClient.prepare('set_vehicle_year', ['year', year]));
},
},
created() {
this.$options.sockets.onmessage = (message) => {
MessageHandler.handle(this, message);
};
},
data() {
return {
title: 'Vehicle Selection',
make_list: [],
make: null,
model_list: [],
model: null,
year_list: [],
year: null,
};
},
};
</script>
There is some repetition here which will probably be refactored out but I'd like to just get it working first.
For context, the MessageHandler here takes a Vue component and sets data for it if the key is present in the API response:
const _ = require('lodash');
export default {
/**
* #param {Object} context
* #param {MessageEvent} message
* #return {void}
*/
handle(context, message) {
const messagePayload = JSON.parse(message.data);
Object.entries(messagePayload).forEach((data) => {
if (_.has(context.$data, data[0])) {
context.$set(context, data[0], data[1]);
}
});
},
};
I'm making some assumptions about your code since I don't know the specifics of it...
I would imagine you're getting a feedback loop if you're calling the API in response to any change made to a specific data property via watch; e.g. if you had code like this:
data() {
return {
year: 2010,
};
},
watch: {
year() {
// this.year changed either by user input or by any other means.
// Calling API here...
this.api();
},
},
You're combining v-model and #input in your template; this will work although it's a bit confusing (v-model is just syntax sugar for :value and #input; will two #inputs work? Yes, it turns out, but I digress).
If you only want to call the API in response to user input, then call the API in the #input handlers of the UI elements that you want to trigger the calls:
<b-select :value="make" #input="setMake">
<!-- For a native DOM element, do this instead -->
<select :value="make" #input="setMake($event.target.value)">
methods: {
// This method is *only* called by #input
setMake(make) {
// Set this.make in response to user input
this.make = make;
// Call API here only; this.make can be changed by other means
// but it won't trigger an API call in those cases
this.api();
},
},
when user open modal window i am calling rest service to get boolean flag that is data in below code, Now if that flag is true i want to disable option in ng-options where id is RA_ATTST_LANGUAGE. How can i disabled dropdown option based on below logic ?
main.html
<select name="adminNotificationTypeCode" class="form-control" ng-model="messageNotificationDTO.adminNotificationTypeCode" required id="adminNotificationTypeCode" ng-change="attestationCheck()" ng-options="adminDataSource.id as adminDataSource.text disable when adminDataSource.id == 'RA_ATTST_LANGUAGE' && disableRA_ATTST_LANGUAGE for adminDataSource in adminDataSource">
<option value="">Select...</option>
</select>
main.js
function getAttestationLanValidation (){
MessageAdminNotificationFactory.getAttestationLanValidation().then(function(response){
if(response){
$scope.disableRA_ATTST_LANGUAGE = response.data;
}
});
};
//Add new Notification
$scope.addMessageNotification = function() {
$scope.messageNotificationModal.open().center();
$scope.clearNotificationForm();
getAttestationLanValidation();
};
dropdown.json
[{
"uid": null,
"index": 0,
"selected": null,
"expanded": null,
"id": "RA_PLTFRM_NOTIF",
"text": "Platform Maintenance Notification",
"parentId": null,
"items": null
}, {
"uid": null,
"index": 0,
"selected": null,
"expanded": null,
"id": "RA_ATTST_LANGUAGE",
"text": "Attestation Language",
"parentId": null,
"items": null
}]
*Edited due to further explanation
try updating your ng-options attribute to this:
ng-options="adminDataSource.id as adminDataSource.text disable when adminDataSource.id == 'RA_ATTST_LANGUAGE' && disableRA_ATTST_LANGUAGE for adminDataSource in adminDataSource"
you will want to create a Boolean member to your scope that you set to true or false depending on the rest result.. I called it $scope.disableRA_ATTST_LANGUAGE
Plunker for demonstration
Assuming everything else is correct in your code, and that your service is making a single call per instance in your dropdown.json; If you only need to know if data is present and don't need to get the actual data content, a quick-and-dirty way is to just store the name in an array and check for presence of the id in that array using the ng-disabled directive. Your variable names were confusing, so I just genericized them and called the source array for your select options "items"
Your js needs something like this
// create an empty array when initializing
$scope.thatArrayOfIdsThatHaveData = [];
function getAttestationLanValidation() {
MessageAdminNotificationFactory.getAttestationLanValidation()
.then(function(response) {
var data = response.data;
console.log('attestation', data);
if (data) {
thatArrayOfIdsThatHaveData.push(id);
}
});
};
In your template something like this:
<select ng-model="messageNotificationDTO.adminNotificationTypeCode"
ng-change="attestationCheck()">
<option value="">Select...</option>
<option ng-repeat="item in items"
value="item.id"
ng-disabled="thatArrayOfIdsThatHaveData.indexOf(item.id)>=0">{{ item.label }}</option>
</select>
I would like to dynamically load select elements from an API request.
Here is my controller:
var myApp = angular.module('myApp',[]).controller('tripCtrl', function($scope){
//Call to API to get people
$scope.people = [
{
"id": 1,
"name": "Joe Hamlet"
},
{
"id": 2,
"name": "Mary Jane"
},
{
"id": 3,
"name": "Tom Lee"
}
];
//Call to API to get the element to load
$scope.selectElement =
{
"Options": "person[dynamicValue] as person[dynamicDisplayName] for person in people",
"DisplayName": "name",
"Value": "id"
};
//Dynamicly load properties
$scope.dynamicValue = $scope.selectElement.DisplayName;
$scope.dynamicDisplayName = $scope.selectElement.Value;
});
HTML:
<select ng-model="selectedPerson" ng-options="{{selectElement.Options}}">
<option value="">Select</option>
</select>
{{selectedPerson}}
I created a JSFiddle trying to accomplish this. http://jsfiddle.net/HB7LU/9493/
I found this question which I was able to implement, but when I tried to set the ng-options from the Element's Options property, it failed to load. When inspected the HTML the code looks to be set properly, but the model binding isn't working.
Edit 12/28/2014:
After updating the Angular version in the original JS Fiddle, it worked properly, however when I expanded to use an actually API, I found another issue with loading ng-options dynamically. Here is the more in depth JS Fiddle: http://jsfiddle.net/zjFp4/330/
Also here is my updated controller. The dataService.getElement() calls a hard coded string, where as the dataService.getElementFromApi() calls the same exact string, just from json-generator (which is the mock API). When inspected the objects set from the API, everything is there, so it must be an issue with the binding in Angular. Any ideas on how to fix this?
function tripCtrl($scope, dataService) {
//Call to API to get people
dataService.getPeople().then(
function (event) {
$scope.people = event;
},
function (s) {
console.log(s); }
);
//Call to API to get the element to load
$scope.selectElement = dataService.getElement();
dataService.getElementFromApi().then(
function (event) {
$scope.apiElement = event;
$scope.dynamicValue = $scope.apiElement.Value;
$scope.dynamicDisplayName = $scope.apiElement.DisplayName;
},
function (s) {
console.log(s); }
);
}
I have quite an interesting question (I hope) for all you AngularJS gurus out there. I am looking to create a dynamic list of form input fields based on a SELECT dropdown. As an example, we have a number of categories with each category having a set of specifications which are unique to that category. To help with the explanation we have the following:
Firstly, in the controller we start by initializing the models.
$scope.category = {};
$scope.category.specs = [];
Next we ready the data to be used in the form (actually retrieved from the server via $http). We also initialize a variable to the first element in the categories array.
$scope.categories = [
{ "id": "1", "name": "mobile", specs: [
{ "id": "1", "label": "Operating System" },
{ "id": "2", "label": "Camera type" } ] },
{ "id": "2", "name": "laptop", specs: [
{ "id": "1", "label": "Operating System" },
{ "id": "2", "label": "Graphics Card" } ] }
};
$scope.selectedCategory = $scope.categories[0];
In the form, we have a dropdown which when selected loads the appropriate input fields specific to that category. We use the ngRepeat directive to accomplish this. This is a dynamic list of fields based on $scope.categories.specs. (please note the ???)
<select ng-model="selectedCategory" ng-options="category.name for category in categories"></select>
<div ng-repeat="spec in selectedCategory.specs">
<label>{{spec.label}}</label>
<input type="text" ng-model="???">
</div>
Ultimately, when the user clicks the submit button, we would like to extract the category he/she has selected and then package it together with the specifications they have filled in. The post request should contain something like the following for instance (of course, I only included one spec item, but in reality there would be many):
{ "id": "1", specs [ { "id": "2", "details": "RADEON HD 8970M" } ] }
Unfortunately I am not really sure how to accomplish this. I need to somehow create an array for the spec model, and then ensure that both the ID and user entered data are appropriately extracted... what goes in the ??? and what do we do after? Any help would be much appreciated.
this is how I do it. I make a form, validate it with angular, and then when its valid I submit it with a function.
<form name="signup_form" novalidate ng-submit="signupForm()"></form>
$scope.signupForm = function() {
var data = $scope.signup;
$http({
method : 'POST',
url : 'http://yoursite.com/mail.php',
data : $.param(data), // pass in data as strings
headers : { 'Content-Type': 'application/x-www-form-urlencoded' } // set the headers so angular passing info as form data (not request payload)
})
.success(function(data) {
});
}
also if you want to look at another form validation system for angular check out http://nimbly.github.io/angular-formly/#!/ It may help you solve your current form system.
In the controller, initialize $scope.specDetails as follows:
$scope.specDetails = {};
angular.forEach($scope.categories, function (category, index1) {
$scope.specDetails[category.id] = {};
angular.forEach(category.specs, function (spec, index2) {
$scope.specDetails[category.id][spec.id] = '';
});
});
In the html, replace "???" with specDetails[selectedCategory.id][spec.id]
I have a view model containing 2 arrays. One array is an array of users, and the other is an array of user levels.
{
"Users": [
{
"UserLevel": {
"Permissions": [],
"Id": 2,
"Name": "Developer",
"SortOrder": 1,
"IsHidden": false
},
"Id": 1,
"Username": "Björn Jakobsson",
"Password": null,
"Fullname": null,
"Email": "bjiorn#bjinteractive.se",
"Phone": null
}
],
"UserLevels": ko.observableArray([
{
"Permissions": [],
"Id": 1,
"Name": "Admin",
"SortOrder": 2,
"IsHidden": false
},
{
"Permissions": [],
"Id": 2,
"Name": "Developer",
"SortOrder": 1,
"IsHidden": false
}
])
}
and the drop down
<select data-bind="options: $parent.UserLevels(), optionsText:'Name', value: UserLevel" class="form-control"></select>
While editing a user from the user array i have a dropdown for choosing user level of this user wich is populated from the UserLevels array. If I choose a user level (in this case Developer) and saves the value in my database is saved and everything, and a reload of the page shows the correct value, but as soon as I choose to edit the user (using a bootstrap modal and with-data binding, the drop down automatically selects Admin (first in the array) and not Developer from my user model, and then the user model is updated since the user level of the user is bound to the drop down.
I believe your binding is incorrect. You are not binding to the observable array, but instead to what the observable array resolves to (i.e. the array)
Use
options: $parent.UserLevels
instead of
options: $parent.UserLevels()
You probably need to add an optionsValue binding.
From doc:
Typically you’d only want to use optionsValue as a way of ensuring
that KO can correctly retain selection when you update the set of
available options. For example, if you’re repeatedly getting a list of
“car” objects via Ajax calls and want to ensure that the selected car
is preserved, you might need to set optionsValue to "carId" or
whatever unique identifier each “car” object has, otherwise KO won’t
necessarily know which of the previous “car” objects corresponds to
which of the new ones.
I found a way to get around the problem with my own biding instead for value. Below is the code I use. Any drawbacks or mistace to edit? I'm pretty new to KO and still have problems sometimes when to use unwrap, unwrapobservable etc, but below code works. Ideas for improvement?
ko.bindingHandlers.valueBasedOnObject = {
init: function (element, valueAccessor, allBindings) {
var prop = allBindings().optionsValue;
var value = valueAccessor();
$(element).change(function () {
var value = $(element).val();
var selectedModel = ko.utils.arrayFirst(ko.unwrap(allBindings().options()), function (item) {
return ko.utils.unwrapObservable(item[prop]) == value;
});
valueAccessor()(selectedModel);
});
var valueUnwrapped = ko.unwrap(value);
$(element).val(valueUnwrapped[prop]);
},
update: function (element, valueAccessor, allBindings) {
/* */
}
};