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

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

Related

Sometimes when I click AngularJS links, the page reloads

I have a single-page app that has several views. On one view, I list all the orders, on another page, I modify those orders. I want to be able to go between the two pages without needing to save any data to a database.
On my development box, this works well. I just save the orders on $window and when I load the details page, I look through window.orders for it. Great. However, when deployed, all my links act like they link to a new page... so the page reloads. This means that when I try to link from my main list page to my details page, the details page doesn't get access to anything on window. Isn't the whole point of a single page app that it doesn't reload the page when changing views? How do I get my links to not load a new page? they're all in the format of text.
So here's part of my orders-list.template.html:
<table class="table-bordered" id="TheTable">
<tr>
<th>Part #</th>
<th>Customer</th>
<th>Job #</th>
<th>Due Date</th>
<th ng-repeat="day in days" class="td-nowrap">{{day}}</th>
</tr>
<tr ng-repeat="order in orders | orderBy:'DueDate'">
<th>{{order.PartNumber}}</th>
<td>{{order.Customer}}</td>
<td class="td-nowrap">{{order.JobNumber}}</td>
<td>{{order.DueDate|date:'shortDate'}}</td>
<td ng-repeat="day in days">
<input type="number" style="width:60px;" ng-model="order.Weeks[day].Pieces" ng-change="calculateHoursOfWork();"/>
{{(order | runTimePlusSetupPlusEfficiency:options:order.Weeks[day].Pieces) | number:3}}
</td>
</tr>
</table>
Note that the part number is a link to #!/orders/{{order.ID}}. I was under the impression this link wouldn't reload the web page.
This is in my main script:
$routeProvider.
when("/orders", {
template: "<orders-list></orders-list>"
}).
when("/orders/:orderId", {
template: "<order-detail></order-detail>"
}).
when("/options", {
template: "<options></options>"
}).
when("/printable", {
template: "<orders-printable></orders-printable>"
}).
otherwise("/orders");
So it should be clear that /orders refers to my ordersDetail component. Here's part of it:
angular.module("orderDetail").component("orderDetail", {
templateUrl: "AngularModules/order-detail/order-detail.template.html",
controller: ['$routeParams', "$http", "$window", function OrderDetailController($routeParams, $http, $window) {
var self = this;
self.TotalNonSetupRunTime = 0.0;
self.options = $window.options;
self.order = null;
$.each($window.orders, function (idx, elem) {
if (elem.ID == $routeParams.orderId) {
self.order = elem;
}
});
if (self.order === null) {
var orderID = null;
if ($routeParams.hasOwnProperty('orderId')) {
orderID = $routeParams.orderId;
}
var windowOrders = "undefined";
if ($window.hasOwnProperty('orders')) {
windowOrders = $window.orders.length;
}
alert("couldn't find order " + orderID + " among " + windowOrders + " orders! try going back and selecting again.");
}
}]
});
because the page is getting reloaded, I'm seeing windowOrders as undefined. Is there some setting in IIS I could change to make links to #<something> not reload the page (as expected)? or do I have to do something on the angularJS side?
Confounding it further... I did find this: hash link reloads page and apparently having a <base> directive screws with this? is there an angular-safe way to do this? I don't think it would make sense to get rid of that directive. I'm actually writing this on an asp.net-MVC page, so I have
<base ng-href="#Url.HttpRouteUrl("Default", new { }).Replace("?httproute=True", "")" />
in my _Layout.cshtml page. This way when it gets deployed (to http://<server>/<app>/) it works as well as when I'm debugging (on locahost:<some port>/).

Angular Updates Model but no Two Way Binding w/ Custom Filter

I recently wrote a simple custom filter which only displays items in my model given a specific model property and it works great. It is below..
Filter
app.filter('status', function() {
return function(input, theStatus) {
var out = [];
for (var i = 0; i < input.length; i++) {
var widget = input[i];
if (widget.status === theStatus)
out.push(widget);
}
return out;
};
});
The filter is applied as such on an ng-repeat.
<tr ng-repeat="widget in pendingWidgets = (widgetList | status: 0)">
<td><span class="glyphicon glyphicon-usd" /></td>
<td><span class="glyphicon glyphicon-usd" /></td>
<td><span class="glyphicon glyphicon-usd" /></td>
<td><span class="glyphicon glyphicon-usd" /></td>
</tr>
And on a panel heading as so
<div class="panel-heading"><span class="badge">{{pendingWidgets.length}}</span></div>
When the glyph is clicked ng-click runs updateStatus() as below...
$scope.updateStatus = function(theId, newStatus) {
widgets.setStatus(tagNumber, newStatus);
$scope.displayAlert = true;
};
And the widget.setStatus() is as such..
app.factory('widgets', ['$http', function($http) {
var o = {
widgets:[]
};
o.setStatus = function(aWidget, theStatus) {
return $http.put('/widgets/' + aWidget, { 'status': theStatus }).success(function (data) {
// do I need to put something here?
});
};
return o;
}]);
My question lies in
How can I get my page to refresh on the ng-click action when the updateStatus() call is made on my model? When the glyph is clicked the model is updated but the page is not. Only on a page refresh or when I visit a different page and then come back does the page display the updated model accurately with respect to the custom filter.
It doesn't look like you're updating the status for a particular widget (on the client side). You're telling your server about the update, but on the client side, no update happens.
That's why when you refresh (i imagine you're loading the widgets from the db / backend) you see the update.
Where you have: // do I need to put something here? you need to do something like:
aWidget.status = data.status; // where data is the updated widget object
(this assumes that your backend is returning the updated widget - which if you're following the same conventions that I'm used to - it should be).

watching ng-repeat objects used outside of of ng-repeat

Ok I have created a ng-repeat to get all users created by an $http.get. This get request updates every 5 secs by using $interval and displays individual user data when clicked by calling $scope.goInfo(data). This $scope.goInfo(data) is used throughout the page to show user data, but is created by the ng-repeat (but not always used in ng-repeat). How can I have this data obj created by ng-repeat update every 5 secs outside of ng-repeat? I can't wrap $scope.goInfo() in a $interval.
EXAMPLE
//CONTROLLER//
function liveFeed(){
$http.get('some URL').then(function (user) {
$scope.user = user.data;
console.log('user data is', $scope.user);
});
}
//Updates get req every five secs//
$interval(liveFeed, 5000);
//gets data obj from ng-repeat, needs to be updated every 5 secs.//
$scope.goInfo = function (data) {
$scope.name = data.name;
$scope.beats = data.beats;
}
HTML
<table>
<th>First Name: John</th>
<th>Last Name:</th>
<tr ng-repeat="data in user" ng-click = "goInfo(data)">
<td>{{data.name}}<td>
</tr>
</table>
<span>{{beats}}</span><!--needs to update every 5 secs, outside of ng-repeat and be binded to the user that was clicked on-->
You need to reset selected object after you retrieve new data. Basically, you just need to find corresponding records in new array of objects and set it as selected again.
Something like this should do the trick:
function liveFeed() {
$http.get('some URL').then(function(user) {
$scope.user = user.data;
// Find the record that was selected before this update
if ($scope.selectedUser) {
$scope.selectedUser = $scope.user.filter(function(obj) {
return obj.name === $scope.selectedUser.name; // or compare by unique id
})[0];
}
});
}
// Updates get req every five secs
$interval(liveFeed, 5000);
// Gets data obj from ng-repeat, needs to be updated every 5 secs
$scope.goInfo = function(data) {
$scope.selectedUser = data;
}
and HTML will use selectedUser:
<table>
<tr>
<th>First Name: John</th>
<th>Beats:</th>
</tr>
<tr ng-repeat="data in user" ng-click="goInfo(data)">
<td>{{data.name}}<td>
<td>{{data.beats}}</td>
</tr>
</table>
<span>{{selectedUser.beats}}</span>

Angular $watch just parent object instead of its multiple children

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>

Angular js redirect URL on same page?

I am building an node js app within which I have one page on angular js. On one of the pages, all the users are listed down(as hyperlinks) and I click on one of them to access the information about that user.
So, if the first page's URL(i.e. all the users) is /users, I want the second URL to look like /users/:id. So, in a way I want to mimic the REST API format.
The user's info has been found and once the user-id has been found, I m trying to use $location.path(index).
Initially I thought using something like ng-if would help me in rendering using the same page. So, on my .jade file, I have added this condition:
div(ng-show='loadAllUsers()')
$scope.loadAllUsers = function(){
console.log($scope.index == -1)
return $scope.index == -1;
};
$scope.index has been initialized to -1. So, I thought something like this would work when a new id value has been generated. But, the problem is it doesn't seem to reload the whole page or div.. I tried using window.location.href as well. That, even though updated the URL but even that breaks...
So, is there a way on doing this on the same jade file by any chance? If not , then how exactly should I go about this problem?
var app = angular.module('myApp', []);
app.controller('myCtrl', function($scope, UserService) {
$scope.userDetails={};
//get the list of users from data base. here i am taking some dummy values.
$scope.users=[{
id:1,
name:"Rahul"
},
{
id:2,
name:"Ajay"
},
{
id:3,
name:"Mohit"
}];
// get user details
$scope.getUserDetails=function(userId){
UserService.get({id:userId},function(userDetails){
$scope.userDetails=userDetails;
});
};
});
// No need to change the page URL. Above function will
// call 'UserService' factory. UserService will communicate
// with REST API with URL pattern 'app/rest/users/:id'.
// ':id' will be replaced by the id passed by above function.
// like 'app/rest/users/2'
angular.module('myApp').factory('UserService',function ($resource) {
return $resource('app/rest/users/:id', {}, {
get':{method:'GET', isArray: false}
});
});
<div ng-app="myApp" ng-controller="myCtrl">
<div>
<table>
<tr>
<th>Id</th>
<th>Name</th>
</tr>
<tr ng-repeat="user in users">
<td>{{user.id}}</td>
<td ng-click="getUserDetails(user.id)">{{user.name}}</td>
<tr>
</table>
</div>
<div>
<h3>User Details</h3>
id : userDetails.id <br/>
name : userDetails.name <br/>
// other details
</div>
</div>

Resources