Persist data across views AngularJS with vs-google-autocomplete - angularjs

I am building my first Angular App, which allows users to search a database of vendors based on a number of criteria. One of these criteria is their location and the distance to the vendor.
I have used the vs-google-autocomplete directive, which has been a great way to link into the google maps API. However, I have a problem around the persistency of this location data across views in the app.
To explain how it works at the moment. A user inputs their search criteria, a snapshot of all potential vendors is shown based on the users search criteria (this has been achieved through a custom directive). The user can then click to read more about a vendor, this takes them to the vendors full page (with more details etc.). It is at this stage that the location data is lost.
I have already created a service to store the information from other aspects of the search query, which is persisting just fine (except for the location data).
It appears that the problem is that the location input field automatically resets when a user leaves the page, thereby resetting the location field to null. But, I can't see where it is doing that in the code.
Does anyone have any thoughts about how to tackle this?
Code Below (unfortunately I could not get the custom directive actually working in JSFiddle, but hopefully it gives a good idea of what I am trying to achieve):
App.factory("searchquery", function() {
return {};
});
App.controller('searchController', ['$scope', '$routeParams', '$rootScope', 'searchquery',
function($scope, $routeParams, $rootScope, searchquery) {
//Create a search query object
$scope.searchquery = {};
//Inject the searchquery service to ensure data persistency when in searchController pages
$scope.searchquery = searchquery;
//DEAL WITH GOOGLE AUTOCOMPLETE ADDRESSES
//Restricts addresses to South Africa only
$scope.options = {
componentRestrictions: {
country: 'ZA'
}
};
//Creates an object to store the address information and parses it into its consitutent parts
$scope.searchquery.address = {
name: '',
components: {
streetNumber: '',
street: '',
city: '',
state: '',
countryCode: '',
country: '',
location: {
lat: '',
long: ''
}
}
};
}
]);
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<form name="Search">
<!-- LOCATION -->
<!-- ask the user to specify their location -->
<div class="form-group">
<label for="address">Address</label>
<input vs-google-autocomplete="options" ng-model="searchquery.address.name" vs-street-number="searchquery.address.components.streetNumber" vs-street="searchquery.address.components.street" vs-city="searchquery.address.components.city" vs-state="searchquery.address.components.state"
vs-country-short="searchquery.address.components.countryCode" vs-country="searchquery.address.components.country" vs-latitude="searchquery.address.components.location.lat" vs-longitude="searchquery.address.components.location.long" type="text" name="address"
id="address" class="form-control">
</div>
</form>
Thanks!
Jack

I don't know your location data but you can persist data across views with two ways:
Service as you mentioned
Nested views
Please have a look at the demo below or here at jsfiddle.
The service data are not shared in the demo but that's also possible by injecting it in other views.
If the data are asynchronous you should have a look at resolve of ui-router. Then the data are loaded before the controller of the view is called.
angular.module('demoApp', ['ui.router'])
.config(AppConfig)
.factory('demoService', DemoService)
.controller('demoController', DemoController);
function AppConfig($stateProvider, $urlRouterProvider) {
$urlRouterProvider.otherwise('/');
$stateProvider.state('home', {
url: '/',
template: 'HELLO from home route <input ng-model="text"/>'+
'<div ui-view=""></div'
})
.state('home.profile', {
url: '/profile',
template: 'Hello from profile <input ng-model="text"/>'
})
.state('otherView', {
url: '/other',
controller: 'demoController',
/*resolve: {
text: function(demoService) {
console.log(demoService);
return demoService.get();
}
},*/
template: 'Hello from other scope <input ng-model="text" ng-change="updateService(text)"/>'
});
}
AppConfig.$inject = ['$stateProvider', '$urlRouterProvider'];
function DemoService() {
return {
_text: 'I am stored in service.',
set: function(text) {
this._text = text;
},
get: function() {
return this._text;
}
}
}
function DemoController($scope, demoService) {
$scope.text = demoService.get();
$scope.updateService = function(newValue) {
console.log(newValue);
demoService.set(newValue);
};
}
DemoController.$inject = ['$scope', 'demoService'];
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js">
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-router/0.2.15/angular-ui-router.js"></script>
<div ng-app="demoApp">
<div ui-view=""></div>
<a ui-sref="home">home</a>
<a ui-sref="home.profile">profile</a>
<a ui-sref="otherView">other</a>
</div>

Related

syncing variable across pages using angularJs

I have a html page with a link as follows:
<div ng-if="!adminCtrl.valid">
<div><a target="_blank" ng-href="https://api.instagram.com/oauth/authorize/?client_id=xxx&redirect_uri=http://localhost:8888/igSuccess.html&response_type=token">Authorize to Instagram</a><br/></div>
</div>
This goes to redirect page on success where the code is
<div ng-controller="AdminController">
<h2>You can close this tab/window</h2>
</div>
The control is same for both pages as follows:
app.controller('AdminController', ['$scope','$routeParams','$location', function($scope,$routeParams,$location){
var actrl = this;
actrl.valid = false;
var token = $location.absUrl();
if(token.indexOf('access_token') > -1){
console.log('found token so will do special');
actrl.valid = true;
$scope.$apply();
}
}}
I am expecting the link to disappear once the new page opens as i am updating the valid variable value.
i know the flaw seems to be the cross page communication. so how to deal with it?
Controllers are 'flushed' when you change views. To keep data from a view/controller to another, store your data within a Service.
UPDATE
controller:
app.controller('AdminController', [
'$scope', '$routeParams', '$location', 'ExampleService', function ($scope, $routeParams, $location, ExampleService) {
var actrl = this;
// Watches the service's value for changes and applies it to the controller
$scope.$watch(function(){return ExampleService.valid}, function(newValidValue){
actrl.valid = ExampleService.valid;
});
var token = $location.absUrl();
if (token.indexOf('access_token') > -1) {
console.log('found token so will do special');
ExampleService.valid = true;
// No need for this
// $scope.$apply();
}
}
}
Service:
app.service('ExampleService', [
function () {
//All properties here are kept through-out your app's life time
this.valid = false; // Init to false
}
}
To share data between Controllers in Angular JS, use a named Service to encapsulate the data. In your case, I would typically define an Auth service that provides a few methods for getting and setting the access_token for a user:
module.factory('Auth', function(){
return {
isValid: function(){ /* Check that a User is authenticated... */ },
setToken: function(token){ /* Store the token somewhere... */ },
getToken: function(){ /* Fetch the token from somewhere... */ }
};
});
To share data across "pages" -- tabs or windows in your browser -- even in a Single Page Application (SPA) like this, store the data in cookies or localStorage. You can use angular-local-storage by grevory (GitHub) to abstract the details of using localStorage with a cookie fall-back in non-compatible browsers.
The reason that one page cannot see the valid value defined in the other is because each page gets a separate instance of AdminController, each of which get their own separate instance of $scope tied to their respective DOM elements. Setting valid on the $scope of the redirect landing page has not effect on the completely detached $scope instance in the originating page.
You'd encounter similar difficulties with a trivial same-page example (CodePen):
angular.module('scope-example', [])
.controller('ExampleCtrl', function($scope) {
$scope.value = 'Initial Value';
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<form class="pure-form" ng-app="scope-example">
<fieldset ng-controller="ExampleCtrl">
First instance of <code>ExampleCtrl</code>:
<br>
<input ng-model="value">
<label>{{value}}</label>
</fieldset>
<fieldset ng-controller="ExampleCtrl">
Second instance of <code>ExampleCtrl</code>:
<br>
<input ng-model="value">
<label>{{value}}</label>
</fieldset>
<fieldset ng-controller="ExampleCtrl">
Third instance of <code>ExampleCtrl</code>:
<br>
<input ng-model="value">
<label>{{value}}</label>
</fieldset>
</form>
Even though each of the <fieldset> elements have identical ng-controller directives associated, each gets its own instance of ExampleCtrl and $scope, so the value property isn't shared between them. This holds true for any directive.

Search Toolbar with AngularJS

This is a newbie question about Angular 1.4.
My Main Question
I have the following search view:
'use strict';
var module = angular.module('myapp.search', ['ngRoute', 'ngResource']);
module.config(function ($routeProvider) {
$routeProvider.when('/search', {
templateUrl: 'search/search.html',
controller: 'SearchCtrl'
});
});
module.factory('Search', function ($resource) {
return $resource('http://www.myapp.com/api/search', {}, {
query: {
method: 'GET',
params: {},
isArray: false
}
});
});
module.controller('SearchCtrl', function ($scope, Search) {
$scope.results = [];
$scope.query = '';
$scope.doSearch = function (query) {
Search.query({q: query}).$promise.then(function (result) {
$scope.results = result.results;
});
};
});
And the partial HTML (search.html):
#search.html
<input placeholder="search" ng-model="query" />
<button ng-click="doSearch(query)" />
<h1>Results</h1>
<ol>
<li ng-repeat="result in results">{{ result }}</li>
</ol>
...
This works perfectly, the problem is when I want to put the search input in a toolbar (defined in index.html):
#index.html
<div ng-controller="SearchCtrl">
<div class="toolbar">
<input placeholder="search" ng-model="query" />
<button ng-click="doSearch(query)">
</div>
...
In this case, by pushing the search button the query is executed in the back-end but the results are not updated in $scope. what's wrong? I've tried calling $scope.$apply() with no luck. I'm lost with this.
Bonus Question
Given that the search functionality is in the toolbar (always visible) users can execute queries in any place of the website. How can I redirect the response to http://www.myapp.com/search?
Thanks in advance.
You have two distinct instances of SearchCtrl, each with its own distinct scope: one in the navbar (dur to ng-controller="SearchCtrl"), and one in the main view (due to controller: 'SearchCtrl').
So the search in the navbar modifies the scope of the navbar controller, and the scope of the main view doesn't know anything about it.
If you want to got to the search main view when something is searched in the navbar, simply use
$location.url('/search?query=' + theEncodedQueryTypedByTheUser);
And in the search route's resolve function, or in its controller, get the route param named query, send the HTTP request, and update the scope with the results.

Pushing data from Angular Bootstrap Typeahead into another tab using UI-Router

I have 3 tabs: customers, billing, and method. On the first tab I have the Bootstrap Typeahead, that is connected to a json file of customers. The second tab is a detailed view of the customer account with outstanding charges. Finally, the third tab is to actually charge the customer with a credit card. Additionally, I am using UI Router to navigate through a complex hierarchy. the tabs are named views of a page called make-payments, which is a child of a parent state called payments. I am trying to use the typeahead in the first tab (customers) to select and view a customer detail in the second tab (billing). I am hoping this happens when the user selects the person from the dropdown, they should immediately navigate to the next tab, and the next tab should show that persons details. I am having some difficulty really understanding how to get the data from the typeahead to the next two tabs, and if I need a service to do this or not.
make-payment.html(My tabs):
<h3 class="text-center">Make a One-Time Payment</h3>
<uib-tabset type="pills nav-pills-centered">
<uib-tab heading="1">
<div ui-view=customers></div>
</uib-tab>
<uib-tab heading="2">
<div ui-view=billing></div>
</uib-tab>
<uib-tab heading="3">
<div ui-view=method></div>
</uib-tab>
</uib-tabset>
customers.html & the typeahead.js ctrl:
angular
.module('myApp')
.controller('TypeAheadCtrl', function($scope, $http) {
$http.get('customers.json').success(function(data) {
$scope.customers = data;
});
var self = this;
self.submit1 = function() {
console.log('First form submit with', self.customer)
};
});
<div ng-controller="TypeAheadCtrl as ctrl">
<form ng-submit="ctrl.submit1()" name="customerForm">
<div class="form-group">
<label for="account">Account Number</label>
<input type="text" class="form-control" placeholder="Search or enter an account number" ng-model="ctrl.customer.account" uib-typeahead="customer as customer.index for customer in customers | filter:$viewValue | limitTo:6" typeahead-template-url="templates/customer-tpl.html"
typeahead-no-results="noResults">
</div>
</form>
</div>
app.js (where I have ui-router stuff)
.config(function($stateProvider, $urlRouterProvider) {
$urlRouterProvider.otherwise('/payments/make-payment');
$stateProvider
// HOME STATES AND NESTED VIEWS ========================================
.state('payments', {
url: '/payments',
templateUrl: 'views/payments.html'
})
.state('activity', {
url: '/activity',
templateUrl: 'views/activity.html'
})
// nested lists
.state('payments.make-payment', {
url: '/make-payment',
views: {
'': {
templateUrl: 'views/make-payment.html'
},
'customers#payments.make-payment': {
templateUrl: 'views/customers.html'
},
'billing#payments.make-payment': {
templateUrl: 'views/billing.html'
},
'method#payments.make-payment': {
templateUrl: 'views/method.html'
});
});
billing.html
<p>{{ctrl.customer.account.name}}</p>
<p>{{ctrl.customer.account.address}}</p>
<p>{{ctrl.customer.account.email}}</p>
<p>{{ctrl.customer.account.phone}}</p>
you can send the selected customer data to a service and pull from the service in the two tabs.
so I guess when you actually select a user from the dropdown or whatever you can call myFactory.setCustomer(data).
and in your other two states you can watch the getCustomer() function in myFactory
.factory('myFactory', myFactory);
function myFactory() {
var customer;
function setCustomer(data) {
customer = data;
}
function getCustomer() {
return customer;
}
//public API
return {
setCustomer: setCustomer,
getCustomer: getCustomer
}
}

Why don't comment upvotes work? thinkster.io, angularjs tutorial, mean, incrementUpvote()

I'm following the thinkster angular js tutorial here:
https://thinkster.io/angulartutorial/mean-stack-tutorial/
and clicking on the upvotes icon next to a comment fails to increment the upvote count.
In a ui-router template(included inline in index.html), an ng-click calls addUpvote()--but addUpvote() is defined by a controller specified in another state. So the only way the template can call addUpvote() is if the template's associated state/controller somehow inherits from the other state/controller. And in fact, according to the ui-router docs here:
https://github.com/angular-ui/ui-router/wiki/Nested-States-%26-Nested-Views
the way the states are defined in my app.js,
app.js:
app.config([
'$stateProvider',
'$urlRouterProvider',
function($stateProvider, $urlRouterProvider) {
$urlRouterProvider.otherwise('home');
$stateProvider
.state('home', {
url: '/home',
templateUrl: '/home.html',
controller: 'MainCtrl' }
)
.state('posts', {
url: '/posts/{id}',
templateUrl: '/posts.html',
controller: 'PostsCtrl' }
);
}]);
...the second state inherits from the first state Edit: A more careful reading of the docs makes me believe that isn't actually true; I think that in order for the posts state to inherit from the home state, I would have to change the name of the posts state to home.posts. However, when I try that, it breaks something else: the comments link next to a post no longer works. In any case, I don't think writing:
$stateProvider
.state(...)
.state(...);
creates any inheritance between the two states.
Here is the first state's controller:
app.controller('MainCtrl', ['$scope', 'posts', function($scope, posts) {
$scope.posts = posts.posts;
/*
[
{title: "post1", upvotes: 3, link: ''},
{title: "post2", upvotes: 5, link: ''},
{title: "post3", upvotes: 1, link: ''}
];
*/
$scope.addPost = function() {
var title = $scope.title;
if (title && (title.trim().length > 0) ) {
$scope.posts.push({
title: title,
upvotes: 0,
link: $scope.link,
comments: [
{author: 'Joe', body: 'Cool post!', upvotes: 0},
{author: 'Bob', body: 'Great idea, but everything is wrong!', upvotes: 0}
]
});
}
$scope.title = '';
$scope.link = '';
}
//*****HERE IS THE FUNCTION THAT IS NOT BEING CALLED*****
$scope.addUpvote = function(post) {
console.log("in addUpvote");
++post.upvotes;
}
}]);
As far as I can determine, my template should be able to call addUpvote(). However, the console.log() line doesn't output anything when I click on the upvote icon next to a comment, so obviously addUpvote() is not being called.
Here is a portion of index.html:
<body ng-app="flapperNews">
<div class="row">
<div class="col-md-6 col-md-offset-3">
<ui-view></ui-view>
</div>
</div>
....
....
....
<script type="text/ng-template" id="/posts.html">
<div class="page-header">
<h3>
<a ng-show="post.link" href="{{post.link}}">
{{post.title}}
</a>
<span ng-hide="post.link">
{{post.title}}
</span>
</h3>
</div>
<div ng-repeat="comment in post.comments | orderBy:'-upvotes'">
<span class="glyphicon glyphicon-thumbs-up"
ng-click="addUpvote(comment)"></span>
{{comment.upvotes}} - by {{comment.author}}
<span style="font-size:20px; margin-left:10px;">
{{comment.body}}
</span>
</div>
</script>
</body>
Towards the bottom of that template, ng-click calls addUpvote(). Why isn't addUpvote() being called.
The explanation was a little further down in the docs:
https://github.com/angular-ui/ui-router/wiki/Nested-States-%26-Nested-Views#scope-inheritance-by-view-hierarchy-only
Scope Inheritance by View Hierarchy Only
Keep in mind that scope properties only inherit down the state chain
if the views of your states are nested. Inheritance of scope
properties has nothing to do with the nesting of your states and
everything to do with the nesting of your views (templates).
It is entirely possible that you have nested states whose templates
populate ui-views at various non-nested locations within your site. In
this scenario you cannot expect to access the scope variables of
parent state views within the views of children states.
And:
https://github.com/angular-ui/ui-router/wiki/Nested-States-%26-Nested-Views#what-do-child-states-inherit-from-parent-states
What Do Child States Inherit From Parent States?
Child states DO inherit the following from parent states:
Resolved dependencies via resolve
Custom data properties
Nothing else is inherited (no controllers, templates, url, etc).
...
...
Child states will inherit data properties from parent state(s), which
they can overwrite.
$stateProvider.state('parent', {
data:{
customData1: "Hello",
customData2: "World!"
}
})
.state('parent.child', {
data:{
// customData1 inherited from 'parent'
// but we'll overwrite customData2
customData2: "UI-Router!"
}
});
So to make the comment upvotes work, I did this:
app.controller('PostsCtrl', [
'$scope',
'$stateParams',
'posts',
function($scope, $stateParams, posts) {
$scope.post = posts.posts[$stateParams.id];
$scope.addUpvote = function(comment) {
console.log('comment upvote');
++comment.upvotes;
}
}]);
I had the same error, and when I ran the url in cURL, it returned req.comment.upvote is not a function at the top of the error stack.
C:\Users\ddavis>curl -X PUT http://localhost:3000/posts/57b380f850eaca0c1233a3b7/comments/57b42f4b305a94e40c75970d/upvote
req.comment.upvote is not a function
...
In my case, when I defined an upvote function in the comments.js model like below it worked:
var mongoose = require('mongoose');
var CommentSchema = new mongoose.Schema({
body: String,
author: String,
upvotes: {type: Number, default: 0},
posts: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Post'}]
});
CommentSchema.methods.upvote = function(cb) {
this.upvotes += 1;
this.save(cb);
};
mongoose.model('Comment', CommentSchema);
C:\Users\ddavis>curl -X PUT http://localhost:3000/posts/57b380f850eaca0c1233a3b7/comments/57b42f4b305a94e40c75970d/upvote
{"_id":"57b42f4b305a94e40c75970d","body":"another comment","author":"user","__v":0,"posts":[],"upvotes":3}

Angular model doesn't scope to variable within child DOM element?

I have this markup:
<div data-ng-model="currentUser.attributes">
<div>{{username}}</div>
</div>
And this is a stripped down version of my controller:
$scope.username = "Alice";
$scope.currentUser = {
attributes: {
username: "Bob"
}
};
I want Bob to display, but instead, I am getting Alice. It works just fine if I use this:
{{currentUser.attributes.username}}
But I don't want to have to scope down to this variable's properties every time I want to access something. How can I get the element to exist within the scope of currentUser.attributes?
While I don't think you should really do this, it is what you're asking for. You can essentially mimic with by using ng-repeat on an array that you populate with the relevant object. For example:
<div ng-repeat="user in [currentUser.attributes]">
{{ user.username }}
</div>
Working plunker: http://plnkr.co/edit/svwYEeWMQXjuAnLkr9Vz?p=preview
Other possible solutions would be to have a service or controller that has functions to get the attributes and return them, cleaning up the syntax of your HTML and making it easier to change backend stuff without breaking your frontend. Your choice.
Edit: I noticed you actually expect to be able to do {{ username }} and get the relevant info, if that's really what you want then I suggest my second proposal. Create functions that return the relevant info.
<div>
{{ getCurrentUserName() }}
</div>
$scope.getCurrentUserName = function() {
return $scope.currentUser.attributes.username;
};
Your call, take it or leave it.
If you want Bob just do the the following in your HTML.
<div>{{current user}}</div>//IGNORE THIS
<div>{{currentUser.attributes.username}}</div>//UPDATED CORRECTED
UPDATED based on clarification.
So in Knockout you do this
<p data-bind="with: currentUser.attributes">
<div data-bind="text: userName></div>
<div data-bind="text: login></div>
<div data-bind="text: bhalBlah></div>
<div data-bind="text: yaddaYadda></div>
</p>
<script type="text/javascript">
ko.applyBindings({
currentUser: {
attributes: {
userName : 'Bob',
login : 't#e',
blahBlah : 'ttttt',
yaddaYadda: 'x'
}
}
});
</script>
Same thing in AngularJS would be
<p ng-controller="myCtrl">
<div>{{currentUser.attributes.userName}}</div>
<div>{{currentUser.attributes.login}}</div>
<div>{{currentUser.attributes.blahBlah}}</div>
<div>{{currentUser.attributes.yaddaYadda}}</div>
</p>
<script type="text/javascript">
angular.module('myApp',[]).controller('myCtrl',function($scope){
$scope = {
currentUser: {
attributes: {
userName : 'Bob',
login : 't#e',
blahBlah : 'ttttt',
yaddaYadda: 'x'
}
};
});
</script>
In this the question is how to avoid how not to repeat the part the full property paths between ** as shown below in angular.
**currentUser.attributes.**userName
**currentUser.attributes.**login
**currentUser.attributes.**blahBlah
**currentUser.attributes.**yaddaYadda
Here is one way see plnkr using ng-init which reduces 'currentUser.attributes' to just 'attr'.
With just attr.<properties> repeated
{{attr.userName}}
{{attr.login}}
{{attr.blahBlah}}
{{attr.yaddaYadda}}
Another way is you restructure your object and flatten it on the $scope.
This is not recommended because now you are putting primitives on to the $scope and are widening the scope with $scope.userName = currentUser.attributes.username. Also your 'repetitive' code is still there just in the Javascript.
In lieu of ng-init
ng-init="attr = currentUser.attributes"
You could also do this in controller
$scope.attr = currentUser.attributes;
This post really got me thinking. I had a theory on how to accomplish this using a directive.
Came up with a proof of concept on plnkr: http://embed.plnkr.co/OJDhpJ1maEdSoPvlbiRA/
If I understand correctly, you want to only display the properties within a given block of your struct.
Given the following struct:
$scope.currentUser = {
attributes: {
username: 'Batman',
age: '99',
address: {
street: 'Bat Cave'
}
}
};
You want to scope things down with something like:
<div scope-with="currentUser.attributes">
Username: {{username}}<br />
Age: {{age}}
<div scope-with="address">
Street: {{street}}
</div>
</div>
Directive:
angular.module('mymodule', [])
.directive('scopeWith', function($interpolate){
return {
restrict: 'A',
scope: {
scopeWith: '='
},
transclude: 'element',
compile: function(tElement, tAttrs, linker) {
return function( scope, element, attr) {
var childScope,
parent = element.parent(),
withBlock = null
;
scope.$watch('scopeWith', function(val){
childScope = scope.$new();
angular.forEach(val, function(val, prop){
childScope[prop] = val;
});
if(withBlock) {
withBlock.el.remove();
withBlock.scope.$destroy();
}
linker(childScope, function(clone){
withBlock = {};
parent.append(clone);
withBlock.el = clone;
withBlock.scope = childScope;
});
}, true);
};
}
};
Use {{currentUser.username}} to show Bob.
The ng-model on the div is irrelevant as it only applies to input elements.

Resources