I was going through the Angular documentation and had a question about directives. This is in regards to the "Creating a Directive that Manipulates the DOM" section of http://docs.angularjs.org/guide/directive
I added some console.log() statements in the following code:
scope.$watch(attrs.myCurrentTime, function(value) {
format = value;
updateTime();
});
like so:
scope.$watch(attrs.myCurrentTime, function(value) {
console.log('attrs.myCurrentTime = ', attrs.myCurrentTime);
console.log('value = ', value);
format = value;
updateTime();
});
When changing the contents of the 'Date format' text box, I was expecting to see the same value in both console.log() statements, namely the actual value of the parent scope's format attribute, but the first console.log() still shows 'format' as a string.
Why do you think this might be?
Link to code: http://plnkr.co/edit/8LkKBiIpqTn0gr5fXQZL?p=preview
First of all, there is no parent scope. Since your directive does not declare a scope (either isolate or "normal") there is no scope created, so the element shares the same scope as its parent-element.
A quick and easy way to check the scope of an element using DevTools is this:
1. Select the element in the "Elements" panel.
2. In the console, execute the command: angular.element($0).scope();
So, what is actually going on ?
attrs.myCurrentTime represents a plain old string value (namely "format").
So console.log('attrs.myCurrentTime = ', attrs.myCurrentTime); is equivalent to console.log('attrs.myCurrentTime = ', 'format');
In the same way, scope.$watch(attrs.myCurrentTime, ...) is equivalent to scope.$watch('format', ...).
According to the docs on Scope, if the first argument of $watch is a string it is "evaluated as expression", which in this case means as scope['format'] (which of course returns the current value of the scope's format property).
Related
vm.Parameters is a list of Parameter objects (vm is an alias for the controller).
Each Parameter has at least these 3 properties (to keep it simple):
param.Name
param.Dependensies
param.Values
Parameter may have dependency on another Parameter, for example, we have 3 parameters (Country, Region and City).
Region depends on Country, and City depends on Region and Country, like this:
vm.Parameters['Region'].Dependencies = ['Country'];
vm.Parameters['City'].Dependencies = ['Country', 'Region'];
When I render UI, I generate dropdowns for each parameter.
When country is selected, I need to populate Region dropdown with regions of selected country.
When region is selected, I need to populate City dropdown with cities of selected region and country.
Question: I want to know if it is possible to use $scope.$watch so that each child parameter watches for changes in parent parameters (param.Values property), listed in param.Dependencies.
I am not sure how exactly this should be implemented.
I added this function to the controller, that loops thru all the parameters in the list, and for each parameter it loops thru all the dependencies (names of parent parameters this parameter depends on, like Country and Region for City)
cascadeReportParameters() {
for (let param of this.reportParameters) {
for (let parentParam of param.Dependencies) {
this.$scope.$watch(parentParam, function (newValue, oldValue) {
this.getDependentParameterValues(param);
});
};
}
}
This function doesnt work.
According the documentation, first param is a string name of controller's property being watched.
So, if I had a property Property1, I could write
this.$scope.$watch('Property1', function (newValue, oldValue){}
However in my case I need to watch for Parameters['SomeName'].Values and I dont know how to set this watch. I am not sure what should be the first parameter to $watch function.
Any help is appreciated.
When used that way, $watch expects a scope variable. Notice the string notation in this example:
$scope.somevariable = 1;
$scope.$watch('somevariable', function(vNew, vOld) {
alert('somevariable has changed');
});
But you can watch a function instead. When watching a function, the watch is set on the function's return value, which can be anything and does not need to be a scope variable:
$scope.$watch(function(){
// return whatever value you'd like to watch
return Parameters['SomeName'].Values;
}, function(vNew, vOld) {
alert('The watch value has changed');
});
Hope that helps. Note that the function watch will be called multiple times per digest, which could potentially create performance issues.
EDIT: This answer: add watch on a non scope variable in angularjs also shows a bind syntax that might help further readability for controllerAs syntax, but it shouldn't be necessary.
I'm an author of angular-input-modified directive.
This directive is used to track model's value and allows to check whether the value was modified and also provides reset() function to change value back to the initial state.
Right now, model's initial value is stored in the ngModelController.masterValue property and ngModelController.reset() function is provided. Please see the implementation.
I'm using the following statement: eval('$scope.' + modelPath + ' = modelCtrl.masterValue;'); in order to revert value back to it's initial state. modelPath here is actually a value of ng-model attribute. This was developed a way back and I don't like this approach, cause ng-model value can be a complex one and also nested scopes will break this functionality.
What is the best way to refactor this statement? How do I update model's value directly through the ngModel controller's interface?
The best solution I've found so far is to use the $parse service in order to parse the Angular's expression in the ng-model attribute and retrieve the setter function for it. Then we can change the model's value by calling this setter function with a new value.
Example:
function reset () {
var modelValueSetter = $parse(attrs.ngModel).assign;
modelValueSetter($scope, 'Some new value');
}
This works much more reliably than eval().
If you have a better idea please provide another answer or just comment this one. Thank you!
[previous answer]
I had trouble with this issue today, and I solved it by triggering and sort of hijacking the $parsers pipeline using a closure.
const hijack = {trigger: false; model: null};
modelCtrl.$parsers.push( val => {
if (hijack.trigger){
hijack.trigger = false;
return hijack.model;
}
else {
// .. do something else ...
})
Then for resetting the model you need to trigger the pipeline by changing the $viewValue with modelCtrl.$setViewValue('newViewValue').
const $setModelValue = function(model){
// trigger the hijack and pass along your new model
hijack.trigger = true;
hijack.model = model;
// assuming you have some logic in getViewValue to output a viewValue string
modelCtrl.$setViewValue( getViewValue(model) );
}
By using $setViewValue(), you will trigger the $parsers pipeline. The function I wrote in the first code block will then be executed with val = getViewValue(model), at which point it would try to parse it into something to use for your $modelValue according the logic in there. But at this point, the variable in the closure hijacks the parser and uses it to completely overwrite the current $modelValue.
At this point, val is not used in the $parser, but it will still be the actual value that is displayed in the DOM, so pick a nice one.
Let me know if this approach works for you.
[edit]
It seems that ngModel.$commitViewValue should trigger the $parsers pipeline as well, I tried quickly but couldn't get it to work.
I have a custom Angular service which creates a custom DOM node using angular.element(). Meanwhile, since I also want the element to have a set of predefined attributes, I pass a JS object as a second parameter to the function:
var element = angular.element('<node-name />', {
class: "some css class",
onclick: "someClickHandler()"
});
Although this works OK as far as the attribute is not specific to Angular.
The problem is that I'm not able to produce Angular-like dashed-case (don't know what their actual name is) attributes (e.g. ng-click).
For now, if I do:
var element = angular.element('<node-name />', {ngClick: 'someClickHandler'}); // ng-click here is definitely not possible as it leads to a syntax error
it will always result in the DOM node as:
<node-name ngclick="someClickHandler"></node-name>
which doesn't work the Angular way.
So, is there any way that a camel-case attribute be converted to its equivalent dashed-case in the DOM?
Any help would be appreciated.
You don't really need any additional code to convert from camelCase to snake-case (although you could). It's better to use snake-case in the first place if you really want to, just make sure you put property name in quotes, otherwise the name is not valid identifier:
var element = angular.element('<node-name />', {
'ng-click': 'someClickHandler'
});
I'm currently in the progress of learning Backbone.js and I'm using the book Developping Backbone Applications.
I have a questions about the reference to HTML elements and how they are stored. For example:
initialize: function() {
this.$input = this.$('#new-todo');
Here the HTML element with ID to-do is stored in the this.$input, why do we use the $ in front of input, is this merely a convention? If I change this.$input to this.input my code works fine. I find this confusing because the book states:
The view.$el property is equivalent to $(view.el) and view.$(selector) is equivalent to $(view.el).find(selector).
I would think that $(view.el) does something completely different than (view.el).
How is this.$input saved in Backbone.js? If I console.log it, it produces:
Object[input#new-todo property value = "" attribute value = "null"]
Could someone give me some insight? :)
Using $ infront of a variable name is just a naming convention. It helps developer in distinguishing variable holding jQuery objects from others.
view.$el is a helper variable provided by Backbone, so that we can use it directly, instead of explicitly forming the jQuery object. Hence view.$el is equivalent to $(view.el).
view.$el is assigned in setElement method:
setElement: function(element, delegate) {
// Some code
this.$el = element instanceof Backbone.$ ? element : Backbone.$(element);
// Some code
}
Backbone.$ is reference to $ global variable exported by jQuery.
view.$(selector) is a method defined in View. It's definition does exactly same as $(view.el).find(selector)
$: function(selector) {
return this.$el.find(selector);
}
I have a fiddler setup, when i click a reset button it should clear out the input controls, this seems to work but not when the input type='url'
Here is the fiddler
Is there an issue or something that I am not understanding.
When I set
$scope.myform = {};
This seems to clear out the other input type but the input type='url' isn't being cleared.
Anyone know why?
The issue you see happens when you don't have a valid value inside the input[type="url"]. An invalid value just stays in the view (the input field) and doesn't get pushed to the scope variable inside ng-model. The variable will be updated only when and if the value is correct.
You can test it by entering a valid value. The reset button will work. If you enter an invalid value it won't.
You can fix it by setting $scope.myform = null instead of $scope.myform = {}. This will empty the field because the scope variable (expression) will be undefined. It will be automatically created by Angular once you enter a valid value inside any of the fields.
Because you need to put a valid url in the 2nd box like http://www.abc.com, then the reset button will work.
In order to correctly update the view/model, I would suggest that you explicitly reset the model's properties like so:
$scope.reset = function() {
$scope.myform = {
foo: '',
bar: ''
};
$scope.formName.$setPristine();
};
Setting 'myform' to an empty object deletes its fields, it doesn't set them to a blank string. It's quite likely angular's cleanup may not be deleting the value the view is referencing, thus the confusion between the application's model and view states.
Hope it helped.