Event driven AngularJS code, case study - angularjs

Coming back from a jQuery background I find myself having a "procedural" way of seeing this when it comes ti "in-view" interactions.
The problem is, this approach leads to ALLOT of errors and the code is not maintainable.
So I have been reading on event driven JavaScript and tried to apply this concept on AngularJS.
For this, I thought about a user submitting a form, having two steps and resetting everything to an original state.
This is what I came up with: here is a plunker
1. How do you feel about this style of programming in Angular?
2. How would you optimize the code?
3. How would you do it?
Bellow is the code:
<div ng-controller="TheController" ng-init="events.init()">
<p ng-if="showName">Name: <input type="text"></p>
<p ng-if="showAge">Age: <input type="text"></p>
<p ng-if="showHeight">Height: <input type="text"></p>
<p ng-if="showWeight">Weight: <input type="text"></p>
<button ng-if="showButtonOne" ng-click="events.submittedAgeAndName()">Submit age and name</button>
<button ng-if="showButtonTwo" ng-click="events.submittedHeightAndWright()">Submit height and weight</button>
<p ng-if="showThankYou">Thank you for submitting!</p>
<button ng-if="showGetEverythingBack" ng-click="events.getEverythingBack()">Get everything back!</button>
</div>
<script type="text/javascript">
var app = angular.module('app', []);
app.controller('TheController', function ($scope) {
$scope.events = {
'init': function () {
$scope.showName = true;
$scope.showAge = true;
$scope.showHeight = false;
$scope.showWeight = false;
$scope.showThankYou = false;
$scope.showButtonOne = true;
$scope.showButtonTwo = false;
$scope.showGetEverythingBack = false;
},
'submittedAgeAndName': function () {
$scope.showName = false;
$scope.showAge = false;
$scope.showHeight = true;
$scope.showWeight = true;
$scope.showThankYou = false;
$scope.showButtonOne = false;
$scope.showButtonTwo = true;
$scope.showGetEverythingBack = false;
},
'submittedHeightAndWright': function () {
$scope.showName = false;
$scope.showAge = false;
$scope.showHeight = false;
$scope.showWeight = false;
$scope.showThankYou = true;
$scope.showButtonOne = false;
$scope.showButtonTwo = false;
$scope.showGetEverythingBack = true;
},
'getEverythingBack': function () {
this.init();
}
}
});
</script>

How do you feel about this style of programming in Angular?
Way too many pointless variables
With no purpose for all events really no function is needed, but if you do you just change the step inside like reset in the plunker.
How would you optimize the code?
Declarative html, data-binding, use directives, all the regular angular principles.
I would probably put the whole wizard in a directive, which would tuck away a lot of the template html and make the code more reusable.
How would you do it?
Here's a Plunker
<div ng-switch="events.step">
<div ng-switch-when="1"><button ng-click="events.step=2">next</button></div>
<div ng-switch-when="2"><button ng-click="events.step=3">next</button></div>
<div ng-switch-when="3"><button ng-click="events.reset();">back</button></div>
</div>

Related

Disable link after click angular js

Could anybody help me with a solution.
I need to make the link disabled (PAY Now) after clicking on that to avoid multi clicking.
<div class="Class1" data-ng-show="ShowButton == 'TRUE'">
PAY NOW</div>
There is no disabled attribute for hyperlinks. If you don't want to do something with that you'll need to add some style into the <a> tag altogether and handle the flag into the controller.
Try this :
angular.module('myApp', [])
.controller('myCtrl', ['$scope', function($scope) {
$scope.isDisabled = false;
if($scope.isDisabled === false) {
$scope.PayNow = function() {
$scope.isDisabled = true;
}
}
}]);
.disabled {
cursor: not-allowed;
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="myApp" ng-controller="myCtrl">
PAY NOW
</div>
You can in the PayNow() function create an extra variable which disables the button like so:
JS:
$scope.PayNow = function() {
$scope.DisabledButton = true;
// other code
}
HTML
PAY NOW
In your html
.disabled {
cursor: not-allowed;
}
<a ng-click="PayNow()" ng-class="{'disabled': DisabledButton}">Add</a>
OR
<button ng-click="PayNow()" ng-disabled="DisabledButton">Add</button>
In JS
$scope.DisabledButton = false;
$scope.PayNow = function() {
$scope.DisabledButton = true;
// other code
...
//when you want to re-enable
$scope.DisabledButton = false;
}
i hope if this can help you!!!. So in my example I am using condition to check the length of the array and constraining the button to make more textboxes. Plus you can use count instead.
$scope.technologies = [];
$scope.addtech = function () {
$scope.minus = true;
if ($scope.technologies.length < 3)
$scope.technologies.push({});
}

Teaxarea have inherent scope when i click on it is collapsing but don't let me to type my comment?

I have created already my code in here . There is three directives which each of responsible to collapse somethings.
If you execute this code you will see there is 2 panel where if you click on address is collapsing. Also if you click on comment is collapsing too. However my problem came when i want to type some comment inside textarea then is going to collapse as well. This is not what i need. I would like to click on add comment [+], then is collapsing and start to type whatever i want. I define inherent scope in script.js. I am not sure how i can change my code in order to solve this problem. I will be thank you if any of you can help me.
This is add comment directive
.directive('addComment', function() {
return {
restrict: "E",
templateUrl: "addComment.html",
scope:true,
controller: function($scope) {
$scope.collapsed = false;
$scope.collapseCom = function() {
$scope.collapsed = true;
}
$scope.extendCom = function() {
$scope.collapsed = false;
}
}
}
})
and this is my html.
<div ng-show="!collapsed" ng-click=collapseCom()>
<h4>Add Comments: [+]<br /></h4>
<div class="form-group">
<div class="col-lg-10">
<textarea name="comment" ng-model="comment" class="form-control" rows="3" required></textarea>
{{comment}}
</div>
</div>
</div>
<div ng-show="collapsed" ng-click=extendCom()>
<h4>Add Comments: [-]</h4>
</div>
To see all the details please click on following link.
replace ng-click=collapseCom()
with ng-click="collapseCom()"
$scope.collapseCom = function() {
$scope.collapsed = true;
}
$scope.extendCom = function() {
$scope.collapsed = false;
}
replace one method
$scope.collapseCom = function() {
if($scope.collapsed){
$scope.collapsed = false;
}else{
$scope.collapsed = true;
}
}
This works like switch then, and should work fine

Start/Stop/Resume all in one button with Angular Timer

Using the pre-built "Angular Timer" directives available here (scroll down to progress bar example), I'm trying to build a progress bar with a Start, Stop and Resume button all-in-one.
Examples on their websites are made of two buttons and I would like to merge them.
One solution could be to use innerHTML to get the current state ("Start" or "Stop") and a if condition in the controller.js but i'd like the button to be a icon-only-button switching from play to pause icon.
An example from simple timer implementation is available here
Here is my html
<button class="button button-icon icon" ng-class="{'icon ion-play': !play, 'icon ion-pause': play}" ng-click="stopResumeTimer(this)"></button>
<timer interval="1000" autostart="false"><div class="progress"> <div class="bar" style="width: {{seconds}}%;"></div> </div></timer>
And the controller.js code with the if condition
$scope.stopResumeTimer = function(btn) {
if (not yet started) {
$scope.startTimer();
}
else if (already started) {
$scope.stopTimer();
}
else {
$scope.resumeTimer();
}
}
I am discouraged by the repeated failure on this, any help would be great! Thanks
In the controller where you are setting the ng-click function, you can set flags to determine whether the click function should start, stop, or resume. Those flags can also set the text for the button to be either "Start", "Stop", or "Resume".
JS:
(function(angular) {
function controller($scope) {
var isStart = false;
var isStop = false;
$scope.timerBtnText = "Start";
$scope.timer = function() {
if (isStart) {
$scope.startTimer();
isStart = false;
isStop = true;
$scope.timerBtnText = "Stop";
return;
}
if (isStop) {
$scope.stopTimer();
isStop = false;
$scope.timerBtnText = "Resume";
} else {
$scope.resumeTimer();
isStop = true;
$scope.timerBtnText = "Stop";
}
};
}
angular.module("app", []).controller("controller", ["$scope", controller]);
})(angular);
HTML:
<button ng-click="timer()" ng-bind="timerText"></butotn>
Here is a working proof of concept: http://plnkr.co/edit/NlQFPysVH1M2EyjogQQv
This is how I would solve it (Plunker)
Controller:
app.controller('MainCtrl', function($scope) {
$scope.name = 'World';
$scope.operation = 'start';
$scope.startOrStop = function(){
document.getElementById('first')[$scope.operation]();
$scope.operation = ($scope.operation === 'start' || $scope.operation === 'resume') ? 'stop' : 'resume';
}
});
Declare it like this:
<button ng-click="startOrStop()" ng-class="{start:operation==='start', stop:operation==='stop', resume:operation==='resume'}"></button>
<timer id="first" interval="1000" autostart="false"><div class="progress"><div class="progress-bar" role="progressbar" style="width: {{seconds}}%;"></div></div></timer>
Css:
.start::before{
content:"Start";
}
.stop::before{
content:"Stop";
}
.resume::before{
content:"Resume";
}

Angularjs toggle image onclick

I'm trying to toggle a button image when a user clicks it. I prefer to use angularjs instead of jquery if possible. Right now I have a working version which toggles the image when clicked, the only problem is it changes ALL the images on click. How do I reduce the scope or pass in the src attribute for the img element?
<div ng-repeat="merchant in merchants">
<div class="followrow">
<a ng-click="toggleImage()"><img id="followbutton" ng-src="{{followBtnImgUrl}}" /></a>
</div>
</div>
app.controller('FollowCtrl', function CouponCtrl($scope) {
$scope.followBtnImgUrl = '/img1'
$scope.toggleImage = function () {
if ($scope.followBtnImgUrl === '/img1.jpg') {
$scope.followBtnImgUrl = baseUrl + '/img2.jpg';
} else {
$scope.followBtnImgUrl = 'img1.jpg';
}
}
});
Can I pass in the img src attribute the function like toggleImage(this.img.src) or similar?
<div ng-repeat="merchant in merchants">
<div class="followrow">
<a ng-click="toggleImage(merchant)"><img id="followbutton" ng-src="{{merchant.imgUrl}}" />
</a>
</div>
</div>
.
app.controller('FollowCtrl', function CouponCtrl($scope) {
$scope.followBtnImgUrl = '/sth.jpg'
$scope.merchants = [{imgUrl: "img1.jpg", name:"sdf"},
{imgUrl: "img2.jpg", name: "dfsd"}];
$scope.toggleImage = function(merchant) {
if(merchant.imgUrl === $scope.followBtnImgUrl) {
merchant.imgUrl = merchant.$backupUrl;
} else {
merchant.$backupUrl = merchant.imgUrl;
merchant.imgUrl = $scope.followBtnImgUrl;
}
};
});
What you want is a new scope for each followrow. As your code stands, there's only one scope that all of the followrows are referencing.
A simple answer is to create a new controller that you attach to each followrow:
<div class="followrow" ng-controller="ImageToggleCtrl">...</div>
And then move the image toggling logic to that new controller:
app.controller('ImageToggleCtrl', function($scope) {
$scope.followBtnImgUrl = '/img1';
$scope.toggleImage = function() { /* the toggling logic */ };
});
Now, a new scope will be instantiated for each row, and the images will toggle independently.
I just added two clickable images:
<div ng-app="FormApp" ng-controller="myController" max-width="1150px;" width="1150px;" >
<input ng-show="ShowDown" type="image" style="width:250px; height:40px;" src="~/Content/Images/contactShow.png" ng-click="ShowHide()"/>
<input ng-show="ShowUp" type="image" style="width:250px; height:40px;" src="~/Content/Images/contactHide.png" ng-click="ShowHide()" />
</div>
They toggle eachothers visibility. At page load one is visible, one is not, and both clickable images call the same function:
<script type="text/javascript">
var app = angular.module('FormApp', [])
app.controller('myController', function ($scope) {
$scope.ShowDown = true;
$scope.ShowUp = false;
$scope.ShowHide = function () {
$scope.ShowDown = $scope.ShowDown ? false : true;
$scope.ShowUp = $scope.ShowUp ? false : true;
}
});
</script>

Reset form to pristine state (AngularJS 1.0.x)

A function to reset form fields to pristine state (reset dirty state) is on the roadmap for AngularJS 1.1.x. Unfortunately such a function is missing from the current stable release.
What is the best way to reset all form fields to their initial pristine state for AngularJS 1.0.x.?
I would like to know if this is fixable with a directive or other simple workaround. I prefer a solution without having to touch the original AngularJS sources. To clarify and demonstrate the problem, a link to JSFiddle. http://jsfiddle.net/juurlink/FWGxG/7/
Desired feature is on the Roadmap - http://blog.angularjs.org/2012/07/angularjs-10-12-roadmap.html
Feature request - https://github.com/angular/angular.js/issues/856
Proposed solution Pull request - https://github.com/angular/angular.js/pull/1127
Updated with possible workaround
Good enough workaround?
I just figured out I can recompile the HTML part and put it back into the DOM. It works and it's fine for a temporarily solution, but also as #blesh mentioned in the comments:
Controllers should be used for business logic only, not for DOM!
<div id="myform">
<form class="form-horizontal" name="form">
</form>
</div>
And in my Controller on resetForm():
Save the original untouched HTML
Recompile the saved original HTML
Remove the current form from the DOM
Insert the new compiled template into the DOM
The JavaScript:
var pristineFormTemplate = $('#myform').html();
$scope.resetForm = function () {
$('#myform').empty().append($compile(pristineFormTemplate)($scope));
}
I think it's worth mentioning that in later versions of Angular (e.g. 1.1.5), you can call $setPristine on the form.
$scope.formName.$setPristine(true)
This will set all the form controls to pristine state as well.
FormController.$setPristine
Solution without a workaround
I came up with a solution which uses AngularJS without any workaround. The trick here is to use AngularJS ability to have more than one directive with the same name.
As others mentioned there is actually a pull request (https://github.com/angular/angular.js/pull/1127) which made it into the AngularJS 1.1.x branch which allows forms to be reset. The commit to this pull request alters the ngModel and form/ngForm directives (I would have liked to add a link but Stackoverflow doesn't want me to add more than two links).
We can now define our own ngModel and form/ngForm directives and extend them with the functionality provided in the pull request.
I have wrapped these directives in a AngularJS module named resettableForm. All you have to do is to include this module to your project and your AngularJS version 1.0.x behaves as if it was an Angular 1.1.x version in this regard.
''Once you update to 1.1.x you don't even have to update your code, just remove the module and you are done!''
This module also passes all tests added to the 1.1.x branch for the form reset functionality.
You can see the module working in an example in a jsFiddle (http://jsfiddle.net/jupiter/7jwZR/1/) I created.
Step 1: Include the resettableform module in your project
(function(angular) {
// Copied from AngluarJS
function indexOf(array, obj) {
if (array.indexOf) return array.indexOf(obj);
for ( var i = 0; i < array.length; i++) {
if (obj === array[i]) return i;
}
return -1;
}
// Copied from AngularJS
function arrayRemove(array, value) {
var index = indexOf(array, value);
if (index >=0)
array.splice(index, 1);
return value;
}
// Copied from AngularJS
var PRISTINE_CLASS = 'ng-pristine';
var DIRTY_CLASS = 'ng-dirty';
var formDirectiveFactory = function(isNgForm) {
return function() {
var formDirective = {
restrict: 'E',
require: ['form'],
compile: function() {
return {
pre: function(scope, element, attrs, ctrls) {
var form = ctrls[0];
var $addControl = form.$addControl;
var $removeControl = form.$removeControl;
var controls = [];
form.$addControl = function(control) {
controls.push(control);
$addControl.apply(this, arguments);
}
form.$removeControl = function(control) {
arrayRemove(controls, control);
$removeControl.apply(this, arguments);
}
form.$setPristine = function() {
element.removeClass(DIRTY_CLASS).addClass(PRISTINE_CLASS);
form.$dirty = false;
form.$pristine = true;
angular.forEach(controls, function(control) {
control.$setPristine();
});
}
},
};
},
};
return isNgForm ? angular.extend(angular.copy(formDirective), {restrict: 'EAC'}) : formDirective;
};
}
var ngFormDirective = formDirectiveFactory(true);
var formDirective = formDirectiveFactory();
angular.module('resettableForm', []).
directive('ngForm', ngFormDirective).
directive('form', formDirective).
directive('ngModel', function() {
return {
require: ['ngModel'],
link: function(scope, element, attrs, ctrls) {
var control = ctrls[0];
control.$setPristine = function() {
this.$dirty = false;
this.$pristine = true;
element.removeClass(DIRTY_CLASS).addClass(PRISTINE_CLASS);
}
},
};
});
})(angular);
Step 2: Provide a method on your controller which resets the model
Please be aware that you must reset the model when you reset the form. In your controller you can write:
var myApp = angular.module('myApp', ['resettableForm']);
function MyCtrl($scope) {
$scope.reset = function() {
$scope.form.$setPristine();
$scope.model = '';
};
}
Step 3: Include this method in your HTML template
<div ng-app="myApp">
<div ng-controller="MyCtrl">
<form name="form">
<input name="requiredField" ng-model="model.requiredField" required/> (Required, but no other validators)
<p ng-show="form.requiredField.$errror.required">Field is required</p>
<button ng-click="reset()">Reset form</button>
</form>
<p>Pristine: {{form.$pristine}}</p>
</div>
</dvi>
EDIT... I'm removing my old answer, as it was not adequate.
I actually just ran into this issue myself and here was my solution: I made an extension method for angular. I did so by following a bit of what $scope.form.$setValidity() was doing (in reverse)...
Here's a plnkr demo of it in action
Here's the helper method I made. It's a hack, but it works:
angular.resetForm = function (scope, formName, defaults) {
$('form[name=' + formName + '], form[name=' + formName + '] .ng-dirty').removeClass('ng-dirty').addClass('ng-pristine');
var form = scope[formName];
form.$dirty = false;
form.$pristine = true;
for(var field in form) {
if(form[field].$pristine === false) {
form[field].$pristine = true;
}
if(form[field].$dirty === true) {
form[field].$dirty = false;
}
}
for(var d in defaults) {
scope[d] = defaults[d];
}
};
Hopefully this is helpful to someone.
Your form fields should be linked to a variable within your $scope. You can reset the form by resetting the variables. It should probably be a single object like $scope.form.
Lets say you have a simple form for a user.
app.controller('Ctrl', function Ctrl($scope){
var defaultForm = {
first_name : "",
last_name : "",
address: "",
email: ""
};
$scope.resetForm = function(){
$scope.form = defaultForm;
};
});
This will work great as long as your html looks like:
<form>
<input ng-model="form.first_name"/>
<input ng-model="form.last_name"/>
<input ng-model="form.address"/>
<input ng-model="form.email"/>
<button ng-click="resetForm()">Reset Form</button>
</form>
Maybe I'm not understanding the issue here, so if this does not address your question, could you explain why exactly?
Here I have found a solution for putting the from to its pristine state.
var def = {
name: '',
password: '',
email: '',
mobile: ''
};
$scope.submited = false;
$scope.regd = function (user) {
if ($scope.user.$valid) {
$http.post('saveUser', user).success(function (d) {
angular.extend($scope.user, def);
$scope.user.$setPristine(true);
$scope.user.submited = false;
}).error(function (e) {});
} else {
$scope.user.submited = true;
}
};
Just write angular.extends(src,dst) ,so that your original object is just extends the blank object, which will appear as blank and rest all are default.
Using an external directive and a lot of jquery
app.controller('a', function($scope) {
$scope.caca = function() {
$scope.$emit('resetForm');
}
});
app.directive('form', function() {
return {
restrict: 'E',
link: function(scope, iElem) {
scope.$on('resetForm', function() {
iElem.find('[ng-model]').andSelf().add('[ng-form]').each(function(i, elem) {
var target = $(elem).addClass('ng-pristine').removeClass('ng-dirty');
var control = target.controller('ngModel') || target.controller('form');
control.$pristine = true;
control.$dirty = false;
});
});
}
};
});
http://jsfiddle.net/pPbzz/2/
The easy way: just pass the form into the controller function. Below the form "myForm" is referenced by this, which is equivalent to $scope.
<div ng-controller="MyController as mc">
<ng-form name="myform">
<input ng-model="mc.myFormValues.name" type="text" required>
<button ng-click="mc.doSometing(this.myform)" type="submit"
ng-disabled="myform.$invalid||myform.$pristine">Do It!</button>
</ng-form>
</div>
The Controller:
function MyController(MyService) {
var self = this;
self.myFormValues = {
name: 'Chris'
};
self.doSomething = function (form) {
var aform = form;
MyService.saveSomething(self.myFromValues)
.then(function (result) {
...
aform.$setPristine();
}).catch(function (e) {
...
aform.$setDirty();
})
}
}

Resources