Occasional AngularJS error when directive is compiling - angularjs

I have an AngularJS directive that renders into a custom social share widget. Out of about 10,000 page views per day, around 1 or 2 of those times, Angular errors out after starting to compile the directive. This leaves the raw HTML partial in the DOM, visible to the user.
I only came to know of this error because it was reported by several users. I can't reproduce it, but I have devised some informative logging which shows that it is occurring.
Each time this has occurred:
Browser is always Chrome
OS is Mac or Windows
Angular starts the compile phase, but fails before starting post link
Angular reports an error during the compile phase, but the 'exception' object passed to the '$exceptionHandler' service is always null.
No other JavaScript errors are reported
This error is occurring for some of the same IPs across multiple days.
Has anyone out there had a similar issue?
Edit
Here's my code...
JavaScript:
(function () {
angular.module('common', []);
angular.module('common')
.filter('encodeURIComponent', function () {
return window.encodeURIComponent;
});
function configure($provide) {
// Pass all Angular errors to Loggly
$provide.decorator("$exceptionHandler", function ($delegate) {
return function exceptionHandlerDecorator(exception, cause) {
$delegate(exception, cause);
_LTracker.push({
'error': 'angularError',
'app': 'shareCounts',
'err': exception,
'element': cause
});
};
});
}
angular.module('common')
.config(['$provide', configure]);
function configure($provide) {
// Defines available share options as well as behaviors of the share popup windows
function shareLinksConfig() {
return {
'facebook': {
width: 670,
height: 200,
urlBase: 'https://www.facebook.com/sharer/sharer.php?',
shareParamPre: 'u=',
msgParamPre: '',
mediaParamPre: '',
addParams: ''
},
'twitter': {
width: 550,
height: 420,
urlBase: 'https://twitter.com/intent/tweet?',
shareParamPre: 'url=',
msgParamPre: '&text=',
mediaParamPre: ''
},
'googlePlus': {
width: 600,
height: 600,
urlBase: 'https://plus.google.com/share?',
shareParamPre: 'url=',
msgParamPre: '',
mediaParamPre: '',
addParams: ''
},
'linkedIn': {
width: 600,
height: 400,
urlBase: 'http://www.linkedin.com/shareArticle?',
shareParamPre: 'url=',
msgParamPre: '',
mediaParamPre: '',
addParams: '&mini=true'
},
'pinterest': {
width: 750,
height: 320,
urlBase: 'https://www.pinterest.com/pin/create/button/?',
shareParamPre: 'url=',
msgParamPre: '&description=',
mediaParamPre: '&media=',
addParams: ''
},
'email': {
width: 0,
height: 0,
urlBase: '',
shareParamPre: '',
msgParamPre: '',
mediaParamPre: '',
addParams: ''
}
};
}
$provide.factory('shareLinksConfig', shareLinksConfig);
}
angular.module('common')
.config(['$provide', configure]);
function ShareLinksController($scope, shareLinksService) {
sendToLoggly.push("A \"ShareLinksController\" started constructing...");
sendToLoggly.push("...and the $scope is typeof...");
sendToLoggly.push(typeof $scope);
var vm = this;
vm.share = function ($event, shareVia) {
if (shareVia !== 'email') {
$event.preventDefault();
// console.log($scope.mediaUrl);
shareLinksService.openPopUp(shareVia, $scope.shareUrl, $scope.shareMsg, $scope.mediaUrl);
}
// Tell Google Analytics share link was clicked
shareLinksService.pushGAEvent($scope.analyticsLocation, shareVia, $scope.shareUrl);
};
$scope.shareLinksShown = true; // Initialized to true, but then this gets set to false in the directive's link function if slideIn is true
vm.toggle = function ($event) {
$event.preventDefault();
$scope.shareLinksShown = !$scope.shareLinksShown;
};
sendToLoggly.push("...and controller finished constructing.");
}
angular.module('common')
.controller('ShareLinksController', ["$scope", "shareLinksService",
ShareLinksController]);
function fuShareLinks($http, shareLinksConfig, testRenderingService) {
function compile() {
sendToLoggly.push("A \"fuShareLinks\" directive started compiling...");
testRenderingService.testShareCounts();
return function postLink(scope) {
sendToLoggly.push("A \"fuShareLinks\" directive started postLinking...");
function Settings(shareVia, slideInDir, slideToggleLabel, colorized, showCounts) {
var self = this,
prop,
splitArray;
/* --------
ShareVia
--------
Comma separated list of ways to share
Accepted options are: 'facebook, twitter, googlePlus, linkedIn, pinterest, email' */
// Copy the properties from the config and initialize to false
self.shareVia = {};
for (prop in shareLinksConfig) {
if (shareLinksConfig.hasOwnProperty(prop)) {
self.shareVia[prop] = false;
}
}
if (typeof shareVia === 'string') {
splitArray = shareVia.split(',');
} else {
splitArray = [];
}
// Check each value of splitArray, if it is in possible share options,
// set that option to true.
angular.forEach(splitArray, function (value) {
// Clean up 'value' a bit by removing spaces
value = value.trim();
if (value.length > 0) {
if (self.shareVia.hasOwnProperty(value)) {
self.shareVia[value] = true;
}
}
});
/* --------
Slide In
--------
The slide-in functionality is activated by passing a value to 'slideInDir'.
Accepted values are 'left' or 'down' (case insensitive)
The 'slideToggleLabel' can be any string, if empty, it defaults to 'Share'. */
self.slideIn = {
direction: '',
label: 'Share'
};
if (typeof slideInDir === 'string') {
slideInDir = slideInDir.toUpperCase();
}
switch (slideInDir) {
case 'LEFT':
self.slideIn.direction = 'left';
break;
case 'DOWN':
self.slideIn.direction = 'down';
break;
}
if (typeof slideToggleLabel === 'string') {
self.slideIn.label = slideToggleLabel;
}
/* ---------
Colorized
---------
'true', 'yes', or 'colorized' (case insensitive) -- results in true
defaults to false */
self.colorized = false;
if (typeof colorized === 'string') {
colorized = colorized.toUpperCase();
}
switch (colorized) {
case 'TRUE':
self.colorized = true;
break;
case 'YES':
self.colorized = true;
break;
case 'COLORIZED':
self.colorized = true;
break;
}
/* -----------
Show Counts
-----------
'true', 'yes', or 'show' (case insensitive) -- results in true
defaults to false */
self.showCounts = false;
if (typeof showCounts === 'string') {
showCounts = showCounts.toUpperCase();
}
switch (showCounts) {
case 'TRUE':
self.showCounts = true;
break;
case 'YES':
self.showCounts = true;
break;
case 'SHOW':
self.showCounts = true;
break;
}
}
scope.settings = new Settings(
scope.shareVia,
scope.slideInDir,
scope.slideToggleLabel,
scope.colorized,
scope.showCounts
);
// Initally hide the share links, if they are set to toggle
if (scope.settings.slideIn.direction !== '') {
scope.shareLinksShown = false;
}
function ShareCounts(shareVia) {
var self = this;
angular.forEach(shareVia, function (value, name) {
self[name] = 0;
});
$http.get(
'/local/social-share-counts/?url=' +
encodeURIComponent(scope.shareUrl)
).success(function (data) {
/* Check for share counts in the returned data.
Must use consistent naming for the social networks
from shareLinksConfig properties all the way to the
JSON data containting the counts.
Expected JSON format:
{
"twitter": {
"count": 42,
"updated": "2015-03-25T15:13:48.355422"
},
"facebook": {
"count": 120,
"updated": "2015-03-25T15:13:47.470778"
}
}
*/
angular.forEach(shareVia, function (value, name) {
if (data[name] && data[name]["count"]) {
self[name] = data[name]["count"];
}
});
}).error(function (data, status) {
sendToLoggly.push("HTTP Response " + status);
});
}
// If showing share counts, get the counts from the specified networks
if (scope.settings.showCounts) {
scope.shareCounts = new ShareCounts(scope.settings.shareVia);
}
sendToLoggly.push("...and directive finished postLinking.");
};
sendToLoggly.push("...and directive finished compiling.");
}
return {
restrict: 'E',
scope: {
shareVia: '#',
shareUrl: '#',
shareMsg: '#',
mediaUrl: '#',
analyticsLocation: '#',
slideInDir: '#',
slideToggleLabel: '#',
colorized: '#',
showCounts: '#'
},
controller: 'ShareLinksController',
controllerAs: 'shrLnksCtrl',
templateUrl: '/angular-partials/common.share-links.html',
compile: compile
};
}
angular.module('common')
.directive('fuShareLinks', ['$http', 'shareLinksConfig', 'testRenderingService', fuShareLinks])
.factory('testRenderingService', function () {
var timerId = null;
function evalShareRender() {
var renderError = (-1 < $('em.ng-binding')
.text()
.indexOf('{{'));
if (renderError) {
console.error('RENDER ERROR');
_LTracker.push({
'error': 'rendering',
'app': 'shareCounts',
'statusMsgs': sendToLoggly,
'userAgent': navigator.userAgent
});
}
}
return {
testShareCounts: function () {
if (!timerId) {
timerId = window.setTimeout(evalShareRender, 5000);
}
}
};
});
function shareLinksService(shareLinksConfig) {
function openPopUp(shareVia, shareUrl, shareMsg, mediaUrl) {
var width,
height,
urlBase,
shareParamPre,
msgParamPre,
mediaParamPre,
addParams,
popUpUrl;
width = shareLinksConfig[shareVia].width;
height = shareLinksConfig[shareVia].height;
urlBase = shareLinksConfig[shareVia].urlBase;
shareParamPre = shareLinksConfig[shareVia].shareParamPre;
msgParamPre = shareLinksConfig[shareVia].msgParamPre;
mediaParamPre = shareLinksConfig[shareVia].mediaParamPre;
addParams = shareLinksConfig[shareVia].addParams;
popUpUrl = encodeURI(urlBase);
popUpUrl += encodeURI(shareParamPre);
popUpUrl += encodeURIComponent(shareUrl);
if (msgParamPre && shareMsg) {
popUpUrl += encodeURI(msgParamPre);
popUpUrl += encodeURIComponent(shareMsg);
}
if (mediaParamPre && mediaUrl) {
popUpUrl += encodeURI(mediaParamPre);
popUpUrl += encodeURIComponent(mediaUrl);
}
popUpUrl += encodeURI(addParams);
// Open the social share window
window.open(popUpUrl, '_blank', 'width=' + width + ',height=' + height);
}
function pushGAEvent(analyticsLocation, shareVia, shareUrl) {
function capitalize(firstLetter) {
return firstLetter.toUpperCase();
}
var gaEventAction = shareVia;
gaEventAction = gaEventAction.replace(/^[a-z]/, capitalize);
gaEventAction += ' - Clicked';
_gaq.push([
'_trackEvent',
analyticsLocation + ' - SocialShare',
gaEventAction,
shareUrl
]);
}
return {
openPopUp: openPopUp,
pushGAEvent: pushGAEvent
};
}
angular.module('common')
.factory('shareLinksService', ['shareLinksConfig', shareLinksService]);
}());
HTML:
<div class="share-links-wrapper" ng-class="{ 'right': settings.slideIn.direction === 'left', 'center': settings.slideIn.direction === 'down' }" ng-cloak>
<a href="#" class="toggle" ng-show="settings.slideIn.direction != ''" ng-click="shrLnksCtrl.toggle($event)">
<i class="fuicon-share"></i>{{ settings.slideIn.label }}
</a>
<div class="share-links" ng-class="{ 'share-links-colorized': settings.colorized }" ng-show="shareLinksShown">
<ul>
<li ng-show="settings.shareVia.facebook">
<a href="#" ng-click="shrLnksCtrl.share($event, 'facebook')"
class="fuicon-hex-facebook">
</a>
<em ng-show="settings.showCounts && shareCounts.facebook > 0">
{{ shareCounts.facebook }}
</em>
</li>
<li ng-show="settings.shareVia.twitter">
<a href="#" ng-click="shrLnksCtrl.share($event, 'twitter')"
class="fuicon-hex-twitter">
</a>
<em ng-show="settings.showCounts && shareCounts.twitter > 0">
{{ shareCounts.twitter }}
</em>
</li>
<li ng-show="settings.shareVia.googlePlus">
<a href="#" ng-click="shrLnksCtrl.share($event, 'googlePlus')"
class="fuicon-hex-googleplus">
</a>
<em ng-show="settings.showCounts && shareCounts.googlePlus > 0">
{{ shareCounts.googlePlus }}
</em>
</li>
<li ng-show="settings.shareVia.linkedIn">
<a href="#" ng-click="shrLnksCtrl.share($event, 'linkedIn')"
class="fuicon-hex-linkedin">
</a>
<em ng-show="settings.showCounts && shareCounts.linkedIn > 0">
{{ shareCounts.linkedIn }}
</em>
</li>
<li ng-show="settings.shareVia.pinterest && mediaUrl">
<a href="#" ng-click="shrLnksCtrl.share($event, 'pinterest')"
class="fuicon-hex-pinterest">
</a>
<em ng-show="settings.showCounts && shareCounts.pinterest > 0">
{{ shareCounts.pinterest }}
</em>
</li>
<li ng-show="settings.shareVia.email">
<a href="mailto:?subject={{ shareMsg | encodeURIComponent }}
&body={{ shareUrl | encodeURIComponent }}"
ng-click="shrLnksCtrl.share($event, 'email')"
class="fuicon-hex-email">
</a>
</li>
</ul>
</div>
</div>

Not had such an issue, but as its so infrequent could you make it reload the page?
Also, do you know about ng-cloak? it can be useful to hide the raw stuff :)
Could it be a race condition?

Related

AngularJS - update ng-repeat list in DOM instantly to allow animation

I have a list to which I can add languages. When a language is added, there is an animation for maximum eye candy sexyness. The animation is triggered by a state change in ng-show="lang.visible" in the HTML. I have to first add the language to the language array, and THEN change the visible state to true for the animation to show.
The problem is, when I add the language to the array, it takes around 700 ms for the DOM to get updated, and then I change the state of visible. Those 700 ms make the page feel slow.
$scope.addTourDescLang = function(newLang) {
newLang.visible = false;
$scope.newClient.tours.languages.push(newLang);
$timeout(function() {
newLang.visible = true;
}, 700 );
};
Here's the animation code:
<script>
app.animation('.slide-toggle', ['$animateCss', function($animateCss) {
var lastId = 0;
var _cache = {};
function getId(el) {
var id = el[0].getAttribute("data-slide-toggle");
if (!id) {
id = ++lastId;
el[0].setAttribute("data-slide-toggle", id);
}
return id;
}
function getState(id) {
var state = _cache[id];
if (!state) {
state = {};
_cache[id] = state;
}
return state;
}
function generateRunner(closing, state, animator, element, doneFn) {
return function() {
state.animating = true;
state.animator = animator;
state.doneFn = doneFn;
animator.start().finally(function() {
if (closing && state.doneFn === doneFn) {
element[0].style.height = '';
}
element[0].style.height = 'auto';
state.animating = false;
state.animator = undefined;
state.doneFn();
});
}
}
return {
addClass: function(element, className, doneFn) {
if (className == 'ng-hide') {
var state = getState(getId(element));
var height = (state.animating && state.height) ?
state.height : element[0].offsetHeight;
var animator = $animateCss(element, {
from: {height: height + 'px'},
to: {height: '0px'}
});
if (animator) {
if (state.animating) {
state.doneFn =
generateRunner(true,
state,
animator,
element,
doneFn);
return state.animator.end();
}
else {
state.height = height;
return generateRunner(true,
state,
animator,
element,
doneFn)();
}
}
}
doneFn();
},
removeClass: function(element, className, doneFn) {
if (className == 'ng-hide') {
var state = getState(getId(element));
var height = (state.animating && state.height) ?
state.height : element[0].offsetHeight;
var animator = $animateCss(element, {
from: {height: '0px'},
to: {height: height + 'px'}
});
if (animator) {
if (state.animating) {
state.doneFn = generateRunner(false,
state,
animator,
element,
doneFn);
return state.animator.end();
}
else {
state.height = height;
return generateRunner(false,
state,
animator,
element,
doneFn)();
}
}
}
doneFn();
}
};
}]);
(function() {
var app = angular.module('app', ['ngAnimate']);
app.animation('.slide-toggle', ['$animateCss', function($animateCss) {
return {
addClass: function(element, className, doneFn) {
if (className == 'ng-hide') {
var animator = $animateCss(element, {
to: {height: '0px'}
});
if (animator) {
return animator.start().finally(function() {
element[0].style.height = '';
doneFn();
});
}
}
doneFn();
},
removeClass: function(element, className, doneFn) {
if (className == 'ng-hide') {
var height = element[0].offsetHeight;
var animator = $animateCss(element, {
from: {height: '0px'},
to: {height: height + 'px'}
});
if (animator) {
return animator.start().finally(doneFn);
}
}
doneFn();
}
};
}]);
})();
</script>
The CSS:
.slide-toggle {
overflow: hidden;
transition: all 0.25s;
}
The HTML:
<table class="table-list">
<tbody ng-repeat="langAdded in newClient.tours.languages">
<tr>
<td>
<div ng-show="langAdded.added" class="table-list-content slide-toggle">
<div>
<img ng-src="/images/icons/flags/flag-{{ langAdded.code }}-30x20.png" style="vertical-align: middle; margin: 0 10px 0 20px;">
{{ langAdded.title }}
</div>
</div>
</td>
<td>
<div ng-show="langAdded.added" class="slide-toggle">
<md-button class="md-icon-button remove-icon-button" ng-click="removeTourDescLang(langAdded)" aria-label="Remove">
<md-icon md-svg-icon="/images/icons/svg/cross-close.svg"></md-icon>
</md-button>
</div>
</td>
</tr>
</tbody>
</table>
How can this be done in a better way?
How can I update DOM instantly, and then directly after changing the visible state to true?

How can I make square-connect work with angularjs?

Basing myself on the example provided by the SquareUp documentation (https://github.com/square/connect-api-examples.git). I am trying to integrate squareup to process payments with CC but I do not know what happens.
the view:
<div class="bg-light lter b-b wrapper-md">
<h1 class="m-n font-thin h3"></h1>
</div>
<div class="wrapper-md" >
<div class="row">
<div class="col-sm-6">
<div class="panel panel-default">
<div class="panel-heading font-bold">CC info</div>
<div class="panel-body">
<div class="no-boot" ng-controller="PaymentController" ng-cloak>
<div id="successNotification" ng-show="isPaymentSuccess">
Card Charged Succesfully!!
</div>
<form novalidate id="payment-form" ng-hide="isPaymentSuccess">
<div id="card-errors" ng-repeat="error in card_errors">
<li>{{error.message}}</li>
</div>
<div>
<label>Card Number</label>
<div ng-model="data.card.card_number" id="sq-card-number"></div>
</div>
<div>
<label>CVV</label>
<div ng-model="data.card.cvv" id="sq-cvv"></div>
</div>
<div>
<label>Expiration Date</label>
<div ng-model="data.card.expiration_date" id="sq-expiration-date"></div>
</div>
<div>
<label>Postal Code</label>
<div ng-model="data.card.postal_code" id="sq-postal-code"></div>
</div>
<div>
<input ng-click="submitForm()" ng-disabled="isProcessing" type="submit" id="submit" value="Buy Now" class="btn btn-primary">
</div>
</form>
<button id="sq-apple-pay" class="button-apple-pay-block" ng-show="supportApplePay"></button>
</div>
</div>
</div>
</div>
</div>
</div>
the controller:
'use strict';
/* Controllers */
// signin controller
app.controller('PaymentController', ['$scope', '$http', function($scope, $http) {
//for showing #successNotification div
$scope.isPaymentSuccess = false;
//for disabling payment button
$scope.isProcessing = false;
//for disabling apple pay button
$scope.supportApplePay = false;
$scope.data = {
product_id: "001",
user: {},
card: {},
products: {
"001": {
"name": "Paper Origami 1:10,000 scale model (11 inch)",
"value":"1.0",
},
"002": {
"name": "Plastic 1:5000 scale model (22 inch)",
"value":"49.0",
},
"003": {
"name": "Metal & Concrete 1:1000 scale replica (9 feet)",
"value":"5000.0",
}
}
};
$scope.submitForm = function(){
console.log($scope.data)
$scope.isProcessing = true;
$scope.paymentForm.requestCardNonce();
return false
}
var cardNumber = $scope.data.card.card_number = 5409889944179029;
var cvv = $scope.data.card.cvv = 111;
var expirationDate = $scope.data.card.expirationDate = '02/21';
var postalCode = $scope.data.card.postalCode = 3311;
$scope.paymentForm = new SqPaymentForm({
applicationId: 'sandbox-sq0idp-IsHp4BXhhVus21G5JPyYpw',
locationId: 'CBASECJCvmqtoIL1fn3iReEjQRcgAQ',
inputClass: 'sq-input',
inputStyles: [
{
fontSize: '14px',
padding: '7px 12px',
backgroundColor: "transparent"
}
],
cardNumber: {
elementId: 'sq-card-number',
placeholder: '5409889944179029',
value: '5409889944179029'
},
cvv: {
elementId: 'sq-cvv',
placeholder: '111',
value: '111'
},
expirationDate: {
elementId: 'sq-expiration-date',
placeholder: '04/21',
value: '04/21'
},
postalCode: {
elementId: 'sq-postal-code',
placeholder: '33114',
value: '33114'
},
applePay: {
elementId: 'sq-apple-pay'
},
// cardNumber:''+cardNumber,
// cvv:''+cvv,
// expirationDate:''+expirationDate,
// postalCode:''+postalCode,
callbacks: {
cardNonceResponseReceived: function(errors, nonce, cardData) {
if (errors){
$scope.card_errors = errors
$scope.isProcessing = false;
$scope.$apply(); // required since this is not an angular function
}else{
$scope.card_errors = []
$scope.chargeCardWithNonce(nonce);
}
},
unsupportedBrowserDetected: function() {
// Alert the buyer
},
methodsSupported: function (methods) {
console.log(methods);
$scope.supportApplePay = true
$scope.$apply(); // required since this is not an angular function
},
createPaymentRequest: function () {
var product = $scope.data.products[$scope.data.product_id];
return {
requestShippingAddress: true,
currencyCode: "USD",
countryCode: "US",
total: {
label: product["name"],
amount: product["value"],
pending: false,
}
};
},
// Fill in these cases to respond to various events that can occur while a
// buyer is using the payment form.
inputEventReceived: function(inputEvent) {
switch (inputEvent.eventType) {
case 'focusClassAdded':
// Handle as desired
break;
case 'focusClassRemoved':
// Handle as desired
break;
case 'errorClassAdded':
// Handle as desired
break;
case 'errorClassRemoved':
// Handle as desired
break;
case 'cardBrandChanged':
// Handle as desired
break;
case 'postalCodeChanged':
// Handle as desired
break;
}
}
}
});
$scope.chargeCardWithNonce = function(nonce) {
alert("no");
var url = "libs/php_payment/process-card.php";
var data = {
nonce: nonce,
product_id: $scope.data.product_id,
name: $scope.data.user.name,
email: $scope.data.user.email,
street_address_1: $scope.data.user.street_address_1,
street_address_2: $scope.data.user.street_address_2,
city: $scope.data.user.city,
state: $scope.data.user.state,
zip: $scope.data.user.zip
};
$http.post(url, data).success(function(data, status) {
if (data.status == 400){
// display server side card processing errors
$scope.isPaymentSuccess = false;
$scope.card_errors = []
for (var i =0; i < data.errors.length; i++){
$scope.card_errors.push({message: data.errors[i].detail})
}
}else if (data.status == 200) {
$scope.isPaymentSuccess = true;
}
$scope.isProcessing = false;
}).error(function(){
$scope.isPaymentSuccess = false;
$scope.isProcessing = false;
$scope.card_errors = [{message: "Processing error, please try again!"}];
})
}
//build payment form after controller loads
var init = function () {
$scope.paymentForm.build()
};
init();
}]);
error: "Error: [$rootScope:inprog] $digest already in progress
I haven't done angular in a while, but I'm betting that your issue is in:
methodsSupported: function (methods) {
console.log(methods);
$scope.supportApplePay = true
$scope.$apply(); // required since this is not an angular function
},
You are calling $apply() after a non-asyc call, generally you apply new data that you got asynchronously. See Angular Docs

directive that controlls upvotes and downvotes

I'm developing a upvote/downvote controlling system for a dynamic bunch of cards.
I can controll if I click to the img the checked = true and checked = false value but The problem I've found and because my code doesn't work as expected is I can't update my value in the ng-model, so the next time the function is called I receive the same value. As well, I can't update and show correctly the new value. As well, the only card that works is the first one (it's not dynamic)
All in which I've been working can be found in this plunk.
As a very new angular guy, I tried to investigate and read as much as possible but I'm not even 100% sure this is the right way, so I'm totally open for other ways of doing this, attending to performance and clean code. Here bellow I paste what I've actually achieved:
index.html
<card-interactions class="card-element" ng-repeat="element in myIndex.feed">
<label class="rating-upvote">
<input type="checkbox" ng-click="rate('upvote', u[element.id])" ng-true-value="1" ng-false-value="0" ng-model="u[element.id]" ng-init="element.user_actions.voted === 'upvoted' ? u[element.id] = 1 : u[element.id] = 0" />
<ng-include src="'upvote.svg'"></ng-include>
{{ element.upvotes + u[1] }}
</label>
<label class="rating-downvote">
<input type="checkbox" ng-click="rate('downvote', d[element.id])" ng-model="d[element.id]" ng-true-value="1" ng-false-value="0" ng-init="element.user_actions.voted === 'downvoted' ? d[element.id] = 1 : d[element.id] = 0" />
<ng-include src="'downvote.svg'"></ng-include>
{{ element.downvotes + d[1] }}
</label>
<hr>
</card-interactions>
index.js
'use strict';
var index = angular.module('app.index', ['index.factory']);
index.controller('indexController', ['indexFactory', function (indexFactory) {
var data = this;
data.functions = {
getFeed: function () {
indexFactory.getJSON(function (response) {
data.feed = response.index;
});
}
};
this.functions.getFeed();
}
]);
index.directive('cardInteractions', [ function () {
return {
restrict: 'E',
link: function (scope, element, attrs) {
scope.rate = function(action, value) {
var check_up = element.find('input')[0];
var check_down = element.find('input')[1];
if (action === 'upvote') {
if (check_down.checked === true) {
check_down.checked = false;
}
} else {
if (action === 'downvote') {
if (check_up.checked === true) {
check_up.checked = false;
}
}
}
}
}
};
}]);
Hope you guys can help me with this.
Every contribution is appreciated.
Thanks in advice.
I have updated your directive in this plunker,
https://plnkr.co/edit/HvcBv8XavnDZTlTeFntv?p=preview
index.directive('cardInteractions', [ function () {
return {
restrict: 'E',
scope: {
vote: '='
},
templateUrl: 'vote.html',
link: function (scope, element, attrs) {
scope.vote.upValue = scope.vote.downValue = 0;
if(scope.vote.user_actions.voted) {
switch(scope.vote.user_actions.voted) {
case 'upvoted':
scope.vote.upValue = 1;
break;
case 'downvoted':
scope.vote.downValue = 1;
break;
}
}
scope.upVote = function() {
if(scope.vote.downValue === 1) {
scope.vote.downValue = 0;
scope.vote.downvotes--;
}
if(scope.vote.upValue === 1) {
scope.vote.upvotes++;
} else {
scope.vote.upvotes--;
}
};
scope.downVote = function() {
if(scope.vote.upValue === 1) {
scope.vote.upValue = 0;
scope.vote.upvotes--;
}
if(scope.vote.downValue === 1) {
scope.vote.downvotes++;
} else {
scope.vote.downvotes--;
}
};
}
};

v-for and computed properties calling methods

I'm trying to update some properties on a VueJS object list, but I'm having some glitches.
Here's the HTML:
<div id="el">
<p>Total Chats: {{ chats.length }} ({{ unreadCount }} unread)</p>
<button v-on:click="__addChat(true)">Add a Chat</button>
<button v-on:click="__makeAllRead">Read All</button>
<pre>{{ $data | json }}</pre>
<ul>
<li v-for="chat in chats">
<p>
Message {{ chat.id }}
<span v-if="chat.unread"><strong>(UNREAD)</strong></span>
</p>
</li>
</ul>
</div>
And the Vue code:
var vm = new Vue({
el: '#el',
data: {
nextID : 2,
chats: {
'6fc5gh4j3kl_FIRST': {
id : 1,
unread : true,
body : Date(),
}
},
},
methods: {
__addChat: function (unread) {
var chat = {
id : this.nextID++,
unread : unread,
body : Date(),
};
this.chats[this.__makeHash()] = chat;
console.log(this.chats);
},
__makeAllRead : function() {
console.log(Object.keys(this.chats));
for ( var key in Object.keys(this.chats) ) {
// if any tests are invalid
if ( this.chats.hasOwnProperty(key) ) {
this.chats[key] = true;
}
}
},
__countUnread : function() {
var unread = 0;
for ( var key in Object.keys(this.chats) ) {
// if any tests are invalid
if ( this.chats.hasOwnProperty(key) && this.chats[key].unread ) {
unread++;
}
}
return unread;
},
__makeHash: function() {
return '6fc5gh4j3kl1AZ0' + Math.floor((Math.random() * 100) + 1);
},
},
ready: function() {
this.__addChat(false);
},
computed: {
unreadCount: function () {
console.log('counting...');
return this.countUnread();
}
}
});
Here's a Fiddle: http://jsfiddle.net/522aw2n5/7/
Things I cannot understand/fix on this code:
1) A computed property using a method to update the count
2) It displays only the object manually created, not the dynamically ones.
3) I cannot make all messages read by clicking a button.
This is Vue 1.0.0 RC2.
Could you, please, tell me what Am I doing wrong?
Answered on VueJS's Gitter
HTML
<div id="el">
<p>Total Chats: {{ totalChats }} ({{ unreadCount }} unread)</p>
<button v-on:click="__addChat(true)">Add a Chat</button>
<button v-on:click="__makeAllRead">Read All</button>
<pre>{{ $data | json }}</pre>
<ul>
<li v-for="chat in chats">
<p>
{{ chat.id }}
<span v-if="chat.unread"><strong>(UNREAD)</strong></span>
</p>
</li>
</ul>
</div>
Vue Code
var vm = new Vue({
el: '#el',
data: {
nextID : 2,
chats: {
'6fc5gh4j3kl_FIRST': {
id : 1,
unread : true,
body : Date(),
}
},
},
methods: {
__addChat: function (unread) {
var chat = {
id : this.nextID++,
unread : unread,
body : Date(),
};
this.$set('chats.' + this.__makeHash(), chat);
},
__makeAllRead : function() {
console.log(Object.keys(this.chats));
for ( var key in this.chats ) {
// if any tests are invalid
if ( this.chats.hasOwnProperty(key) ) {
this.chats[key].unread = false;
}
}
},
__makeHash: function() {
return 'fc5gh4j3kl1AZ0' + Math.floor((Math.random() * 100) + 1);
},
},
ready: function() {
this.__addChat(false);
},
computed: {
totalChats: function() {
var size = 0, key;
for (key in this.chats) {
if (this.chats.hasOwnProperty(key)) size++;
}
return size;
},
unreadCount: function () {
var unread = 0;
for ( var key in this.chats ) {
// if any tests are invalid
if ( this.chats.hasOwnProperty(key) && this.chats[key].unread ) {
unread++;
}
}
return unread;
}
}
});

How to do the Logic behind the next button in angularjs wizard

I have a customers.create.html partial bound to the WizardController.
Then I have 3 customers.create1,2,3.html partial files bound to WizardController1,2,3
Each WizardController1,2 or 3 has an isValid() function. This function determines wether the user can proceed to the next step.
The next button at the bottom of the pasted html should be disabed if ALL ? isValid() functions are false...
Thats my question but the same time that seems not correct to me.
I guess I am not doing the Wizard correctly...
Can someone please guide me how I should proceed with the architecture that the bottom next button is disabled when the current step isValid function returns false, please.
How can I make a connection from the WizardController to any of the WizardController1,2 or 3 ?
Is Firing an event like broadcast a good direction?
<div class="btn-group">
<button class="btn" ng-class="{'btn-primary':isCurrentStep(0)}" ng-click="setCurrentStep(0)">One</button>
<button class="btn" ng-class="{'btn-primary':isCurrentStep(1)}" ng-click="setCurrentStep(1)">Two</button>
<button class="btn" ng-class="{'btn-primary':isCurrentStep(2)}" ng-click="setCurrentStep(2)">Three</button>
</div>
<div ng-switch="getCurrentStep()" ng-animate="'slide'" class="slide-frame">
<div ng-switch-when="one">
<div ng-controller="WizardController1" ng-include src="'../views/customers.create1.html'"></div>
</div>
<div ng-switch-when="two">
<div ng-controller="WizardController2" ng-include src="'../views/customers.create2.html'"></div>
</div>
<div ng-switch-when="three">
<div ng-controller="WizardController3" ng-include src="'../views/customers.create3.html'"></div>
</div>
</div>
<a class="btn" ng-click="handlePrevious()" ng-show="!isFirstStep()">Back</a>
<a class="btn btn-primary" ng-disabled="" ng-click="handleNext(dismiss)">{{getNextLabel()}}</a>
'use strict';
angular.module('myApp').controller('WizardController', function($scope) {
$scope.steps = ['one', 'two', 'three'];
$scope.step = 0;
$scope.wizard = { tacos: 2 };
$scope.isFirstStep = function() {
return $scope.step === 0;
};
$scope.isLastStep = function() {
return $scope.step === ($scope.steps.length - 1);
};
$scope.isCurrentStep = function(step) {
return $scope.step === step;
};
$scope.setCurrentStep = function(step) {
$scope.step = step;
};
$scope.getCurrentStep = function() {
return $scope.steps[$scope.step];
};
$scope.getNextLabel = function() {
return ($scope.isLastStep()) ? 'Submit' : 'Next';
};
$scope.handlePrevious = function() {
$scope.step -= ($scope.isFirstStep()) ? 0 : 1;
};
$scope.handleNext = function(dismiss) {
if($scope.isLastStep()) {
dismiss();
} else {
$scope.step += 1;
}
};
});
durandalJS wizard sample code which could be used to rewrite a wizard for angularJS:
define(['durandal/activator', 'viewmodels/step1', 'viewmodels/step2', 'knockout', 'plugins/dialog', 'durandal/app', 'services/dataservice'],
function (activator, Step1, Step2, ko, dialog, app, service) {
var ctor = function (viewMode, schoolyearId) {
debugger;
if (viewMode === 'edit') {
service.editSchoolyear(schoolyearId);
}
else if (viewMode === 'create') {
service.createSchoolyear();
}
var self = this;
var steps = [new Step1(), new Step2()];
var step = ko.observable(0); // Start with first step
self.activeStep = activator.create();
var stepsLength = steps.length;
this.hasPrevious = ko.computed(function () {
return step() > 0;
});
self.caption = ko.observable();
this.activeStep(steps[step()]);
this.hasNext = ko.computed(function () {
if ((step() === stepsLength - 1) && self.activeStep().isValid()) {
// save
self.caption('save');
return true;
} else if ((step() < stepsLength - 1) && self.activeStep().isValid()) {
self.caption('next');
return true;
}
});
this.isLastStep = function() {
return step() === stepsLength - 1;
}
this.next = function() {
if (this.isLastStep()) {
$.when(service.createTimeTable())
.done(function () {
app.trigger('savedTimeTable', { isSuccess: true });
})
.fail(function () {
app.trigger('savedTimeTable', { isSuccess: false });
});
}
else if (step() < stepsLength) {
step(step() + 1);
self.activeStep(steps[step()]);
}
}
this.previous = function() {
if (step() > 0) {
step(step() - 1);
self.activeStep(steps[step()]);
}
}
}
return ctor;
});

Resources