I am using AngularJS to create a page which contains a list of products that shows information such as a name, price, region etc. This is displayed kind of like an accordion with the name in the header and extra information in the body.
Since there could be a large amount of these items displayed I am using dirPagination (https://github.com/michaelbromley/angularUtils/tree/master/src/directives/pagination) to paginate these items. My markup at the moment looks like this:
<div class="custom-list" dir-paginate="asset in assets | itemsPerPage: 10 track by $index">
<div class="custom-list-item" ng-class="{'tag-hover': isHoveredTag(asset)}">
<div class="custom-list-item-heading" ng-click="toggleShowAssetDetails(asset)">
<p class="col-lg-5">{{ asset.name }}</p>
<div class="col-lg-offset-2 col-lg-3 rating">
<rating value="asset.rating" />
</div>
<button ng-click="addAsset(asset)"><span class="glyphicon glyphicon-plus"></span></button>
<div class="clearfix"></div>
</div>
<div class="custom-list-item-content" style="display: none" animate="shouldShowAssetDetails(asset)">
...
</div>
</div>
</div>
As you can see I'm using paginate in a pretty standard way just looping through the items in an array and displaying 10 per page. I also have a directive called rating which looks at a value called rating in the item. This is a number from 1 - 5 which is used to display a star rating system next to the name. The directive looks like this:
var rating = function ($compile) {
return {
restrict: "E",
scope: {
value: "="
},
link: function (scope, element, attrs) {
scope.$watch(attrs.rating, function () {
for (var i = 1; i <= 5; i++) {
if (i <= scope.value) {
var starElement = angular.element("<span class=\"icon icon-crown\"></span>");
$compile(starElement)(scope);
element.append(starElement);
} else {
var emptyStarElement = angular.element("<span class=\"icon-empty icon-crown\"></span>");
$compile(emptyStarElement)(scope);
element.append(emptyStarElement);
}
}
})
}
}
}
This looks at the value and inserts the icons based on the value of rating (e.g if the rating was 2 the directive would insert two icon-crown icon spans and 3 icon-empty icon-crown icon spans.
This was working perfectly fine before I included the pagination. However now it will only work for the first 10 items. When you change the page, the rating will not change and just keep the same icons from the previous page, even if the values are different.
I understand this is because the directive sets everything at the beginning and it will not run when you change page because the items aren't reloading they are just being shown and hidden again. But the directive is manipulating the DOM so it doesn't update when the page changes.
The problem is I don't know how to resolve this. I thought about changing the directive to look for the pagination current page instead but then I don't have access to the current list item.
I'd appreciate any help on getting the directive to update the icons when the page is changed.
Update
Here's a link to a Plunker project showing the problem I'm having: http://plnkr.co/edit/VSQ20eWCwVpaCoS7SeQq?p=preview
This is a very stripped down version of the section on my app that I'm having an issue with. There's no styling included although I have kept the CSS class structure. I've also changed the icons to use bootstrap ones just to simplify the Plunker project.
The functionality is the same however. If you go from page 1 to page 2 notice how the stars remain the same despite that fact that the asset rating values are different. However if you go to page 3 and back to page 2 or 1 they will change. The reason this happens is because there are less items on page 3 and therefore when you go back to page 1 or 2 the remaining items will be called again to retrieve the rating values.
You simply need to remove or replace track by $index.
Tracking by $index will give you the following behavior:
There is an array of max 10 length that represents the items to show. The first item will have index 0.
You go to the next page and the items in the array are replaced.
The first item in the array will still have index 0. Since you are tracking by index and the index has not changed, AngularJS will not recreate the DOM node representing the item. Since the DOM node isn't recreated, the directive will not execute this time and the rating will not update.
If you go from page 3 to page 2, the directive will execute for the 7 last elements on page 2, since they didn't exist on page 3, so they are considered new this time.
If you really need to use track by you should use a property that is actually related to the object (and unique), for example:
dir-paginate="asset in assets | itemsPerPage: 10 track by asset.name"
Demo: http://plnkr.co/edit/A80tSEliUkG5idBmGe3B?p=preview
Related
I created a page where I list all my items in a BootstrapVue b-table. for each item, I added the possibility to delete the item.
When I activate pagination with b-pagination element and :per-page attribute, I have an unexpected behavior on the page. The problem is it works only for the 1st page, but all others pages get the data of the 1st page. For example, if the total item is 10 and I'm displaying 5 items per page, the second page displaying the next 5 items will still try to delete the first page items.
<b-table
id="traffic-routes"
:fields="fieldsForTrafficRoutes"
:items="itemsForTrafficRoutes"
:per-page="perPageForTrafficRoutes"
:current-page="currentPageForTrafficRoutes"
>
<template v-slot:cell(action)="data">
<div class="d-flex align-items-center justify-content-end">
<a class="action-icon mr-2"
#click="removeRow(data.index)"
> Delete </a>
</div>
</template>
</b-table>
<b-pagination
v-model="currentPageForTrafficRoutes"
aria-controls="traffic-routes"
align="right"
:total-rows="
itemsForTrafficRoutes ? itemsForTrafficRoutes.length : 0"
:per-page="perPageForTrafficRoutes"
></b-pagination>
export default {
data() {
return {
perPageForTrafficRoutes: 5,
currentPageForTrafficRoutes: 1,
// ...
}
},
computed: {
...mapGetters([
'getAllTrafficRoutes',
]),
itemsForTrafficRoutes() {
return JSON.parse(JSON.stringify(this.getAllTrafficRoutes));
},
}
}
However, I tried to make the items dynamic:
itemsForTrafficRoutes() {
const foo = JSON.parse(JSON.stringify(this.getAllTrafficRoutes));
return foo.slice((this.currentPageForTrafficRoutes - 1) *
this.perPageForTrafficRoutes, this.currentPageForTrafficRoutes * this.perPageForTrafficRoutes
);
},
This will make the items unique for each page, it will only contain the first 5 on the first and then update to the next 5 items on the next page, however, the table only displays the first page and is empty on the next page even though the items are present
That's because you're using the index from the scoped slot.
The index will be based the displayed rows, and not the actual index of the item in the array provided to the items prop.
This is described on the docs down in the notes under the Scoped field slots section.
index will not always be the actual row's index number, as it is computed after filtering, sorting and pagination have been applied to the original table data. The index value will refer to the displayed row number. This number will align with the indexes from the optional v-model bound variable.
I would suggest either using a unique identifier from your object if you have one, or using the reference to remove it.
Codesandbox link.
Okay, so the high level functionality of what I want this app to do:
Upon load, you're presented with the characters on the left, and on the right an intro screen.
When a user clicks on the first character (Cloud) for example, then the intro screen is replaced with Cloud's stats. If a user clicks on the second, third, etc character, same thing. The intro screen on the right is replaced with that character's specific stats.
That's basically it. I'm finding it really tough to do, for some reason.
For the /components/ folder breakdown (I'll go over the important ones):
/components/content/ - this is where the characters information lives, housed in the State. Each character is uniquely identified by an 'id'.
/components/sidebar/ - this is the sidebar component
/components/card-overview/ - this is a single component, repeated a few times, that houses the 'overview' of each character (image, character name), that you see in the sidebar itself
/components/card-detailed/ - this is where it grabs the array of objects from /components/content/, filters through and maps the character you're currently seeing to the DOM. Problem is, on line 13 of this component, you'll see I've hard-coded in the id #. Obviously, I want this to change to whatever character the user clicks, it then loads THAT character's info to the DOM. I just don't know how to do that.
In short, what I'm trying to do:
When a character on the left is clicked, hide whatever is on the right and replace it with that character's information.
I made your intention working here: https://codesandbox.io/s/l2lqm76z0q
As Chris pointed out, the way to go is using react state.
Therefore, I introduced a state with the default selected item 4 and created a handleClick function that should fire once you click on one item in your sidebar. Both of them got created in App.js:
state = {
selected: 4
};
handleClick = id => {
this.setState({ selected: id });
};
Now, I transferred the handleClick function where it is needed, in this case in Sidebar.js:
<Sidebar onClick={this.handleClick} />
And the currently selected item needs to be transferred to your content (again, where it is needed).
<Content selected={this.state.selected} />
In the sidebar, I send the handleClick function again to the it's child, CardOverview, with the related id(*):
<CardOverview
image={cloudImage}
name="Cloud Strife"
onClick={() => this.props.onClick(1)}
/>
And in CardOverview, finally, we use that function we passed down the road:
<section className="card-overview" onClick={this.props.onClick}>
...
</section>
(Whether it is working or not, we see in the console - when clicking on on item, it should show the state with the currently selected item id.)
Now, in content.js, we use the selected value and pass it down to CardDetailed where we use it in the following way:
// Grab the 'characters' object from App.js, and assign it to 'this.props'
const { characters, selected } = this.props;
// Filter the chracters and return only whose 'id' belongs to that of the selected one
const filteredCharacters = characters
.filter(character => character.id === selected)
.map(character => (
<div className="characters" key={character.id}>
<p>Name: {character.Name}</p>
<p>Job: {character.Job}</p>
<p>Age: {character.Age}</p>
<p>Weapon: {character.Weapon}</p>
<p>Height: {character.Height}</p>
<p>Birthdate: {character.Birthdate}</p>
<p>Birthplace: {character.Birthplace}</p>
<p>Bloodtype: {character.Bloodtype}</p>
<p>Description: {character.Description}</p>
</div>
));
...
That`s it.
(*) Be aware, I did not improve your code, I just made it working. I see several issues that you should tackle. E.g. it is not needed to list all the characters in your sidebar manually. I'd suggest to create a file that contains your list of characters and use it in sidebar and in content. This way, you will reduce your code and maintain your characters list (and all belonging information like id) in one file.
This question already has an answer here:
Problems with `track by $index` with Angular UI Carousel
(1 answer)
Closed 4 years ago.
I am having an issue with AngularJs ng-repeat and angular-bootstrap-switch
I am using:
ng-repeat="item in items track by $index"
When I added a new item into the first index of array by:
newItem = {};
items.splice(0, 0, newItem);
The DOM contain a bs-switch:
When a new item added it reuse the first element in array base on $index, so it doesn't re-render (that's what I get from this docs). The problem I have is the previous element has switch-on.
The DOM effect issue I have is the class "switch-on" doesn't refresh on new item and it keep on.
Expected: In this case I want to switch is off instead of on like the image. Cause it's an empty object
P/s: Cause of the business so
I cannot add default value for that Switch. It need to be an empty object
I also cannot use any identifier of the item object to track replace to $index
If I use default track by $id it will cause some business issue also
TEMP SOLUTION FOR WHO WORKING ON Angular 1.5 or Upper:
With angular 1.5 and you can use angular-bootstrap-switch 0.5.1
It will fixed the issue, after frapontillo release a changed: "Make "switch-change" trigger when model changes"
Thank you so much for supporting.
What I perceive from your question is that you want to add a new item to main array and when It renders, you want it's switch to be off by default.
Well there are couple of things you need to keep in mind.
ng-repeat only behaves as a loop in our DOM.
If you want to have the switch off by default, you must need to have a model for that in every item of the main array. It'll keep track of every switch's state in DOM.
Here's what you should try
From your controller:
newItem = {switchState: 0};
items.splice(0, 0, newItem);
From your HTML:
<div ng-repeat="item in items track by $index">
<!-- your Name and Decription input fields.. -->
<input
bs-switch
ng-model="item.switchState" />
So I think you're ignoring ng-model of angular-bootstrap-switch which causes you the problem.
I'm trying to do a search with product results using AngularJS. I've obtained an JSON object with my results": example below
[{"store_id":"17","user_id":"29","company_name":"Liquor R Us","company_type":"Alcohol",
"phone":"(303) 555-5555","website":"http:\/\/liqourrus.com","address":"5501 Peoria St",
"address_2":"","city":"Denver","state":"CO","zip":"80239","lat":"39.796181",
"lng":"-104.84863","mon_open":"","mon_close":"","tues_open":"","tues_close":"",
"wed_open":"","wed_close":"","thurs_open":"","thurs_close":"","fri_open":"","fri_close":"",
"sat_open":"","sat_close":"","sun_open":"","sun_close":"","distance":"1.1668156112981596",
"s":"s","st":"Every Store"}]
I'm using ng-repeat="store in storeResult" and the results will not display until I click on my Filter function, please note the filter is not applied!
<div class="product" ng-repeat="store in storeResult">
{{store.company_name}}
</div>
ng-click="setFilter('all');"
$scope.setFilter = function(filter) {
if(filter == 'all') {
$scope.searchProduct.product_type = '';
$scope.searchStore.company_type = '';
}
}
If I click the "setFilter" button, all results show. I'm trying to figure out how to make it display without having to click the button.
This work around only works in Chrome. Firefox and IE, never display results.
Plunker: link
My best guest is that by clicking the setFilter button you are triggering a digest cycle and your ngRepeat will be executed because of that. I suspect that you are assigning your storeResult outside the digest cycle and that's the reason is not displaying initially. I cannot tell for sure because is not in your description how is that JSON assigned to your storeAssignment .
Can you check how is that variable assigned?
Can you change the way it's and do this:
$timeout(function() {
$scope.storeAssignment = _your_json_value;
});
You will have to include the $timeout dependency but with this, a change in storeAssignment will be inside your digest cycle and the ngRepeat will see that change.
Hope that helps.
It looks like you're not setting the filter to 'all' until you click the filter function. Make sure that filter starts out as 'all'.
I am using AngularJS with Angular Strap plugin for one of my projects. On the page I have a table. One of the columns contains some links.
After clicking on the link I pop-ups Angular Strap's dropdown (as contextual menu).
This menu contains two items:
details,
board.
Clicking on details item open-ups Angular Strap's aside (with some data to be shown) - this one is working well.
The problem is, the second item should redirects to a specific page (using dropdown's href attribute). My question is - how to use angular expression in this href attribute?
Part of my view's code - with dropdown:
<button type="button" class="btn btn-link"
ng-click="selectGroup(group)"
data-html="1"
data-animation="am-flip-x"
data-placement="bottom",
bs-dropdown="dropdown">
{{ group.name }}
</button>
Bs-dropdown's dropdown definitions from controller:
$scope.dropdown = [
{
text: "Details",
click: "showDetails()" // Opens up Angular Strap's aside.
},
{
text: "Board",
href: "/group/{{ $scope.group.number }}" // Not evaluated.
}
];
Clicking on "Board" dropdown's item is redirecting to something like: "127.0.0.1:8000/#/group/{{group.number}}" rather than (i.e.) "127.0.0.1:8000/#/group/2".
As a workaround I have created another method (i.e.) openBoard() as follows:
var openBoard = function() {
var groupNumber = $scope.group.number;
window.location.href="127.0.0.1:8000/#/" + groupNumber;
}
Calling this method in "Board" item's click is working, but causes some other problems and more - is not the way I want to go (semantically incorrect).
One more thing at the end. Whenever page loads (and controller has been initialized) value stored in $scope.group is undefined. I am setting a proper value whenever group link (from above mentioned table) is being clicked (and a moment before dropdown is displayed).
Anyone has any ideas how to use Angular expressions with Angular Strap's dropdown (in href)?
As $scope.group member doesn't available at starting while creating dropdown array, that will result href: "/group/{{ $scope.group.number }}" this to href: "/group/undefined". So I'd suggest you to initially load dropdown with one value,
$scope.dropdown = [
{
text: "Details",
click: "showDetails()" // Opens up Angular Strap's aside.
}
];
& whenever group values gets fetched, do assign the second dropdown option
$scope.dropdown.push({
text: "Board",
href: "/group/" + $scope.group.number
});
Hope this work around would help you, Thanks.
I think that you must change {{ $scope.group.number }} for this /group/group.number