Since I began learning AngularJS I've seen different approaches for calling controller functions from a view.
Suppose we have a Todo list application in AngularJS where you can add and remove Todo list items:
function TodoListCtrl() {
var vm = this;
vm.addItem = addItem;
vm.removeItem = removeItem;
activate();
function activate() {
vm.list = [];
}
function addItem() {
vm.list.push(vm.newItem);
// reset the form
vm.newItem = null;
}
function removeItem(item) {
vm.list.splice(vm.list.indexOf(item, 1));
}
}
And our HTML:
<h3>Todo List</h3>
<ul>
<li ng-repeat="item in vm.list">
{{ item }} <a ng-click="vm.removeItem(item)">Remove</a>
</li>
</ul>
<h4>Add Item</h4>
<input type="text" ng-model="vm.newItem" /> <button ng-click="vm.addItem()">Add</button>
In this example the addItem function depends on vm.newItem being set in order to add the new list item. However, it could also be re-written as:
function addItem(item) {
vm.list.push(item);
// reset the form
vm.newItem = null;
}
With our HTML updated as so:
<button ng-click="vm.addItem(vm.newItem)">Add</button>
I can see that this makes the function easier to test since we're not dependent on the state of the controller but we don't avoid it completely since we're resetting vm.newItem after an item is added.
Are there any best practices for when we should pass parameters from our views and when we can just rely on the internal state of the controller?
Passing vm.newItem means you have it in 2 places in the View. While it may be clear, it's also repeating yourself and leaves you open to possibly having one get out of sync. And at what additional value? I think it is clear already without that like this.
<input type="text" ng-model="vm.newItem" />
<button ng-click="vm.addItem()">Add</button>
Otherwise you have this duplication.
<input type="text" ng-model="vm.newItem" />
<button ng-click="vm.addItem(vm.newItem)">Add</button>
You say it is easier to test, but why? You are testing a function on the controller, so it's perfectly fine to expect that function is use a property on the same controller. You mock dependencies to the controller, but not members of it.
In both cases you showed, the function addItem creates side-effects on the internal state of the controller (with vm.newItem = null;), so it cannot be tested in isolation.
In the second case, however, this doesn't even make sense to pass a different variable, since it would make the statement vm.newItem = null; potentially erroneous.
To make the function completely state-less:
vm.addItem(item){
vm.list.push(item);
}
you'd need to reset the form from the View:
<input type="text" ng-model="vm.newItem">
<button ng-click="vm.addItem(); vm.newItem = null">Add</button>
This could be acceptable if resetting the form is a View-only concern. If not, then this approach would not work altogether because it could leave your controller in an erroneous state (where vm.newItem is still pointing to the newly added item of the list)
At the end of the day, it depends on your particular use case. If you always have only a single item that you can "add", then passing an explicit parameter is redundant at best.
If, however, addItem can be called for any newly created item in the View, then passing the reference explicitly is probably the only way to go.
Testing-wise, you should always test that a controller is in a consistent state after an operation.
Related
I've noticed that if I call functions from my angular view, the functions are called a lot. Also when the data hasn't changed.
For example:
<div ng-repeat="day in days_array">
{{getWeek(day)}}
</div>
Then getWeek() gets called for all the items in days_array every time almost anything in the application changes. So I wondered if this is what filters are solving? So filters are only called when days_array is actually changed, and therefore gives better performance?
Would not be easier to optimize to map week once and use it directly in HTML? In some place you load days:
$scope.loadDays = function () {
service.getDays().then(function (days_array) {
$scope.days_array = days_array.
});
}
Or you have it hard-coded:
$scope.days_array = [{index: 0, code: MONDAY}...];
You can easily add week property:
$scope.days_array = $scope.days_array.map(function (day) {
day.week = getWeek(day);
return day;
});
And use it in HTML like this:
<div ng-repeat="day in days_array">
{{day.week}}
</div>
And it performs as usual Angular binding.
Moreover, you can optimize it further - if week never changes you can use one-time binding:
<div ng-repeat="day in days_array">
{{::day.week}}
</div>
Angular forgets about checking this variable. And even one more step - if days_array is set once and never changes, you can forget about list at all:
<div ng-repeat="day in ::days_array">
{{::day.week}}
</div>
Yes it's bad performance. Filters would help, yes. If you can have a way when something is added to know if it's newly added.
I will leave this link with performance tips on angularjs loops
https://codeutopia.net/blog/2014/11/10/angularjs-best-practices-avoid-using-ng-repeats-index/
I hope it helps.
I am using a backemnd service (parse in this case but that doesn't really matter for this question) and wanted to simply search it. I have a textbox that upon text being entered searches the server and returns an array of matchs.
My next step is to simply display my returned objects nicely in a list. Easy enough with ng-repeat but because the view has already been loaded the UI won't update to reflect the array being loading into the list. Does that make sense?
I was wondering if there was a technique to Refresh the list and show the returned search elements, and hopefully I am not being to greedy here but doing it in a way that looks good and not clunky.
I did a lot of googling with NO luck :( any advice would be amazing.
Without any code provided it is hard to guess what is wrong. Angular has two-way binding, so view should be updated automatically after changing content of an array. If it's not, it means that you probably did something wrong in your code. I present an example code which should work in this case.
Controller
angular.module('moduleName')
.controller('ViewController', ['ViewService', ViewController]);
function ViewController(ViewService) {
var self = this;
self.arrayWithData = [];
self.searchText = "";
// ---- Public functions ----
self.searchData = searchData;
// Function which loads data from service
function searchData(searchText) {
ViewService.getData(searchText).then(function(dataResponse) {
// Clear the array with data
self.arrayWithData.splice(0);
// Fill it again with new data from response
angular.forEach(dataResponse, function(item) {
self.arrayWithData.push(item);
});
});
}
// --- Private functions ---
// Controller initialization
function _initialize() {
self.searchData(self.searchText);
}
_initialize();
}
View
<div ng-controller="ViewController as view">
<input type="text" ng-model="view.searchText" />
<input type="button" value="Search!" ng-click="view.searchData(view.searchText)" />
<!-- A simple ngRepeat -->
<div ng-repeat="item in view.arrayWithData">
<!-- Do what you want with the item -->
</div>
</div>
Conclusion
By using splice() and push() you make sure that reference to your array is not changed. If you are using controllerAs syntax (as in the example), assigning new data with '=' would probably work. However, if you are using $scope to store your data in controller, losing reference to the array is the most probable reason why your code doesn't work.
I'm building an app on top of MEANJS. Here is my view:
<section data-ng-controller="PostsController" data-ng-init="findOne()">
...
<span data-ng-controller="FavsController" ng-init="isFaved(post._id)">
<button type="button" ng-hide="faved[post._id]"></button>
</span>
...
</section>
And here is my function located in FavsController:
$scope.isFaved = function(postId) {
console.log(postId);
// other stuff. create an array named faved etc.
};
Problem is, post._id passes as undefined to function. But when I put say {{ post._id }} in my view, it prints the current post id, no matter where in the html. When I provide post id by typing (I mean like isFaved(123)), it is working and the inner button's ng-hide is also working properly. I'm using same html and same js with some other views, but only this one throws an error. Any ideas why is this happening?
The ng-init is great for pre-setting values, and is intended to be used for aliasing properties inside of an ng-repeat.
What you would really want is to either tie the function call to an appropriate event like...
ng-click="isFaved(post._id)"
Or initialize the values or variables in the controller so you can ensure that they will exist with a value. The use of ng-init in this situation isn't really how it was intended so you can expect the unexpected.
When my user select one of the object in my observable array, I want to copy it to a "selectedObject". But when I do that, the layout binding on the "SelectedObject" are not updated.
So I've created an update method but I find it very difficult to maintain. is there a better way?
Here is my selected object ui:
<div class="row" data-bind="with: SelectedFlightObject">
<div>select object:</div>
<div data-bind="html: FlightNumber"></div>
</div>
Here is the js that I want to work but doesn't:
//this do not update the layout:
this.OnFlightClick = function (selectObject) {
this.SelectedFlightObject = selectObject;
}.bind(this);
Here is the js that update the ui but find it hard to maintain.
UpdateFlightObject: function (currentObj, newObj) {
currentObj.AirplaneType(newObj.AirplaneType());
currentObj.ArrivingDate(moment(newObj.ArrivingDate()));
currentObj.FlightNumber(newObj.FlightNumber());
currentObj.Duration(newObj.Duration());
currentObj.ArrivalCode(newObj.ArrivalCode());
currentObj.DeparturCode(newObj.DeparturCode());
},
this.OnFlightClick = function (selectObject) {
FlightFactory.UpdateFlightObject(this.SelectedFlightObject, selectObject);
}.bind(this);
Knockout requires you to use their observable wrappers. These wrappers are where the magic happens, once bound, they are what reports changes in values and receive user input back. Your code should look something like this.
Create:
this.SelectedFlightObject = ko.observable(someInitialValueOrNull);
Retrieve:
this.SelectedFlightObject();
Update:
this.SelectedFlightObject(someNewValueOrNull);
You should make your SelectedFlightObject observable so that when it changes your layout is updated.
this.OnFlightClick = function (selectObject) {
this.SelectedFlightObject(selectObject);
}.bind(this);
Your initial SelectedFlightObject would of course need to have some initial values for flight number, etc...
Remember that doing this does not create a new object
My problem is part of a larger program but I extracted a simulation of the problem in a simpler jsfiddle.
I'm implementing a "detailed view" show/hide controller for a todo item. When I click on the link, a detailed view is shown or hidden. The detailed view state is stored in todo.showDetails. This works fine.
When I mark a todo as completed, I persist its current state on the server using:
// Persist immediately as clicked on
$scope.checkTodo = function (todo) {
todo.$save();
};
This causes my detailed view to go hidden as the todo.showDetails state is lost because it's not part of the persisted item (i.e., it's overridden with the server's view of the todo item, which doesn't contain the UI state todo.showDetails). I'm not surprised, this makes sense.
My question is: how can my todo items contain both local UI state and data that's persisted on the server? When I todo.$save(), I'd like the todo's data to be saved on the server while retaining the value of todo.showDetails in the client code. I.e., whenever I call todo.$save(), I'd like my UI state to remain unchanged.
Is there an idiomatic way to do this in AngularJS? I wouldn't want to persist UI values like showDetails on the server.
One way I've been thinking of implementing this would be to have a separate service that stores UI state for each todo item. When I need to access local state, instead of accessing like todo.showDetails, I could do something like AppState.lookup(todo.id).showDetails or similar. But perhaps there's a simpler way..
-- UPDATE 2013-02-25 --
I did as in the below answer and simply separated UI state from todo items created by $resource.get/query calls. It was a lot simpler than I thought (see commit):
My controller changes:
$scope.todos = Todo.query();
+ $scope.todoShowDetails = [];
+ $scope.toggleShowDetails = function (todo) {
+ $scope.todoShowDetails[todo.id] = !$scope.todoShowDetails[todo.id];
+ }
+
+ $scope.showDetails = function (todo) {
+ return $scope.todoShowDetails[todo.id];
+ }
Template changes:
- <label class="btn btn-link done-{{todo.done}}" ng-click="todo.showDetails = !todo.showDetails">
+ <label class="btn btn-link done-{{todo.done}}" ng-click="toggleShowDetails(todo)">
..
- <div ng-show="todo.showDetails" ng-controller="TodoItemCtrl">
+ <div ng-show="showDetails(todo)" ng-controller="TodoItemCtrl">
It's simple when I put it in code. My jsfiddle example is a little bit contrived, it makes more sense in the context of my todo app. Perhaps I should just delete the question if it's either difficult to understand or too trivial?
Is it possible for you to extract that UI state from your todo object? You'd still place the state object on the scope, just not inside the todo object. It sounds like that type of state doesn't belong in the todo object anyway, since you don't want to persist it.