I wrote an AngularJS directive with a form. The form has a required text field as well as two other forms. Each of them child forms has another required text field.
The difference between the 2 child forms is how I create them:
The first child form is compiled and appended to a div.
The second child form is directly included in the template of the directive.
If the second child form is invalid, the whole outter form becomes invalid. This is what I expected. However, if the first child form (the one I compiled manually) becomes invalid, it has no influence on the outter parent form. Why?
var app = angular.module('plunker', []);
app.component('generator', {
template: "<ng-form name=\"outterForm\">" +
"<input name=\"out\" ng-model=\"$ctrl.out\" ng-minlength=\"5\" ng-required=\"true\" type=\"text\" />" +
"<div id=\"component-container\"></div>" +
"<my-text></my-text>" +
"<div>Valid outterForm: {{outterForm.$valid}}</div>" +
"</ng-form>",
controller: function($compile, $scope, $document) {
var componentContainer = $document.find('#component-container');
var template = "<my-text></my-text>";
var childScope = $scope.$new();
var result = componentContainer.append(template);
$compile(result)(childScope);
}
});
app.component('myText', {
template: "<ng-form name=\"innerForm\"><input name=\"name\" ng-model=\"$ctrl.name\" ng-minlength=\"5\" ng-required=\"true\" type=\"text\" />Valid innerForm: {{innerForm.$valid}}</ng-form>"
});
Here's the running Plunker:
https://plnkr.co/edit/YfBRY4xPvKgqDtWXFMUi
That's because $$parentForm of sub-form's formController hasn't been set after you compile that sub-form. And I don't know why, it needs more deep knowledge I suppose.
I tried to $compile()() in different compilation stages (preLink, postLink) and had same result. However I almost achieve the goal with two methods:
First is to assign $$parentForm directly like this childScope.innerForm.$$parentForm = scope.outterForm;. Here is my plunker example (notice I changed components to directives, cause they are more flexible).
Second is to recompile parent form (but this makes useless manual sub-form compilation). Here is the second plunker example.
But! In both methods there is one huge problem - setting sub-forms names and models dynamically (it should be so, cause you want to use one directive on multiple sub-forms).
In first method there is no errors, but one bug: when you change model of the second sub-form it changes model of the first one (it stops when you once adjust model of the first sub-form).
In the second method everything seems to work fine, but at backstage there are a lot of errors occurs each time you change model.
Related
You'll have to bear with me on this one, because it's a little involved and isn't answered by the following questions:
Two way binding Angularjs directives isn't working
AngularJS: Parent scope is not updated in directive (with isolated scope) two way binding
Both of those answers are more than 3 years old and no longer apply. Please do not mark this as a duplicate of those questions.
I wish that I could make a simple, reproducible version of this problem, but my Angular is honestly not good enough, and it is a fairly complex setup with lots of meddling from the outside. But the title is correct.
TL;DR version:
User presses escape from input box made by custom directive
Code in parent scope is called through ngKeyup in order to change the value, by updating the bound model
This update is not reflected in the data displayed by the directive
Putting code to do that update inside the directive itself, which directly updates its OWN bound model, to escape the problem of parent-scope bound-model changes not working, does not work.
In fact, even though it is changed inside its own directive controller, it sets the parent-scope model and its own model to the input value instead of the one I'm trying to overwrite with.
And it still doesn't work.
???
Long version with code:
I had a bunch of code I was repeating over and over again in my HTML, and decided to make a custom directive. The code uses many functions from the parent scope. You don't need to know what all these functions do, just note there's a ton of them that happen on click, on blur, etc. So, it looks like this:
template html file
<div ng-if="should_display" class="myclass" ng-class="cssSizeClass">
<div ng-if="allow_edits">
<span
class="text myapp-edit-hover"
ng-click="showEditField = true; captureValue(propertyString,binValue, $event)"
ng-hide="showEditField"
{{ binValue | customNumber: numberPlaceholder }}{{ numberType }}
</span>
<input
ng-show="showEditField"
class="myapp-editable-input"
type="text"
focus-on-show
ng-focus="selectText($event)"
ng-model="binValue"
ui-number-mask="numberPlaceholder"
ui-hide-group-sep
ng-blur="showEditField = false; inputUpdate(propertyString, binValue, 27, false )"
ng-keyup="inputUpdate(propertyString, binValue, $event, false)">
<span class="numberSuffix">{{ numberSuffix }}</span>
</div>
In my directive controller, in order to use those pesky parent-scope functions...
.directive('appDetailCardInput', function() {
return {
restrict: 'E',
templateUrl: '/static/js/directives/mytemplate.html',
scope: { // # = string, = = object, & = function
propertyName: '#',
cssSizeClass: '#',
numberPlaceholder: '#',
numberType: '#',
binValue: '=',
editable: '=',
numberSuffix: '#'
},
controller: function($scope) {
// define local properties
$scope.should_display = $scope.$parent.thing;
$scope.propertyString = 'thing.' + $scope.propertyName;
// HERE, outside functions are "captured"
$scope.captureValue = $scope.$parent.captureValue;
$scope.inputUpdate = $scope.$parent.inputUpdate;
$scope.selectText = $scope.$parent.selectText;
// check if field is editable
if (typeof $scope.editable === 'undefined') {
$scope.allow_edits = true;
}
else {
$scope.allow_edits = $scope.editable;
}
}}
};
});
This actually works 90%! When the bound model in the parent scope is updated outside the Angular digest cycle (the usual case is during a callback from an async messaging service), it updates the displayed value properly. Other things that occur "outside" the directive update it properly.
However. When escape is pressed or a blur happens, or any code is called that updates the value from "inside" the custom directive, no such luck.
I'd like to take a moment here to note that the $event target will blur properly when programmatically told to.
In the case of the escape key, the inputUpdate() function is supposed to set the value back as if someone hadn't typed something in the input. The problem comes here:
// (in inputUpdate function)
$event.target.blur(); // This works
$event.target.value = $scope.previouslyCapturedValue; // This works
setstr = "$scope.thing." + ser + " = " + $scope.previouslyCapturedValue + ";"; // This does not work
eval(setstr);
The eval() changes the parent scope model back, but that isn't reflected in the child scope.
So I tried both methods from the answers above. Adding a $watch in a link function only fires the SECOND time escape is pressed, not the first, so the first time someone hits escape on a field, it doesn't help! Fail.
The Angular docs suggest wrapping the changes to the value in a $timeout, so that the changes are applied outside of the digest cycle. Doing so doesn't help...
$timeout(function() {
console.log('evaluating outside digest supposedly');
$scope.nexttarget = $scope.nextval;
eval($scope.nextstr);
}, 0, false);
Nothing changes, the same behavior happens. The displayed value does not change (although the value that you change when clicking on it does).
So with some more digging, I find out that $event.target.value = $scope.previouslyCapturedValue; is just setting the value of the input, and not updating the actual bound model. That's fine. The bound model just needs to be updated too.
If I could somehow directly overwrite the text displayed on the , that would almost be a worthwhile solution compared to going back to how things were before.
But like I said before, when it is updated exactly the same as eval($scope.nextstr); from an external messaging service, the update happens perfectly correctly.
So at this point in the story I get it into my head: why not 'intercept' the ngKeyup directive and set the model right there inside the directive? I put this in my controller:
$scope.inputUpdate = function(ser, value, $event, dropdown) {
var keyCode = $event.keyCode;
console.log('got inputupdate!');
if ((parseInt(keyCode) == 27)
|| (typeof keyCode === 'undefined')) {
console.log('escape or deblur!');
if (ser !== $scope.dontBlur) {
console.log('going to set it!!!')
$scope.binValue = $scope.$parent.previouslyCapturedValue; // This is the correct value confirmed by console logs
}
}
$scope.$parent.inputUpdate(ser, value, $event, dropdown);
}
Nope! It gets there in code, but it still doesn't display the correct value! In fact, the bound model is updated to what had been typed in, even though it had been programmatically changed back right there!
Why does it update fine from outside its own code from another controller, but NOT from its own code, even if it's being done directly in the directive controller itself in the "intercepted" function?
Can't wrap my head around this. It's like the bound model is "locked" during this time period or something.
Thanks for any guidance.
I am currently testing an angularjs directive. This directive has a templateUrl. I would lke to test the view to make sure it was initialized correctly i.e. correct number of buttons, certain elements hidden.
The problem I am having is that when I insert my html file into the template cache I still get :
"message": "Unexpected request: GET partials/stuff/stuff-leader.html
I assumed that when I used the templateCache I would no longer have to use:
$httpBackend.whenGET("partials/stuff/stuff-leader.html").respond([{
userId: 000
}]);
but this does not seem to be the case. I am wondering am I correctly inserting the template, here is how I am doing it:
template = $templateCache.get('/full/root/disk/path/to/file/stuff/stuff-leader.html');
$templateCache.put('/myApp/templates/stuff-leader.html',template);
Is this correct, or should I be placing it somewhere else?
Your normal template is looked for at /partials/stuff/stuff-leader.html, so this is what you need to inject into the template cache instead of /myApp/templates/stuff-leader.html.
You are performing a request with the $templateCache.get. Instead do:
beforeEach(inject(function ($templateCache) { $templateCache.put('partials/stuff/stuff-leader.html', '< div >...TemplateCode....< /div >'); }));
I am trying to mix Angular and React.JS. You can see my code here. Most examples I have seen involve an input written in basic Angular, and then output written in React like in this example. I am trying to do the opposite. I have two input fields as React components created in a directive and then an output of the model done with Angular.
I cannot get the output to reflect changes in the model. The model being $scope.formData = {name: "name", email: "email"};
I tried to follow the example in this video, and create an isolated scope in the directive
scope: {
formname: '=formname',
formemail:'=formemail'
},
and then pass the proper values through to the React template. It doesn't seem to have any affect. My input is not connected to the formData. In my non-React version of this, the model data fills in the input immediately and the output reflects changes to the input.
Because binding that data in an Angular style is breaking, I tried to have the React components update the model in a more Reactish way. I wrote a function in the controller called update_model,
$scope.update_model = function(data_type, updated_data){
$scope.$apply(function() {
$scope.formData[data_type] = updated_data;
});
console.log($scope.formData);
}
I then passed that function through the directive to the React template using scope.update_model, in the link function.
link: function(scope, element) {
React.renderComponent(Form_Profile_Component({update_model: scope.update_model, name_label: 'name', email_label: 'email', name_data: scope.formname, email_data: scope.formemail}), element[0]);
}
Now in the template, the input elements have and onChange event detector that will call this, and update the Model. The Model is supposed to be displayed at the bottom of the page with
<pre>
{{ formData }}
</pre>
The console.log shows a change in formData, but the {{formData}} display does not change.
Why won't my display {{formData}} show what is going on in the actual formData object?
EDIT: Just to make sure there wasn't a problem with {formData} I threw <input ng-model="formData.name"> in a few places in form.html, and there was no problem. The two way binding worked there.
EDIT 2: I tried adding the code below to the directive.
link: function(scope, element) {
scope.$watch('formData.name', function(newVal, oldVal) {
console.log("---");
React.renderComponent(Form_Profile_Component({update_model: scope.update_model, name_label: 'name', email_label: 'email', name_data: scope.formData.name, email_data: scope.formData.email}), element[0]);
});
}
I believe that when there is a change in formData, it will call this function and update the React component, or at the very least print '---' to the console. When I use the React input or the proper input nothing happens, which is not so surprising at this point. But when I tried the working input from the first edit, which does alter {{formData}}, nothing happens. It doesn't trigger the watch at all, I don't understand why.
You're on the right track with your 2nd edit where you put the renderComponent call inside a scope.$watch. This will make sure that the react component is re-rendered every time your watch variable formData.name changes.
However if you're looking for the changes you make within your React Component to propagate back out to angular, you need to make sure that the callback function you're passing to react has scope.$apply() call in it. Either encapsulating the change, or just after it.
Here's a quick example:
In your controller:
$scope.myVar = 'foo';
$scope.setVar = function(value) {
$scope.myVar = value;
$scope.$apply();
};
In your directive's link function (assuming no restricted scope, else change name as needed)
scope.$watch('myVar', function() {
React.renderComponent(
MyComponent({
myVar: scope.myVar,
setVar: scope.setVar
}),
element.get(0)
);
});
Then just call the this.props.setVar function from within your react component with the new value and it'll update correctly on the model.
I'm still quite new to AngularJS and I think I'm having trouble understanding the timing with $scope. In a controller, I've setup a watcher for some model data that is bound to various form elements. The watcher fires off an ajax request when the data changes, except if the form is not valid. I'm checking the form validity with myForm.$valid. This is all pretty straight forward, however, except when the model data is updated in the controller, and not the form. The validations runs as expected, but form.$valid still has the previous value, not what it should be with the updated data. For example, if the form was previously valid, then I delete the model value bound to a required input, the watcher will fire because the model data has changed, but when I log the value of myForm.$valid it's value is still true, even though it should be false.
So my question is A. why is this happening?, but more importantly B. what is the correct way to handle what I am trying to accomplish? Would a custom directive make sense?
Here is a simplified example.
HTML:
<div ng-controller="MyCtrl">
<form name="myForm">
<input type="text" name="myField" ng-model="myData" required>
<button type="button" ng-click="myData=''">Delete</button>
</form>
<div>
The watcher says the form is: <strong>{{ formStatus }}</strong>
</div>
</div>
Controller:
myApp.controller('MyCtrl', ['$scope', function($scope) {
$scope.myData = 'abc';
$scope.formStatus = '';
$scope.$watch('myData', function(newVal, oldVal) {
if (newVal != oldVal) {
console.log("my data changed");
console.log("my form valid = ", $scope.myForm.$valid);
$scope.formStatus = $scope.myForm.$valid ? 'Valid' : 'Invalid';
}
});
}]);
Fiddle: http://jsfiddle.net/anpsince83/cK6cc/1/
A custom directive is the right way to go. It's generally recommended for a handful of reasons that watches be put in directives vs controllers. At a high level, one reason is that Angular recommends controllers be thin and only operate as the glue between the view and services. But you're hitting a specific, interesting reason.
Controllers run before Angular directives are linked in. So your controller watch gets added to the watch list before Angular's ngModelWatch() does for myData. ngModelWatch() is where Angular runs $formatters (recalling that $formatters, amongst other things, are where validation is triggered when a change happens to the model).
Since watches are executed in the order in which they were added to the watch list, your controller $watch executes prior to validation being done by $formatters.
If you instead put the same $watch inside a directive it'll be added during link after ngModelWatch() has been added to the watch list. Thus the directive's $watch will execute after validation is done.
Here's an updated fiddle where you can watch the order of execution of $formatters and each $watch. And you'll see the directive version works as expected- running after $formatters.
Final Solution? It looks like I had my routing and controllers a little confused. I had my routing in a file with my controller module. I moved my routeProvider into the main module, like in the tutorial example. My controller and my routeProvider where under the same module. Maybe that was confusing everything. So now I have the controller in it's own module, and the routeProvider is under the main module. That seems to have fixed the problem without needing to initialize the search field to force the bindings to update the data.
Update:
The solution to showing my data at the time the page loads was to use an older version of angularJs, or use ng-Init and initialize the search field to a blank space.
I'm getting JSON data from the firebase website to update a table. I have a search field that works. The data will not display in the table until I type something into the search field. I don't know why the data won't just display in the table as soon as the controller is done getting the data.
Note: The link to the backend data is now removed, I don't want to keep that database file there indefinitely.
Here is the link to the jsFiddle code:
Last Version jsFiddle
Here is code for the controller.
'use strict';
/* App Module */
// Create the module named 'testApp'
var testApp = angular.module('theTestApp', [
'ngRoute',
'testServices'
]);
'use strict';
/* Services */
var testServices = angular.module('testServices', []);
testServices.controller('CommonController',
// function($scope, $http, $route) {
function($scope, $http) {
//access the customInput property using $route.current
//var dbKey = $route.current.customInput;
var dbKey = 'test-a-db-12345';
var urlToDb = 'https://' + dbKey + '.firebaseio.com/rows/.json';
$http.get(urlToDb).success(function(data) {
$scope.UsedItems = data;
});
});
How do I get the data to display as soon as it's loaded?
Update 1: I'm assuming that the data is already there, but the event of typing something into the search field triggers the filter, and then the data shows up. It shows up filtered.
Update 2: I'm reading about $Watch There is constantly and event loop listening for events. When a key is pressed in the search box, the bindings {{name}} get updated if something has changed. In this case, the content of the search input field was changed. So the issue seems to have something to do with when the bindings get updated, not whether the data is getting retrieved.
Update 3:
This version of the code runs. It loads the data when the page is rendered. Here is a working example in jsBins.
Update 4:
As of angularJs version 1.2.0 the behavior changes. Versions 1.0.8, 1.0.7 will instantly display my data when the page loads, 1.2.0 will NOT! I just happened to be using jsBins which uses 1.0.7, and it started working. Didn't know why until I started comparing the differences. Hopefully, there is a way to make it work in newer versions.
jsBins Working Example
The problem is that theSearch.Txt is empty. And you are filtering by that. My guess is that Angular at that moment decides to not let anything through and thus doesn't display anything.
What you should do:
Initialize the variable with a space filled string. (i.e. ' ')
Here is a working jsFiddle.
I used ngInit here. But that is because you decided to link?? the controller instead of putting it into a script tag. I suggest that next time you rather take the additional effort into pasting it in, as using the ngInit directive makes me feel like using eval.
Try
$scope.$apply(function(){
$scope.UsedItems=data;
}