AngularJS - 3-button group acting as radio buttons - javascript

Using the Ionic framework, I'm trying to create a group of three buttons that act as radio buttons:
If I click on Breakfast, I would like Lunch and Dinner to return to their normal (white) state, and Breakfast to turn Blue.
With my current code, I can't get this functionality to work, although I can get the buttons to switch color, slightly randomly (perhaps I just don't understand the ng-class directive).
Here is my HTML code:
<div class="bar bar-subheader">
<div class="button-bar">
<a class="button" ng-class="{'button-positive' : !isActiveB, 'none': isActiveB}" ng-click="active('breakfast')">Breakfast</a>
<a class="button" ng-class="{'button-positive' : !isActiveL, 'none': isActiveL}" ng-click="active('lunch')">Lunch</a>
<a class="button" ng-class="{'button-positive' : !isActiveD, 'none': isActiveD}" ng-click="active('dinner')">Dinner</a>
</div>
</div>
My JS:
$scope.active = function(meal) {
switch (meal) {
case 'breakfast':
$scope.$broadcast('slideBox.setSlide', 0);
$scope.isActiveB = $scope.isActiveB;
$scope.isActiveL = !$scope.isActiveL;
$scope.isActiveD = !$scope.isActiveD;
break;
case 'lunch':
$scope.$broadcast('slideBox.setSlide', 1);
$scope.isActiveB = !$scope.isActiveB;
$scope.isActiveL = $scope.isActiveL;
$scope.isActiveD = !$scope.isActiveD;
break;
case 'dinner':
$scope.$broadcast('slideBox.setSlide', 2);
$scope.isActiveB = !$scope.isActiveB;
$scope.isActiveL = !$scope.isActiveL;
$scope.isActiveD = $scope.isActiveD;
break;
}
};
I can put the code in JSFidle if you require more information and a working solution.
Thanks for your help.
NOTE: I would like to maintain my active() function, and use the ng-class directive if possible, as I have a lot of other code dependent on this function.

Maybe this simplified example will help you a little:
angular.module('plunker', []).controller('MainCtrl', function($scope) {
$scope.active = 'breakfast';
$scope.setActive = function(type) {
$scope.active = type;
};
$scope.isActive = function(type) {
return type === $scope.active;
};
});
<link rel="stylesheet" href="http://code.ionicframework.com/0.9.26/css/ionic.min.css">
<script src="http://code.angularjs.org/1.2.13/angular.js"></script>
<div ng-app="plunker" ng-controller="MainCtrl" class="bar bar-subheader">
<div class="button-bar">
<a class="button" ng-class="{'button-positive': isActive('breakfast')}" ng-click="setActive('breakfast')">Breakfast</a>
<a class="button" ng-class="{'button-positive': isActive('lunch')}" ng-click="setActive('lunch')">Lunch</a>
<a class="button" ng-class="{'button-positive': isActive('dinner')}" ng-click="setActive('dinner')">Dinner</a>
</div>
</div>
Demo: http://plnkr.co/edit/9HmuTStz70x5KoAvLaP4?p=preview

Here is a more flexible solution for future Googlers.
Working plunker:
http://plnkr.co/edit/U2Hvx4?p=preview
.directive('barSelect',function($parse){
return {
restrict: 'A',
require: 'ngModel',
scope: {
model: '=ngModel',
value: '=barSelect'
},
link: function(scope, element, attrs, ngModelCtrl){
element.addClass('button');
element.on('click', function(e){
scope.$apply(function(){
ngModelCtrl.$setViewValue(scope.value);
});
});
scope.$watch('model', function(newVal){
element.removeClass('active');
if (newVal === scope.value){
element.addClass('active');
}
});
}
};
});
And a usage example:
<div class="button-bar">
<a bar-select="button.value"
ng-repeat="button in clientSideList"
ng-model="data.clientSide"
>{{button.text}}</a>
</div>

Here's another alternative approach which combines the other two here. It requires just a single <button-group> element with the following attributes:
ng-model
buttons - array of objects containing 'text' and 'value' properties
button-class - optional string containing CSS class(es) to apply to the rendered links, in addition to the default 'group-btn' and 'group-btn-active' classes
.
.directive('buttonGroup',function($parse){
return {
restrict: 'E',
require: 'ngModel',
scope: {
model: '=ngModel',
buttons: '=',
buttonClass: '='
},
template: '<a class="group-btn {{buttonClass}}" ' +
' ng-repeat="button in buttons" ' +
' ng-class="{\'group-btn-active\': isActive(button.value)}" ' +
' ng-click="buttonClicked(button.value)"> ' +
' {{button.text}} ' +
'</a>',
controller: ['$scope', function($scope) {
$scope.buttonClicked = function(value) {
$scope.value = value;
};
$scope.isActive = function(value) {
return $scope.value === value;
};
}],
link: function(scope, element, attrs, ngModel) {
element.on('click', function(e){
scope.$apply(function(){
ngModel.$setViewValue(scope.value);
});
});
scope.$watch('model', function(newVal){
scope.value = newVal;
});
}
};
})
And the example usage:
<button-group ng-model="sortOrder" buttons="sortOptions"
button-class="'md-button my-other-class'"></button-group>
Where sortOptions would be an array of the form:
$scope.sortOptions = [
{ value: 'priority', text: 'Priority' },
{ value: 'duration', text: 'Call Duration' }
];

Related

Problem creating custom validator for an angularjs directive

So I have a custom directive which works fine as it is. This directive is being used at multiple places. This is an element directive.
This element directive has certain attributes. I have added a custom attribute for only 1 instance of this directive i.e. only at 1 particular usage of this directive I have added an extra attribute for this element.
Here is the directive being used in the HTML:
<attribute-types target-model="patient" attribute="::attribute"
field-validation="::fieldValidation"
is-auto-complete="isAutoComplete"
get-auto-complete-list="getAutoCompleteList"
get-data-results="getDataResults" is-read-only="isReadOnly"
handle-update="handleUpdate" validate-autocomplete="true">
</attribute-types>
The validate-autocomplete is the extra attribute I have used at 1 place use of this directive.
Here is the template for the directive:
<div class="left" data-ng-switch-when="org.openmrs.Concept" ng-if="attribute.name == 'PATIENT_OCCUPATION'" style="position: absolute">
<input type="text"
class="ui-autocomplete-input"
id="{{::attribute.name}}"
name="{{::attribute.name}}"
ng-model="targetModel[attribute.name].value"
ng-keyup="suggest(targetModel[attribute.name])"
ng-required="{{::attribute.required}}">
<ul class="ui-front ui-autocomplete ui-menu ui-widget ui-widget-content ui-corner-all" ng-if="showTag" ng-hide="hideList"
style="position:absolute; top:30px; width:192px">
<li class="ui-menu-item" role="presentation" ng-repeat="info in filterOcuppation"
ng-click="hideSuggestions(info)">
<a class="ui-corner-all" tabindex="-1">{{info.description}}</a>
</li>
</ul>
</div>
And this is the directive definition:
angular.module('bahmni.common.attributeTypes', [])
.directive('attributeTypes', [function () {
var link = function (scope, element, attrs, ngModelCtrl) {
var formElement = element[0];
if (attrs.validateAutocomplete) {
ngModelCtrl.$setValidity('selection', true);
}
};
return {
link: link,
scope: {
targetModel: '=',
attribute: '=',
fieldValidation: '=',
isAutoComplete: '&',
handleLocationChange: '&',
handleSectorChange: '&',
getAutoCompleteList: '&',
getDataResults: '&',
handleUpdate: '&',
isReadOnly: '&',
isForm: '=?'
},
templateUrl: '../common/attributeTypes/views/attributeInformation.html',
restrict: 'E',
controller: function ($scope) {
var dateUtil = Bahmni.Common.Util.DateUtil;
$scope.getAutoCompleteList = $scope.getAutoCompleteList();
$scope.getDataResults = $scope.getDataResults();
$scope.today = dateUtil.getDateWithoutTime(dateUtil.now());
// to avoid watchers in one way binding
$scope.isAutoComplete = $scope.isAutoComplete() || function () { return false; };
$scope.isReadOnly = $scope.isReadOnly() || function () { return false; };
$scope.handleUpdate = $scope.handleUpdate() || function () { return false; };
$scope.handleLocationChange = $scope.handleLocationChange() || function () { return false; };
$scope.handleSectorChange = $scope.handleSectorChange() || function () { return false; };
$scope.suggestions = $scope.attribute.answers;
$scope.showTag = false;
$scope.itisinvalid = true;
$scope.appendConceptNameToModel = function (attribute) {
var attributeValueConceptType = $scope.targetModel[attribute.name];
var concept = _.find(attribute.answers, function (answer) {
return answer.conceptId === attributeValueConceptType.conceptUuid;
});
attributeValueConceptType.value = concept && concept.fullySpecifiedName;
};
$scope.suggest = function (string) {
$scope.hideList = false;
$scope.showTag = true;
var output = [];
angular.forEach($scope.suggestions, function (suggestion) {
if (suggestion.description.toLowerCase().indexOf(string.value.toLowerCase()) >= 0) {
output.push(suggestion);
}
});
$scope.filterOcuppation = output;
};
$scope.hideSuggestions = function (object) {
$scope.targetModel[$scope.attribute.name] = object;
$scope.targetModel[$scope.attribute.name].value = object.description;
$scope.targetModel[$scope.attribute.name].conceptUuid = object.conceptId;
$scope.hideList = true;
};
}
};
}]);
When running this I get TypeError: ngModelCtrl.$setValidity is not a function
What I'm basically doing it validating whatever is entered into the input text is valid or not. For that I would also need the ng-model, how would I access that in my link function?
If I have written some wrong, feel free to correct me. I'm still in the process of learning AngularJS
You should use directive like that:
directive('attributeTypes', [function() {
return {
require: '?ngModel', // get a hold of NgModelController
link: function(scope, element, attrs, ngModel) {
...
ngModel.$setValidity(...

How to pass a custom directive attribute to custom directive child element in Angular 1.5?

I'm currently trying to pass a validation directive to a custom element directive. But I'm struggling to make it work since it should receive model as an input while I am using bind to controller.
I have to premise that I cannot upgrade to a more recent version of Angular, so 1.5 is the limitation, together with the fact I cannot edit validation directive.
I thought transclude would have helped but with directive attribute it looks not so promising.
What the following code should do is to validate vm.model on input element.
Here's the HTML:
<body ng-controller="MainCtrl">
<div class="myClass">
<my-custom-directive data-placeholder="No text"
data-id="myModel.id"
data-model="myModel.text"
not-editable-directive-attribute >
</my-custom-directive>
</div>
</body>
And here the app.js:
var myTemplate = '<div class="myContainer">' +
'<input class="myInput"' +
' ng-mousedown="$event.stopPropagation();"' +
' ng-show="vm.isFocused"' +
' ng-model="vm.model"' +
' ng-change="vm.onChange()"' +
' type="text">' +
'<span ng-show="!vm.isFocused">{{vm.model}}</span>' +
'<span ng-show="!vm.isFocused && !vm.model && vm.placeholder">{{vm.placeholder}}</span>' +
'</div>';
app.controller('MainCtrl', function($scope) {
$scope.myModel = {
id: 'test',
text: 'this is text'
};
});
app.directive('myCustomDirective', ['$timeout', function($timeout) {
return {
restrict: 'E',
replace: true,
template: myTemplate,
controllerAs: 'vm',
bindToController: {
id: '#',
model: '=',
onChange: '&',
placeholder: '#'
},
scope: {},
controller: angular.noop,
link: function(scope, element) {
var input = element.find('input')[0];
var spans = Array.from(element.find('span'));
var vm = scope.vm;
vm.isFocused = false;
vm.focus = function() {
vm.isFocused = true;
scope.$applyAsync(function() {
$timeout(function() {
input.focus();
input.select();
});
});
};
spans.forEach(span => span.addEventListener('click', vm.focus));
}
};
}]);
app.directive('notEditableDirectiveAttribute', [function() {
return {
require: 'ngModel',
link: function(scope, elem, attrs, ctrl) {
ctrl.$validators.myCustomDirectiveAttribute = function(modelValue, viewValue) {
if (viewValue) {
return viewValue.indexOf('e') < 0;
}
return false;
};
}
};
}]);
I've created a plunker to make it clearer:
http://plnkr.co/edit/auminr?p=preview
So clicking on span element i should be able to edit text and directive should validate it (in this specific case check if it contains letter "e").
Is it even possible or am I struggling against windmills?
One approach to adding directives to templates based on component attributes is to use the function form of the template property:
<my-custom-directive data-placeholder="No text"
model="vm.data"
custom="not-editable-directive-attribute" >
</my-custom-directive>
app.directive("myCustomDirective", function() {
return {
template: createTemplate,
scope: {},
//...
});
function createTemplate(tElem, tAttrs) {
var placeholder = tAttrs.placeholder;
var model = tAttrs.model;
var custom = tAttrs.custom;
return `
<input placeholder=${placeholder}
ng-model=${model}
${custom} />
`;
}
})
The createTemplate function copies attributes and used them in a template literal.
For more information, see
AngularJS Comprehensive Directive API - template
MDN JavaScript Reference - Template Literals

Angular directive dynamic template not working with bindings

This is a simplified version of a real problem but I want to understand what is happening and how to fix it. I have a directive that should load different templates deppending on a parameter that is being bind to it.
var app = angular.module('my_app', [])
.controller('controller', Controller)
.directive('dirTwo', DirTwo);
function Controller() {
var este = this;
this.list = ["number", "text", "text"];
this.change = function() {
this.list = this.list.reverse();
}
}
function DirTwo() {
return {
restrict: 'E',
//template: ' Name: {{name}}{{type}}',
template: my_template,
scope: {
type: "="
},
link: function($scope) {
$scope.name = "Pepito";
console.log("scope type: " + $scope.type);
}
}
}
var my_template = function(elem, attrs, $scope) {
console.log("template type: " + attrs.type);
switch (attrs.type) {
case 'number':
return '{{type}}';
break;
default:
return ' ---- '
break;
}
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.7/angular.min.js"></script>
<div class="app" ng-app="my_app" ng-controller="controller as App">
<p>{{App.list}}
<button ng-click="App.change()">Reverse</button>
</p>
<dir-two ng-repeat="item in App.list track by $index" type="item"></dir-two>
</div>
The log in the template function prints the word item instead of number or text. How can I fix it to load the template correctly?
Unfortunately there is no way to use template like that because the template resolution happens before the compile phase where your type would assume a value on scope. To change the template of a directive based on a scope value, you must watch that value and recompile the directive element when it gets changed. It will recompile the directive entirely with a new template but with the same scope. You could do this once (i.e., without the $watch) as well.
The following snippet implements this solution.
var app = angular.module('my_app', [])
.controller('controller', Controller)
.directive('dirTwo', DirTwo);
function Controller() {
var este = this;
this.list = ["number", "text", "text"];
this.change = function() {
this.list = this.list.reverse();
}
}
function DirTwo($compile) {
return {
restrict: 'E',
template: '',
scope: {
type: "="
},
link: function(scope, element) {
var tmplScope;
scope.name = "Pepito";
scope.$watch('type', function(type) {
if (tmplScope) tmplScope.$destroy();
tmplScope = scope.$new();
element.html(getTemplate(type));
$compile(element.contents())(tmplScope);
});
}
}
}
var getTemplate = function(type) {
switch (type) {
case 'number':
return '{{type}}';
break;
default:
return ' ---- '
break;
}
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.1/angular.min.js"></script>
<div class="app" ng-app="my_app" ng-controller="controller as App">
<p>{{App.list}}
<button ng-click="App.change()">Reverse</button>
</p>
<dir-two ng-repeat="item in App.list track by $index" type="item"></dir-two>
</div>
UPDATE NOTE: I've changed the actual compile system to create a new child scope every time it gets compiled and also destroy this child scope when it gets recompiled to prevent adding new watchers when recompiling it without discarding the previous ones. Thanks to #georgeawg who warnned about this issue in the comments.

Angular Directive mouseenter/mouseleave working but not setting to initial state after mouseleave

I have a directive that shows a list of student information on a template and on mouseenter it then shows additional student information. I want to be able to go back to the initial state on mouseleave.
Tried all the resources and not much luck.
html - this is where i'm injecting my directive
<div ng-repeat="student in studentPortfolio">
<portfolio-view student="student"></portfolio-view>
</div>
html directive template
<div class="outer-box">
<img src="{{student.picture}}" alt="{{student.name.first}} {{student.name.last}}" style="width: 200px; height: 200px">
Name: {{student.name.first}} {{student.name.last}}
<br>Bio: {{student.Bio}}
<br>
Skills:
<div ng-repeat="skill in student.skills">
{{skill.title}}
</div>
<br>
</div>
directive
app.directive('portfolioView', function() {
return {
restrict: 'E',
scope: {
student: "="
},
templateUrl: '/html-templates/hoverPortfolio.html',
link: function(scope, elem, attrs) {
//gets the first project and shows it
var project = scope.student.projects;
var firstProject = project[0];
var fp_name = firstProject.name;
var fp_type = firstProject.projectType;
var fp_description = firstProject.description;
//gets the second project and shows it
var secondProject = project[1];
var sp_name = secondProject.name;
var sp_type = secondProject.projectType;
var sp_description = secondProject.description;
//the template that shows the second project
var newHtml =
'<div class="projects outer-box"><div class="firstproject"> Project Name: ' +
fp_name + '<br>Type: ' + fp_type + '<br>Description: ' +
fp_description +
'</div><br><div class="secondproject"> Project Name: ' +
sp_name + '<br>Type: ' + sp_type + '<br>Description: ' +
sp_description +
'</div> </div>';
elem.on('mouseenter', function() {
elem.html(
newHtml
)
});
elem.on('mouseleave', function() {
//return to intial state
});
}
}
});
I didn't have your data, but the ng-show thing works, like in this fiddle.
Here's a simpler variant. If your template includes the parts you wish to show or hide, with an ng-show variable on it, your directive could be fairly simple:
return {
restrict: 'EAC',
replace: true,
template: '<div><div ng-show="show">show</div><div ng-show="!show">hide</div></div>',
link: function (scope, element, attrs, controller) {
scope.show = true;
element.on('mouseenter', function () {
scope.$apply(function () {
scope.show = false;
});
});
element.on('mouseleave', function () {
scope.$apply(function () {
scope.show = true;
});
});
}
};

The attributes passed to directive in AngularJS change only into directive scope but not outside

I want to use a directive to customize my code.
I have created a button to switch isCollapsedUpload flag defined in the controller as: #scope.isCollapsedUpload=false.
When the user presses the button, the isCollapsedUpload turns to true or vice versa and the icon changes.
From the controller:
$scope.switcher = function (booleanExpr, trueValue, falseValue) {
return booleanExpr ? trueValue : falseValue;
}
$scope.isCollapsedUpload = false;
<button class="btn" ng-click="isCollapsedUpload = !isCollapsedUpload">
<span>Upload file</span>
<i class="{{ switcher( isCollapsedUpload, 'icon-chevron-right', 'icon-chevron-down' )}}"></i>
</button>
I wrote this directive:
feederliteModule.directive('collapseExtend', function() {
return {
restrict: 'E',
scope: { isCollapsed:'#collapseTarget' },
compile: function(element, attrs)
{
var htmlText =
'<button class="btn" ng-click="isCollapsed = !isCollapsed">'+
' <span>'+attrs.label+'</span>'+
' <i class="{{ switcher(isCollapsed, \'icon-chevron-right\', \'icon-chevron-down\' )}}"></i>'+
'</button>';
element.replaceWith(htmlText);
}
}
});
And now I can use it like:
<collapse-extend
collapse-target="isCollapsedUpload"
label="Upload file"
></collapse-extend>
It doesn't work. No icon changes. No errors,
isCollapsedUpload flag doesn't change. It changes only into directive
Did I miss something?
The reason the class doesn't change correctly is because you are not linking the template properly. This is easy to fix if you use the built in functionality:
var feederliteModule = angular.module('feederliteModule', []);
feederliteModule.directive('collapseExtend', [function() {
return {
restrict: 'E',
scope: {
isCollapsed:'=collapseTarget',
label: '#'
},
template: '<button class="btn" ng-click="isCollapsed = !isCollapsed">'+
'<span>{{ label }}</span>'+
'<i ng-class="{ \'icon-chevron-right\': isCollapsed, \'icon-chevron-down\': !isCollapsed }"></i>'+
'</button>'
}
}]);
feederliteModule.controller('test', ['$scope', function($scope) {
$scope.isCollapsedUpload = false;
}]);
To the best of my understanding, by replacing the parent element, you were removing the isolate scope this object was tied to without creating a new one on the button itself.
EDIT: See a complete working fiddle with multiple buttons
I suggest using a service instead of a controller to maintain your model data. This allows you better separation of concerns as your app gets more complex:
var feederliteModule = angular.module('feederliteModule', []);
feederliteModule.service('btnService', function(){
this.isCollapsedUpload = false;
this.isCollapsedSomething = false;
});
feederliteModule.controller('btnController', function($scope, btnService){
$scope.isCollapsedUpload = btnService.isCollapsedUpload;
$scope.isCollapsedSomething = btnService.isCollapsedSomething;
});
feederliteModule.directive('collapseExtend', function() {
return {
restrict: 'E',
scope: {
isCollapsed:'=collapseTarget',
label:'#'
},
replace: true,
link: function (scope, element, attrs){
scope.switcher = function (booleanExpr, trueValue, falseValue) {
return booleanExpr ? trueValue : falseValue;
};
scope.toggleCollapse = function() {
scope.isCollapsed = !scope.isCollapsed;
}
},
template: '<button class="btn" ng-click="toggleCollapse()">'+
'<span>{{label}}</span>'+
'<i ng-class="switcher(isCollapsed, \'icon-chevron-right\', \'icon-chevron-down\')"></i>'+
'</button>'
}
});
Also, notice that you must use '=' instead of '#' in order for isCollapsed to work as you expect. The answer above needs this as well.

Categories

Resources