Add message after ng-if removes all elements - angularjs

Here's what I'm trying to do. I'm building an app using ionic, angular and I have two tabs with lists of questions that users had entered. Once a question is answered on the first tab (tab a) it gets hidden using ng-if because a property gets added called 'conversation' so the question moves from one tab to the other via it being hidden in one and shown in the other. This all works fine.
However, once there's no question left because they've all been hidden in tab a, i'd like to show a message. This has been the most exhausting trial an error (mostly error) i've ever done. i've different directive configurations and, most recently, a custom filter, nothing seems to work perfectly.
Here's some experimental code. I tried listening on the tab. (this is just one try. I also tried custom filter and another configuration of this directive)
//template
<ion-view view-title="Questions">
<ion-content>
<ion-list ng-show="rooms" class="display-text">
<ion-item class="item-icon-left questions" ng-repeat="room in rooms" ng-if="!room.conversation" type="item-text-wrap" ng-click="openChatRoom(room.schoolid, room.$id, room.userid, room.question)">
<i class="icon {{room.icon}}"></i>
<h2>{{room.question}}</h2>
</ion-item>
</ion-list>
<div class="msg"></div>
<ion-list ng-hide="rooms.length">
<ion-item class="textCenter">
<i class="icon ion-loading-c"></i> Loading Rooms
</ion-item>
</ion-list>
</ion-content>
restrict: 'C',
link: function(scope, elem, attrs) {
scope.$watch('tabs', function (){
var myEl = angular.element( document.querySelector('.questions'));
if(scope.tabs == 'ctrl1'){
if(!myEl){
var temp = $compile('<ion-item class="textCenter"><i>No conversations<i></ion-item>');
var content = temp(scope);
elem.find('div').append(content);
}
}
if(scope.tabs == 'ctrl2'){
if(!myEl){
var temp = $compile('<ion-item class="textCenter"><i>No current questions<i></ion-item>');
var content = temp(scope);
elem.find('div').append(content);
}
}
});

It looks like you are trying to write Angular as though it is jQuery. From what you've described you shouldn't need to manually append elements to the DOM like that, at least not for something as simple as this. Just put the message you want to display in the template and show/hide it based on how many questions you have left (your questions model).
Here's a possible solution just using Angular an ui.bootstrap for the tabs. This is just an example of a possible approach you might take and you will need to adjust it to your particular case but hopefully you will find it helpful.
They key point here is that you should be thinking in terms of adding/removing data from a model and dealing with basic display logic directly inside your template rather than manually adding/removing elements to the DOM a la jQuery.
DEMO
js
var app = angular.module('plunker', ['ui.bootstrap']);
app.controller('MainCtrl', function ($scope, $window) {
$scope.questions = [
{
content: 'What is the capital of Australia?',
answer: 'Canberra'
},
{
content: 'How many days are there in a week?',
answer: '7'
}
];
$scope.answeredQuestions = [];
$scope.userAnswer = {
value: ''
};
$scope.submitAnswer = function(){
var userAnswer = $scope.userAnswer.value,
// remove the fist question from the array
question = $scope.questions.shift();
question.userAnswer = userAnswer;
question.result = (question.answer === userAnswer);
// add the answered question object to the answeredQuestions array
$scope.answeredQuestions.push(question);
$scope.userAnswer.value = '';
};
});
html
<body ng-controller="MainCtrl">
<tabset>
<tab heading="Questions">
<p ng-hide="questions.length">There are no more questions</p>
<form ng-submit="submitAnswer()" ng-show="questions.length">
<p>{{questions[0].content}}</p>
<input type="text" ng-model="userAnswer.value">
<input type="submit" value="submit answer">
</form>
</tab>
<tab heading="Answered Questions">
<p ng-hide="answeredQuestions.length">There are no conversations</p>
<table ng-show="answeredQuestions.length" class="table table-striped">
<thead>
<tr>
<th>Question</th>
<th>User Answer</th>
<th>Result</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="q in answeredQuestions">
<td>{{q.content}}</td>
<td>{{q.userAnswer}}</td>
<td>{{q.answer}}</td>
<td>{{q.result ? 'correct' : 'incorrect'}}</td>
</tr>
</tbody>
</table>
</tab>
</tabset>
</body>

Related

Duplicated Paypal buttons with Paypal REST API and AngularJS

I'm building an Single Page App where the paypal button is generated on ng-click from a button (Add products).
The problem I'm facing, is that if the user clicks this button several times, the app will generate several buttons one after the other.
This can very well happen as the user might click the button, but then go back and add an extra product, before finish the purchase.
How could I manage to remove all existing buttons before adding the new one?
The function looks like this:
$scope.formulari = function(){
paypal.Button.render({
env: 'production', // Or 'sandbox'
locale: 'es_ES',
style: {
label: 'paypal',
...
And after a few clicks, my initial HTML button <a id="paypal-button"></a> looks like this:
<a id="paypal-button">
<div id="xcomponent-paypal-button-6d3dcbc0c4" class="paypal-button paypal-button-context-iframe paypal-button-label-paypal paypal-button-size-large paypal-button-layout-horizontal" style=""></div>
<div id="xcomponent-paypal-button-46823018c3" class="paypal-button paypal-button-context-iframe paypal-button-label-paypal paypal-button-size-large paypal-button-layout-horizontal" style=""></div>
<div id="xcomponent-paypal-button-41aad29e14" class="paypal-button paypal-button-context-iframe paypal-button-label-paypal paypal-button-size-large paypal-button-layout-horizontal" style=""></div>
<div id="xcomponent-paypal-button-48d3247535" class="paypal-button paypal-button-context-iframe paypal-button-label-paypal paypal-button-size-large paypal-button-layout-horizontal" style=""></div>
</a>
Generating a button on click might not be the way you want to go with an AngularJs structure. Editing your DOM structure is more a jQuery thing and in general you don't want to mix the two (Some explanations of why: 1, 2).
An Angular way to pick this up would be the following (Explanation beneath snippet):
var app = angular.module('myApp', []);
app.controller('myCtrl', function($scope) {
$scope.articles = ['PC', 'Playstation', 'Xbox'];
$scope.cart = [];
$scope.addArticleToCart = function(article) {
$scope.cart.push(article);
}
$scope.clearCart = function() {
$scope.cart = [];
}
$scope.doPaypalThings = function() {
//REST API stuff
}
});
<!DOCTYPE html>
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.4/angular.min.js"></script>
</head>
<body>
<div ng-app="myApp" ng-controller="myCtrl">
<div ng-repeat="article in articles">
<button ng-click="addArticleToCart(article)">
{{article}}
</button>
</div>
<br>
<div ng-show="cart.length > 0">
<button id="paypal-button" ng-click="doPaypalThings()">
Paypal
</button>
</div>
<br>
<div>
In cart:
</div>
<div ng-repeat="item in cart">
{{item}}
</div>
<br>
<div>
<button ng-click="clearCart()">
Clear cart
</button>
</div>
</div>
</body>
</html>
With this method the button always exists, it just isn't visible until the requirements within the ng-show are met. In the example, the requirement is that there are items in the cart. You will notice that once the requirements are no longer met the button will disappear again.
An opposite of ng-show is ng-hide which can be used in the same way:
ng-hide="cart.length == 0" or ng-hide="cart.length < 1
If you're dead set on using your current method, you can check out this answer here, although it is not Angular.

Reset Angular dropdown option in Ionic - won't reset after value is changed from initial value

I'm trying to figure out why I can't seem to wrangle a simple angular dropdown reset.
When the view loads, you hit the button "Reset", and it changes whatever value is selected to a different one, based on what the functionality in the controller dictates. This works fine on page load, but if you change the value in the dropdown THEN click "Reset", it does nothing. It doesn't reset the dropdown as specified in the function and I can't figure it out.
Steps to reproduce:
Page loads
Click "Reset" button
Dropdown value changes to "Japanese"
Change the value back to "English"
Click "Reset" button
Nothing. It should change the value back to English, but doesn't.
I've boiled everything down to the most basic example using an extremely basic ionic example in my Codepen:
http://codepen.io/starshock/pen/EPdXpz
Basically, here is my controller code:
.controller('DropdownController', [ '$scope', '$state', function($scope, $state) {
$scope.navTitle = 'Dropdown Reset';
$scope.reset = function() {
$scope.selectedOption = $scope.languages[0];
}
$scope.languages = [
{ name: "English"},
{ name: "Japanese"}
];
$scope.selectedOption = $scope.languages[1];
}]);
And here is my template:
<script id="entry.html" type="text/ng-template">
<ion-nav-bar animation="nav-title-slide-ios7"
type="bar-positive"
back-button-type="button-icon"
back-button-icon="ion-ios7-arrow-back">
</ion-nav-bar>
<ion-view title="{{navTitle}}" class="bubble-background">
<ion-content has-header="true" padding="true">
<div class="item item-input item-select" href="#">
<label>
<div class="input-label">
Select language
</div>
<select
data-ng-options="language.name for language in languages"
data-ng-model="selectedOption">
</select>
</label>
</div>
<button class="button button-positive" ng-click="reset()">Reset</button>
</ion-content>
</ion-view>
</script>
I've been struggling with this problem for months and I have a number of instances that utilize similar code. Any Angular masters have any thoughts? :D
You need to change your controller slightly to reference the same object each time (see https://stackoverflow.com/a/17607794/360067 and the answer referenced i.e. https://stackoverflow.com/a/14049482/360067 for the basics).
Assuming the default value you want is English (if it's Japanese, just change the index to 1 in both places)
$scope.reset = function() {
$scope.filter.selectedOption = $scope.languages[0];
}
...
$scope.filter = {
selectedOption: $scope.languages[0]
};
and in your HTML
<select
data-ng-options="language.name for language in languages"
data-ng-model="filter.selectedOption">
</select>
CodePen - http://codepen.io/anon/pen/KVGqOG

Live search in AngularJS: updating the results

I want a live search: the results are queried from web api and updated as the user types.
The problem is that the list flickers and the "No results" text appears for a fraction of second, even if the list of results stays the same. I guess I need to remove and add items with special code to avoid this, calculating differences between arrays, etc.
Is there a simpler way to avoid this flicker at least, and probably to have possibility to animate the changes?
It looks like this now:
The html part is:
<div class="list-group">
<a ng-repeat="test in tests track by test.id | orderBy: '-id'" ng-href="#/test/{{test.id}}" class="list-group-item">
<h4 class="list-group-item-heading">{{test.name}}</h4>
{{test.description}}
</a>
</div>
<div ng-show="!tests.length" class="panel panel-danger">
<div class="panel-body">
No tests found.
</div>
<div class="panel-footer">Try a different search or clear the text to view new tests.</div>
</div>
And the controller:
testerControllers.controller('TestSearchListCtrl', ['$scope', 'TestSearch',
function($scope, TestSearch) {
$scope.tests = TestSearch.query();
$scope.$watch('search', function() {
$scope.tests = TestSearch.query({'q':$scope.search});
});
}]);
You should use ng-animate module to get the classes you need for smooth animation. For each ng-repeat item that's moved, added, or removed - angular will add specific classes. Then you can style those classes via CSS or JS so they don’t flicker.
An alternative way of doing what you require is to use the angular-ui bootstrap Typeahead component (check at the bottom of the post). It has a type-ahead-wait property in milliseconds and also a template url for customising it.
<div ng-app>
<div ng-controller="MyController">
<input type="search" ng-model="search" placeholder="Search...">
<button ng-click="fun()">search</button>
<ul>
<li ng-repeat="name in names">{{ name }}</li>
</ul>
<p>Tips: Try searching for <code>ann</code> or <code>lol</code>
</p>
</div>
</div>
function MyController($scope, $filter) {
$scope.names = [
'Lolita Dipietro',
'Annice Guernsey',
'Gerri Rall',
'Ginette Pinales',
'Lon Rondon',
'Jennine Marcos',
'Roxann Hooser',
'Brendon Loth',
'Ilda Bogdan',
'Jani Fan',
'Grace Soller',
'Everette Costantino',
'Andy Hume',
'Omar Davie',
'Jerrica Hillery',
'Charline Cogar',
'Melda Diorio',
'Rita Abbott',
'Setsuko Minger',
'Aretha Paige'];
$scope.fun = function () {
console.log($scope.search);
$scope.names = $filter('filter')($scope.names, $scope.search);
};
}

duplicates in a repeater are not allowed angular

I'm trying to create a form like below, this using ng-repeat directive in angular and it whenever I created a new row complains
"Duplicates in a repeater are not allowed.".
While I understand the solution for this is by putting "track by $index", however it causes another issue, which clicking delete on one row deletes the value of other field. So I suspect that track by index is OK for static text but not input form. So how to use ng-repeat correctly for my case?
My jsfiddle : demo.
Edit : I do aware that json array of object will solve my issue ( because for object angular create $$hashKey ) and already implemented this for most of my other module. But I am actually expecting some fix that can be done without really change my json array of string. Sorry for not being clear.
My current code :
HTML
<div class="row-fluid spacer10">
<a ng-click="addAKA()" class="btn btn-primary spacer5 left30"><i class="icon-plus icon-white"></i> Add New Alias</a>
</div>
<div class="row-fluid spacer10"></div>
<div class="row-fluid spacer5" ng-repeat="item in aliasList track by $index">
<input type="text" class="span6 left30" ng-model="item">
<button class="btn btn-danger" ng-click="deleteAKA($index)">delete</button>
<BR/>
</div>
Javascript
$scope.addAKA = function ()
{
if($scope.aliasList == null)
{
$scope.aliasList = [];
}
$scope.aliasList.push("");
$scope.aliasjson = JSON.stringify($scope.aliasList);
}
$scope.deleteAKA = function (idx)
{
var aka_to_delete = $scope.aliasList[idx];
$scope.aliasList.splice(idx, 1);
$scope.aliasjson = JSON.stringify($scope.aliasList);
}
I would guess this is caused when there are more than one empty strings in the list.
If this is the case, it is caused because any two empty strings are equals in JS and Angular repeater does not allow duplicate values (as clearly stated in the message). This is a valid decision as they have to relate an object in the list with its DOM tree to minimize DOM manipulation.
A solution would be to insert simple objects containing the string in the model:
$scope.addAKA = function () {
...
$scope.aliasList.push({value:""});
...
};
And adjust your template:
<input type="text" class="span6 left30" ng-model="item.value">
Since all new objects are different, your problem should be solved.
See a fiddle where a filter is implemented to transform the model back to a list of strings.
When you type in a new created input, your list stays the same. Angular on any list change will update the view (ng-repeat) and remove all new stored text. Therefore we need to add ng-change to update our list on any input change
Add ng-change="change(i, $index) to your item and it should work
HTML
<div ng-controller='ctrl'>
<ol>
<li ng-repeat='i in list track by $index'>
<input type='text' ng-model='i' ng-change="change(i, $index)"></input>
<button ng-click='deleteItem($index)'>Delete</button>
</li>
</ol>
<button ng-click='addItem()'>Add</button>
<div>ITEM: {{list | json}}</div>
</div>
Javascript
angular.module("app", []).controller("ctrl", function ($scope) {
$scope.list = ["one","two"];
$scope.addItem = function ()
{
$scope.list.push("");
};
$scope.deleteItem = function (idx)
{
var item_to_delete = $scope.list[idx];
$scope.list.splice(idx, 1);
};
$scope.change = function (item, idx)
{
$scope.list[idx] = item;
};
});
See fixed Demo in DEMO
Yes, pushing more than one empty string will result in ng-repeat complaining.
In addition, you can also try:
if ($scope.aliasList.indexOf(VALUE_TO_ADD) === -1) {
...
}

ng-repeater issue Duplicates in a repeater is not allowed

I'm trying to create a form like below, this using ng-repeat directive in angular and it whenever I created a new row complains
"Duplicates in a repeater are not allowed.".
While I understand the solution for this is by putting "track by $index", however it causes another issue, which clicking delete on one row deletes the value of other field. So I suspect that track by index is OK for static text but not input form. So how to use ng-repeat correctly for my case? See my JSFiddle for demo.
My current code :
HTML
<div class="row-fluid spacer10">
<a ng-click="addAKA()" class="btn btn-primary spacer5 left30"><i class="icon-plus icon-white"></i> Add New Alias</a>
</div>
<div class="row-fluid spacer10"></div>
<div class="row-fluid spacer5" ng-repeat="item in aliasList track by $index">
<input type="text" class="span6 left30" ng-model="item">
<button class="btn btn-danger" ng-click="deleteAKA($index)">delete</button>
<BR/>
</div>
Javascript
$scope.addAKA = function ()
{
if($scope.aliasList == null)
{
$scope.aliasList = [];
}
$scope.aliasList.push("");
$scope.aliasjson = JSON.stringify($scope.aliasList);
}
$scope.deleteAKA = function (idx)
{
var aka_to_delete = $scope.aliasList[idx];
$scope.aliasList.splice(idx, 1);
$scope.aliasjson = JSON.stringify($scope.aliasList);
}
Just change the way you iterate. Instead of this:
<div ng-repeat="item in aliasList track by $index">
do this:
<div ng-repeat="item in aliasList">
$index will be still available inside the element, use like this:
<button ng-click='deleteItem($index)'>Delete</button>
See this JSFiddle for a correct solution
There are multiple problems with your approach.
Since you are directly binding a string to ng-model and ng-repeat creates a child scope, any change to the value would not reflect back. Change you scope model to something like
$scope.list = [{text:"one"},{text:"two"}];
and bind to i.text instead of binding to i as you did earlier.
The deleteItem method was called using item instead of index. See my fiddle here
http://jsfiddle.net/JS6aJ/1/

Resources