Adding ng-model to directive - angularjs

I have the following directive to reverse geocode a lat & long into a place:
/* global app*/
app.directive('reverseGeocode', function () {
return {
restrict: 'A',
template: '<div ng-model="autoLocation"></div>',
link: function (scope, element, attrs) {
var geocoder = new google.maps.Geocoder();
var latlng = new google.maps.LatLng(attrs.lat, attrs.lng);
geocoder.geocode({ 'latLng': latlng }, function (results, status) {
if (status == google.maps.GeocoderStatus.OK) {
if (results[1]) {
element.text(results[1].formatted_address);
} else {
element.text('Location not found');
}
} else {
element.text('');
}
});
},
require: "ngModel",
replace: true
}
});
But for some reason, it doesn't seem to retrieve the ng-model and display it:
<div class="form-group move-down" ng-class="{ 'has-error': place === null }">
<label for="place">Picture taken in:</label>
<input type="text" class="form-control" ng-if="!capture.geo" id="place" ng-model="place.formatted_address" ng-autocomplete options="options" required details="place" ng-click="checkPlace(place)"
uib-tooltip="Please pick the location where you captures your photo!"
tooltip-placement="top-right"
tooltip-trigger="mouseenter"
tooltip-enable="!details.formatted_address"
tooltip-class="tooltip">
// DIRECTIVE USED HERE
<reverse-geocode class="form-control" ng-if="capture.geo" lat="{{capture.latitude}}" lng="{{capture.longitude}}">
<div ng-show="place === null" class="noPlace">
<i class="glyphicon glyphicon-remove"></i> Please fill in a valid location!
</div>
</div>
What my goal is, is to display the location below using:
<div ng-if="capture.geo">Place: {{autoLocation}}</div>

First you need to drop restrict or at least use E (element) instead of A (attribute), since you are using the directive as element.
Second, you don't need to add or pass ng-model, which is a directive itself, to your directive. You can access the controller scope variable capture directly. If you prefer directive to have it's own, isolated scope, then you should two-way bind the controller variable to directive's scope using = (or # if you prefer passing literal strings).
If I understood correctly, below is what you are after (simplified but functionality is there).
HTML
<body ng-controller="MyCtrl">
lat <input type="number" ng-model="capture.lat">
lng <input type="number" ng-model="capture.lng">
<reverse-geocode capture="capture"></reverse-geocode>
</body>
JavaScript
angular.module('app', [])
.controller('MyCtrl', function($scope) {
$scope.capture = {
lat: 10.0,
lng: 20.0
};
})
.directive('reverseGeocode', function () {
return {
restrict: 'E',
scope: {
capture: '='
},
template: '<div ng-bind="autoLocation"></div>',
link: function(scope) {
scope.$watch('capture', function() {
if (scope.capture.lat && scope.capture.lng) {
scope.autoLocation = 'reverse geocode address for [' +
scope.capture.lat + ' ' + scope.capture.lng + '] here';
} else {
scope.autoLocation = 'invalid coordinates';
}
}, true);
},
};
});

It won't work in that way. ng-model only works for input type element where a user can pass some input.
You need to use two-way data communication so that you can modify a scope variable from directive to it's associated scope's variable.

Related

Angular 1.6+ - How to set ng-options dynamically from the DB using the element inside a forEach loop

I want to take a response from a web service and apply it to the ng-options inside the angular.forEach loop. All I think I have to access it is the element itself. Here is the code. Normally I would just write:
scope.whatIWantTheOptionsToB = DBResponse;
But inside the forEach loop I can't do that. How would I set the options variable inside the for each loop.
Here is my code so far:
HTML:
<div class="form__row">
<label>Injury Type:</label>
<select
class="form__select"
ws="populateDDL"
param="tblInjuryTypes"
ng-model="injuryType_id"
ng-options="injury.id as injury.name for injury in injuryTypes"
ng-change="injuryUpdate()"
name="injuryTypes"
required
></select>
</div>
<div class="form__row">
<label>Work Status:</label>
<select
class="form__select"
ws="populateDDL"
param="tblWorkStatus"
ng-model="workStatus_id"
ng-options="status.id as status.name for status in workStatus"
name="workStatus"
required
></select>
</div>
Directive:
app.directive(
'providerForm',
['$http', 'populateDDL', 'classHandler', function ($http, populateDDL, classHandler){
var directive = {
link: link,
scope: true,
restrict: 'E',
templateUrl: '/modules/providerForm.html',
};
return directive;
function link(scope, element, attrs, ctrl) {
var selects = element.find('select');
angular.forEach(selects, function(e,i) {
if (e.attributes['ws'].value === 'populateDDL') {
scope.data = {
sKey: sessionStorage.getItem('key'),
sTableName: e.attributes['param'].value,
iInjuryTypeId: scope.injuryType_id,
iBodyPartId: scope.bodyPart_id
};
scope.getSelectOptions = new populateDDL(e.attributes['ws'].value, scope.data).
then(function(response) {
console.log(e)
scope.whatIWantTheOptionsToBeEachTimeThru = response;
});
}
});
}
}]
);

directive reacting to attribute change

I have a directive, with an attribute :
html :
<my-directive id="test" myattr="50"></my-directive>
js :
myApp.directive('myDirective', function() {
var link = function(scope, element, attrs) {
scope.$watch('myattr', function(value) {
element.attr('myattr', value);
});
scope.change = function() {
// some code
};
};
return {
restrict: 'E',
template: '<input type="text" ng-change="change()" ng-model="myattr"/>',
scope: {myattr: '='},
link: link
};
});
My goal would be to keep myattr and the value of the input equal. With element.attr('myattr', value) I can force myattr to have the correct value, but how am I supposed to update the input when myattr changes?
For example, in this jsfiddle, when clicking on the button, I try to do :
$('#test').attr('myattr', Math.random() * 100);
But I can't find a way to 'catch' the change from within the directive.
I would like some help modifying the jsfiddle so that :
the function change is called after the jquery call.
the value of the input is always equal to myattr
You need to store the value of myattr as a property on a scope not as a value on an attribute.
Your directive is correct you need to also attach a controller.
var myApp = angular.module('myApp', []);
myApp.controller('MainController', function ($scope) {
$scope.calculate = function () {
// your logic here
alert($scope.val);
}
});
myApp.directive('myDirective', function() {
var link = function(scope, element, attrs) {
scope.change = function() {
console.log("change " + scope.myattr);
};
};
return {
restrict: 'E',
template: '<input type="text" ng-change="change()" ng-model="myattr"/>',
scope: {
myattr: '='
},
link: link
};
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="myApp">
<div ng-controller="MainController">
My Value: {{val}} <br/>
<button type="button" ng-click="calculate()">ok</button>
<my-directive id="test" myattr="val"></my-directive>
</div>
</div>

How to access ng-model value in directive?

I created a directive for google map auto-complete. everything is working fine, but the problem is when I need to access the value of input and re-set it. it doesn't work. Here is code:
<div controller='mainCtr'>
<span click='reset(destination)'>Reset</span>
<div class='floatleft' style='width:30%;margin-right:40px;'>
<smart-Googlemaps locationgoogle='destination.From'></smart-Googlemaps>
<label>From</label>
</div>
</div>
In the directive:
angular.module('ecom').directive('smartGooglemaps', function() {
return {
restrict:'E',
replace:false,
// transclude:true,
scope: {
locationgoogle: '='
},
templateUrl: 'components/directives/autocomplete/googlemap-search.html',
link: function($scope, elm, attrs){
var autocomplete = new google.maps.places.Autocomplete($(elm).find("#google_places_ac")[0], {});
google.maps.event.addListener(autocomplete, 'place_changed', function() {
var place = autocomplete.getPlace();
// $scope.location = place.geometry.location.lat() + ',' + place.geometry.location.lng();
// console.log(place);
$scope.locationgoogle = {};
$scope.locationgoogle.formatted_address = place.formatted_address;
$scope.locationgoogle.loglat = place.geometry.location;
$scope.locationgoogle.locationText = $scope.locationText;
$scope.$apply();
});
}
}
})
Here is html for directive:
<input id="google_places_ac" placeholder="Please enter a location" name="google_places_ac" type="text" class="input-block-level" ng-model='locationText'/>
The directive works fine, I create a isolated scope(locationgoogle) to pass the information I need to parent controller(mainCtr), now in the mainCtr I have a function calld reset(), after I click this,I need to clean up the input make it empty. How Can I do it?
One way to access the value of the model in your directive from a parent controller is to put that on the isolate scope too and use the two-way binding flag = like you've done with the locationgoogle property. Try this:
DEMO
html
<body ng-controller="MainCtrl">
<button ng-click="reset()">Reset</button>
<smart-googlemaps location-text="locationText"></smart-googlemaps>
</body>
js
app.controller('MainCtrl', function($scope) {
// need to define model in parent and pass to directive
$scope.locationText = {
value: ''
};
$scope.reset = function(){
$scope.locationText.value = '';
}
});
app.directive('smartGooglemaps', function() {
return {
restrict:'E',
replace:false,
// transclude:true,
scope: {
locationgoogle: '=',
locationText: '='
},
// ng-model="locationText.value"
template: '<input id="google_places_ac" placeholder="Please enter a location" name="google_places_ac" type="text" class="input-block-level" ng-model="locationText.value"/>',
link: function($scope, elm, attrs){
// implement directive googlemaps logic, set text value etc.
$scope.locationText.value = 'foo';
}
}
})

How to watch and re-evaluate expression in directive?

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);

Two-way binding between controller and directive with $watch in controller not working

I've come across a quite a few similar questions and tried them all with no success. I'm not sure if I'm doing this the "Angular way" so I might need to change my approach. In short I'm initialising a scoped variable from within a controller, the variable is then shared with the directive's scope. However when this value is changed as part of a user interaction the new value is not being synchronised with the controllers variable. My abbreviated code follows:
Controller
The $watch method is only called once i.e. when the page loads.
.controller('NewTripCtrl', ['$scope', function($scope) {
$scope.pickupLocation;
$scope.dropoffLocation;
$scope.$watch('dropoffLocation', function(oldVal, newVal) {
console.log('dropofflocation has changed.');
console.log(oldVal);
console.log(newVal);
});
}])
HTML
<div class="padding">
<input ion-google-place type="text" class="ion-google-place" autocomplete="off" service="places" placeholder="Pickup Address" location="pickupLocation"></input>
</div>
<div class="padding">
<input ion-google-place type="text" class="ion-google-place" autocomplete="off" service="places" placeholder="Dropoff Address" location="dropoffLocation"></input>
</div>
Directive
angular.module('ion-google-place', [])
.directive('ionGooglePlace', [
'$ionicTemplateLoader',
'$ionicBackdrop',
'$q',
'$timeout',
'$rootScope',
'$document',
function ($ionicTemplateLoader, $ionicBackdrop, $q, $timeout, $rootScope, $document) {
return {
restrict: 'A',
scope: {
location: '=location'
},
link: function (scope, element, attrs) {
scope.locations = [];
console.log('init');
console.log(scope);
console.log(attrs);
// var geocoder = new google.maps.Geocoder();
var placesService = new google.maps.places.AutocompleteService();
var searchEventTimeout = undefined;
var POPUP_TPL = [
'<div class="ion-google-place-container">',
'<div class="bar bar-header item-input-inset">',
'<label class="item-input-wrapper">',
'<i class="icon ion-ios7-search placeholder-icon"></i>',
'<input class="google-place-search" type="search" ng-model="searchQuery" placeholder="' + 'Enter an address, place or ZIP code' + '">',
'</label>',
'<button class="button button-clear">',
'Cancel',
'</button>',
'</div>',
'<ion-content class="has-header has-header">',
'<ion-list>',
'<ion-item ng-repeat="l in locations" type="item-text-wrap" ng-click="selectLocation(l)">',
'{{l.formatted_address || l.description }}',
'</ion-item>',
'</ion-list>',
'</ion-content>',
'</div>'
].join('');
var popupPromise = $ionicTemplateLoader.compile({
template: POPUP_TPL,
scope: scope,
appendTo: $document[0].body
});
popupPromise.then(function (el) {
var searchInputElement = angular.element(el.element.find('input'));
// Once the user has selected a Place Service prediction, go back out
// to the Places Service and get details for the selected location.
// Or if using Geocode Service we'll just passing through
scope.getDetails = function (selection) {
//console.log('getDetails');
var deferred = $q.defer();
if (attrs.service !== 'places') {
deferred.resolve(selection);
} else {
var placesService = new google.maps.places.PlacesService(element[0]);
placesService.getDetails({
'placeId': selection.place_id
},
function(placeDetails, placesServiceStatus) {
if (placesServiceStatus == "OK") {
deferred.resolve(placeDetails);
} else {
deferred.reject(placesServiceStatus);
}
});
}
return deferred.promise;
};
// User selects a Place 'prediction' or Geocode result
// Do stuff with the selection
scope.selectLocation = function (selection) {
// If using Places Service, we need to go back out to the Service to get
// the details of the place.
var promise = scope.getDetails(selection);
promise.then(onResolve, onReject, onUpdate);
el.element.css('display', 'none');
$ionicBackdrop.release();
};
function onResolve (details) {
console.log('ion-google-place.onResolve');
scope.location = details;
$timeout(function() {
// anything you want can go here and will safely be run on the next digest.
scope.$apply();
})
if (!scope.location) {
element.val('');
} else {
element.val(scope.location.formatted_address || '');
}
}
// more code ...
};
}
]);
The $watch function only detects a change when the page loads but never again. What is the correct way to provide the controller with the value that the user enters into the directive's input element?
As angular docs says:
Scope inheritance is normally straightforward, and you often don't even need to know it is happening... until you try 2-way data binding (i.e., form elements, ng-model) to a primitive (e.g., number, string, boolean) defined on the parent scope from inside the child scope.
This is a common issue when doing 2-way data binding with primitives. Try doing this same but try to share an object instead of a primitive.
i.e:
.controller('NewTripCtrl', ['$scope', function($scope) {
$scope.pickupLocation = {location: ''};
$scope.dropoffLocation = {location: ''};
...
Here are your options
Option#1 using ng-model
with watch function in parent controller
<input ion-google-place type="text" class="ion-google-place" autocomplete="off" service="places" placeholder="Dropoff Address" ng-model="dropoffLocation"></input>
and in directive
scope: {
location: '=ngModel'
},
You don't need Watch here.
Option#2 Through Object literal
.controller('NewTripCtrl', ['$scope', function($scope) {
$scope.locationChanged = function(location){
//you watch code goes here
}
}
<input ion-google-place type="text" class="ion-google-place" autocomplete="off" service="places" placeholder="Dropoff Address" location="dropoffLocation" location-changed="locationChanged(location)" ></input>
//directive scope
scope: {
location: '=',
locationChanged: '&'
}
//in link function invoke parent controllers method as
scope.locationChanged({location:scope.location});
Option#3 Through Function reference
controllect and directives scope same as option#2
<input ion-google-place type="text" class="ion-google-place" autocomplete="off" service="places" placeholder="Dropoff Address" location="dropoffLocation" location-changed="locationChanged" ></input>
//in link function invoke parent controllers method as
scope.locationChanged()(scope.location);
Option#2 is recommended for better readability.
watching variables should be avoided as much as possible.

Resources