Angular $watch just parent object instead of its multiple children - angularjs

I have a div, listing properties of the object POI = {"address":"Martinsicuro (TE), Italy", "phone":"+39 333 45657", "website':'http://mysite.it"}. The object POI si owned by a Service. The directive's controller has the function getPoi() that gets the POI from the service, and returns it to the directive.
My current HTML is something like this:
<table ng-controller="Controller as ctrl">
<tr> <!-- address -->
<td>{{ctrl.getPoi().address}}</td>
</tr>
<tr> <!-- phone -->
<td>{{ctrl.getPoi().phone}}</td>
</tr>
<tr> <!-- website -->
<td>
<a ng-href="{{ctrl.getPoi().website}}">
{{ctrl.getPoi().website}}
</a>
</td>
</tr>
</table>
The controller
.controller('Controller', function(CurrentPoiService)
{
this.getPoi = function()
{ return CurrentPoiService.POI; }
}
The service
.service('CurrentPoiService', function()
{
this.POI = {"address":"Martinsicuro (TE), Italy", "phone":"+39 333 45657", "website':'http://mysite.it"}
}
In this way I am adding 3 watchers. Is there a way to add just 1 watcher, since it's the same parent object? Here it is a JSFiddle
Thank you
[UPDATE 1]
This is the (still not working) JSFiddle using the solution proposed by #Charlie
[UPDATE 2]
This is the working JSFiddle

As Claies has mentioned in a comment, you should never call your data from
the view through a function this way.
In this scenario you can create a watch for the POI object with the objectEquality argument true to watch the properties of the object in a single $watch. Then find your elements inside the listener and change the value in the way you want.
$scope.$watch('POI', function(){
//Assume that $scope.propertyIndex is the current property to display
angular.element($document[0].querySelector("#myTd" + $scope.propertyIndex)).html(POI.address);
angular.element($document[0].querySelector("#myTd" + $scope.propertyIndex)).html(POI.phone);
//etc...
}, true);
You have a better control this way. But please keep in mind that this method is not suitable if POI is a complex object.
UPDATE:
Here is a working example of showing a random number every second using a watch and a factory. You should be able to learn from this and apply it to your project.
myApp.controller('myController', function($scope, dataSource) {
$scope.poi = {rand: 0};
$scope.$watch('poi', function() {
$('#rand').html($scope.poi.rand);
}, true);
dataSource.open($scope);
});
myApp.factory('dataSource', function($interval) {
return {
open: function(scope){
$interval(function() {
scope.poi.rand = Math.random();
}, 1000);
}
}
});

Try inside your controller :
$scope.POI = ctrl.getPoi();
HTML :
<tr> <!-- address -->
<td>{{POI.address}}</td>
</tr>
<tr> <!-- phone -->
<td>{{POI.phone}}</td>
</tr>

Related

AngularJS UI-Router nested state view in table row (inline edit)

Working on an AngularJS + UI-Router project. Got a task with these (simplified here) requirements:
display a list of items in a table with Edit button at the end of the table row
clicking Edit button should turn a table row into item edit form (inline edit)
Item list and item edit views should be accessible via url.
So I have defined my states:
// app.js
$stateProvider
.state("list", {
url: "/",
component: "listComponent"
})
.state("list.edit", {
url: "/{id}/edit",
component: "editComponent"
});
}
ListComponent template looks like this:
<table>
<tr>
<th>ID</th>
<th>Name</th>
<th> </th>
</tr>
<!-- Hide this row when NOT in edit mode -->
<tr ng-repeat-start="item in $ctrl.items" ng-if="$ctrl.editIndex !== $index">
<td>{{ item.id }}</td>
<td>{{ item.name }}</td>
<td>
<button type="button" ng-click="$ctrl.onEditClick($index, item.id)">Edit</button>
</td>
</tr>
<!-- Show this row when in edit mode -->
<tr ng-repeat-end ng-if="$ctrl.editIndex === $index">
<td colspan="3">
<ui-view></ui-view>
</td>
</tr>
</table>
And main parts from ListComponent itself:
function ListController($state) {
this.editIndex = null;
this.items = [
{ id: 1, name: "Y-Solowarm" },
// ...
{ id: 10, name: "Keylex" }
];
this.onEditClick = function(index, id) {
this.editIndex = index;
$state.go("list.edit", { id: id });
};
}
Problem:
When I was working on EditComponent I noticed that it initiates http requests twice. After a couple of hours later I came up with such EditComponent that showed what actually was happening:
function EditController() {
// random number per component instance
this.controllerId = Math.floor(Math.random() * 100);
this.$onInit = function() {
console.log("INIT:", this.controllerId);
};
this.$onDestroy = function() {
console.log("DESTROY:", this.controllerId);
};
}
Console showed this output:
DESTROY: 98
INIT: 80
DESTROY: 80
INIT: 9
When clicking Edit for a second time this output shows that
EditComponent#98 is destroyed as we navigate away from it (expected)
EditComponent#80 is created and immediately destroyed (unexpected)
EditComponent#9 is created as we are now 'editing' new item (expected)
This just shows me that many <ui-view>s together with ng-ifs does not play very nice but I have no idea how to fix that.
One thing that I have tried was I created one <ui-view> in ListComponent and was moving it around on ui-router state change by means of pure javascript. But that did not work as I soon started getting errors from ui-router's framework that were related to missing HTML node.
Question:
What am I doing wrong here? I think that angular's digest cycle (and related DOM changes) end later than ui-router starts transitions and related component creation and destruction and that might be a reason of EditComponent#80 being created and quickly destroyed. But I have no idea how to fix that.
Here is a codepen showing what is happening:
https://codepen.io/ramunsk/project/editor/AYyYqd
(don't forget to open developer console to see what's happening)
Thanks
Let's say you're switching from index 2 to index 3. I think this might be what is happening:
The ui-view at index 2 is currently active. In the click handler you call state.go and the ui-view at index 2 briefly receives the updated state parameter id: 3. Then it is destroyed when the ng-if takes effect and the ui-view at index 3 is created.
Change your code so it destroys the ui-view at index 2 first. Add a timeout so it calls state.go shows the second ui-view in the next digest cycle.
this.onEditClick = function(index, id) {
this.editIndex = null;
$timeout(() => {
this.editIndex = index;
$state.go("list.edit", { id: id });
});
};
https://codepen.io/christopherthielen/project/editor/ZyNpmv

Change vm variable value after clicking anywhere apart from a specific element

When I click anywhere in the page apart from ul element (where countries are listed) and the suggestion-text input element (where I type country name), vm.suggested in controller should be set to null. As a result ul element will be closed automatically. How can I do this?
I've seen Click everywhere but here event and AngularJS dropdown directive hide when clicking outside where custom directive is discussed but I couldn't work out how to adapt it to my example.
Markup
<div>
<div id="suggestion-cover">
<input id="suggestion-text" type="text" ng-model="vm.countryName" ng-change="vm.countryNameChanged()">
<ul id="suggest" ng-if="vm.suggested">
<li ng-repeat="country in vm.suggested" ng-click="vm.select(country)">{{ country }}</li>
</ul>
</div>
<table class="table table-hover">
<tr>
<th>Teams</th>
</tr>
<tr ng-if="vm.teams">
<td><div ng-repeat="team in vm.teams">{{ team }}</div></td>
</tr>
</table>
<!-- There are many more elements here onwards -->
</div>
Controller
'use strict';
angular
.module('myApp')
.controller('readController', readController);
function readController() {
var vm = this;
vm.countryNameChanged = countryNameChanged;
vm.select = select;
vm.teams = {.....};
vm.countryName = null;
vm.suggested = null;
function countryNameChanged() {
// I have a logic here
}
function select(country) {
// I have a logic here
}
}
I solved the issue by calling controller function from within the directive so when user clicks outside (anywhere in the page) of the element, controller function gets triggered by directive.
View
<ul ng-if="vm.suggested" close-suggestion="vm.closeSuggestion()">
Controller
function closeSuggestion() {
vm.suggested = null;
}
Directive
angular.module('myApp').directive('closeSuggestion', [
'$document',
function (
$document
) {
return {
restrict: 'A',
scope: {
closeSuggestion: '&'
},
link: function (scope, element, attributes) {
$document.on('click', function (e) {
if (element !== e.target && !element[0].contains(e.target)) {
scope.$apply(function () {
scope.closeSuggestion();
});
}
});
}
}
}
]);
This is just an example but you can simply put ng-click on body that will reset your list to undefined.
Here's example:
http://plnkr.co/edit/iSw4Fqqg4VoUCSJ00tX4?p=preview
You will need on li elements:
$event.stopPropagation();
so your html:
<li ng-repeat="country in vm.suggested" ng-click="vm.select(country); $event.stopPropagation()">{{ country }}</li>
and your body tag:
<body ng-app="myWebApp" ng-controller="Controller01 as vm" ng-click="vm.suggested=undefined;">
UPDATE:
As I said it's only an example, you could potentially put it on body and then capture click there, and broadcast 'closeEvent' event throughout the app. You could then listen on your controller for that event - and close all. That would be one way to work around your problem, and I find it pretty decent solution.
Updated plunker showing communication between 2 controllers here:
http://plnkr.co/edit/iSw4Fqqg4VoUCSJ00tX4?p=preview
LAST UPDATE:
Ok, last try - create a directive or just a div doesn't really matter, and put it as an overlay when <li> elements are open, and on click close it down. Currently it's invisible - you can put some background color to visualize it.
Updated plunker:
http://plnkr.co/edit/iSw4Fqqg4VoUCSJ00tX4?p=preview
And finally totally different approach
After some giving it some thought I actually saw that we're looking at problem from the totally wrong perspective so final and in my opinion best solution for this problem would be to use ng-blur and put small timeout on function just enough so click is taken in case someone chose country:
on controller:
this.close = function () {
$timeout(()=>{
this.suggested = undefined;
}, 200);
}
on html:
<input id="suggestion-text" type="text" ng-model="vm.countryName" ng-change="vm.countryNameChanged()" ng-blur="vm.close()">
This way you won't have to do it jQuery way (your solution) which I was actually trying to avoid in all of my previous solutions.
Here is plnker: http://plnkr.co/edit/w5ETNCYsTHySyMW46WvO?p=preview

function not executing on different view

I am new to angular JS. I have created a simple page using ngRoute.
In the first view I have a table, and on clicking it redirects to second view.
But I have called two functions using ng-click the changeView functions is running fine. But the second function fails to execute on the second view.
But Its running fine if using on the first view itself.
Heres the code for first view
Angular.php
<div ng-app="myApp" ng-controller="dataCtr">
<table class='table table-striped'>
<thead>
<tr>
<td>Title</td>
<td>Stream</td>
<td>Price</td>
</tr>
</thead>
<tbody>
<tr ng-repeat = "x in names | filter:filtr | filter:search" ng-click=" disp(x.id);changeView()">
<td >{{ x.Title }}</td>
<td>{{ x.Stream }}</td>
<td>{{ x.Price }}</td>
</tr>
</tbody>
</table>
</div>
heres the second View
details.php:
<div ng-app="myApp" ng-controller="dataCtr">
<div class="container">
SELECTED:<input type="textfield" ng-model="stxt">
</div>
</div>
heres the js file:
var app = angular.module("myApp", ['ngRoute']);
app.config(function($routeProvider) {
$routeProvider
.when('/angular', {
templateUrl: 'angular.php',
controller: 'dataCtr'
})
.when('/details', {
templateUrl: 'details.php',
controller: 'dataCtr'
})
.otherwise({
redirectTo: '/angular'
});
});
app.controller('dataCtr', function($scope ,$http ,$location ,$route ,$routeParams) {
$http({
method: "GET",
url: "json.php"})
.success(function (response) {$scope.names = response;});
$scope.changeView = function()
{
$location.url('/details');
};
$scope.disp = function(id)
{
$scope.stxt = $scope.names[id-1].Title;
};
});
The disp function is working fine on the angular view. But not being routed on the second view. I think the syntax for calling the two views in ng click is correct. OR if there any other method to call the associated table cell value to the second view. Please Help.
After a lots of research i figured it out.I used a factory service
app.factory('Scopes', function ($rootScope) {
var mem = {};
return {
store: function (key, value) {
$rootScope.$emit('scope.stored', key);
mem[key] = value;
},
get: function (key) {
return mem[key];
}
};
});
Added this to JS.
Because The scope gets lost on second Controller ,Services help us retain the scope value and use them in different controllers.Stored the scope from first controller
app.controller('dataCtr', function($scope ,$http ,$location,$rootScope,Scopes) {
Scopes.store('dataCtr', $scope);
//code
});
and loaded in the seconded controller.
app.controller('dataCtr2', function($scope ,$timeout,$rootScope,Scopes){
$scope.stxt = Scopes.get('dataCtr').disp;
});
Second view is not working because you cannot use $scope.$apply() method.
$apply() is used to execute an expression in angular from outside of the angular framework.$scope.$apply() right after you have changed the location Angular know that things have changed.
change following code part ,try again
$location.path('/detail');
$scope.$apply();

Using Angular Promises and Defers effectively

In Angular (and all SPA JS Frameworks), it is assumed that page navigation be extremely quick and "seamless" to the user. The only bottleneck to this speed is the use of API calls to retrieve data from our server. Therefore, it seems reasonable to find a solution where we can render an entire page, except for the physical data being presented, while we wait for our API call to get a response.
I am fairly new to Angular and I am running into a few problems that are keeping me from achieving the above goal and functionality. Let me take you through the 2 problems I came across while learning Angular
Problem 1: API response is too slow for Angular
At first, I was simply calling my API from within the controller. I have multiple tables of data that are going to be populated after I receive my response. Before the response is gotten, my tables have a simple <tr> that contains a .gif "spinner" that represents the loading of data. I have a $scope.gotAPIResponse property that is set to false initially and then after the API responds, it gets set to true.
I use this boolean in order to show and hide the "spinner" and the table data respectably. This causes the HTML to render successfully and everything looks fine to the user. However, a closer look at the JavaScript console and you can see that Angular threw many errors because it was attempting to $compile my templates BEFORE the API call got a response. Errors are never a good thing, so I had to improvise.
Inside the 'LandingController'
function init() {
$scope.gotAPIResponse = false;
// Get Backup data
API.Backups.getAll(function (data) {
console.log(data);
$scope.ActiveCurrentPage = 0;
$scope.InactiveCurrentPage = 0;
$scope.pageSize = 25;
$scope.data = data.result;
$scope.gotAPIResponse = true;
});
}
Here is the HTML template that throws errors.. both native template and pagination directive throws errors
<tbody>
<tr ng-hide="gotAPIResponse"><td colspan="6"><p class="spinner"></p></td></tr>
<tr ng-if="gotAPIResponse" ng-repeat="active in data.ActiveBackups | paginate:ActiveCurrentPage*pageSize | limitTo:pageSize">
<td><a ui-sref="order.overview({ orderId: active.ServiceId })" ng-click="select(active)">{{ active.Customer }}</a></td>
<td>{{ active.LastArchived }}</td>
<td>{{ active.ArchiveSizeGB | number:2 }}</td>
<td>{{ active.NumMailboxes }}</td>
<td>{{ active.ArchivedItems }}</td>
<td><input type="button" class="backup-restore-btn" value="Restore" /></td>
</tr>
</tbody>
</table>
<div pagination backup-type="active"></div>
Problem 2: Using resolve property is blocking everything
I then looked towards the resolve property that is available in the $stateProvider (ui-router) and $routeProvider (basic Angular). This allowed me to defer the instantiation of the Controller until the $promise on my API call was received.
This worked how it should have and I no longer received any errors because the API response was available before the HTML template could be rendered. The problem with this however, was that when you attempted to go to that state or route, the entire view would be unavailable for a split second until the $promise was received. This would appear to "lag" or "block" to the user (which just looks terrible)
Using resolve property
$stateProvider
.state('dashboard', {
url: '/',
controller: 'LandingController',
resolve: {
res: ['API', function(API){
return API.Backups.getAll(function (data) {
return data.result;
}).$promise;
}]
}
})
Then this controller is instantiated after API call, but the time between this is like 1-2 seconds...way too slow!
app.controller('LandingController', ['$scope', 'API', 'res', function ($scope, API, res) {
$scope.data = res.result;
...
Solution: Right in between
In order to solve this, we need a solution that is right in between the 2 that I have already tried. We need to instantiate the controller as quick as possible so $scope.gotAPIResponse can be set and therefore cause the "spinner" and rest of the DOM to show, however, we need to defer just specific parts of the HTML template and other Directives so that Errors are not thrown.
Is there an effective solution to this problem that people are using in production?
Update
In the end, this ended up being a much simpler fix. After some discussion we refactored the view a bit in order to avoid the ng-repeat executing before the ng-if which wasn't the desired behavior.
So instead of this:
<tbody>
<tr ng-if="!gotAPIResponse">
<!-- show spinner -->
</tr>
<tr ng-if="gotAPIResponse" ng-repeat="stuf in stuffs">
<!-- lots o' DOM -->
</tr>
</tbody>
We pulled the ng-if up one level to this:
<tbody ng-if="!gotAPIResponse">
<tr >
<!-- show spinner -->
</tr>
</tbody>
<tbody ng-if="gotAPIResponse">
<tr ng-repeat="stuf in stuffs">
<!-- lots o' DOM -->
</tr>
</tbody>
This way the ng-if isn't being duplicated for each row, and will halt further processing until the condition is satisfied.
Original Answer:
This is actually simple to achieve by using a directive and listening to some route change events on the $rootScope
The idea is to toggle a property on scope that can be used to Show/Hide some element. In this case a simple "Loading..." text, but in your case it could easily be used to show a spinner.
var routeChangeSpinner = function($rootScope){
return {
restrict:'E',
template:"<h1 ng-if='isLoading'>Loading...</h1>",
link:function(scope, elem, attrs){
scope.isLoading = false;
$rootScope.$on('$routeChangeStart', function(){
scope.isLoading = true;
});
$rootScope.$on('$routeChangeSuccess', function(){
scope.isLoading = false;
});
}
};
};
routeChangeSpinner.$inject = ['$rootScope'];
app.directive('routeChangeSpinner', routeChangeSpinner);
Then inside your HTML you would simply do this:
<div class="row">
<!-- I will show up while a route is changing -->
<route-change-spinner></route-change-spinner>
<!-- I will be hidden as long as a route is loading -->
<div ng-if="!isLoading" class="col-lg-12" ng-view=""></div>
</div>
Voila! Simple way to show a loading indicator while changing routes.
Full Punker Example
This is what you can do (example below):
Encapsulate your HTML in a directive
Create data in your controller (which you receive from the server side) and initialize it to an empty or initial value
Pass this data to your directive to have an isolated scope
Now fetch the data in your controller explicitly using the $http service and on resolution of the promise set the data tied to the directive to the data retrieved from the server
The directive will automatically update when the $http service code runs in the controller
Here's an explanation of the example I provided:
The controller initializes data to [1,2] but then calls the function changedata() which calls $http service to get the weather in New York and adds that to the data array. Hence you see "Sky is Clear" (or something else depending on when you run this example) rendered in your directive.
Here's the plnkr showing directive update with $http
// index.html
<!DOCTYPE html>
<html ng-app="plunker">
<head>
<meta charset="utf-8" />
<title>AngularJS Plunker</title>
<script>document.write('<base href="' + document.location + '" />');</script>
<link rel="stylesheet" href="style.css" />
<script data-require="angular.js#1.2.x" src="https://code.angularjs.org/1.2.20/angular.js" data-semver="1.2.20"></script>
<script src="app.js"></script>
</head>
<body ng-controller="MainCtrl">
<p>Hello {{name}}!</p>
<input type="button" ng-click="changedata()" value="Change"/>
<div myview data="model.data"></div>
</body>
</html>
// app.js
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope, $http) {
$scope.name = 'World';
$scope.model = {
data: ["1", "2"]
};
$scope.changedata = function() {
// updating data explicitly
$scope.model.data = [4, 5, 6];
url = "http://api.openweathermap.org/data/2.5/weather"
return $http.get(url, {
params: {
q: "New York, NY"
}
}).then(function(result) {
console.log("result=");
console.log(result);
if (result !== null) {
$scope.model.data.push(result.data.weather[0].description);
return result.data.weather[0].description;
}
return "Weather not found";
});
};
// updating data implicitly
$scope.changedata();
});
app.directive("myview", function() {
return {
restrict: 'EA',
template: '<table><tr ng-repeat="d in data"><td>{{d}}</td></tr></table>',
scope: {
data: '='
}
};
});
Your first problem of getting errors when the template renders is because data does not exist until after the promise resolves. A simple $scope.data = { ActiveBackups:null } before initiating your API call should solve that. When angular evaluates the ng-repeat expression active in data.ActiveBackups it will no longer be trying to reference the undefined attribute data.
If you have a longer loading request then you can have more control over the UX by not using resolves. Otherwise resolves tend to make for simpler controller code.

ng-repeat ng-click when the click function has already been called earlier in the code

First off, I read the plethora of other questions and answers regarding ng-click, ng-repeat, and child and parent scopes (especially this excellent one.)
I think my problem is new.
I'm trying to call a function using ng-click within a table. The app allows for the sorting of Soundcloud likes. The problem is that when I try to call the ng click function using new data, it still tries to call the function using the old data. Let me explain better with the example:
Controller:
function TopListCtrl($scope, $http) {
$scope.sc_user = 'coolrivers';
$scope.getData = function(sc_user) {
var url = 'http://api.soundcloud.com/users/'+ $scope.sc_user +'/favorites.json?client_id=0553ef1b721e4783feda4f4fe6611d04&limit=200&linked_partitioning=1&callback=JSON_CALLBACK';
$http.jsonp(url).success(function(data) {
$scope.likes = data;
$scope.sortField = 'like.title';
$scope.reverse = true;
});
}
$scope.getData();
$scope.alertme = function(permalink) {
alert(permalink);
};
}
HTML
<div id="topelems">
<p id="nowsorting">Now sorting the Soundcloud likes of <input type=text ng-model="sc_user"><button class="btn-default" ng-click="getData(sc_user);">Sort</button></p>
<p id="search"><input ng-model="query" placeholder="Filter" type="text"/></p>
</div>
<table class="table table-hover table-striped">
<thead>
<tr>
<th>Song</th>
<th>Artist</th>
<th>Likes</th>
<th>Played</th>
<th>Tags</th>
</tr>
</thead>
<tr ng-repeat="like in likes.collection | filter:query | orderBy:sortField:reverse">
<td width="30%"><a href="{{ like.permalink_url }}">{{like.title}}</td>
(Trying to call it here) <td>{{like.user.username}}</td>
<td>{{like.favoritings_count}}</td>
<td>{{like.playback_count}}</td>
<td>{{like.tag_list}}</td>
</tr>
</table>
</div>
</body>
</html>
So I have that function getData. I use it to load the data in using the soundcloud api. I have a form at the top that uses getData to load a new user's Soundcloud data in. I also call the getData function in the controller so that there is an example on the page upon loading.
The problem is when I try to load a new user's data from a <td> I want to be able to click on the user to see and sort their likes.
How do I 'clear' the function or the global namespace (am I even refering to the right thing)? How can I reuse the getData function with a new variable?
Working Jsfiddle for this
In your getData function you have this line:
var url = 'http://api.soundcloud.com/users/'+ $scope.sc_user +'/favorites.json...
but you are passing in the variable sc_user to your getData function and should be using it like this (no $scope):
var url = 'http://api.soundcloud.com/users/'+ sc_user +'/favorites.json...
That being said... your initial data load fails because you are calling:
$scope.getData();
and not:
$scope.getData($scope.sc_user);

Resources