Async Angular/Node $scope calls - javascript

really new to Angular here and I have a problem that I need some help with. Basically, I need to have a selection of items that user can click on. When an item is clicked, the page needs to show some of the properties that the item has like it's description, etc. The first part is not a problem, but I'm having trouble with the second part, which is displaying the data. So here is what I have:
On the front end, I have an angular ng-click chooseItem(item) function that takes the clicked item as its paramater:
<div ng-repeat="item in items" class="col-xs-2 col-md-1">
<div ng-click="chooseItem(item)" class="thumbnail">
<img src="/images/items/{{item.name}}.png"/>
</div>
</div>
This is then passed on to the items factory through items.getChosenItemData(item) function. Since the real item data is stored in Mongo and not the factory, this function queries the db to retrieve the item data. This retrieved data is stored into the chosenItem object, which is then passed back to the controller as $scope.chosenItem.
app.factory('items', ['$http', function($http){
var objects = {
items: [
// ... more items before these
{name: "Pencil"},
{name: "Pen"}
/* I use these item objects as keys to the items themselves.
The ng-repeat iterates through all of the names for each item
which allows me to display static images for each item to the page.
There aren't many items, about 100, but they have tons of json information
so to hardcode it all into here is not an option
A way to do this without any hardcoding would be nice! */
// more items after these
],
// this is used to store a currently clicked item's values
chosenItem: null
}
objects.getChosenItemData = function(name){
return $http.get('/items/' + name).success(function(data){
// console.log(data);
angular.copy(data, objects.chosenItem);
console.log("Chosen Item: ", objects.chosenItem);
})
}
return objects
}]);
app.controller('MainCtrl', [
'$scope',
'items',
function($scope, items){
$scope.items = items.items;
$scope.chosenItem = null;
$scope.chooseItem = function(item){
items.getChosenItemData(item.name);
$scope.chosenItem = items.chosenItem; //chosen item object attribute in factory
console.log("$scope item: ", $scope.chosenItem);
}
}
});
This almost all works. I can query the data of the clicked item successfully, but returning it is another story. Upon first click, the value of $scope.chosenItem is null. Then upon second click, it stores the value of the click item. This also causes the problem where if I click on n amount of items, the value stored is always the value of the n-1 item, not the current item. I need it to store the value of the clicked item on the first click, not the second.
I have a feeling I need to add a callback somewhere in here to make it work, but I'm new to Angular/JS so I'm not sure where it should even go.
Thanks for any help! Also any tips or leads on Angular design patterns would be much appreciated, since I have the feeling that this is a terrible implementation of something that seems rather simple.

I suggest you to expose the service directly:
$scope.serviceItem = items;
and then you can call it in the view like that:
{{serviceItem.chosenItem}}
It will be always updated to the latest clicked value.
I hope it helps.

Related

dynamic routing with array item in angularjs after filtering array

I have a problem with my angularjs app where my app is routing to the wrong page when using an ng-repeat array to determine the route.
data looks like this and is accessed in the person controller:
[
{
"name":"AJ lastname",
"img_name":"AJ_lastname",
"location":"Baltimore, Maryland",
"info":"stuff"
},
{
"name":"Albert lastname",
"img_name":"Albert_lastname",
"location":"Boston, Massachusetts",
"info":"stuff"
} // ... more data
]
html: (the anchor tag links to the person based on their index in the array (I believe this may be what I need to change to fix the problem, but I'm not sure)
<ul class="main-list">
<li class="list-item fade" ng-repeat="student in students | filter:filter">
<a href="/#person/{{$index}}">
<img class="portrait listimg" ng-src="/images/{{student.img_name}}.jpg" alt="portrait of {{student.name}}">
<h2>{{student.name}}</h2>
<h4>{{student.location}}</h4>
</a>
</li>
</ul>
Routing from angular: (the route with '/person/:itemId' is routing to a page specific to a specific user, where their index in the array determines their id)
app.config(function ($routeProvider, $httpProvider) {
$routeProvider
.when('/list', {
templateUrl: './js/templates/list.html',
controller: 'ListController'
})
.when('/person/:itemId', {
templateUrl: './js/templates/person.html',
controller: 'PersonController'
})
.otherwise('/list');
});
Here is the controller for the dynamic page. It works perfectly for the original array, but once I attempt to sort the array, the index no longer corresponds to the correct student.
app.controller('PersonController', function ($scope, $http, $routeParams) {
$scope.person = 'Someone\'s name';
$http.get('../js/students.json').success(function (data) {
$scope.allStudents = data;
$scope.studentId = $routeParams.itemId;
$scope.student = data[$scope.studentId];
});
So the functional problem is that the index applies to the first student in the large array of data. It appears to work perfectly, and the correct data populates the page, but when I use the html/text input to filter the list, the original indices are updated on the html side, and they do not correspond to the original array. So the routing sends them to the wrong page.
How can I make the routing work even for a filtered list?
One way you can do this is by using a function which returns you the index a student had in the original array for each student in your ng-repeat.
$scope.getIndex = function(student) {
return $scope.students.indexOf(student);
}
You can then call the function in your list like:
<a ng-href="/#person/{{getIndex(student)}}">
This though is not quite the most performant code you could imagine.
Another way would be to just temporarily store the index of the student as a property and use that one to reference it, again not quite the nicest solution:
$scope.students = $scope.students.map(function(student, index) {
student.index = index;
return student;
});
And in the list:
<a ng-href="/#person/{{student.index}}">
However, if you can somehow assign the students a unique id that would definitely be the preferred way. That way you also make sure that you always reference the same student. If your students.json somehow changes between the time you create the list and the time the user clicks on an item you may reference the wrong one again...
By the way always use ng-href when including placeholders in the link. Why you should do so is well described in the Angular API docs:
Using Angular markup like {{hash}} in an href attribute will make the link go to the wrong URL if the user clicks it before Angular has a chance to replace the {{hash}} markup with its value. Until Angular replaces the markup the link will be broken and will most likely return a 404 error. The ngHref directive solves this problem.
You are creating the ng-repeat using some object on $scope called students, correct? If this is being built from the same students.json as in your controller then their student ids should logically be equivalent. So just change the href from "/#person/{{$index}}" to "/#person/{{student.studentId}}".
If for some reason they aren't the same then when you create the students object you can add a new attribute, studentId, which holds the value of their index in the array and then use the previous suggestion.
Just remember that when using ng-repeat if you have identical objects it'll throw an error so you have to add the "track by $index" to it.

Knockout component updating observable not being subscribed to by parent view model?

I've written a component called Upload which allows users to upload files and then report back with a JSON object with these files. In this particular instance, the Upload component has a parameter which comes from a parent view model:
<upload params="dropzoneId: 'uploadFilesDropzone', postLocation: '/create/upload', uploadedFiles: uploadedFiles"></upload>
The one of importance is called uploadedFiles. The parameter binding here means I can reference params.uploadedFiles on my component and .push() new objects onto it as they get uploaded. The data being passed, also called uploadedFiles, is an observableArray on my parent view model:
var UploadViewModel = function () {
// Files ready to be submitted to the queue.
self.uploadedFiles = ko.observableArray([]);
};
I can indeed confirm that on my component, params.uploadedFiles is an observableArray, as it has a push method. After altering this value on the component, I can console.log() it to see that it has actually changed:
params.uploadedFiles.push(object);
console.log(params.uploadedFiles().length); // was 0, now returns 1
The problem is that this change does not seem to be reflected on my parent viewmodel. self.uploadedFiles() does not change and still reports a length of 0.
No matter if I add a self.uploadedFiles.subscribe(function(newValue) {}); subscription in my parent viewmodel.
No matter if I also add a params.uploadedFiles.valueHasMutated() method onto my component after the change.
How can I get the changes from my array on my component to be reflected in the array on my parent view model?
Why do you create a new observable array when the source already is one? You can't expect a new object to have the same reference as another one: simply pass it to your component viewModel as this.uploads = params.uploads. In the below trimmed-down version of your example, you'll see upon clicking the Add button that both arrays (well the same array referenced in different contexts) stay in sync.
ko.components.register('upload', {
viewModel: function(params) {
this.uploads = params.uploads;
this.addUpload = function() { this.uploads.push('item'); }.bind(this);
},
template: [
'<div><button type="button" data-bind="click: addUpload">Add upload</button>',
'<span data-bind="text: uploads().length + \' - \' + $root.uploads().length"></span></div>'].join('')
});
var app = {
uploads: ko.observableArray([])
};
ko.applyBindings(app);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div data-bind="component: {name: 'upload', params: {uploads: uploads}}"></div>
It is only in case your source array is not observable that things get a little more complicated and you need to have a manual subscription to update the source, eg. you would insert the following in the viewModel:
this.uploads.subscribe(function(newValue) { params.uploads = newValue; });
Additionally the output in the text binding would not be updated for the source because it is not observable. If for some reason that I cannot conceive of you would want to have 2 different observableArrays (1 source & 1 component), you should still be able to do with the line above, but replace the function code with params.uploads(newValue)
The problem may be related to this bug (to be confirmed): https://github.com/knockout/knockout/issues/1863
Edit 1: So this was not a bug. You have to unwrap the raw param to access the original observable. In your case, it would be:
params.$raw.uploadedFiles() //this would give you access to the original observableArray and from there, you can "push", "remove", etc.
The problem is that when you pass a param to a component, it gets wrapped in a computed observable and when you unwrap it, you don't have the original observableArray.
Reference: http://knockoutjs.com/documentation/component-custom-elements.html#advanced-accessing-raw-parameters
While Binding Property that involves Parent --> Child Relation
Use Binding in this way
If You want to bind data to Child Property
data-bind='BindingName : ParentViewmodel.ChildViewModel.ObservableProperty'
Here it seems you want to subscibe to a function when any data is pushed in Array for that you can write subscribe on Length of Observable array which can help you capture event that you want.
This should solve your problem.

Avoiding another API call with Angular and JSON

Probably an easy question, but I am making a call to an API that returns a full list of products in JSON. The products are listed under 4 categories in the JSON - 'connectivity','cables','routers' and 'servers'. Using the getProducts() function below, I assign the list of 'connectivity' products to a variable called $scope.connectivitylistOfProducts - and this is what I display to the user in the UI as a default.
productsServices.getProducts()
.then(function (allProducts) {
$scope.listOfProducts = allProducts.data.category[0];
$scope.connectivitylistOfProducts = allProducts.data.category[0].connectivity;
})
.finally(function () {
});
In the UI, I have a select box that's contains a list of the categories where the user can change to view the products under the category they choose. changeProduct() is what is called to change the category
$scope.changeProduct = function () {
// change the product
};
I am already loading the full list of categories and products into $scope.listOfProducts and I dont want to make another API call by calling getProducts again. I'm not sure how to set up another variable (for example $scope.routerslistOfProducts) and assing the correct products to it. Could anyone tell me the best way to handle this? Many thanks
Could you try to access lists by array notation:
you have:
$scope.listOfProducts = allProducts.data.category[0];
you could create a category variable:
$scope.category = 'connectivity';
and to access using, for example:
<div ng-repeat="product in listOfProducts[category]">
If your payload has all the arrays then on API call back assign to scope variable say $scope.mylist, now you bind $scope.mylist.categories to drop down and on drop down change send category Id to change function () and filter using for loop and equal operator , while running through loop I.e filtering , which ever product matches category push to a variable say $scope.filteredproducts ....and here you got your filtered products.
This is simple way to understand , there are better ways to maintain filtered arrays too.

List not refreshing in my angular site

I use Ionic and AngularJS and have the following problem:
Here is the view:
<ion-list>
<ion-item ng-repeat="item in items track by $index" >
<h1>{{item.text}}</h1>
{{item.timestring}}
</ion-item>
</ion-list>
and the controller:
$scope.items = foo.getItems();
the method in foo looks like:
foo.loclStorages = JSON.parse(localStorage.getItem("foo"));
foo.getItems = function () {
if (foo.loclStorages === null) {
return [];
} else {
return jsdata.loclStorages.items;
}
};
Now the problem:
When the list is empty and I add the first element, then I first need to update the site to see it.
When I already have items in the list, i can see it instantly after the update.
What I need to do to also see it instant after adding the first element?
When your list is empty, then $scope.items equals some empty array [].
When your list isn't empty, then $scope.items is a reference to jsdata.loclStorages.items.
Since these two arrays aren't the same reference, then when your list starts as empty, adding items to localStorages.items will have no effect on the array in $scope.items.
To solve this, you need to check when you add an item if the reference in the scope is the same reference as the array in jsdata. If not, make sure to call $scope.items = foo.getItems(); again.

How does passing data around different views/pages work on angularJS

I am learning angularJS, went through few tutorials and sort of know my why around. It seems that the page never refreshes, therefore a value created in one view should be available in another view, right? I am testing this in a shop scenario. If we are at the main view, and we click on "add to cart" that should trigger a function in the background and add the item in an array. Then when we go to the cart view, we can see the item listed there. But this does not work.
I have a cart controller:
angular.module('shoppingCartApp')
.controller('CartCtrl', function ($scope) {
$scope.cart = [
'one item'
];
$scope.pushing = function(item){
this.cart.push(item);
};
});
In the main view (which doesn't have access to this controller) I have.
<div ng-controller="CartCtrl">
add to chart
</div>
And on the cart view I display the cart object
<div ng-repeat="item in cart">
{{item}}
</div>
We only see the one item. I have also added the ng-click attribute to this page as well, just to test, and it does work, however, if we go home and come back, the item is gone.
From the idea that the page never reloads, should the pushed items stay in the array? here is the simple example in action
Thanks
Controllers are not singletons, so when you change the view the $scope gets destroyed and a new controller will be initialized. If you want persistent data across different views then you want to look at using a service to store it, since they are singletons.
If you create something like
angular.module('app')
.service('cartService', [function() {
var cart = [];
var add = function(item) {
cart.push(item);
};
var get = function() {
return cart;
};
return {
add: add,
get: get
};
}]);
Then you can add that as a dependency in your controllers and use that for backing your data rather than using the $scope.
angular.module('app')
.controller('Ctrl', ['cartService', function(cartService) {
$scope.cart = cartService.get();
$scope.pushing = function(item) {
cartService.add(item);
};
}]);

Categories

Resources