Learning Ember.js- Persistence In Many To Many Relationships - javascript

To learn Ember.js I started writing a small bookmark application. I'm struggling with issues related to wrong data handling now.
To Explain The Application
User can add label
User can add links to selected labels
A Label can have n links
A Link can have n labels
Show links by selecting the associated labels
The Issue
Link data is not writtern to the data store. Because of this, local updates to the links model due selection changes are overwritten. Also, implementing true persistence later wouldn't work.
Narrowing down the issue
In IndexRoute I initialize the model:
model: function(params) {
return {
labels: this.get("store").find("label"),
// TODO: this is probably wrong.
links: Ember.A()
};
}
On the one side I fetch label data from the data store. On the other side I initialize link data with an empty ember array.
In my opinion this is the root of the malfunction but I don't know how to implement this properly.
I tried to replace this with bogus references to the storage adapter:
this.get("store").find("label").filter("_")
This is neither right nor does it work properly. Then it continues to the point where I then can't use the storage adapter to update records:
// TODO: this is probably wrong.
this.get("links").addObject({
name: linkName,
url: linkUrl,
labels: this.selectedLabels
});
/*store.push("link", {
name: newLink,
url: linkUrl,
labels: this.selectedLabels
});*/
And so on.
Jsbin: http://jsbin.com/ucanam/1751/edit
How to store link data properly so changing the local data of the controler won't break the application?
Edit:
I think I found my conceptual mistake with your advice. Does that mean that I always have to handle the local copy aswell?
var link = this.get("model");
link.deleteRecord();
link.save().then(function(link) {
var indexController = this.get('controllers.index');
indexController.get("links").removeObject(link);
});
In your example you used a promise to add the object to the controller. In this code sample the promise will never fulfill- therefore it only works without.
Also in the LabelController the remove method should also deletes associated links. If I use deleteRecord in the forEach loop it only deletes one label and then it somehow brings the loop to exit. Is this intentionally or have I made a mistake?
I have updated your JsBin.
http://jsbin.com/ucanam/1987/edit

I modified your JSbin http://jsbin.com/ucanam/1975/
If you want to persist records you must create them in the store with createRecord() and then save() them. The new newLink function
newLink: function() {
var store = this.get("store"),
linkName = this.get("linkName"),
linkUrl = this.get("linkUrl"),
links = this.get("links"),
selectedLabels = this.get('selectedLabels');
if(selectedLabels.get('length') > 0) {
if(linkName.length > 0 && linkUrl.length > 0) {
var newLink = store.createRecord('link',
{
name: linkName,
url: linkUrl
});
selectedLabels.forEach(function(label){
newLink.get('labels').addObject(label);
});
newLink.save().then(function(link){
links.addObject(link);
});
this.set("linkName", "");
this.set("linkUrl", "");
}
} else {
alert("You have to select a label!");
}
},
For deleting records there are problems using forEach because the result of a find to the store is a live array. You can see this discussion in GitHub https://github.com/emberjs/data/issues/772.
So your remove label function should be (note the use of toArray() to make a static copy of the live array)
remove: function() {
var indexController = this.get('controllers.index'),
label = this.get('model'),
linksPromise = this.get('store').find('link');
linksPromise.then(function(links){
links.toArray().forEach(function(link){
var linkLabels = link.get('labels'),
linkLabelsIds = linkLabels.mapProperty('id');
if(linkLabelsIds.contains(label.get("id"))) {
if(linkLabelsIds.get("length") == 1) {
console.log("delete link: "+link.get("name"));
indexController.get("links").removeObject(link);
link.deleteRecord();
link.save();
}
}
});
label.deleteRecord();
label.save();
});
}
Final note, don't forget to make a save() on the records after deleting them, jsBin here: http://jsbin.com/ucanam/1989/

Have you defined your models?
App.Label = DS.Model.extend({
title: DS.attr('string'),
links: DS.hasMany('link')
});
App.Link = DS.Model.extend({
name: DS.attr('string'),
url: DS.attr('string'),
labels: DS.hasMany('label')
});
App.IndexRoute = Ember.Route.extend({
model: function() {
this.store.find('label')
}
});
App.LabelController = Ember.ObjectController.extend({
actions:
<!-- functions here -->
});
<script type='text/handlebars' data-template-name="index">
{{#each label in model itemController='label'}}
{{label.title}}
{{#each link in label.links}}
{{link.title}}
{{/each}}
{{/each}}
</script>

Related

JavaScript Selecting Filter

Background: There's a table from which I can choose employees. I want to filter these employees by name.(I know name is not a good way to filter, this is just an example)
Basically I have a drop down from which I choose one of the filters.
My declaration: $scope.filters = null;.
I also have this deceleration to choose my filter $scope.chosenFilter= null;.
I use the following to retrieve the different filters I have $scope.filters = retrieveFilters(info.Names);
retrieveFilters looks like the following:
var retrieveFilters = function(rows) {
var filterWrapper = document.querySelector(".filterWrapper");
var datasetOptions = [];
$scope.predicate = 'Name';
if (rows) {
//Add Default option
datasetOptions.push({
name: $scope.data.Fnames.defaultFilterOptionLabel,
value: ''
});
$scope.chosenFilter = datasetOptions[0];
_.forEach(rows, function(ele) {
datasetOptions.push({
name: ele,
value: ele
});
});
} else {
filterWrapper.style.display = "none";
}
return datasetOptions;
};
I use the following to choose my filter:
$scope.$watch('chosenFilter', function() {
var filterSearchInput = document.querySelector(".filterWrapper input");
ng.element(filterSearchInput).triggerHandler('input');
});
Everything is fine and the display works on first load as I have set the default with
//Add Default option
datasetOptions.push({
name: $scope.data.Fnames.defaultFilterOptionLabel,
value: ''
});
From the default table whenever I click on an employees name hes details are displayed. However whenever I filter and click on the employees name, nothing is displayed. Whenever I click on a specific employees name at the default table and then filter in the same name the information also shows up, as I cache it each time.
I assume that you're displaying this data somewhere in your GUI using ng-repeat. Angular has a lot of great built-in features for this. Check out the answer here: AngularJS custom search data by writing custom filter for a way to approach this more from an Angular direction. You also might want to check out this question and answer: "Thinking in AngularJS" if I have a jQuery background?.

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.

How to clear backbone collection before fetching new data?

Hi all i have been working on backbone application where images are being added and existing images are being deleted or edited. Now am using router for the various section like gallery, forms but when i edit something in gallery goes back to forms and come back i am not getting the changes that i have done earlier. Instead it shows old data. I need to refresh i mean CTRL+F5 to see that changes which is not good.
I tried the .clear and .reset its resetting the collection and model but doesn't effects the data inside the collection. Can any one please help me that where i am going wrong.
Below is code for Collection and model :
var GalleryImage = Backbone.Model.extend({});
var GalleryImages = Backbone.Collection.extend({
model : GalleryImage,
url: '######',
initialize : function(){
this.reset();
if(this.reset())
{
console.log("model reset", GalleryImage.cid)
}else{
console.log("model not set")
}
this.on("reset", this.loadImagesView,this);
this.on("error", this.errorMessage,this);
//console.log(GalleryImages.get(0))
},
loadImagesView : function(){
//console.log(this);
//console.log(this.CollView.$el);
if(!this.CollView){
this.CollView = new GalleryImagesView({collection:this})
}
this.CollView.render();
},
errorMessage : function(){
jQuery('#imageBlocksDiv').html('<span id="noMsg"><h4>No images to display.</h4></span>');
}
});
And for the router is :
initGallery : function(){
jQuery('#invset').hide();
jQuery('#imagelist').children().eq(0).append(jQuery('.floor_img').find('#box').css({'z-index':'1','position':'relative','margin-top':'0px','margin-left':'0px'}));
pinterest(undefined);
jQuery("#box").show();
jQuery('#invset').hide();
jQuery('#inventorycontent').hide();
jQuery("#boxnews").hide();
jQuery("#boxzip").hide();
jQuery(".select").attr('style','display:block');
jQuery(".select").attr('id','ugallerygal');
jQuery(".pagetitle").html('Gallery');
var that = this,projectId = jQuery('#ugallerygal').val();
jQuery('#imageBlocksDiv').html('');
var imgObj = new GalleryImages();
//this.reset(imgObj);
imgObj.fetch({data: jQuery.param({projectId: projectId, add:true, reset:true}), success:function(){
console.log(imgObj)
imgObj.trigger("reset");
},
error : function(){
imgObj.collection.trigger("error");
}
});
},
Not only this i have a section that when user click on images the image open up and by clicking anywhere on image the can place a point and add details in that point which i am storing in parse, However the value on changes get saved but when i come back to the image the older position and number images are displayed instead of the newer one and updated point that are stored in parse. I guess the issue is same for both state so the the solution for one will work for all.
Any help is deeply appreciated. Thanking in advance
Thanking you,
Santosh Upadhayay
Problem could be with the order of reset and adding reset listener. Try changing
this.reset();
if(this.reset())
{
console.log("model reset", GalleryImage.cid)
}else{
console.log("model not set")
}
this.on("reset", this.loadImagesView,this);
this.on("error", this.errorMessage,this);
to
this.on("reset", this.loadImagesView,this);
this.on("error", this.errorMessage,this);
this.reset();
if(this.reset())
{
console.log("model reset", GalleryImage.cid)
}else{
console.log("model not set")
}

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.

Backbone js code smell - better way to embed a sub-view?

I'm building a backbone app using backbone-relational models (but that shouldn't matter for this question).
Basically, I have an edit button that will display a hidden div. Inside the hidden div id a sub-view (called DetailsView) that renders table elements to populate a list of users. My model (for the whole app) looks roughly like this:
{ 'id': 'foo',
'users':
{
'username': 'bobby',
'password': 'peanuts'
},
{
'username': 'sally',
'password': 'watermellon'
}
}
In my main view (that is fed by a collection of the above models), when the user clicks the edit button, this is triggered:
edit: function(){
var userModels = this.model.get('users'),
detailViewContainer = this.$('tbody.users');
console.log(name + ' has ' + userModels.length + ' models');
//clear out eveything in tbody.users to prevent dupes
detailViewContainer.html('');
//check if there are actually user models
if(userModels.length > 0){
userModels.forEach(function(user) {
var details = new DetailsView({model: user});
details.render();
detailViewContainer.append(details.el);
});
}
The code smell comes from the fact that I have to declare that detailViewContainer explicitly.
Originally, in my forEach loop would call another function in the view that contained the code to declare and render the DetailsView. However, I would loose the context of this.
My original code looked something like this:
edit: function() {
var userModels = this.model.get('users'),
detailViewContainer = this.$('tbody.users');
console.log(name + ' has ' + userModels.length + ' models');
//clear out eveything in tbody.users to prevent dupes
detailViewContainer.html('');
//check if there are actually user models
if(userModels.length > 0){
userModels.forEach(function(user) {
this.renderDetailsView;
});
}
},
renderDetailsView: function(user) {
var details = new DetailsView({model: user});
this.$('tbody.users').append(details.render());
},
In the renderDetailsView, I would loose context of this and could not append the DetailsView to the proper DOM element (the view would append to all of the tbody.users DOM elements , as the this context became the window since it was in a loop).
Having to explicitly declare a detailsViewContainer seems hacky to me, and I'd like to be able to keep the this context pointing to the main view, not the entire window.
The DetailsView template just a set of <tr><td></td></tr> elements. Is there a better way to embed this view without having to resort to creating the detailViewContainer?
(One possible option was having the DetailView loop through the collection returned from this.model.get('users') all by itself... is that a good idea?)
If you're doing what you're doing because of the loss of 'this,' you can pass your context to forEach.
userModels.forEach(function(user) {
this.renderDetailsView();
},this);
Now you have the proper 'this' available to you. Hope this helps.

Categories

Resources