ng-class not updating properly on value change - javascript

I'm working on a project that need to allow users to add/remove tags on images.
There is grid view, single view and mixed view.
The grid view displays image thumbs in a grid,
Single view displays images one by one
and the mixed view has the grid in the background, and a single images in the front (a zoom in feature).
All those views have a footer which contains the tags that can be applied to an image. However, the grid has its own footer, while the single and mixed views share theirs.
Here is the HTML side code for those :
<section id="noRefContainer" class="photosWrapper photosWrapper-cq" style="display: block"> <!--ng-controller="gridController">-->
<div class="images-cq__wrapper" ng-show="displayStyle.style == 'grid' || displayStyle.style == 'both'">
<div class="images-cq__item" ng-repeat="photo in displayedPhotos">
<div ng-class="{active: photo.selected}">
<label for="{{photo.uuid}}">
<div class="img-cq">
<img ng-src="{{photo.thumbPath100}}" alt="Alternate Text" ng-click="selectionEvent({value: photo.uuid, event: $event, index: $index})" />
zoom
</div>
<p>
{{photo.title}}
</p>
</label>
</div>
</div>
<div class="images-cq__footer open">
<p>
<span>Tagger les</span>
<strong>{{selectedPhotos.length}}</strong>
<span>éléments sélectionnés</span>
</p>
<div class="images-cq__dropdown top">
...
<ul>
<li>Sélectionner toutes les images</li>
<li>Désélectionner toutes les images</li>
</ul>
</div>
<div class="images-cq__tags">
<ul>
<li ng-repeat="tag in tags">
{{tag.name}}
</li>
</ul>
</div>
<small>Attention, ceci effacera les précédents tags.</small>
</div>
</div>
<div ng-class="{'images-cq__lightbox': displayStyle.style == 'both', 'images-cq__wrapper': displayStyle.style == 'single', single: displayStyle.style == 'single'}" ng-show="displayStyle.style == 'both' || displayStyle.style == 'single'">
<div class="images-cq__carousel">
<a href="" class="images-cq__carouselclose" ng-click="zoomClose()" ng-show="displayStyle.style == 'both'">
Close
</a>
<div class="images-cq__carouselcontent" id="carousel">
</div>
<div class="images-cq__carouselfooter">
<div class="images-cq__tags">
<ul>
<li ng-repeat="tag in tags">
{{tag.name}}
</li>
</ul>
</div>
</div>
</div>
</div>
</section>
And the app.js side code :
$scope.tags = [];
$scope.tags = [{ name: 'CQ:OK', count: 0, status: '', value: 'CQ:OK' },
{ name: 'CQ:OK_NO_ZOOM', count: 0, status: '', value: 'CQ:OK_NO_ZOOM' },
{ name: 'CQ:KO', count: 0, status: '', value: 'CQ:KO' },
{ name: 'Chromie', count: 0, status: '', value: 'Chromie' },
{ name: 'Détourer', count: 0, status: '', value: 'Détourer' },
{ name: 'Nettoyer_redresser', count: 0, status: '', value: 'Nettoyer_redresser' },
{ name: 'Interne', count: 0, status: '', value: 'Interne' },
{ name: 'Otsc', count: 0, status: '', value: 'Otsc' }];
$scope.zoomTagSelectionEvent = function (tag) {
var photo = $scope.carousel.settings.images[$scope.carousel.settings.currentImage];
if ($scope.hasTag(photo, tag.value)) {
$scope.removeTag(photo, tag.value);
}
else {
$scope.addTag(photo, tag.value);
}
$scope.updateTagStatus(tag.value);
}
$scope.tagSelectionEvent = function (tag) {
if ($scope.allHaveTag($scope.selectedPhotos, tag.value)) {
$scope.allRemoveTag($scope.selectedPhotos, tag.value);
}
else {
$scope.allAddTag($scope.selectedPhotos, tag.value);
}
$scope.updateTagStatus(tag.value);
}
$scope.updateAllTagStatus = function () {
angular.forEach($scope.tags, function (value, key) {
$scope.updateTagStatus(value.value);
});
}
$scope.updateTagStatus = function (tag) {
var currentTag = $scope.getTag(tag);
if ($scope.displayStyle.style == 'grid')
{
var tagged = $scope.countTagged($scope.selectedPhotos, tag);
if (tagged == 0) {
currentTag.status = 'none';
}
else if (tagged < $scope.selectedPhotos.length) {
currentTag.status = 'selected';
}
else {
currentTag.status = 'active';
}
}
else {
if ($scope.carousel.settings.currentImage !== false)
{
var photo = $scope.carousel.settings.images[$scope.carousel.settings.currentImage];
var res = $scope.hasTag(photo, tag);
if (res) {
currentTag.status = 'active';
}
else {
currentTag.status = 'none';
}
}
}
console.log('tag ' + tag + ' status updated');
}
Each time a tag is applied to an image, the tag status is updated, which should update the ng-class expression result. The only part that gets properly updated is the grid footer. That is shared between single/mixed view updates only when the view is shown.
As for what i tried to fix this, I've tried using $scope.apply() after each call for tag update, tried placing at the end of the updateTagStatus function. I also tried changing the expressions (class names with/without quotes, setting class to the tag status...), which all worked only for the grid footer, but not for the other. I also checked that the statuses were properly updated, the only problem is in the updating of the display.
Please help.
Update :
I am sorry for not answering in here, there was a huge list of evolutions for the project in a short amount of time so the code causing this problem is no more, which also removes the problem altogether. I was busy working on the project and forgot to update this.
However, thank you for taking the time to come here and try to help me.
I am not sure what i should do in a situation such a this.

For the first time it looks like var currentTag = $scope.getTag(tag); this line is the culprit. It might not be returning proper angular object for tag.
Or you can try using $apply function properly.
i.e. user $apply for every update statement like:
$scope.$apply(function () { currentTag.status = 'selected'; });
$scope.$apply(function () { currentTag.status = 'none'; });

Related

create observables dynamically and show it in html ,knockout?

HTML :-
<div class="row" style="margin-left: 10px;" data-bind="foreach: {data: data, as: 'data1'}">
<span style="color: red; display: none;" data-bind="attr:{'id':data1.mandatory!=0?'id'+data1.dynamicid:''},text: 'You have selected ' +app.vm.observables.totalSelected()+ ' Observation areas. Please restrict to only 2.'"></span>
</div>
here the observable is same for every div element id so value is showing same how to do it dynamically?
JAVASCRIPT :-
`$('#id'+dynamicid+' input:checkbox.paramValues:checked').each(function(e){
DataImprove++;
});`
if(DataImprove>2){
vm.observables.totalSelected(DataImprove);
}
Need to create observables dynamically based on the div element ids and show it in the frontend HTML.
I'm not entirely sure I understand what you are trying to do, but hopefully this helps
I think it would make sense to move some of the functionality out out the view and into a viewModel
var externalData = [{
mandatory: false,
dynamicid: 1,
}, {
mandatory: false,
dynamicid: 2,
}, {
mandatory: 1,
dynamicid: 3,
}, {
mandatory: false,
dynamicid: 4,
}];
function ViewModel() {
var self = this;
self.data = ko.observableArray([]);
self.totalSelected = ko.pureComputed(function() {
return self.data().filter(function(i) {
return i.selected() === true;
}).length;
});
self.selectedText = ko.pureComputed(function(){
return 'You have selected ' + self.totalSelected() + ' Observation areas.';
});
var mappedData = externalData.map(function(item) {
return {
mandatory: item.mandatory,
dynamicid: item.dynamicid,
selected: ko.observable(false),
mandatoryStatus: item.mandatory == true ? 'mandatory' : ''
};
});
self.data(mappedData);
}
var vm = new ViewModel();
ko.applyBindings(vm);
.mandatory {
color: red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<span data-bind="text: selectedText"></span>
<ul data-bind="foreach: {data: data, as: 'data1'}">
<li data-bind="class: mandatoryStatus">
<label data-bind="text: dynamicid"></label>
<input type="checkbox" data-bind="checked: selected" />
</li>
</ul>

How do I toggle a knockout foreach div individually without adding more html?

I used the foreach method to create markup foreach item in an observable array to create a treeview.
output example
category name1
content
content
category name 2
content
content
when I click on the category name I want just its content to show/hide, currently when I click on the category name it shows and hides all the categories.
var reportFilters = [
{ Text: "Campaign", Value: primaryCategories.Campaign },
{ Text: "Team", Value: primaryCategories.Team },
{ Text: "Agent", Value: primaryCategories.Agent },
{ Text: "List", Value: primaryCategories.List },
{ Text: "Inbound", Value: primaryCategories.Inbound },
{ Text: "Daily", Value: primaryCategories.Daily },
{ Text: "Services", Value: primaryCategories.Services },
{ Text: "Occupancy", Value: primaryCategories.Occupancy },
{ Text: "Data", Value: primaryCategories.Data }
];
self.showCategory = ko.observable(false);
self.toggleVisibility = function (report) {
var categoryName = report.PrimaryReportCategory;
var categoryContent = report.ID;
if (categoryName == categoryContent ) {
self.showCategory(!self.showCategory());
};
}
<div class="report-category-treeview" data-bind="foreach: $root.categories, mCustomScrollBar:true">
<ul class="column-list" >
<li class="report-category-heading" data-bind="click: $root.toggleVisibility"><span class="margin-top10" ><i class="fas fa-chevron-down"></i> <span class="report-category-name" data-bind="text: categoryName"></span></span></li>
<li id="panel" class="report-category-container" data-bind="foreach: reports, visible: $root.showCategory">
<div class="column-list-item" data-bind="click: $root.report_click, css: { 'selected': typeof $root.selectedReport() != 'undefined' && $data == $root.selectedReport() }">
<span class="column-list-text" data-bind="text: ReportName"></span>
</div>
</li>
</ul>
</div>
currently, when I click on the category name, it shows and hides all the
categories.
It's because showCategory is your single observable responsible for showing\hiding. What you really want is one show\hide observable per category.
I'm not sure how your entire data model looks like, but since you specifically asked about categories, then you should create a category view model, and probably some container view model, which I'll name here master:
var categoryVM = function (name) {
var self = this;
self.name = ko.observable(name);
self.isVisible = ko.observable(false);
self.toggleVisibility = function () {
self.isVisible(!self.isVisible());
}
// ... add here your other observables ...
}
// name 'masterVM' whatever you like
var masterVM = function () {
var self = this;
self.categories = ko.observables([]);
// ... probably add here other observables, e.g. 'reports' ...
self.init = function (rawCategories) {
rawCategories.forEach(function (item) {
categories.push(new categoryVM(item.name)); // replace 'name' with your property
}
}
}
var master = new masterVM();
master.init(getCategories()); // pass in your categories from wherever they come from
ko.applyBindings(master);
Then, in your html, this would be your outer foreach:
<div class="report-category-treeview" data-bind="foreach: categories ... />
and your lis (for brevity, I'm ommiting nested tags under your lis):
<li class="report-category-heading"
data-bind="click: toggleVisibility">
<li id="panel" class="report-category-container"
data-bind="foreach: $root.reports, visible: isVisible">

How to hide an alert and show another alert after a button is clicked in Vue JS?

I'm designing a game in Vue JS called Higher or Lower. The code below will trigger an alert to be shown on the webpage every time one of the buttons is clicked. However, the alert does not disappear after a button is clicked again.
I want to make is so that the current alert disappears after a button click so that the next alert shows up. That way, there is only one relevant alert on the webpage.
This is what the webpage looks like:
<template>
<div class="main">
<h2>
{{ message }}
</h2>
<div class="alert alert-success" v-if="correctMessage">
{{ correctMessage }}
</div>
<div class="alert alert-danger" v-if="incorrectMessage">
{{ incorrectMessage }}
</div>
<h3>
The current number is: <strong> {{ currentNumber }} </strong>
</h3>
<h4 class="score"> SCORE: {{ score }} / 8 </h4>
<p> Is the next number higher or lower? </p>
<button #click="guessHigher" class="button1">
Higher
</button>
<button #click="guessLower" class="button2">
Lower
</button>
<p> {{ numbers }} </p>
</div>
</template>
<script>
export default {
name: "HighLow",
data: function () {
return {
// the collection of numbers that the user will need to progress thru
numbers: [],
// the users current guess
guess: null,
// the users score
score: 0,
// Some info text to display to the user after their guess
correctMessage: '',
incorrectMessage: ''
}
},
computed: {
currentNumber: function () {
return this.numbers[this.score];
},
nextNumber: function () {
return this.numbers[this.score + 1];
}
},
methods: {
// Prepare the numbers array
// reset the game logic
initGame: function () {
this.score = 0;
this.guess = null;
this.numbers = this.generateNumbers(10)
},
guessHigher: function () {
// work out if the guess is correct...
var correct = this.nextNumber > this.currentNumber;
if (correct)
{
this.correctMessage = 'Correct!';
this.score++;
}
else
{
this.incorrectMessage = 'Incorrect! Game Over!';
this.shuffle(this.numbers);
this.score = 0;
}
if (this.score === 8)
{
Swal.fire({
title: 'Brilliant. Absolutely brilliant. You are not a monkey after all.',
text: 'Play again?',
imageUrl: 'https://unsplash.it/400/200',
imageWidth: 400,
imageHeight: 200,
imageAlt: 'Custom image',
})
this.shuffle(this.numbers);
this.score = 0;
}
},
guessLower: function () {
// work out if the guess is correct...
var correct = this.nextNumber < this.currentNumber;
if (correct)
{
this.correctMessage = 'Correct!';
this.score++;
}
else
{
this.incorrectMessage = 'Incorrect! Game Over!';
this.shuffle(this.numbers);
this.score = 0;
}
if (this.score === 8)
{
Swal.fire({
title: 'Brilliant. Absolutely brilliant. You are not a monkey after all.',
text: 'Play again?' ,
imageUrl: 'https://unsplash.it/400/200',
imageWidth: 400,
imageHeight: 200,
imageAlt: 'Custom image',
})
this.shuffle(this.numbers);
this.score = 0;
}
},
shuffle: function (array) {
return array.sort(() => Math.random() - 0.5);
},
generateNumbers: function (num) {
var randomNumbers = [1, 2, 3, 4, 5, 6, 7, 8, 9];
return this.shuffle(randomNumbers).slice(0, num);
},
// MY METHODS
disableButton: function() {
},
getHint: function () {
for (var i in this.numbers) {
return i;
}
// currentNumber and numbers
},
},
mounted: function () {
// component is ready for action!
this.initGame();
},
}
</script>
you can add this lines inside of your functions guessHigher & guessLower
if (correct) {
this.incorrectMessage = '' // +
this.correctMessage = 'Correct!'
this.score++
} else {
this.correctMessage = '' // +
this.incorrectMessage = 'Incorrect! Game Over!'
this.shuffle(this.numbers)
this.score = 0
}
also should be more convenient to use v-show directive in your template:
<div class="alert alert-success" v-show="correctMessage">
{{ correctMessage }}
</div>
<div class="alert alert-danger" v-show="incorrectMessage">
{{ incorrectMessage }}
</div>
My bad. there is no .remove() function in Vuejs. Mixed something up there.
var vue = new Vue({
el:"YourDivClassOrID",
data: {
isShowing:false,
}
})
Then you can change the isShowing variable to true or false to see or hide it.
Hope this would help you :)

Keeping track of added element in an array?

I'm playing around with vue.js and the .vue components, and as newbie, I'm wondering how can I keep track of the element I add in the array.
The situation is the following :
The user add a new element from a form
When he submit, the data are automatically added to a ul>li element, and a POST request is made to the API
When the POST is done, I want to update the specific li with the new data from the server.
The thing is, I can not target the last li because the server can take time to process the request (he do a lot of work), so the user may have added 1, 5, 10 other entries in the meantime.
So how can I do ?
Here's my code so far :
<template>
<form method="post" v-on:submit.prevent="search">
<input type="text" placeholder="Person name" required v-model="name" v-el="nameInput" />
<input type="text" placeholder="Company" required v-model="company" v-el="domainInput" />
<input type="submit" value="Search" class="btn show-m" />
</form>
<ul>
<li transition="expand" v-for="contact in contacts">
<img v-bind:src="contact.avatar_url" width="40px" height="40px" class="cl-avatar" />
<div class="cl-user">
<strong class="cl-name">{{contact.name}} <span class="cl-company">{{contact.company}}</span></strong>
</div>
</li>
</ul>
</template>
<script>
export default {
data () {
return {
contacts: [],
name: null,
company: null
}
},
methods: {
search: function (event) {
this.$http.post('/search', {
name: this.name,
company: this.company
}).then(function (xhr) {
// HERE ! How can I target the exact entry ?
this.contacts.unshift(xhr.data)
})
this.name = ''
this.company = ''
this.contacts.unshift({'name': this.name, 'company': this.company})
},
}
}
</script>
Thank you for your help ! :)
If you know that the name and company fields are unique you could search through the array to find it... otherwise you can just wait to append it to the array until the return function:
search: function (event) {
this.$http.post('/search', {
name: this.name,
company: this.company
}).then(function (xhr) {
this.contacts.unshift(xhr.data)
})
this.name = ''
this.company = ''
},
I finally found a working solution : I use a component instead of <li /> for each entries, and manage the state of these inside the component :
<ul>
<contact-entry v-for="contact in contacts" v-bind:contact="contact"></contact-entry>
</ul>
That way, when I add a new entry in the array (described above), a new instance of the component contact-entry is made.
Inside that component, I did the following :
<script>
export default {
props: ['contact'],
created: function () {
if (this.contact.id === undefined) { // If it's a new contact
this.$http.post('search/name', { // I do the post here
name: this.contact.name,
domain: this.contact.company.name
}).then(function (xhr) {
this.contact = xhr.data // This will update the data once the server has replied, without hassle to find the correct line
})
}
}
}
</script>
That's it ! :) In the parent's component, I removed the xhr request and simplified the method to :
<script>
export default {
data () {
return {
contacts: [],
name: null,
company: null
}
},
methods: {
search: function (event) {
this.name = ''
this.company = ''
this.contacts.unshift({'name': this.name, 'company': this.company})
}
}
}
</script>

why din't computed don't track changes in this simple knockout app?

The menu is what I want, when mouse over the left, the right should changes but doesn't.
Here is my simplified viewmodel:
var currentSelectIndex = 0;
var AppModel = {
CurrentIndex: ko.observable(currentSelectedIndex),
OnMouseOver: function (data, event) {
// change currentIndex or currentSelectedIndex here
// CurrentSubCategory didn't updated
},
CurrentSubCategory: ko.computed({
read: function() {
return AppModel.Menu[AppModel.CurrentIndex()].subcategory;
},
deferEvaluation: true
}),
Menu: [
{
subcategory: [
{ name: '1', id: 50000436 },
{ name: '2', id: 50010402 },
{ name: '3', id: 50010159 }
],
}
};
And my html:
<div class="categories" id="categories">
<div class="first-category" id="first-category">
<ul data-bind="foreach:Menu">
<li data-bind="text:name,attr:{id:id,class:className},event{ mouseover: $root.myfunction}"></li>
</ul>
</div>
<div class="sub-category" id="sub-category">
<ul data-bind="foreach:CurrentSubCategory()">
<li><a data-bind="text:name,attr:{href:getListUrl(id)}"></a></li>
</ul>
<div class="clear">
</div>
</div>
<div class="clear">
</div>
</div>
Sorry, can't post images due to less than 10 reputation.
Thanks for any help!
There were several syntax errors in your code which I imagine are a result of your making it simpler to post.
I have posted a working jsFiddle here: http://jsfiddle.net/Gy6Gv/2/
I changed Menu to be an observable array only because knockout provides the helper method .indexOf to make it easier to get the index of the menu from the mouseover. Other than that there was no problem with the computed. I imagine there is some other syntactical error in your actual code.

Categories

Resources