Deleting entry with Restangular - javascript

I am using Restangular in my AngularJS app. I have a table with a delete link for each item. I would like to delete the item and have the row automatically removed. But as things are it only deletes from DB. How can I refactor things so that it the DOM is updated automatically?
// The controller
angular.module('myApp').controller('ManageCtrl', function($scope, Restangular) {
$scope.delete = function(e) {
Restangular.one('product', e).remove();
};
Restangular.all('products').getList({}).then(function(data) {
$scope.products = data.products;
$scope.noOfPages = data.pages;
});
});
// The view
<li ng-repeat="product in products">
</li>
I would also love to find an example of this - even with Angular resource. All the admin/data table demos seem to work from static data.

According to Restangular https://github.com/mgonto/restangular#restangular-methods they mention that you should use the original item and run an action with it, so in your html code you should:
<li ng-repeat="product in products">
</li>
Then in your controller:
$scope.delete = function( product) {
product.remove().then(function() {
// edited: a better solution, suggested by Restangular themselves
// since previously _.without() could leave you with an empty non-restangular array
// see https://github.com/mgonto/restangular#removing-an-element-from-a-collection-keeping-the-collection-restangularized
var index = $scope.products.indexOf(product);
if (index > -1) $scope.products.splice(index, 1);
});
};
Notice they use the underscore.js without which will remove the element from the array. I guess that if they post that example in their readme page that means the .remove() function doesn't remove the original item from the collection. This makes sense, since not every item you remove you want removed from the collection itself.
Also, what happens if the DELETE $HTTP request fails? You don't want to remove the item then, and you have to make sure to handle that problem in your code.

In my case the above didn't quite work. I had to do the following:
$scope.changes = Restangular.all('changes').getList().$object;
$scope.destroy = function(change) {
Restangular.one("changes", change._id).remove().then(function() {
var index = $scope.changes.indexOf(change);
if (index > -1) $scope.changes.splice(index, 1);
});
};

Related

AngularJS + ag-grid: sticky/remembered selections with virtual paging/infinite scrolling

In an AngularJS application, I have an ag-grid that uses virtual paging/infinite scrolling to lazy-load rows from a dataset that is too large to show at once. I have turned on check-box selection in the first column, so that the user should be able to select individual rows for arbitrary application-specific actions.
The AngularJS application uses ui-router to control multiple views. So, building on the virtual-paging example with "sorting & filtering", with constructed data about Olympic winners, from the ag-grid documentation, I've further extended the code a bit. From index.html:
<body ng-controller="MainController" class="container">
<div ui-view="contents"></div>
</body>
and the following ui-router states:
myapp.config(function($stateProvider, $urlRouterProvider) {
$urlRouterProvider.otherwise("example.page1")
$stateProvider
.state('example', {
abstract: true,
views: {
contents: {
template: '<div ui-view="example"></div>'
}
}
})
.state('example.page1', {
url: '/page1',
views: {
example: {
templateUrl: 'page1.html'
}
}
})
.state('example.page2', {
url: '/page2',
views: {
example: {
template: 'Go back to the <a ui-sref="example.page1">example grid</a>.'
}
}
});
});
where page1.html looks like the following:
<div ng-controller="GridController">
<div ag-grid="gridOptions" class="ag-fresh" style="height: 250px;"></div>
</div>
<div>
<h3>Selected rows:</h3>
<ul class="list-inline">
<li ng-repeat="row in currentSelection track by row.id">
<a ng-click="remove(row)">
<div class="badge">#{{ row.id }}, {{ row.athlete }}</div>
</a>
</li>
</ul>
</div>
<p>Go to <a ui-sref="example.page2">the other page</a>.</p>
What I want to accomplish:
That selections made in the ag-grid is remembered (sticky) when scrolling a (virtual) page out of view and back again, so that a user can select multiple rows on separate pages.
That the remembered selections are available outside the grid, and support adding and removing selections (as intended by the ng-click="remove(row)" in page1.html, shown above).
That the selections should be remembered when switching from the view with the ag-grid to another one, and back again.
(Optional) That the selections are remembered for the user's session.
How can I accomplish this?
I've created a working example of this can be implemented.
First of all, we'll write a AngularJS service, selectionService to keep track of the selections:
function _emptyArray(array) {
while (array.length) {
array.pop();
}
}
function _updateSharedArray(target, source) {
_emptyArray(target);
_.each(source, function _addActivity(activity) {
target.push(activity);
});
}
myapp.factory('selectionService', function ($rootScope, $window) {
var _collections = {},
_storage = $window.sessionStorage,
_prefix = 'selectionService';
angular.element($window).on('storage', _updateOnStorageChange);
function _persistCollection(collection, data) {
_storage.setItem(_prefix + ':' + collection, angular.toJson(data));
}
function _loadCollection(collection) {
var item = _storage.getItem(_prefix + ':' + collection);
return item !== null ? angular.fromJson(item) : item;
}
function _updateOnStorageChange(event) {
var item = event.originalEvent.newValue;
var keyParts = event.originalEvent.key.split(':');
if (keyParts.length < 2 || keyParts[0] !== _prefix) {
return;
}
var collection = keyParts[1];
_updateSharedArray(_getCollection(collection), angular.fromJson(item));
_broadcastUpdate(collection);
}
function _broadcastUpdate(collection) {
$rootScope.$emit(_service.getUpdatedSignal(collection));
}
function _afterUpdate(collection, selected) {
_persistCollection(collection, selected);
_broadcastUpdate(collection);
}
function _getCollection(collection) {
if (!_.has(_collections, collection)) {
var data = _loadCollection(collection);
// Holds reference to a shared array. Only mutate, don't replace it.
_collections[collection] = data !== null ? data : [];
}
return _collections[collection];
}
function _add(item, path, collection) {
// Add `item` to `collection` where item will be identified by `path`.
// For example, path could be 'id', 'row_id', 'data.athlete_id',
// whatever fits the row data being added.
var selected = _getCollection(collection);
if (!_.any(selected, path, _.get(item, path))) {
selected.push(item);
}
_afterUpdate(collection, selected);
}
function _remove(item, path, collection) {
// Remove `item` from `collection`, where item is identified by `path`,
// just like in _add().
var selected = _getCollection(collection);
_.remove(selected, path, _.get(item, path));
_afterUpdate(collection, selected);
}
function _getUpdatedSignal(collection) {
return 'selectionService:updated:' + collection;
}
function _updateInGridSelections(gridApi, path, collection) {
var selectedInGrid = gridApi.getSelectedNodes(),
currentlySelected = _getCollection(collection),
gridPath = 'data.' + path;
_.each(selectedInGrid, function (node) {
if (!_.any(currentlySelected, path, _.get(node, gridPath))) {
// The following suppressEvents=true flag is ignored for now, but a
// fixing pull request is waiting at ag-grid GitHub.
gridApi.deselectNode(node, true);
}
});
var selectedIdsInGrid = _.pluck(selectedInGrid, gridPath),
currentlySelectedIds = _.pluck(currentlySelected, path),
missingIdsInGrid = _.difference(currentlySelectedIds, selectedIdsInGrid);
if (missingIdsInGrid.length > 0) {
// We're trying to avoid the following loop, since it seems horrible to
// have to loop through all the nodes only to select some. I wish there
// was a way to select nodes/rows based on an id.
var i;
gridApi.forEachNode(function (node) {
i = _.indexOf(missingIdsInGrid, _.get(node, gridPath));
if (i >= 0) {
// multi=true, suppressEvents=true:
gridApi.selectNode(node, true, true);
missingIdsInGrid.splice(i, 1); // Reduce haystack.
if (!missingIdsInGrid.length) {
// I'd love for `forEachNode` to support breaking the loop here.
}
}
});
}
}
var _service = {
getCollection: _getCollection,
add: _add,
remove: _remove,
getUpdatedSignal: _getUpdatedSignal,
updateInGridSelections: _updateInGridSelections
};
return _service;
});
The selectionService service allows adding and removing arbitrary objects to separate collections, identified by collection, a name you find suitable. This way the same service can be used for remembering selections in multiple ag-grid instances. Each object will be identified using a path parameter. The path is used to retrieve the unique identifier using lodash's get function.
Furthermore, the service uses sessionStorage to persist the selections during the user's whole tab/browser session. This might be overkill; we could have just relied on the service to keep track of the selections since it will only get instantiated once. This can of course be modified to your needs.
Then there were the changes that had to be done to the GridController. First of all the columnDefs entry for the first column had to be changed slightly
var columnDefs = [
{
headerName: "#",
width: 60,
field: 'id', // <-- Now we use a generated row ID.
checkboxSelection: true,
suppressSorting: true,
suppressMenu: true
}, …
where the new, generated row ID is generated once the data has been retrieved from the remote server
// Add row ids.
for (var i = 0; i < allOfTheData.length; i++) {
var item = allOfTheData[i];
item.id = 'm' + i;
}
(The 'm' in the ID was included just to make sure I didn't confused that ID with other IDs used by ag-grid.)
Next, the necessary changes to gridOptions were to add
{
…,
onRowSelected: rowSelected,
onRowDeselected: rowDeselected,
onBeforeFilterChanged: clearSelections,
onBeforeSortChanged: clearSelections,
…
}
Were the different handlers are quite straight forward, communicating with the selectionService
function rowSelected(event) {
selectionService.add(event.node.data, 'id', 'page-1');
}
function rowDeselected(event) {
selectionService.remove(event.node.data, 'id', 'page-1');
}
function clearSelections(event) {
$scope.gridOptions.api.deselectAll();
}
Now, the GridController needs to handle updates signalled by the selectionService too
$scope.$on('$destroy',
$rootScope.$on(selectionService.getUpdatedSignal('page-1'),
updateSelections));
and
function updateSelections() {
selectionService.updateInGridSelections($scope.gridOptions.api, 'id', 'page-1');
}
calls selectionService.updateInGridSelections which will update the in-grid selections of the grid in question. That was the most cumbersome function to write. For example, if a selection has been added externally (outside the grid), then we'll have to perform a forEachNode run, even if we know all the necessary nodes have already been selected in-grid; there's no way to exit that loop early.
Finally, another crux was to clear and reapply the selections before and after, respectively, when the filters or sort orders are changed, or when new data is retrieved from the server (which is only simulated in the demo). The solution was to include a call to updateSelections after the params.successCallback inside the getRows handler
params.successCallback(rowsThisPage, lastRow);
updateSelections();
Now, the most puzzling findings during the implementation of this solution was that the ag-grid API grid options onAfterFilterChanged and onAfterSortChanged couldn't be used for reapplying the selections because they trigger before the (remote) data has finished loading.

Updating Json Data for angular-flexslider breaks on 'removeSlide'

Problem: My json object is updating, but the slider does not update for all resorts. It should be updating as the json object changes but sometimes does not.
For the resorts (image collections) that do not update, it gives me an error: "cannot read property 'element' of undefined and breaks on angular-flexslider.js line 104. I cannot find any relation with the resorts that are giving me this error vs the ones that do not.
Summary of my script: I'm using angular-flexslider with a slider sync. I have a service that grabs image data and sends it to the controller. The controller picks it up and runs reorganize(), which takes the object it is given and reformats it into an array that flexslider supports.
This object needs to be updated as the images are updated. I have a dropdown that allows users to change the resort and I want the slider to reflect those changes.
Here is my code
JS:
resortModule.controller('galleryController', ['$scope', 'locaService', function($scope, locaService) {
//object to receive images
$scope.images;
//object used for image slider
$scope.imagePaths = [];
//variable that gives me resort ID
$scope.resort;
//restructures images array to work better with image slider
$scope.reorganize= function(){
$scope.imagePaths.length= 0;
for(var i = 0; i < $scope.images.length; i++) {
var obj= {custom: "assets/images/resorts/" + $scope.images[i].resort + "/gallery/" + $scope.images[i].file_name, thumbnail:"assets/images/resorts/" + $scope.images[i].resort + "/thumbnail/" + $scope.images[i].file_name}
$scope.imagePaths.push(obj);
}
}
//watches factory for updates to objects/ variables
$scope.$on('imagesUpdated', function() {
$scope.images = locaService.images;
$scope.reorganize();
});
$scope.$on('resortUpdated', function() {
$scope.resort = locaService.resort;
});
}]);
HTML:
<flex-slider slider-id="slider" flex-slide="image in imagePaths track by $index" animation="fade" animation-loop="false" sync="#carousel" slideshow="false" control-nav="false" prev-text="" next-text="" init-delay="100">
<li>
<img ng-src="{{image.custom}}" alt="Luxury Resort Rental Image">
</li>
</flex-slider>
<flex-slider class="slides hide-tablet-down" slider-id="carousel" flex-slide="image in imagePaths track by $index" animation="slide" animation-loop="false" item-width="210" item-margin="5" as-nav-for="#slider" slideshow="false" prev-text="" next-text="" control-nav="false">
<li>
<img ng-src="{{image.thumbnail}}" alt="Luxury Resort Rental Image">
</li>
</flex-slider>
Does anyone have any insight on this error? Or what I might be doing wrong? I've been researching it all day but have only found this:
https://github.com/thenikso/angular-flexslider/issues/53
and this:
https://github.com/thenikso/angular-flexslider/pull/63
Which did nothing for me, but I might not have understood them entirely. I'm not a seasoned angular developer and I'm learning as I go along. Thanks in advance for your help.
I commented out the following lines in the angular-flexslider.js and it seemed to fix the problem. Let me know if you have a better solution:
if ((toAdd.length === 1 && toRemove.length === 0) || toAdd.length === 0) {
...
return $scope.$evalAsync(function() { return slider.addSlide(item.element, idx); }); }); } return; }`

AngularJS + Rails $scope model is updated but not firing a re-render

Not sure if I'm using Angular correctly. I basically want to make a simple Link/Story sharing site.
var Story = $resource("api/stories/:id", {}, {
query: { method: "GET", isArray: false }
});
Story.query().$promise.then(function (data) {
$scope.stories = data.stories;
$scope.test = 1;
});
$scope.submitForm = function(formData) {
Story.save({}, formData, function(data) {
$scope.$apply(function() {
$scope.stories = data.stories;
$scope.test = 2;
});
});
}
Changes to $scope.test get reflected, but not stories.
Edit: Uhhh so apparently if I remove "ng-controller"
<ul ng-controller="storyController">
<li ng-repeat="story in stories">{{ story.title }}</li>
<p>{{ test }}</p>
</ul>
Everything works now..... I'm not even sure why....
In the query() method, you are setting $scope.stories to an Array however, it appears you are then attempting to set $scope.stories to, what I assuming, is a single story object. What exactly are you expecting to see when $scope.stories changes? A new story in the list of stories? If so, you need to do the following:
$scope.stories.push(data.stories);
You are not replacing the array, you are adding a new index to it.
Okay so apparently the answer was that I had already defined ng-view + a route that assigned a storyController to that view already. I was assigning it twice essentially by adding an ng-controller directive.

Unable to get Quicksand.js working

I'm trying to filter collections of data using quicksand.js. However, I'm having problems getting my data to appear. I have been able to get the data to disappear. Yet, it won't re-appear. I've created a jsfiddle, which is available here. Basically, my JavaScript looks like this:
var $content = $('#stage');
var $data = $content.clone();
function filterData(tag) {
var data = [];
if (tag === null) {
data = $data.find('li');
} else {
data = $data.find('li[data-tags=' + tag + ']');
}
console.log(data);
$content.quicksand(data, {
duration: 800,
easing: 'easeInOutQuad'
});
return false;
}
Everything looks correct to me. I can't figure out what I'm doing wrong.
First, your fiddle is broken. One, you link quicksand 1.3 and pair it with a recent jquery version it doesn't support. Two, you call out the easeInOutQuad without linking the jquery.easing.1.3.js. Three, you have scope issues, the filterData function is not defined globally.
Your real problem, though, is this line in the documentation:
attribute – attribute containing unique value able to identify same item within source and destination collection, default: 'data-id'
None of your "stage" data lis have this attribute so it won't filter them properly. Add it and all seems to work:
<ul id="stage">
<li data-tags="A" data-id="1">Item A-1</li>
<li data-tags="A" data-id="2">Item A-2</li>
<li data-tags="B" data-id="3">Item B-1</li>
<li data-tags="B" data-id="4">Item B-2</li>
</ul>
Updated fiddle.

Creating Bootstrap tabs using Knockout.js foreach

I'm trying to create some tabs, one per profile the user chooses to save. Each profile is a ViewModel. So I thought I'd just create another ViewModel that contains an observableArray of objects of type: {name: profile_name, model: model_converted_to_json}.
I followed this example to create my code - but I get nothing bound, for some reason.
Here's my code:
-ViewModel (I use Requirejs, that explains the external wrapper):
"use strict";
// profiles viewmodel class
define(["knockout"], function(ko) {
return function() {
var self = this;
this.profilesArray = ko.observableArray();
this.selected = ko.observable();
this.addProfile = function(profile) {
var found = -1;
for(var i = 0; i < self.profilesArray().length; i++) {
if(self.profilesArray()[i].name == profile.name) {
self.profilesArray()[i].model = profile.model;
found = i;
}
}
if(found == -1) {
self.profilesArray().push(profile);
}
};
};
});
-The JS code (excerpt of larger file):
var profiles = new profilesViewMode();
ko.applyBindings(profiles, $("#profileTabs")[0]);
$("#keepProfile").on("click", function() {
var profile = {
name: $("#firstName").text(),
model: ko.toJSON(model)
};
profiles.addProfile(profile);
profiles.selected(profile);
console.log(profile);
$("#profileTabs").show();
});
-The HTML (Thanks Sara for correcting my HTML markup)
<section id="profileTabs">
<div>
<ul class="nav nav-tabs" data-bind="foreach: profilesArray">
<li data-bind="css: { active: $root.selected() === $data }">
</li>
</ul>
</div>
</section>
I have verified that the observableArray does get new, correct value on button click - it just doesn't get rendered. I hope it's a small thing that I'm missing in my Knockout data-bind syntax.
Thanks for your time!
You will want to call push directly on the observableArray, which will both push to the underlying array and notify any subscribers. So:
self.profilesArray.push(profile);
You are setting name using name: $('#firstName').text(); you may need to change that to .val() if this is referencing an input field (which I assumed here).
You are using .push() on the underlying array which bypasses ko's subscribers (the binding in this case)
Here is a working jsfiddle based on your code. I took some liberties with model since that wasn't included.

Categories

Resources