HTML:
...
<input ng-init="item.check=false" type="checkbox" ng-model="item.check">
...
<input ng-init="item.name=''" class="form-control" ng-model="item.name">
...
JS:
var myApp = angular.module('myApp', []);
myApp.controller('AppCtrl', function ($scope, $http) {
var refresh = function() {
$scope.item.check = false;
$scope.item.name = "";
}
//Call upon loading app to initialize values, also called upon events to refresh the field
refresh();
I initially thought that just the JS is sufficient to initialize the values and so I got rid of both the checkbox and the form's ng-init then ended up getting the error:
TypeError: Cannot set property 'name' of undefined
The app works if I just leave both ng-init. It also works when I remove the ng-init from only one of the 2 elements (so either from the checkbox or the form) but it will not work if I remove both. What is going on here? Am I doing this correctly or is there a better way to initialize?
Thanks.
You can add $scope.item = {} in first line of your controller.
Javascript reports error when you are trying to access $scope.item.name when $scope.item itself doesn't exist (or not an object in some cases).
When you do ng-init, angular creates the object for you so you don't get the error.
Add this to the controller
$scope.item = {};
Related
I have a function which uses papaparse.js to immediately get the content of a .csv file selected via a file input as a JSON object list, I then pass this list to a $scope which I console.log.
This is triggered on a button click and works, the issue is the view can't immediately see this $scope has been updated until another action happens, such as clicking the button again, or clicking another button which calls an empty $scope function.
HTML View:
<div ng-controller="myController">
<input id="csvfile" type="file" accept=".csv">
<button type="button" ng-click="readDataList()">
Preview Data
</button>
{{dataList}}
</div>
And the .js is
var App = angular.module('App', ["ui.bootstrap"]);
App.controller('myController', function ($scope, $http) {
$scope.dataList;
$scope.readDataList = function ()
{
var myfile = $("#csvfile")[0].files[0];
var json = Papa.parse(myfile,
{
header: true,
skipEmptyLines: true,
complete: function (results)
{
$scope.dataList = results.data;
console.log($scope.dataList);
}
});
};
});
The list is in the console as soon as the button is clicked, but won't appear in the view unless I clikc the button again.
If I add into the contoller the following:
$scope.testScope = 'hello';
$scope.changeScope = function () {
$scope.testScope = 'goodbye';
}
and in the view have the new $scope displayed and the function on a new ng-click this works immediately displaying the new value as soon as the button is clicked. But it doesn't work for the data from the csv even though it appears in console.
Try $scope.$apply(); after the line $scope.dataList = results.data;
Your Papa.parse function is out of the scope of angular, due to which any changes done in its callback are not captured by angular. So you have to manually trigger the new digest cycle with $scope.$apply() function so that angular checks if anything has changed in its scope and updates the view.
Read this for more detailed information http://jimhoskins.com/2012/12/17/angularjs-and-apply.html
I could able to resolve the issue by using $rootScope, I have used $rootScope and the value is automatically updating the view (HTML template).
Controller:
$rootScope.myVariable = 'update label'
Now, if you update the value of myVariable via any click/change events the updated value will be updated in the HTML template.
HTML file:
<h3>{{myVariable}}</h3>
I have multiple controllers on a small app I'm writing, and I have successfully shared a 'selected' variable between the controllers like so.
app.service('selectedEmployee', function () {
var selected = null;
return
{
getSelected: function() {
return selected;
},
postSelected: function(employee) {
selected = employee;
}
};
});
I have a side nav bar with a list of employees. When I click on an employee I call the postSelected function then the getSelected to set $scope.selected.
$scope.selectEmployee = function(employee) {
//Calling Service function postSelected
selectedEmployee.postSelected(employee);
$scope.selected = selectedEmployee.getSelected();
if ($mdSidenav('left').isOpen()) {
$mdSidenav('left').close();
}
}
I have a third controller for my main content area, and this is where I don't understand what to do. I want information from the selected employee to be displayed, but angular is compiling the whole page before the first employee has a chance to get set as selected, and subsequent selections of an employee aren't reloading the main content page (because I haven't told them to I think). Here's my main content controller:
app.controller('mainContentController', ['$scope','selectedEmployee',
function ($scope, selectedEmployee) {
$scope.selected = selectedEmployee.getSelected();
console.log($scope.selected);
}
]);
My main content view is very simple right now
<h2>{{selected.firstName}}{{selected.lastName}}</h2>
My question is how I can tell one controller to effectively update its partial view so that when I select an employee it displays information.
GitLab repo
Don't rely on messy broadcasts if your goal is simply to display & modify the data in the controller's template.
Your controllers do NOT need to "know" when the Service or Factory has updated in order to use it in the template as Angular will handle this for you, as you access the data via dot notation. This is the important concept which you should read more about.
This Fiddle shows both ways of accessing the data, and how using the container object in the template causes Angular to re-check the same actual object on changes - instead of the primitive string value stored in the controller:
http://jsfiddle.net/a01f39Lw/2/
Template:
<div ng-controller="Ctrl1 as c1">
<input ng-model="c1.Bands.favorite" placeholder="Favorite band?">
</div>
<div ng-controller="Ctrl2 as c2">
<input ng-model="c2.Bands.favorite" placeholder="Favorite band?">
</div>
JS:
var app = angular.module("app", []);
app.factory('Bands', function($http) {
return {
favorite: ''
};
});
app.controller('Ctrl1', function Ctrl1(Bands){
this.Bands = Bands;
});
app.controller('Ctrl2', function Ctrl2(Bands){
this.Bands = Bands;
});
First of all lets start by good practices, then solve your problem here...
Good Practices
At least by my knowledge, i dont intend to use services the way you do... you see, services are more like objects. so if i were to convert your service to the way i normally use it would produce the following:
app.service('selectedEmployee', [selectedEmployeeService])
function selectedEmployeeService(){
this.selected = null;
this.getSelected = function(){
return this.selected;
}
this.postSelected = function(emp){
this.selected = emp;
}
}
You see there i put the function seperately, and also made the service an actual object.. i would reccomend you format your controller function argument like this... If you want to disuss/see good practices go here. Anways enough about the good practices now to the real problem.
Solving the problem
Ok The Andrew actually figured this out!! The problem was:that he need to broadcast his message using $rootScope:
$rootScope.$broadcast('selected:updated', $scope.selected);
And then you have to check when $scope.selected is updated.. kinda like $scope.$watch...
$scope.$on('selected:updated', function(event, data) {
$scope.selected = data;
})
After that it autmoatically updates and works! Hope this helped!
PS: Did not know he anwsered already...
So after much research and a lot of really great help from Dsafds, I was able to use $rootScope.$broadcast to notify my partial view of a change to a variable.
If you broadcast from the rootScope it will reach every child controller and you don't have to set a $watch on the service variable.
$scope.selectEmployee = function(employee) {
selectedEmployee.postSelected(employee);
$scope.selected = selectedEmployee.getSelected();
$rootScope.$broadcast('selected:updated', $scope.selected);
if ($mdSidenav('left').isOpen()) {
$mdSidenav('left').close();
}
}
And in the controller of the main content area
function ($scope) {
$scope.$on('selected:updated', function(event, data) {
$scope.selected = data;
})
}
I don't think you have to pass the data directly, you could also just as easily call selectedEmployee.getSelected()
$rootScope also has to be included in the Parent controller and the broadcasting controller.
We have a mover directive that uses 2 list controls. The problem is that when I simply click on an item in the assigned items list, it makes my form dirty. I tried the following solution in the directive controller onChanged event (which is called by the second list ng-change):
$scope.onChanged = function (assigned) {
var currentState = $scope.form.$dirty;
$scope.selectedItem = assigned[0];
if (currentState === false)
$scope.form.$setPristine();
}
However, the currentState is already true, so my code does nothing. How can I prevent the clicking in the list control to set form's dirty status? I found two related questions How can I exclude controls from making form Dirty? and How can I denote which input fields have changed in AngularJS but it's not clear to me if I should try either of these solutions. The code checks for $scope.form.$dirty in a few places, so the best solution is somehow to make sure that clicking on the list doesn't make it dirty. I also have noDirty directive which I haven't yet tried applying to that list. I'm going to try that now.
The solution I implemented for now is the following change in the directive:
var directive = {
controller: ['$scope', '$timeout', function ($scope, $timeout) {
$scope.upDisabled = true;
$scope.downDisabled = true;
$scope.assigned = null;
$timeout(function () {
$scope.form.assignedList.$pristine = false;
});
based on last answer in that thread Prevent input from setting form $dirty angularjs. In my quick limited test it seems to work. I originally tried to just use our noDirtyCheck directive on the list, but somehow it didn't work in my first try, so I already switched to this solution.
I am trying to to share a selected option with another controller (And have it update when I select a new option). Something that would work like the 2-way data binding between controllers.
I've attempted this by setting up a factory like so
.factory("shareObjective", function($scope){
var shareObjective = {};
return {
shareObjective: shareObjective,
};
})
Then I inject this into the controller and bind it to the model of the select like so
$scope.selectModel = shareObjective.shareObjective;
I seem to be having some trouble getting this to work. I Basically just want to share the selected option (it's .name to be precise) with another controller and am struggling to do so. My first step was to get it to share into the factory to begin with, but I seem to be having no luck attempting this. Should I be using something like the $broadcast to keep the stream of information open? Thanks!
Edit - here's a plunkr http://plnkr.co/edit/L5lz4etQ7mUEhf9viNOk?p=preview
Yes, this won't work by default because you use two different scopes thus different ngModels.
Using a service also won't help because even if a click on a select with a ngModel in one scope will trigger a digest loop in that scope, it won't trigger it in the other scope.
As you suggest yourself you need to somehow notify the other scope to update itself, and yes you can do this through events ($broadcast and $on):
Both controllers contain
<select
ng-model="foo"
ng-options="foo for foo in foos"
></select>
And this is the JS:
var foos = [
'foo1',
'foo2',
'foo3'
];
function MyCtrl($scope, $rootScope) {
$scope.foo = foos[0];
$scope.foos = foos;
// detect broadcasts from other controllers and set our value to the one we received
$rootScope.$on('foo-event', function(e, data) {
$scope.foo = data;
});
// detect when foo changes and broadcast it's value
$scope.$watch('foo', function() {
$rootScope.$broadcast('foo-event', $scope.foo);
});
}
myApp.controller('MyCtrl1', ['$scope', '$rootScope', MyCtrl]);
myApp.controller('MyCtrl2', ['$scope', '$rootScope', MyCtrl]);
Here's the fiddle.
Notice that my code does not use a single controller, just a single controller implementation. You can write your own MyCtrl1 and Myctrl2 implementations that both have this code inside.
Also, while it would look like this generates an infinite loop between $watch and $on, it does not. $scope.foo = data; does not trigger $watch,
I want to make an include refresh itself when the state changes in my app.
Example:
<div ng-include="'partials/search-form.html'"></div>
Global listener:
phonecatApp.run(function ($state,$rootScope, $log) {
$rootScope.$state = $state;
$rootScope.$on('$stateChangeSuccess', function(){
if( !$state.includes('search') ) {
// refresh the search-form include in the header
}
});
});
How can I do this? Because as the form is outside of the ui-view and when I change the page it still has a value from the previous search query as it's still in the previous state... How can I refresh this so it's updated???
Note: creating another ui-view isn't a solution as this form is just a simple include shown in a header across all pages and doesn't relate to any states, etc.
Update: So you can see what I am doing with the form and controller:
phonecatControllers.controller('SearchCtrl', function($rootScope, $scope, $state, $location) {
$scope.query = ($state.includes('search') ? $location.search()['q'] : '');
$scope.filterQuery = ($state.includes('search') ? $location.search()['q'] : '');
if(!$scope.query){
$location.search('q', null);
}
$scope.queryChanged = function () {
if($scope.query){
$state.go('search', {'q': $scope.query} );
} else {
$location.search('q', null);
}
$scope.filterQuery = $scope.query;
}
});
<form class="form-search" ng-controller="SearchCtrl" ng-submit="queryChanged()">
<input name="q" ng-model="query" id="filter" type="text">
<button type="submit" class="btn">Search</button>
</form>
Update: The best idea I could come up with, would be to empty the input field like so:
var q = document.getElementsByName('q');
q.setAttribute('value', '');
When the state changes is NOT the search state. This automatically fires a change event on the input and then causes it to have it's ng-dirty class removed, etc.
Since the form is available globally, my advice is that you put the query in the $rootScope. The rootscope is available on the entire application (if you inject it, of course), so you would be able to reset the value at any moment.
An even better solution would be to store the query value in the rootscope and access it using a service.
For reloading the route you can use:
$route.reload();
But i'm not sure if that will reload the included file.
Perhaps you can set the string of the included file as a $scope variable and then set it as an empty string first and then put it back again? Not sure if it will work, probably needs a $timeout, $digest or $watch to make sure it is ready to flip.
Another option would be to set an
ng-if="someForm"
var to the first route and when you are done, set it to false and have another
ng-if="someOtherForm"
for the second include. Or use the same but set it to something falsy first.
But if it is about cleaning the form, there are other ways to do that. Like setting all model-vars to an empty string or null. So if you have it at $scope.searchEntry, you set it undefined or "".
In your example:
$scope.query = "";
This is what I have at the moment:
phonecatApp.run(function ($state, $rootScope, $log, $location) {
$rootScope.$state = $state;
$rootScope.$on('$stateChangeSuccess', function(){
if( !$state.includes('search') ) {
$('[name=q]').val('');
} else {
$('[name=q]').val($location.search()['q']);
}
});
});
I've had to use jQuery because Vanialla JS couldn't see the element. But it basically just sets the input value depending on where you are in the app.
The ONLY issue is that if you submit the form when it's empty it will then default back to what it was before! Presumably because the model hasn't been reset!
So if I do this instead:
$('[name=q]').val('').change();
It then triggers the model to be reset (from what I can tell) as it no longer submits the previous result... Thoughts? It feels dirty, but it works!