I have a directive like this:
app.directive("myData", function() {
return {
restrict: "E",
templateUrl: "myTemplate.html"
};
});
Then in myTemplate.html I have something like:
<input type="number"
ng-if="(getMyObject(item.DataId)).Valid"
ng-model="(getMyObject(item.DataId)).Price"/>
<div ng-bind="(getMyObject(item.DataId)).Price"
ng-if="!(getMyObject(item.DataId).Valid"></div>
This directive is placed inside an ng-repeat (hence the item.).
My question is how can I store the object I get from getMyObject() somewhere so I don't have to call it repeatedly? I tried to use ng-init as:
<p ng-init="dataObject=getMyObject(item.DataId)"/>
and reference it like:
<input type="number"
ng-if="dataObject.Valid"
ng-model="dataObject.Price"/>
<div ng-bind="dataObject.Price"
ng-if="!dataObject.Valid"></div>
But this doesn't work once I submit any changes and change the data in the model since the ng-init only works on the first time when the page loads.
You could set up the one time binding in your linking function:
app.directive("myData", function() {
return {
restrict: "E",
templateUrl: "myTemplate.html",
link: function(scope) {
scope.dataObject = scope.getMyObject(scope.item.DataId);
}
};
});
This way you will have one dataObject per instanciation of your directive, but only computed once. Now if you need to "recompute" this dataObject after some change, you could do that in a function or on a watcher:
link: function(scope) {
scope.dataObject = scope.getMyObject(scope.item.DataId);
// Option 1
scope.$watch('somethingToWatch', function() {
scope.dataObject = scope.getMyObject(scope.item.DataId);
});
// Option 2 (choose one or the other)
scope.onSubmit = function() {
scope.dataObject = scope.getMyObject(scope.item.DataId);
};
}
Related
I am trying to write custom directive and one of the requirements is that I should be able to disable one of the elements based on the expression set on attribute of the directive. Directive is declared like this
<sr-payment-edit payment="vm.paymentInfo" disablepaymenttypeselection="!vm.isPolicySelected || someOtherCondition">
Basically it is supposed to hide a payment type if a policy is not selected yet.
Once policy gets selected, payment type would be enabled. Here is html template for that portion of the directive
<div class="row" data-ng-hide='hidePaymentType'>
<div class="col-xs-12 p-l-0 p-r-0">
<div class="col-xs-3">
<div class="form-group">
<label>Payment Type:</label>
<select data-ng-model="payment.paymentTypeCode"
data-ng-disabled="disablePaymentType" class="form-control" style="width: 150px">
<option value="CASH">Cash</option>
<option value="CHCK">Check</option>
<option value="CCPP">Credit Card - Pre Pay</option>
<option value="MNOD">Money Order</option>
</select>
</div>
</div>
</div>
</div>
Here is directive code, in early stages
(function (angular) {
'use strict';
angular.module('app').directive('srPaymentEdit', srPaymentEditDirectiveFactory);
function srPaymentEditDirectiveFactory() {
return {
restrict: 'E',
templateUrl: 'app/app_directives/sr-paymentEdit/sr-paymentEdit.html',
scope: {
payment: '=',
disablepaymenttypeselection: '#',
hidepaymenttypeselection: '#'
},
transclude: false,
controller: controller,
link: link
};
}
function link($scope, element, attrs, model) {
if (attrs.hidepaymenttypeselection) {
$scope.hidePaymentType = $scope.$eval(attrs.hidepaymenttypeselection);
}
if (attrs.disablepaymenttypeselection) {
$scope.$watch(attrs.disablepaymenttypeselection, function (value) {
$scope.disablePaymentType = $scope.$eval(attrs.disablepaymenttypeselection);
});
}
}
function controller($scope) {
if ($scope.payment != null) {
if ($scope.payment instanceof PaymentInfo === false) {
throw 'Invalid datasource type, must be instance of PaymentInfo';
}
} else {
var info = new PaymentInfo();
info.paymentTypeCode = 'CHCK';
$scope.payment = info;
}
}
})(angular);
So far so good, watch fires and disables the selection, but after "vm.isPolicySelected" changes, naturally, watch for the attribute does not fire.
Is it possible to trigger watch so that "disablepaymenttypeselection" is re-evaluated?
Looks like the problem is that in directive scope configuration you need to use = binding, not attribute #. So try this:
scope: {
payment: '=',
disablepaymenttypeselection: '=',
hidepaymenttypeselection: '#'
},
and also change watch part a little (use just property name):
$scope.$watch('disablepaymenttypeselection', function(value) {
$scope.disablePaymentType = $scope.$eval(attrs.disablepaymenttypeselection);
});
You declare that your attributes 'disablepaymenttypeselection' and 'hidepaymenttypeselection' are one way binded as text:
scope: {
payment: '=',
disablepaymenttypeselection: '#',
hidepaymenttypeselection: '#'
},
I think you need to use a two way binding ('=') so you can toggle the (boolean) property to hide or show your html.
With your code if you 'console.log($scope.disablepaymenttypeselection)', the you would get the following output as string:
!vm.isPolicySelected || someOtherCondition
Read this for more info about directives and bindings: When writing a directive in AngularJS, how do I decide if I need no new scope, a new child scope, or a new isolated scope?
Got it working. My problem was that I had created an isolated scope, yet I wanted to respond to changes in parent scope, without explicitly passing in all dependencies. So I re-wrote directive like this
function srPaymentEditDirectiveFactory() {
return {
restrict: 'E',
templateUrl: 'app/app_directives/sr-paymentEdit/sr-paymentEdit.html',
scope: true,
//scope: {
// vm:'=',
// payment: '=',
// disablepaymenttypeselection: '=',
// hidepaymenttypeselection: '#'
//},
transclude: false,
//controller: controller,
link: link
};
}
function link($scope, element, attrs, model) {
var info = null;
if (attrs.payment == undefined) {
info = new PaymentInfo();
info.paymentTypeCode = 'CHCK';
} else {
info = $scope.$eval(attrs.payment);
if (info instanceof PaymentInfo === false) {
throw 'Invalid datasource type, must be instance of PaymentInfo';
}
}
$scope.payment = info;
if (attrs.hidepaymenttypeselection) {
$scope.hidePaymentType = $scope.$eval(attrs.hidepaymenttypeselection);
}
if (attrs.disablepaymenttypeselection) {
$scope.$watch(attrs.disablepaymenttypeselection, function (value) {
$scope.disablePaymentType = $scope.$eval(attrs.disablepaymenttypeselection);
});
}
}
})(angular);
First foray into custom directives and getting a bit stuck on binding to a method in the parent scope.
So when I use my custom directive in my app:
<dropdown x-label="Stuff" x-divider="-"
x-list = "listOfStuff"
x-ng-model="id"
x-change-select="controllerMethodToBeCalled(id)">
</dropdown>
The template is as follows and triggers the 'update' function in the directive controller on ng-change.
<div class="dropdown_container">
<div class="select_label">{{label}}</div>
<div class="select_divider">{{divider}}</div>
<select class="dropdown_select" id="dropdownList"
ng-model="ngModel"
ng-options="option.id as option.name for option in list | orderBy:'name'"
ng-change="update(ngModel)">
</select>
</div>
The directive code is below and I can call the method in parent scope directly whcih works fine but I'd like to trigger the 'change-select' that I've bound function 'controllerMethodToBeCalled' passing the id. Otherwise the component is not truly self contained.
angular.module('myApp.component.dropdown', [])
.directive('dropdown', function() {
return {
restrict: 'E',
require: "^ngModel",
replace: true,
scope: {
list: "=",
ngModel: '=',
changeSelect: '&'
},
templateUrl: 'component/dropdown/dropdown.tpl.html',
link: function ($scope, element, attrs) {
attrs.$observe('label', function (value) {
$scope.label = value;
});
attrs.$observe('divider', function (value) {
$scope.divider = value;
});
},
controller: function($scope){
$scope.update = function(id){
//replace this line with call to changeSelect passing id
$scope.$parent.controllermethodToBeCalled(id);
};
}
};
});
Probably very easy to fix but I just can't see it. Any ideas/suggestions?
You only need to call $scope.changeSelect(). See this Plunker. If you don't want to use the id from a parent scope, but rather pass the id argument from the function, you can do that do:
$scope.update = function(id){
$scope.changeSelect({id: id});
};
It basically says use the value of the local variable id for id in the expression controllerMethodToBeCalled(id).
After looking for examples of how set focus elements with angular, I saw that most of them use some variable to watch for then set focus, and most of them use one different variable for each field they want to set focus. In a form, with a lot of fields, that implies in a lot of different variables.
With jquery way in mind, but wanting to do that in angular way, I made a solution that we set focus in any function using the element's id, so, as I am very new in angular, I'd like to get some opinions if that way is right, have problems, whatever, anything that could help me do this the better way in angular.
Basically, I create a directive that watch a scope value defined by the user with directive, or the default's focusElement, and when that value is the same as the element's id, that element set focus itself.
angular.module('appnamehere')
.directive('myFocus', function () {
return {
restrict: 'A',
link: function postLink(scope, element, attrs) {
if (attrs.myFocus == "") {
attrs.myFocus = "focusElement";
}
scope.$watch(attrs.myFocus, function(value) {
if(value == attrs.id) {
element[0].focus();
}
});
element.on("blur", function() {
scope[attrs.myFocus] = "";
scope.$apply();
})
}
};
});
An input that needs to get focus by some reason, will do this way
<input my-focus id="input1" type="text" />
Here any element to set focus:
<a href="" ng-click="clickButton()" >Set focus</a>
And the example function that set focus:
$scope.clickButton = function() {
$scope.focusElement = "input1";
}
Is that a good solution in angular? Does it have problems that with my poor experience I don't see yet?
The problem with your solution is that it does not work well when tied down to other directives that creates a new scope, e.g. ng-repeat. A better solution would be to simply create a service function that enables you to focus elements imperatively within your controllers or to focus elements declaratively in the html.
DEMO
JAVASCRIPT
Service
.factory('focus', function($timeout, $window) {
return function(id) {
// timeout makes sure that it is invoked after any other event has been triggered.
// e.g. click events that need to run before the focus or
// inputs elements that are in a disabled state but are enabled when those events
// are triggered.
$timeout(function() {
var element = $window.document.getElementById(id);
if(element)
element.focus();
});
};
});
Directive
.directive('eventFocus', function(focus) {
return function(scope, elem, attr) {
elem.on(attr.eventFocus, function() {
focus(attr.eventFocusId);
});
// Removes bound events in the element itself
// when the scope is destroyed
scope.$on('$destroy', function() {
elem.off(attr.eventFocus);
});
};
});
Controller
.controller('Ctrl', function($scope, focus) {
$scope.doSomething = function() {
// do something awesome
focus('email');
};
});
HTML
<input type="email" id="email" class="form-control">
<button event-focus="click" event-focus-id="email">Declarative Focus</button>
<button ng-click="doSomething()">Imperative Focus</button>
About this solution, we could just create a directive and attach it to the DOM element that has to get the focus when a given condition is satisfied. By following this approach we avoid coupling controller to DOM element ID's.
Sample code directive:
gbndirectives.directive('focusOnCondition', ['$timeout',
function ($timeout) {
var checkDirectivePrerequisites = function (attrs) {
if (!attrs.focusOnCondition && attrs.focusOnCondition != "") {
throw "FocusOnCondition missing attribute to evaluate";
}
}
return {
restrict: "A",
link: function (scope, element, attrs, ctrls) {
checkDirectivePrerequisites(attrs);
scope.$watch(attrs.focusOnCondition, function (currentValue, lastValue) {
if(currentValue == true) {
$timeout(function () {
element.focus();
});
}
});
}
};
}
]);
A possible usage
.controller('Ctrl', function($scope) {
$scope.myCondition = false;
// you can just add this to a radiobutton click value
// or just watch for a value to change...
$scope.doSomething = function(newMyConditionValue) {
// do something awesome
$scope.myCondition = newMyConditionValue;
};
});
HTML
<input focus-on-condition="myCondition">
I like to avoid DOM lookups, watches, and global emitters whenever possible, so I use a more direct approach. Use a directive to assign a simple function that focuses on the directive element. Then call that function wherever needed within the scope of the controller.
Here's a simplified approach for attaching it to scope. See the full snippet for handling controller-as syntax.
Directive:
app.directive('inputFocusFunction', function () {
'use strict';
return {
restrict: 'A',
link: function (scope, element, attr) {
scope[attr.inputFocusFunction] = function () {
element[0].focus();
};
}
};
});
and in html:
<input input-focus-function="focusOnSaveInput" ng-model="saveName">
<button ng-click="focusOnSaveInput()">Focus</button>
or in the controller:
$scope.focusOnSaveInput();
angular.module('app', [])
.directive('inputFocusFunction', function() {
'use strict';
return {
restrict: 'A',
link: function(scope, element, attr) {
// Parse the attribute to accomodate assignment to an object
var parseObj = attr.inputFocusFunction.split('.');
var attachTo = scope;
for (var i = 0; i < parseObj.length - 1; i++) {
attachTo = attachTo[parseObj[i]];
}
// assign it to a function that focuses on the decorated element
attachTo[parseObj[parseObj.length - 1]] = function() {
element[0].focus();
};
}
};
})
.controller('main', function() {});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.3/angular.min.js"></script>
<body ng-app="app" ng-controller="main as vm">
<input input-focus-function="vm.focusOnSaveInput" ng-model="saveName">
<button ng-click="vm.focusOnSaveInput()">Focus</button>
</body>
Edited to provide more explanation about the reason for this approach and to extend the code snippet for controller-as use.
You can try
angular.element('#<elementId>').focus();
for eg.
angular.element('#txtUserId').focus();
its working for me.
Another option would be to use Angular's built-in pub-sub architecture in order to notify your directive to focus. Similar to the other approaches, but it's then not directly tied to a property, and is instead listening in on it's scope for a particular key.
Directive:
angular.module("app").directive("focusOn", function($timeout) {
return {
restrict: "A",
link: function(scope, element, attrs) {
scope.$on(attrs.focusOn, function(e) {
$timeout((function() {
element[0].focus();
}), 10);
});
}
};
});
HTML:
<input type="text" name="text_input" ng-model="ctrl.model" focus-on="focusTextInput" />
Controller:
//Assume this is within your controller
//And you've hit the point where you want to focus the input:
$scope.$broadcast("focusTextInput");
I prefered to use an expression. This lets me do stuff like focus on a button when a field is valid, reaches a certain length, and of course after load.
<button type="button" moo-focus-expression="form.phone.$valid">
<button type="submit" moo-focus-expression="smsconfirm.length == 6">
<input type="text" moo-focus-expression="true">
On a complex form this also reduces need to create additional scope variables for the purposes of focusing.
See https://stackoverflow.com/a/29963695/937997
I have a directive defined as
Application.Directives.directive('listview', function() {
return {
restrict: 'EAC',
templateUrl: 'directives/listview/view.html'
};
});
And then want to include it from the main view like this
<div class="{{directiveName}}">
</div>
where directiveName equals "listview". However, it does not work. It generates the below code, but the listview directive does not get loaded
<div class="listview">
</div>
Yet, when I type the above generated code directly into the main template, it does load the directive. How come? How can I make it work?
So I found a way. What you'd want is something like this
<div {{directiveNameInScope}}></div>
But again, that doesn't work. So I created a directive to do it for you. It works like
<div loaddirective="directiveNameInScope"></div>
where the loaddirective directive looks like
Application.Directives.directive('loaddirective', function($compile) {
return {
restrict: 'A',
scope: { loaddirective : "=loaddirective" },
link: function($scope, $element, $attr) {
var value = $scope.loaddirective;
if (value) {
// Load the directive and make it reactive
$element.html("<div "+value+"></div>");
$compile($element.contents())($scope);
}
},
replace: true
};
});
I put it up on github here: https://github.com/willemmulder/loaddirective
To be able to watch the contents change for a contenteditable div or an input element, I have created the following directive:
app.directive('contenteditable',function() { return {
require: 'ngModel',
link: function(scope, element, attrs, ctrl) {
// view -> model
element.bind('input', function() {
scope.$apply(function() {
ctrl.$setViewValue(element["0"].tagName=="INPUT" ? element.val() : element.text());
scope.watchCallback(element.attr('data-ng-model'));
});
});
// model -> view
ctrl.$render = function() {
element.text(ctrl.$viewValue);
element.val(ctrl.$viewValue);
};
}};
});
My Test Controller looks like:
function TestController($scope) {
$scope.singleVal = "X";
$scope.multiVal = ["A"];
$scope.addRow = function() {
$scope.multiVal.push("");
};
$scope.watchCallback = function(modelName) {
console.log(modelName+" was changed");
};
}
When I test it against the following html, the singleVal (statically created) behaves well, but my multiVal (dynamically created using ng-repeat) doesnt. When I input a value, it just retains the original value (i.e the model is not getting refreshed). Please help.
<div data-ng-controller="TestController">
<div contenteditable="true" data-ng-model="singleVal"></div>
<button data-ng-click="addRow()">Add Row</button>
<table data-ng-repeat="val in multiVal"><tr><td>
<div contenteditable="true" data-ng-model="val"></div>
</td></tr></table>
</div>
You can't bind ngModel directly to a string in an array. You'll need to store an array of objects inside of multiVal:
$scope.multiVal = [{property: "A"}];
Demonstrated here: http://jsfiddle.net/YMJzN/
Btw, you'll also want to adjust $scope.addRow to do the same...
$scope.addRow = function() {
$scope.multiVal.push({property:'new'});
}