Angular - update models using $resource after $update - angularjs

I'm trying to figure out how to update my models with services, when I get a response from the server.
My JSON structure:
var dgs = [{id :1,
driver:'Sam',
type: 'bus',
segments:[ {id:1,origin:'the bakery',arrival:'the store'},
{id:2,origin:'the store' ,arrival:'somewhere'}]
},
{ ... },
{ ... }
];
index.html:
<body ng-controller="dgCtrl">
<div ng-repeat="dg in dgs" drivegroup></div>
</body>
dg.html:
<div> {{ dg.driver }} </div>
<div> {{ dg.type }} </div>
<div ng-repeat="seg in dg.segments" segments></div>
segments.html:
<div>
<input ng-model="seg.origin" ng-blur="updateSegment()"/></div>
<div>
<input ng-model="seg.arrival" ng-blur="updateSegment()"/></div>
My controller is the following:
function dgCtrl($scope,$http,DriveGroup,Segment) {
var segment = new Segment;
$scope.dgs = DriveGroup.query(); // load & display all drive groups
$scope.updateSegment = function() {
var seg = this.seg;
segment.seg = this.seg;
segment.id = this.seg.id;
segment.$update(function(data,x) {
// seg.origin = data.origin - This updates the origin.
seg = data; // This doesn't work. How can I do this?
});
}
My services:
angular.module('dgService',['ngResource']).
factory("DriveGroup",function($resource) {
return $resource(
'/path/dgs',
{},
{update:{method:'PUT'}}
);
});
angular.module('Segment',['ngResource']).
factory('Segment',function($resource) {
return $resource(
'/path/seg/:id',
{id:'#id'},
{update:{method:'PUT'}}
);
});
My problem is I'm not sure how to update on a per-segment basis using $resource. I want to make a change on a certain segment, so I make a put request. I get the response from the server and want to update my model (which isn't working).
I'm new to Angular, so if you see any other blatant mistakes/or structure issues suggestions are appreciated. Thanks.
EDIT:
The following achieves the results I want, but is this correct?
$scope.updateSegment = function() {
var seg = this.seg,
segment = new Segment(seg);
segment.$update(function(data,x) {
angular.extend(seg,data);
});
}

Related

Why is my state being updated here?

I'm pretty new to React, but liking it so far. I'm building a large application, which is going well, except I've run into an issue. I'm building a list of responses to questions, and they can be deleted, but I also want to have a "Cancel" button so all unsaved changes can be reverted. What is confusing me is the cancel button reverts to the initial state for the name value, but not the responses. If I add in some console logging to the response deletion script, I would expect to see log lines 1 & 2 match, with 3 being different. However, I'm seeing that 1 is the original, but 2 & 3 match. Why is state being updated before I call setState, and why does updating state seem to update the my initial props?
EDIT: I added a jsFiddle
getInitialState: function() {
return {
name: this.props.question.name,
responses: this.props.question.responses,
};
},
handleCancelButtonClick: function(e) {
this.replaceState(this.getInitialState());
},
handleNameChange: function(e) {
this.setState({name: e.target.value});
},
handleResponseDeletion: function(e) {
var resp = this.state.responses;
var from = Number(e.target.value);
console.log(JSON.stringify(this.state.responses));
resp.splice(from, 1);
console.log(JSON.stringify(this.state.responses));
this.setState({responses: resp});
console.log(JSON.stringify(this.state.responses));
},
render: function() {
var key = "mp" + this.props.question.name;
var resp = [];
if (this.state.responses) {
this.state.responses.forEach(function(response, i) {
var rkey = "r_" + this.props.question.name + "_" + i;
resp.push(<ModalResponse response={response} key={rkey} value={i} deleteResponse={this.handleResponseDeletion} />);
}.bind(this));
}
return (
<layer id={this.props.question.name} style={questionModal} key={key}>
<h2>Edit {this.state.name}</h2>
<button onClick={this.handleCancelButtonClick}>Cancel</button>
<div class='form-group'>
<label for='client_name' style={formLabel}>Question Name:</label><br />
<input type='text' style={formControl} id='question_name' name='question_name' value={this.state.name} onChange={this.handleNameChange} required />
</div>
<div class='form-group'>
<label style={formLabel}>Responses:</label><br />
<ul style={responseList} type="response_list" value={this.props.qname}>
{resp}
</ul>
</div>
</layer>
);
}
});
The problem is that splice modifies original array. It means the one that belongs to the original question. So when you call getInitialState from within handleCancelButtonClick you get modified array.
To avoid this you need to somehow clone original data inside getInitialState. For example
getInitialState: function() {
//copy array and responses
const copy = resp => ({...resp})
return {
name: this.props.question.name,
responses: this.props.question.responses.map(copy)
};
}
Here's what I did to fix the issue:
handleResponseDeletion: function(e) {
var resp = []
var from = Number(e.target.value);
this.state.responses.forEach(function(res, i) {
if (i != from) {
resp.push(res);
}
});
this.setState({responses: resp});
},

How do I push review objects into an existing model's array using Angular and a form?

First off, I am very new to Angular, and working on my first real MEAN app.
I am building an app that allows users to leave reviews on surf spots. I already have a model for surf spots, and each surf spot has an array that is supposed to hold reviews.
I'm not sure how I can go about using a form to add a review into the array for that spot. Do I need to create a Schema for the reviews? Do I need to create another controller and service for reviews in order to add them to my spots?
I know this is a pretty broad question, but I could really use some direction if anyone can offer it.
Spot Model:
var mongoose = require("mongoose"),
Schema = mongoose.Schema;
var SpotSchema = new Schema({
name : String,
location : String,
reviews : Array,
latitude : Number,
longitude : Number,
tide : String,
rating : Number,
region : String
});
module.exports = mongoose.model("Spot", SpotSchema);
Spot Controller:
angular.module("spotCtrl", ["spotService"])
.controller("spotController", function(Spot) {
var vm = this;
vm.processing = true;
Spot.all()
.success(function(data) {
vm.processing = false;
vm.spots = data;
});
})
.controller("singleSpotController", function($routeParams, Spot) {
var vm = this;
vm.processing = true;
Spot.get($routeParams.spot_id)
.success(function(data) {
vm.processing = false;
vm.spotData = data;
})
})
Spot Service:
angular.module("spotService", [])
.factory("Spot", function($http) {
var spotFactory = {};
spotFactory.get = function(id) {
return $http.get("/api/spots/" + id);
};
spotFactory.all = function() {
return $http.get("/api/spots/");
};
return spotFactory;
});
Finally, the view that displays the data for a single Spot. This is where the form needs to go, but I'm not sure how I can use a form to create a review object, and then push it into the array for that spot.
Spot View:
<div ng-model="singleSpot.spotData">
<div class="text-center">
<div class="single-spot">
{{ singleSpot.spotData.name }} <br>
{{ singleSpot.spotData.location }} <br>
Rating: {{ singleSpot.spotData.rating }} <br>
Current Tide: {{ singleSpot.spotData.tide }}
</div>
</div>
<form>
<!-- What do I do here? -->
</form>
</div>
Thanks!
As far as I understand you want a spot to have multiple reviews.
var SpotSchema = new Schema({
name : String,
location : String,
reviews : [{type: Schema.Types.ObjectId, ref: 'Review'}],
latitude : Number,
longitude : Number,
tide : String,
rating : Number,
region : String
});
Then create a new schema for your reviews
var ReviewSchema = new mongoose.Schema({
review: String,
_Spot: {type: Schema.Types.ObjectId, ref: 'Spot'},
created_at: Date
});
Inside your Html
<div ng-controller="reviewController"
<form>
<textarea name="review" ng-model="new_review"></textarea>
<input type="submit" value="Submit" ng-click="addReview(singlespot.SpotData)">
</form>
</div>
Then create your reviewController and factory and send the data to the Db as you are already doing. Note you have passed the singlespot.SpotData along with ur addReview so you can repack it and use it to findOne spot and then push the review inside the spot.reviews
your backend will look something like this
Spot.findOne({_id: req.body.spot_id}, function (err, spot){
var review = new Review(req.body);
review._Spot = Spot._id;
spot.reviews.push(review);
review.save(function(err){
spot.save(function(err){
if(err) {
console.log("Error");
}
else{
console.log("Succesfully added Review");
}
});
});
});
basically you have to find your spot then push the review into the spot.reviews and save the review as well.
Hope it makes sense.

How can I display one attribute of a set of data just once?

I'm doing some testing with Angular to see if I can replicate what I already have in PHP more efficiently.
I have a set of data stored in JSON:
[
{
"name":"Blue Widget",
"description":"blue-widget",
"snippet":"The best blue widget around!",
"category":"Home Widgets",
"popular":true
},
{
"name":"Red Widget",
"description":"red-widget",
"snippet":"The best red widget around!",
"category":"Outdoor Widgets",
"popular":true
},
{
"name":"Green Widget",
"description":"green-widget",
"snippet":"The best green widget around!",
"category":"Work Widgets",
"popular":true
},
{
"name":"Yellow Widget",
"description":"yellow-widget",
"snippet":"The best yellow widget around!",
"category":"Home Widgets",
"popular":true
}
]
I'm grabbing this in my controller and adding it to my view in a fairly standard way (yes, I know not to use $http directly in a controller in production):
widgetApp.controller('widgetListCtrl', function($scope,$http){
$http.get('widgets/widgets.json').success(function(data){
$scope.widgets = data
})
})
If I use:
<li ng-repeat="widget in widgets">{{widget.category}}</li>
Then naturally it will just go through and list:
Home Widgets
Outdoor Widgets
Work Widgets
Home Widgets
What I'd like to do is generate a list of each widget.category but with each category only appearing once, so a user could then click on a category and be shown all the widgets in that category. How can I go about this? Sorry, I haven't got anything to go on because I pretty much have no idea where to start.
You can use the existing 'unique' filter from AngularUI.
<li ng-repeat="widget in widgets | unique: 'widget.category' ">{{widget.category}}</li>
Be sure to include a reference to the filters module in your app as well (e.g. angular.module('yourModule', ['ui', 'ui.filters']);).
You'd have to build a list of unique categories:
widgetApp.controller('widgetListCtrl', function($scope,$http){
$http.get('widgets/widgets.json').success(function(data){
$scope.uniqueCategories = [];
for (var i = 0; i < $scope.widgets.length; i++) {
if ($scope.uniqueCategories.indexOf($scope.widgets[i].category) === -1)
$scope.uniqueCategories.push($scope.widgets[i].category);
}
});
});
Make a dropdown with the model set to the category:
<select ng-model="categoryFilter" ng-options="category as category for category in uniqueCategories"></select>
And use a filter on your repeat:
<li ng-repeat="widget in widgets | filter: { category: categoryFilter }">{{widget.category}}</li>
Create a filter
app.filter('unique', function() {
return function (arr, field) {
return _.uniq(arr, function(a) { return a[field]; });
};
});
In Markup
<li ng-repeat="widget in widgets | unique:'category'">{{widget.category}}</li>
Create a distinct filiter and use it on your view:
angular.filter("distinct", function () {
return function (data, propertyName) {
if (angular.isArray(data) && angular.isString(propertyName)) {
var results = [];
var keys = {};
for (var i = 0; i < data.length; i++) {
var val = data[i][propertyName];
if (angular.isUndefined(keys[val]) && val != null) {
keys[val] = true;
results.push(val);
};
};
return results;
}
else {
return data;
}
}
})
<li ng-repeat="widget in widgets | distinct:'category'">{{widget.category}}</li>

Should $bind save child data added in an ng-repeat

Hi I have a problem with $bind, I am binding a model and outputting the models via a ng-repeat. The ng-repeat outputs the stored data and also offers some fields for adding/changing data. The changes are reflected in the scope but are not being synced to Firebase.
Is this a problem with my implementation of $bind?
The HTML:
<iframe id="fpframe" style="border: 0; width: 100%; height: 410px;" ng-if="isLoaded"></iframe>
<form>
<ul>
<li ng-repeat="asset in upload_folder" ng-class="{selected: asset.selected}">
<div class="asset-select"><input type="checkbox" name="selected" ng-model="asset.selected"></div>
<div class="asset-thumb"></div>
<div class="asset-details">
<h2>{{asset.filename}}</h2>
<p><span class="asset-filesize" ng-if="asset.size">Filesize: <strong><span ng-bind-html="asset.size | formatFilesize"></span></strong></span> <span class="asset-filetype" ng-if="asset.filetype">Filetype: <strong>{{asset.filetype}}</strong></span> <span class="asset-dimensions" ng-if="asset.width && asset.height">Dimensions: <strong>{{asset.width}}x{{asset.height}}px</strong></span> <span class="asset-type" ng-if="asset.type">Asset Type: <strong>{{asset.type}}</strong></span></p>
<label>Asset Description</label>
<textarea ng-model="asset.desc" cols="10" rows="4"></textarea>
<label>Creator</label>
<input type="text" ng-model="asset.creator" maxlength="4000">
<label>Release Date</label>
<input type="text" ng-model="asset.release">
<label for="CAT_Category">Tags</label>
<input type="text" ng-model="asset.tags" maxlength="255">
</div>
</li>
</ul>
</form>
The Controller: (fpKey is a constant that stores the Filepicker API key)
.controller('AddCtrl',
['$rootScope', '$scope', '$firebase', 'FBURL', 'fpKey', 'uploadFiles',
function($rootScope, $scope, $firebase, FBURL, fpKey, uploadFiles) {
// load filepicker.js if it isn't loaded yet, non blocking.
(function(a){if(window.filepicker){return}var b=a.createElement("script");b.type="text/javascript";b.async=!0;b.src=("https:"===a.location.protocol?"https:":"http:")+"//api.filepicker.io/v1/filepicker.js";var c=a.getElementsByTagName("script")[0];c.parentNode.insertBefore(b,c);var d={};d._queue=[];var e="pick,pickMultiple,pickAndStore,read,write,writeUrl,export,convert,store,storeUrl,remove,stat,setKey,constructWidget,makeDropPane".split(",");var f=function(a,b){return function(){b.push([a,arguments])}};for(var g=0;g<e.length;g++){d[e[g]]=f(e[g],d._queue)}window.filepicker=d})(document);
$scope.isLoaded = false;
// Bind upload folder data to user account on firebase
var refUploadFolder = new Firebase(FBURL.FBREF).child("/users/" + $rootScope.auth.user.uid + "/upload_folder");
$scope.upload_folder = $firebase(refUploadFolder);
$scope.upload_folder.$bind($scope,'upload_folder');
// default file picker options
$scope.defaults = {
mimetype: 'image/*',
multiple: true,
container: 'fpframe'
};
// make sure filepicker script is loaded before doing anything
// i.e. $scope.isLoaded can be used to display controls when true
(function chkFP() {
if ( window.filepicker ) {
filepicker.setKey(fpKey);
$scope.isLoaded = true;
$scope.err = null;
// additional picker only options
var pickerOptions = {
services:['COMPUTER', 'FACEBOOK', 'GMAIL']
};
var storeOptions = {
location: 'S3',
container: 'imagegrid'
};
var options = $.extend( true, $scope.defaults, pickerOptions );
// launch picker dialog
filepicker.pickAndStore(options, storeOptions,
function(InkBlobs){
uploadFiles.process(InkBlobs, $scope.upload_folder);
},
function(FPError){
$scope.err = FPError.toString();
}
);
} else {
setTimeout( chkFP, 500 );
}
})();
}])
I also have a service handling the input from Filepicker, this creates new entries in the firebase at the reference that is bound (using Firebase methods rather than AngularFire maybe this breaks the binding?)
.service('uploadFiles', ['$rootScope', 'FBURL', function($rootScope, FBURL) {
return {
process: function(InkBlobs, upload_folder) {
var self = this;
var countUpload = 0;
// write each blob to firebase
angular.forEach(InkBlobs, function(value, i){
var asset = {blob: value};
// add InkBlob to firebase one it is uploaded
upload_folder.$add(asset).then( function(ref){
self.getDetails(ref);
countUpload++;
});
});
// wait for all uploads to complete before initiating next step
(function waitForUploads() {
if ( countUpload === InkBlobs.length ) {
self.createThumbs(upload_folder, { multi: true, update: false, location: 'uploads' });
} else {
setTimeout( waitForUploads, 500 );
}
})();
},
getDetails: function(ref) {
// after InkBlob is safely stored we will get additional asset data from it
ref.once('value', function(snap){
filepicker.stat(snap.val().blob, {size: true, mimetype: true, filename: true, width: true, height: true},
function(asset) {
// get asset type and filetype from mimetype
var mimetype = asset.mimetype.split('/');
asset.type = mimetype[0].replace(/\w\S*/g, function(txt){return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();});
asset.filetype = mimetype[1];
// add metadata to asset in upload folder
ref.update(asset);
});
});
},
createThumbs: function(ref, options) {
var self = this;
// default options
options.multi = options.multi || false;
options.update = options.update || false;
options.location = options.location || 'asset';
// if pathbase is not provided generate it based on provided location
if (!options.pathbase) {
if (options.location === 'assets') {
options.pathbase = FBURL.LIBRARY + "/assets/";
} else if (options.location === 'uploads') {
options.pathbase = "/users/" + $rootScope.auth.user.uid + "/upload_folder/";
} else {
throw new Error('SERVICE uploadFiles.createThumbs: options.location is not valid must be assets or uploads');
}
}
var generateThumb = function(blob, path) {
filepicker.convert( blob,
{ width: 200, height: 150, fit: 'crop' },
{ location: 'S3', access: 'public', container: 'imagegrid', path: '/thumbs/' },
function(tnInkBlob){
var refThumbBlob = new Firebase(FBURL.FBREF).child(path);
refThumbBlob.set(tnInkBlob);
},
function(FPError){
alert(FPError);
},
function(percentage){
// can use to create progress bar
}
);
};
if (options.multi) {
// look at all assets in provided ref, if thumbnail is mission or update options is true generate new thumb
angular.forEach(ref, function(value, key){
if (typeof value !== 'function' && (!value.tnblob || options.update)) {
// thumb doesn't exist, generate it
var blob = value.blob;
var path = options.pathbase + key + '/tnblob';
generateThumb(blob, path);
}
});
} else {
// getting thumbnail for a single asset
var refAsset = new Firebase(FBURL.FBREF).child(options.pathbase + ref);
var blob = refAsset.val().blob;
var path = options.pathbase + ref + '/tnblob';
generateThumb(blob, path);
}
}
};
}]);
So to recap, data is being saved to /users/$rootScope.auth.user.uid/upload_folder and this is being rendered in the HTML. Changes in the HTML form are reflected in the scope but not in Firebase, despite the binding:
var refUploadFolder = new Firebase(FBURL.FBREF).child("/users/" + $rootScope.auth.user.uid + "/upload_folder");
$scope.upload_folder = $firebase(refUploadFolder);
$scope.upload_folder.$bind($scope,'upload_folder');
Any ideas as to why this is? Is my implementation incorrect or am I somehow breaking the binding? Is $bind even supposed to work with ng-repeat in this manner?
Thanks
Shooting myself for how simple this is, the error was in how I defined the binding. You can't set the binding on itself, you need two separate objects in the scope...
The firebase reference $scope.syncdata loads the initial data and all modifications made to $scope.upload_folder will be synced to firebase.
var refUploadFolder = new Firebase(FBURL.FBREF).child("/users/" + $rootScope.auth.user.uid + "/upload_folder");
$scope.syncdata = $firebase(refUploadFolder);
$scope.syncdata.$bind($scope,'upload_folder');

Displaying the array elements one by one in a form in Angular js

I am building an application using Angular js and Taffy Db.
I have got a resultset from Taffy DB which is an an array.
I want to display the elements one by one in my HTML page.
javascript:
$scope.viewList = function () {
$scope.sharelists = [];
$scope.resultSet = teamlist().get();
var teamdata = $scope.resultSet;
var length = teamdata.length;
angular.forEach(teamdata, function (teamdata, i) {
if (i < length) {
console.log(i);
$scope.teamlistresult = teamdata.text;
$scope.sharelists.push({
text: $scope.teamlistresult
});
}
});
};
});
HTML:
<label for="sharedby">Shared by</label>
<input class="btn-primary" type="submit" value="View" ng-click="viewList()" />
<ul class="unstyled">
<li ng-repeat="share in sharelist">
<input type="checkbox" ng-model="share.done">
<span>{{share.text}}</span>
</li>
</ul>
But I couldnt display the array elements one by one.
Please advice
I am not sure but if this method
$scope.resultSet = teamlist().get(); is async and returns promise then you should use
teamlist().get().then(function(data) {
var teamdata = data;
var length = teamdata.length;
angular.forEach(teamdata, function (teamdata, i) {
if (i < length) {
console.log(i);
$scope.teamlistresult = teamdata.text;
$scope.sharelists.push({
text: $scope.teamlistresult
});
}
});
}

Resources