AngularJS: How to implement a directive that outputs its markup? - angularjs

DEMO
Imagine I have some markup, e.g.:
<my-input model="data.firstName"></my-input>
Now, I would like to create a my-markup directive that will add a button to show/hide its markup.
So, this:
<div my-markup>
<my-input model="data.firstName"></my-input>
</div>
should result in this:
and when the button is clicked, the markup should appear:
The my-markup directive should not break any data bindings of its children.
Here is my attempt to implement this.
The markup appears, but the button doesn't work. Any ideas how to fix this?
PLAYGROUND HERE

Here is my approach. Couple of things:-
1) Instead of isolated scope on myMarkup, create a child scope, ultimately the actual directive myInput will be isolated. This would be required if you do need to support multiple myMarkup directive under the same scope.
2) You need a click event on the button, i wouldn't do logic on the markup instead abstract out to a method on the scope.
3) You would just need one button, do not need 2 buttons. Just change the text of the button.
.directive('myMarkup', function($compile) {
return {
restrict: 'A',
scope: true, //Create a child scope
compile: function(element) {
//Just need one button
var showButton = '<button ng-click="toggleMarkup()">{{model.showMarkup ? "Hide": "Show"}} Markup</button>';
var markup = '<pre ng-show="model.showMarkup">' + escapeHtml(element.html()) + '</pre>';
//append the markups
element.append(showButton).append(markup);
return linker;
}
};
function linker(scope, element) {
scope.model = {
showMarkup: false
};
//Click event handler on the button to toggle markup
scope.toggleMarkup = function(){
scope.model.showMarkup = !scope.model.showMarkup;
}
};
});
Demo

Please see below
function escapeHtml(html) {
return html.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
}
angular.module('App', []).controller('AppCtrl', function($scope) {
$scope.data = {
firstName: 'David'
};
}).directive('myInput', function() {
return {
restrict: 'E',
scope: {
model: '='
},
template: '<input class="my-input" type="text" ng-model="model">'
};
}).directive('myMarkup', function() {
return {
restrict: 'A',
scope: {},
link: function(scope, elem, attr) {
},
compile: function(element) {
var showButton = '<button ng-if="data.showMarkup" ng-click="data.showMarkup=!data.showMarkup">Hide Markup</button>';
var hideButton = '<button ng-if="!data.showMarkup" ng-click="data.showMarkup=!data.showMarkup">Show Markup</button>';
var markup = '<pre ng-if="data.showMarkup">' + escapeHtml(element.html()) + '</pre>';
element.append(showButton);
element.append(hideButton);
element.append(markup);
return function(scope, element) {
scope.data = {
showMarkup: true
};
};
}
};
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<body ng-app="App" ng-controller="AppCtrl">
<pre>data = {{ data | json }}</pre>
<div my-markup>
<my-input model="data.firstName"></my-input>
</div>
</body>

Related

Strange behaviour of Angular's $watch

Today I encounter some really strange behaviour in AngularJS using $watch. I simplified my code to the following example:
https://jsfiddle.net/yLeLuard/
This example contains a service which will keep track of a state variable. The directives are used to bind a click event to the DOM changing the state variable through the service.
There are two problems in this example:
The first close button (with the ng-click property on it) only changes the state on the second click
The two buttons without the ng-click are not changing the state at all
main.html
<div ng-controller="main">
State: {{ api.isOpen | json }}
<div ng-click="" open>
<button>Open - Working fine</button>
</div>
<div ng-click="" close>
<button>Close - Works, but only on second click</button>
</div>
<div open>
<button>Open - Not Working</button>
</div>
<div close>
<button>Close - Not Working</button>
</div>
</div>
main.js
var myApp = angular.module('myApp', []);
myApp.controller('main', ['$scope', 'state', function($scope, state) {
$scope.api = state;
$scope.api.isOpen = false;
$scope.$watch('api.isOpen', function() {
console.log('state has changed');
});
}]);
myApp.directive('open', ['state', function(state) {
return {
restrict: 'A',
scope: {},
replace: true,
template: '<button>Open</button>',
link: function(scope, element) {
element.on('click', function() {
state.isOpen = true;
});
}
};
}]);
myApp.directive('close', ['state', function(state) {
return {
restrict: 'A',
scope: {},
replace: true,
template: '<button>Close</button>',
link: function(scope, element) {
element.on('click', function() {
state.isOpen = false;
});
}
};
}]);
myApp.service('state', function() {
return {
isOpen: null
};
});
That's because you are using a native event listener on click. This event is asynchronous and out of the Angular digest cycle, so you need to manually digest the scope.
myApp.directive('open', ['state', function(state) {
return {
restrict: 'A',
scope: {},
link: function(scope, element) {
element.on('click', function() {
scope.$apply(function() {
state.isOpen = true;
});
});
}
};
}]);
Fixed fiddle: https://jsfiddle.net/7h95ct1y/
I would suggest changing the state directly in the ng-click: ng-click="api.isOpen = true"
You should put your link function as a pre-link function, this solves the problem of the second click.
https://jsfiddle.net/2c9pv4xm/
myApp.directive('close', ['state', function(state) {
return {
restrict: 'A',
scope: {},
link:{
pre: function(scope, element) {
element.on('click', function() {
state.isOpen = false;
console.log('click', state);
});
}
}
};
}]);
As for the not working part, you're putting your directive on the div, so when you click on the div it works but not on the button. Your open directive should be on the button.
Edit: other commenters suggested that you change the state directly in the ng-click. I wouldn't recommend it, it might work in this case but if you're bound to have more than an assignation to do it's not viable.

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>

Angular directive for bootstrap popover

I write my custom directive for bootstrap popover, but face some trouble.
This is the code:
angular.module('CommandCenterApp')
.directive('bzPopover', function($compile,$http, $commandHelper) {
return{
restrict: "A",
replace: false,
scope: {
currencies:"=data",
selected:"=selected"
},
link: function (scope, element, attrs) {
var html = '<div class="currency-popup">' +
'<span class="select-label">Select currency:</span>'+
'<select class="custom-select" ng-model="selected" ng-options="currency.CurrencyName for currency in currencies track by currency.CurrencyId">' +
'</select>' +
'<button class="btn btn-green" ng-click="saveCurrency()">Save</button>'+
'</div>';
var compiled = $compile(html)(scope);
$(element).popover({
content:compiled,
html: true,
placement:'bottom'
});
scope.saveCurrency = function () {
var obj = {
Currency:scope.selected,
venueId: $commandHelper.getVenueId()
}
$http.post("/api/currencyapi/changecurrency", obj).success(function() {
scope.$emit('currencySaved', scope.selected);
});
//$(element).popover('hide');
}
scope.$watch('selected', function() {
console.log(scope.selected);
});
}
}
});
When I first time invoke popover all works fine, I click on button and it trigger scope.saveChanges function. Then I close popover and invoke it again, and directive doesnt work anymore.
In markup popover present as:
<a bz-popover data="controller.currencies" selected="controller.selectedCurrency" class="change-currency hidden-xs hidden-sm" href>Change currency</a>
Can anyone help me with this?
UPDATE: it looks like all bindings(scope.saveCurrency,watched on selected property) stop working after popover hidding.
Not really sure if this is the problem you're describing because in my fiddle I had to click twice on the button to show the popover after closing the popover.
I don't know what's the problem but with trigger: 'manual' and binding to click event it is working as expected.
Please have a look at the demo below or in this jsfiddle.
I've commented some of your code because it's not needed to show the popover behaviour and also the ajax call is not working in the demo.
angular.module('CommandCenterApp', [])
.controller('MainController', function() {
this.currencies = [{
CurrencyId: 1,
CurrencyName: 'Dollar'},{
CurrencyId: 2,
CurrencyName: 'Euro'}];
})
.directive('bzPopover', function($compile,$http) { //, $commandHelper) {
return{
restrict: "A",
replace: false,
scope: {
currencies:"=data",
selected:"=selected"
},
link: function (scope, element, attrs) {
var html = '<div class="currency-popup">' +
'<span class="select-label">Select currency:</span>'+
'<select class="custom-select" ng-model="selected" ng-options="currency.CurrencyName for currency in currencies track by currency.CurrencyId">' +
'</select>' +
'<button class="btn btn-green" ng-click="saveCurrency()">Save</button>'+
'</div>';
var compiled = $compile(html)(scope);
$(element).popover({
content:compiled,
html: true,
placement:'bottom',
trigger: 'manual'
});
$(element).bind('click', function() {
$(element).popover('toggle');
});
scope.saveCurrency = function () {
var obj = {
Currency:scope.selected,
venueId: 1//$commandHelper.getVenueId()
}
$http.post("/api/currencyapi/changecurrency", obj).success(function() {
scope.$emit('currencySaved', scope.selected);
});
$(element).popover('hide');
}
scope.$watch('selected', function() {
console.log(scope.selected);
});
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.6/angular.js"></script>
<script src="https://code.jquery.com/jquery-1.11.3.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.5/js/bootstrap.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0-alpha/css/bootstrap.css" rel="stylesheet"/>
<div ng-app="CommandCenterApp" ng-controller="MainController as controller">
<button bz-popover data="controller.currencies" selected="controller.selectedCurrency" class="change-currency hidden-xs hidden-sm">Change currency</button>
</div>
Shameless self-promotion here, but you may want to take a look at the Angualr UI Bootstrap library as we've already done this for you. And even if you don't want to use it, you can just grab the code you need...

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

AngularJS: How to get the original directive HTML in the "template" function when using transclusion?

DEMO
To get the original HTML of the directive in the template function one could do:
HTML:
<div my-directive>
<input type="text">
</div>
JS:
angular.module('App', []).directive('myDirective', function() {
return {
template: function(element) {
console.log(element.html()); // Outputs <input type="text">
return '<div>Template</div>';
}
};
});
But, when the directive has transclude: true, this method doesn't work anymore:
angular.module('App', []).directive('myDirective', function() {
return {
transclude: true,
template: function(element) {
console.log(element.html()); // Outputs empty string
return '<div>Template</div>';
}
};
});
Is there a way to get the original HTML in the template function when using transclusion?
The ultimate goal is to present the original HTML to the user:
angular.module('App', []).directive('myDirective', function() {
return {
transclude: true,
template: function(element) {
var originalHTML = "How do I get it?";
return '<div>' +
' <pre>' +
escapeHtml(originalHTML) + // This is the original HTML
' </pre>' +
' <div ng-transclude></div>' + // and this is how it looks like
'</div>';
}
};
});
PLAYGROUND HERE
One way i could think of is to use another directive which will save the content to a service accessibly by an identifier. So it mean you would need to add another directive which does this purpose. The directive which does the capture must have higher priority that any other directive that uses it.
Example:-
.directive('myDirective', function(origContentService) {
return {
priority:100,
transclude: true,
template: '<div>Template</div>',
link:function(scope, elm){
//get prop and get content
console.log(origContentService.getContent(elm.idx));
}
};
}).directive('capture', function(origContentService){
return {
restrict:'A',
priority:200, //<-- This must be higher
compile:function(elm){
//Save id and set prop
elm.idx = origContentService.setContent(elm.html());
}
}
}).service('origContentService', function(){
var contents = {};
var idx = 0;
this.getContent= function(idx){
return contents[idx];
}
this.setContent = function(content){
contents[++idx] = content;
return idx;
}
this.cleanup = function(){
contents = null;
}
});
and use capture directive along with this:-
<div my-directive capture>
<input type="text">
</div>
Demo
Or just save the content as data (or a property) itself on the element. so that when the element gets destroyed so will its property.
.directive('myDirective', function() {
return {
priority:100,
transclude: true,
template: '<div>Template</div>',
link:function(scope, elm){
console.log(elm.data('origContent'));
}
};
}).directive('capture', function(){
return {
restrict:'A',
priority:200,
compile:function(elm){
elm.data('origContent', elm.html());
}
}
});
Demo
You have to define where in your template the original HTML code should be inserted.
For Example:
angular.module('App', []).directive('myDirective', function() {
return {
template: '<div>Template</div><ng-transclude></ng-transclude>';
}
});
This will place original HTML after <div>Template</div>

Resources