Keep selected option when new options array does not contain the value - angularjs

I have a select with ngOptions based on an array. This array can change.
If the new array value does not contain the selected option value, the option value is set to undefined by the selectController. Is there a way to prevent this ?
Plunker : https://plnkr.co/edit/kao3h5ivHXlP1Wrdx1Ib?p=preview
Scenario:
select Blue/Red or Green color
click on Reduced to only have Black and White options
See that the model value is left to blank
Wanted behavior : that the model value stays at Blue/Red or Green
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<script src="//code.angularjs.org/snapshot/angular.min.js"></script>
</head>
<body ng-app="selectExample">
<script>
angular.module('selectExample', [])
.controller('ExampleController', ['$scope', function($scope) {
$scope.colorsFull = [
{id:"bk", name:'black'},
{id:"w", name:'white'},
{id:"r", name:'red'},
{id:"be", name:'blue'},
{id:"y", name:'yellow'}
];
$scope.colors = $scope.colorsFull;
$scope.selectedColor =$scope.colorsFull[0];
$scope.colorsReduced = [
{id:"bk", name:'black2'},
{id:"w", name:'white2'}];
}]);
</script>
<div ng-controller="ExampleController">
<button ng-click="colors=colorsReduced">Reduced</button>
<button ng-click="colors=colorsFull">Full</button>
<br/>
Colors : {{colors}}
<hr/>
<select ng-model="selectedColor" ng-options="color.name for color in colors track by color.id">
</select>
selectedColor:{{selectedColor}}
</div>
</body>
</html>

You can achieve this by keeping track of what color is selected in the full colors dropdown, and inserting it into the reduced colors array. First, add an ng-change directive so that we can keep track of the selected color:
<select ng-model="selectedColor" ng-options="color.name for color in colors track by color.id" ng-change="setColor(selectedColor)">
And in your controller:
$scope.setColor = function(color) {
if(color !== null) {
// Keep track of the color that is selected
$scope.previousColor = color;
}
else {
// Changed arrays, keep selected color in model
$scope.selectedColor = $scope.previousColor;
}
}
Now ng-model is set to the correct color whenever the arrays are changed, but it will appear blank in the reduced colors dropdown because the option doesn't exist. So, we need to insert that option into the array. However, switching back and forth between dropdowns will cause the reduced colors array to keep on adding more options, and we only want to remember the option we selected from the full colors array. So, we need create an initial set of colors to revert back to when switching.
// Keep a copy of the original set of reduced colors
$scope.colorsReducedInitial = [
{id:"bk", name:'black2'},
{id:"w", name:'white2'}];
Finally, we need to insert the selected option into the reduced colors array. Change the ng-click on the Reduced button to use a function:
<button ng-click="setColorsReduced()">Reduced</button>
Now, we can insert the option, after resetting the reduced colors array to its initial state:
$scope.setColorsReduced = function() {
// Revert back to the initial set of reduced colors
$scope.colors = angular.copy($scope.colorsReducedInitial);
if($scope.previousColor !== undefined) {
var found = false;
angular.forEach($scope.colorsReducedInitial, function(value, key) {
if(value.id == $scope.previousColor.id) {
found = true;
}
});
// If the id is found, no need to push the previousColor
if(!found) {
$scope.colors.push($scope.previousColor);
}
}
}
Note that we are looping through the reduced colors array to ensure we aren't duplicating any colors, such as black or white.
Now, the reduced colors ng-model has the previous dropdown's selected color.
Updated Plunkr Demo

Use below code in script
$scope.makeSelected=function(){
$scope.selectedColor =$scope.colorsReduced[0];
}
And just add this function call in reduced button line like below
<button ng-click="colors=colorsReduced;makeSelected()">Reduced</button>
This will do what you want to achieve.

Using Jukebox's answer, I ended-up writing a directive, using the modelCtrl.$formatters to get the initial value. It also offer the possibility to store the previousValue in the scope or in a local variable :
Usage: <select .... select-keep> or <select .... select-keep="previousColor">
Directive:
.directive('selectKeep', function($parse) {
return {
require: 'ngModel',
link: function (scope, element, attrs, modelCtrl) {
var previousValueGetter;
var previousValueSetter;
if (attrs.selectKeep) { //use a scope attribute to store the previousValue
previousValueGetter = $parse(attrs.selectKeep);
previousValueSetter = previousValueGetter.assign;
}
else { //use a local variable to store the previousValue
var previousValue;
previousValueGetter = function(s) { return previousValue;};
previousValueSetter = function(s, v) { previousValue = v;};
}
//store the initial value
modelCtrl.$formatters.push(function(v) {
previousValueSetter(scope, v);
return v;
});
//get notified of model changes (copied from Jukebox's answer)
modelCtrl.$viewChangeListeners.push(function() {
if (modelCtrl.$modelValue !== null) {
previousValueSetter(scope, modelCtrl.$modelValue);
} else {
modelCtrl.$setViewValue(previousValueGetter(scope));
}
});
}
};
Plunker
Edit : it has a flaw, the form gets dirty even if the value does not change. I had to add these lines in the else of the viewChangeListener but it doesn't look nice. Any ideas ?:
...
} else {
modelCtrl.$setViewValue(previousValueGetter(scope));
//set pristine since this change is not a real change
modelCtrl.$setPristine(true);
//check if any other modelCtrl is dirty. If not, we will have to put the form as pristine too
var oneDirty =_.findKey(modelCtrl.$$parentForm, function(otherModelCtrl) {
return otherModelCtrl && otherModelCtrl.hasOwnProperty('$modelValue') && otherModelCtrl !== modelCtrl && otherModelCtrl.$dirty;
});
if (!oneDirty) {
modelCtrl.$$parentForm.$setPristine(true);
}
}

Related

Remove empty option in the select using ng-model

I am new to angular js. In my code user changes the value of radio buttons. And depending on the value of the selected radio button, a piece of code is loaded from the ng-switch
HTML:
<body ng-app="">
<div ng-repeat="button in modes">
<label>
<input type="radio" ng-model="data.mode" value="{{button.value}}" ng-click="clearObjectIdModal()" name="e_modes">
{button.label}}
</label>
</div>
<div ng-switch on="data.mode">
<div ng-switch-when="client">
<label for="e_selected_object_item_id">Select Client name: </label>
<select id="e_selected_object_item_id" name="e_selected_object_item_id" ng-model="currentDataItem.object_id" required>
<option ng-repeat="item in customersListArr" value="{{ item.id }}">{{ item.Name }}</option>
</select>
</div>
<div ng-switch-when="agent">
// This part is similar to the previous one
</div>
</div>
</body>
Controller part:
$scope.data = {};
$scope.setFile = function () {
if ($scope.data.mode == 'client')
return 'client';
else if ($scope.data.mode == 'agent')
return 'agent';
$scope.modes = [{
value: 'client',
label: 'Client'
},{
value: 'agent',
label: 'Agent'
}];
$scope.currentDataItem = data; // data is preloaded from inputs in form
There is also a ng-click="clearObjectIdModal()" that clears the model when switching radio buttons:
$scope.clearObjectIdModal = function() {
$scope.currentDataItem = "";
}
The problem is that every time when the radio button is switched to the select value, which dynamically changes, the value of the first option in it becomes equal to undefined. Because in the array from where these options are built there is no such object_id (This is the id that is not there, so an empty field is drawn).
That is, there are all works. But the first option in the select(after switching to another radio button) is rendered as an empty string.
There are thoughts, how it can be fixed?
I'm not sure if I understand you problem correctly but I would suggest a few improvements.
change your setFile function to as follows
$scope.setFile = function (){return $scope.data.mode;}
I also do not see the closing brackets for your function in your code. Besides if your function will only return the data.mode then why need the function?
I would suggest initialize your data object properly like:
$scope.data = {mode:'client'};
Change your clearObjectIdModal function as:
$scope.clearObjectIdModal = function(mode)
{
$scope.currentDataItem = "";
$scope.data.mode=mode;
}
and in your HTML use it as ng-click="clearObjectIdModal(button.mode)"
So in function clearObjectIdModal() I wrote:
$scope.clearObjectIdModal = function() {
if ($scope.e_data["mode"] == 'client') {
if ($scope.customersListArr.length > 0) {
$scope.currentDataItem.object_id = $scope.customersListArr[0]['id'];
}
}
else if ($scope.e_data["mode"] == 'agent') {
if ($scope.agentsListArr.length > 0) {
$scope.currentDataItem.object_id = $scope.agentsListArr[0]['id'];
}
}
}
And after this when I change radio buttons the first option in current select(which every time is changed) will be not empty.
Also the problem with an additional empty option is possible to solve when you add a title as the first item in the list:
<option value="" disabled>Select</option>

Checkbox with ng-model that reflect if another element is showed

I have an element checkbox, that I want to reset the value of ng-model when another element is not showed, example: set to false if another element is not showed in my view. ng-show only hide my checkbox, but not reflect in object of controller.
<select ng-model="item.myOption" convert-to-boolean>
<option value="false" selected>Option false</option>
<option value="true">Option true</option>
</select>
<input type="checkbox" ng-model="item.myChecked" ng-show="item.myOption">
You can write small function on ng-change of select field, in which you can set model value of checkbox to either true or false or null.
app.controller('myCtrl', function($scope) {
$scope.item = {myOption: "true"}
$scope.myCheckbox = true;
$scope.changed = function() {
if($scope.item.myOption == "false") {
$scope.myCheckbox = false;
} else {
$scope.myCheckbox = true;
}
}
});
http://plnkr.co/edit/ekNxZImNIBisBCrfZsFS?p=preview
Similarly, you can change any dependent field's value (like finding age field value right after user selects his DOB using datepicker, etc). You can use switchcase either for large set of values instead of if else inside change function

add new select element whenever previous one is clicked in angularjs

I want to add new select list whenever the exisisting and which is last one is clicked.
I want to have a select list, a text box infront of it and remove option to remove the row.
So far I have added a view for the default select list which will be there when form opens i.e.
<div id="inputMaterialContainer">
<md-select aria-label="selectMaterial" ng-model="inputMaterial" ng-change="appendSelectOption()" md-on-open="loadMaterials()">
<md-option><em>None</em></md-option>
<md-option ng-repeat="material in inputMaterialArray" ng-value="material">{{material}}</md-option>
</md-select>
</div>
I have a controller for the form which is as follows
app.controller('AddPeopleDialogCtrl', AddPeopleDialogCtrl);
function AddPeopleDialogCtrl($scope, $timeout)
{
var $materialSelectHTML = "<md-select ng-change='appendSelectOption' ng-model='inputMaterialArray'><md-option><em>None</em></md-option><md-option ng-repeat='material in inputMaterial' ng-value='material'>{{material}}</md-option></md-select>";
$scope.materialArray.push($materialSelectHTML);
$scope.appendSelectOption = function(){
var materialContainer = angular.element(document.querySelector('#inputMaterialContainer'));
materialContainer.append($materialSelectHTML);
$compile($materialSelectHTML)($scope);
};
$scope.loadMaterials = function(){
return $timeout(function(){
$scope.inputMaterialArray = $scope.inputMaterialArray || ['sensor1','sensor2','sensor3','sensor4','plate1','plate2','material1','material2'];
}, 650);
};
}
But it fails to get the output as I need.
you should not use jquery in your angular application, you can do something like this when you are appending the md select.
Maintain an array of the select elements in your controller(just objects, not actual html elements) and render them using ng-repeat.
Whenever you want to add or delete just add or delete from the array in the controller and angular will do the rest.
In the html change to this
<div id="inputMaterialContainer">
<md-select aria-label="selectMaterial"
ng-model="inputMaterial"
ng-repeat="selectOption in materialSelectArray"
ng-change="appendSelectOption()"
md-on-open="loadMaterials()">
<md-option><em>None</em></md-option>
<md-option ng-repeat="material in inputMaterialArray"
ng-value="material">{{material}}</md-option>
</md-select>
</div>
And in the controller just initialize the array and add new object to it every time you want to append a new select
something like this
app.controller('AddPeopleDialogCtrl', AddPeopleDialogCtrl);
function AddPeopleDialogCtrl($scope, $timeout){
$scope.materialSelectArray = [
{
name:'select0'
}
]
$scope.appendSelectOption = function(){
$scope.materialSelectArray.push({
name:'select'+ $scope.materialSelectArray.length
})
};
$scope.loadMaterials = function(){
return $timeout(function(){
$scope.inputMaterialArray = $scope.inputMaterialArray || ['sensor1','sensor2','sensor3','sensor4','plate1','plate2','material1','material2'];
}, 650);
};
}

How can I synchronize DOM element width in AngularJS?

I would like to synchronize the width of DOM elements such that the width is always the smallest width that can accommodate the content. I can successfully increase the size by $watching for an increase (based on ref1 & ref2), but I can't figure out how to shrink the element after it's been enlarged.
Angular directive:
app.directive('syncWidth', function () { //declaration; identifier master
return {
restrict: 'A',
link: function (scope, element) {
var linkElement = element[0];
scope.$watch(
function () {
return (linkElement.scrollWidth);
},
function (newValue, oldValue) {
if (newValue > oldValue) {
scope.testWidth = {
'min-width': newValue + 'px'
}
}
},
true
);
}
};
});
HTML:
<input type="text" ng-model="myText">
<input type="text" ng-model="myText2">
<div sync-width="test" style="background-color: lightblue" ng-style="testWidth">1 [{{myText}}]</div>
<div sync-width="test" style="background-color: lightcoral" ng-style="testWidth">2 [{{myText2}}]</div>
I'm assuming the issue is that the two elements are referencing another. I'm thinking the pseudocode would look something like:
Set a global groupMinWidth variable
Detect element.contentWidth changed
If newContentWidth > groupMinWidth {
groupMinWidth = newContentWidth;
}
If oldContentWidth == groupMinWidth {
check all synchronized elements and set groupMinWidth to the largest contentWidth;
}
Thanks for the help.
I'm now working on the plunkr but not fully working yet : https://plnkr.co/edit/6XfGMzI1su8cpHjkyDWC?p=preview . The point is $watching the change on the text, when the text gets reduced, use a shared value/global value to record and check which one's width is the widest , set the min-width to that value to both of the elements.
===EDIT===
Finally make this work: https://plnkr.co/edit/c2FOu5LfPfGfnkU12KTG?p=preview
So the principle behind this is : I need a changeable div to measure the scrollWidth/offsetWidth when the content changed. So I add a new nested div which include the content. I made the inner div as a float and once the content changed I will update the outer div's width by setting it's width to the inner div's width. The key point is because the inner div is FLOAT, it can overflows the outer div's right border, but the out div is watching the change on the content, so the outer div's width will be updated immediately.

FabricJS with Angular: Change Font Family, Size, Alignment, etc

I am using FabricJS with an AngularJS application. I am able to add text to a canvas and, using the kitchensink example located here, I can perform functions such as bold, italic, underline, etc.
However, the issue I have is how to change the font family, text align, font size, etc. since when I make a selection from a dropdown for font family, no changes occur... but it works in the Kitchensink example.
I am using the Kitchensink example as I need to not only add text, but edit it once it shows on the Canvas, and this appears to have what I need.
A button (which is working) has an HTML element such as:
<button class="btn btn-object-action" type="button" ng-class="{'btn-inverse': isBold()}" ng-click="toggleBold()">
Bold</button>
Which is backed up by the following in the Controller:
$scope.toggleBold = function () {
setActiveStyle('fontWeight',
getActiveStyle('fontWeight') === 'bold' ? '' : 'bold');
};
As I stated, this works as intended. Where I am having challenges is changing something like the Font Family or Font Size in that the change is achieved without a button click. Here is sample HTML for the Font Family select from the Kitchensink example:
<label style="display: inline-block;" for="font-family">Font family:</label><select class="btn-object-action" id="font-family" bind-value-to="fontFamily">
<option value="arial">Arial</option>
<option value="helvetica" selected="">Helvetica</option>
<option value="myriad pro">Myriad Pro</option>
</select>
This is backed up by this in the controller:
function getActiveProp(name) {
var object = canvas.getActiveObject();
if (!object) return '';
return object[name] || '';
}
function setActiveProp(name, value) {
var object = canvas.getActiveObject();
if (!object) return;
object.set(name, value).setCoords();
canvas.renderAll();
}
$scope.getFontFamily = function () {
return getActiveProp('fontFamily').toLowerCase();
};
$scope.setFontFamily = function (value) {
setActiveProp('fontFamily', value.toLowerCase());
};
function watchCanvas($scope) {
function updateScope() {
$scope.$$phase || $scope.$digest();
canvas.renderAll();
}
canvas
.on('object:selected', updateScope)
.on('group:selected', updateScope)
.on('path:created', updateScope)
.on('selection:cleared', updateScope);
}
$scope.getSelected = function () {
return canvas.getActiveObject();
};
$scope.canvas = canvas;
$scope.getActiveStyle = getActiveStyle;
addAccessors($scope);
watchCanvas($scope);
I am more used to using ng-model than bind-value-to, in fact, I have never seen or used bind-value-to before in Angular apps/not sure how and if I should be using it.
My main question is how do I get the dropdowns working where if I select a value it updates per the Kitchensink example for text here? What I am missing/is there a better way given that my need is to add and edit stylized text.
You need to bind a change event to your select element so that when you change the select value, you execute the function that will change the font family.
bind-value-to is a custom directive in the kitchensink app so don't worry about it.

Resources