AngularJS + SwipeJS not working without setTimeout of 0 milliseconds - angularjs

I am writing an AngularJS directive that makes it easy for me to create swipeable pages (such as the viewpager on Android). I use SwipeJS as library for this.
This is how I use the directive
<ng-swipe ng-if="model.messages">
<div class="page" ng-repeat="message in model.messages">
{{message.title}}
</div>
</ng-swipe>
And the code of the directive looks like this.
var swipe = angular.module('ngSwipe', []);
swipe.directive('ngSwipe', function() {
return {
restrict: 'EA',
replace: false,
transclude: true,
scope: {},
template:
'<div>' +
' <div id="slider" class="swipe">' +
' <div class="swipe-wrap" ng-transclude></div>' +
' </div>' +
' <div class="pagecontrol">' +
' <div class="pagedot" ng-repeat="p in swipe.pages" ng-click="swipe.switchPage($index)"></div>' +
' </div>' +
'</div>',
link: function($scope, $element, $attrs) {
var $model = $scope.swipe = {
pages: [],
switchPage: function(index) {
$model.swipe.slide(index);
}
}
setTimeout(function() {
$model.swipe = new Swipe(document.getElementById('slider'), {
continuous: false,
callback: function(index, elem) {
$model.currentTab = index;
}
});
for(var i=0; i<$model.swipe.getNumSlides(); i++) {
$model.pages.push(i);
}
$scope.$apply();
}, 0);
}
};
});
First of all, the reason I use ng-if in the ng-swipe directive is because the link method should be invoked after my messages are loaded. The messages are being retrieved from the server and it can take up to 2 seconds before they are retrieved. If I don't wait untill the messages are loaded, the new Swipe() object will be created but it won't find pages so it will not work.
But besides that, you can also see that I have a setTimeout() function of 0 milliseconds. If I don't use that one, it just doesn't render the swipe pages.
I made a JSFiddle for this issue. Thanks in advance!

switchPage: function(index) {
$model.swipe.slide(index);
$scope.$apply();
}

Related

How to recompile an Angular 1.x directive that uses "replace: true"

Summary
I am using a third party directive that configures replace: true.
I want to recompile the directive every time the user clicks a button. I've tried several things without luck. For example, I tried wrapping the Cloudinary directive in my own directive, but I can't seem to get it. Any help will be greatly appreciated.
Dependencies
"angular": "1.6.2",
"cloudinary-core": "^2.5.0",
"cloudinary_ng": "^1.1.1",
Controller
$scope.rotate = (leader) => {
leader.cloudinary_angle = {
'0': '90',
'90': '180',
'180': '270',
'270': '0'
}[leader.cloudinary_angle] || '0';
};
View
<div ng-repeat="leader in leaders">
Rotate
<cloudimage leader="leader"></cloudimage>
</div>
Did not work #1
angular.module('app').directive('cloudimage', ($compile) => {
return {
restrict: 'E',
replace: false,
scope: {
leader: '='
},
link: (scope, element) => {
let cloudinaryImage = $compile('<cl-image angle="' + scope.leader.cloudinary_angle + '"' +
' crop="fit"' +
' format="jpg"' +
' height="150"' +
' public-id="' + scope.leader.cloudinary + '"' +
' quality="80"' +
' width="150"' +
'></cl-image>'
)(scope);
element.html(cloudinaryImage[0]);
scope.$watch('scope.leader.cloudinary_angle', (cloudinaryImage) => element.html(cloudinaryImage[0]));
}
};
});
Did not work #2
angular.module('app').directive('cloudimage', ($compile) => {
return {
restrict: 'E',
replace: false,
scope: {
leader: '='
},
template: '<cl-image crop="fit" format="jpg" height="150" angle="{{angle}}" public-id="{{id}}" quality="80" width="150"></cl-image>',
link: (scope, element) => {
scope.angle = scope.leader.cloudinary_angle || 0;
scope.id = scope.leader.cloudinary;
}
};
});
Did not work #3
I can decorate the 3rd party directive to make its replace: false, but that breaks its transclude.
angular.module('app').config(['$provide', function($provide) {
$provide.decorator('clImageDirective', function($delegate) {
var directive = $delegate[0];
directive.replace = false;
return $delegate;
})
}]);
#1 doesn't work because html() expects a string, not DOM element. cloudinaryImage element shouldn't be converted to a string because this will destroy information about the element.
It should be:
scope.$watch('scope.leader.cloudinary_angle', (cloudinaryImage) => {
element.empty().append(cloudinaryImage)
});
Time for dirty hacks... You can recompile anything (including one-time bindings, any directive) using ng-repeat on array with single element:
<body ng-controller="MainCtrl" ng-init="arr = [{}]">
<button ng-click="arr = [{}]">Recompile</button>
<p test ng-repeat="o in arr"></p>
</body>
where test directive is
app.directive('test', function() {
return {
replace: true,
template: '<span>hello</span>',
link: function() {
console.log('I am linked');
}
}
})
P.S. you can do pretty same with ng-if, but then you will require 2 digests.
P.P.S. I agree this is kinda weird.
http://plnkr.co/edit/vlC7TV8eIrW1tLgcvP7Y?p=preview

ui boostrap typeahead directive doesn't work when wrapped in a directive and loaded dynamically

I've a directive that creates an input text element and uses ui bootstrap directive to attach typeahead functionality to the input field.
This input field is dynamically appended to one of the field on the form on dom ready event. I've to do this since, I don't have access to edit/modify html page generated by server. i.e - Dynamically add a typeahead field using angularjs and bootstrap angularjs as well.
I'm using ui boostrap - v0.12.0, angularjs version - v1.2.26 and jquery - v1.8.3
Problem: the directive is not working (or may be not correctly compiled or access scope) in IE 11, whereas works perfectly in chrome browser without any problem. I can see the appended elements on form load with no errors or exceptions on the console, however no typeahead magic.
here's the what i've -
// added required js references
// initialize angular app
var typeAheadApp = angular.module("typeAheadApp", ['smart-table', 'ui.bootstrap']);
controller:
typeAheadApp.controller('TypeaheadCtrl', ['$scope', '$http', '$compile', function ($scope, $http, $compile) {
$scope.getCategoriesSize = 1;
$scope.categorylkp = getCategoryField().val();
$scope.getCategories = function (val) {
return $http({
url: "/some/data/source/url",
method: 'GET',
headers: {
"Accept": "application/json;odata=verbose"
}
}).then(function (response) {
$scope.getCategoriesSize = response.data.d.results.length;
return response.data.d.results.map(function (item) {
return item.categoryName;
});
}, function (ex) {
alert("ERROR!!");
});
};
$scope.selectedCategory = function (item, model, label) {
getCategoryField().val(label);
};
$scope.updateCategory = function (setVal) {
getCategoryField().val(setVal);
};
}]);
directive:
typeAheadApp.directive('categoryLookup', ['$compile', function ($compile) {
return {
restrict: 'A',
link: function (scope, element, attrs) {
var typeAheadTemplate = angular.element('<div class="form-inline">' +
'<input id="categorylkpTxt" type="text" ng-model="categorylkp" ng-change="updateCategory(categorylkp)" typeahead="category for category in getCategories($viewValue)" typeahead-on-select="selectedCategory($item, $model, $label)" typeahead-min-length="3" typeahead-loading="loadingCategories" style="width: 345px;" autocomplete="off">' +
'</div>' +
'<div ng-show="loadingCategories">' +
'<i class="icon-refresh"></i> Loading...' +
'</div>' +
'<div ng-show="!getCategoriesSize">' +
'<i class="icon-remove"></i> No Results Found ' +
'</div>');
var compiled = $compile(angular.element('<div>').append(typeAheadTemplate).html())(scope);
element.append(compiled);
}
}
}]);
init function:
function initTypeAhead(){
var typeAheadField = getCategoryField(); // some field on the form
typeAheadField.parent().append('<div id="typeAheadEl"><div ng-controller="TypeaheadCtrl"><div id="category-lookup" class="custom-typeahead" category-lookup></div></div></div>');
// manual bootstrapping the angular
angular.bootstrap($('#typeAheadEl'), ['typeAheadApp']);
}
angular.element(document).ready(function() {
initTypeAhead();
});
Any advise or comments ?
Thanks in advance!
I'd start fixing it from category lookup directive as it looks rather messy, you are compiling in link method what should be in template
typeAheadApp.directive('categoryLookup', function () {
return {
restrict: 'A',
template: '<div class="form-inline">' +
'<input id="categorylkpTxt" type="text" ng-model="categorylkp" ng-change="updateCategory(categorylkp)" typeahead="category for category in getCategories($viewValue)" typeahead-on-select="selectedCategory($item, $model, $label)" typeahead-min-length="3" typeahead-loading="loadingCategories" style="width: 345px;" autocomplete="off">' +
'</div>' +
'<div ng-show="loadingCategories">' +
'<i class="icon-refresh"></i> Loading...' +
'</div>' +
'<div ng-show="!getCategoriesSize">' +
'<i class="icon-remove"></i> No Results Found ' +
'</div>',
controller: 'TypeaheadCtrl'
}
});
and then init function
function initTypeAhead(){
var typeAheadField = getCategoryField(); // some field on the form
typeAheadField.parent().append('<div id="typeAheadEl"><div id="category-lookup" class="custom-typeahead" category-lookup></div></div>');
// manual bootstrapping the angular
angular.bootstrap($('#typeAheadEl'), ['typeAheadApp']);
}
angular.element(document).ready(function() {
initTypeAhead();
});

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 use defined text/ng-template in AngularJS directive

I try to write a very flexible directive. For doing so i want the user to define a template used in part of my return (as seen in the ui-bootstrap typeahead directive).
So i define my template like this:
<script type="text/ng-template" id="myDirectivesCustomTemplate.html">
<ul>
<li ng-repeat="value in values">
<a ng-click="doSomething(value.id)">
{{value.name}}
</a>
</li>
</ul>
</script>
I set this template in my directive
<div
my-directive
my-directive-custom-template="myDirectivesCustomTemplate.html"
my-directive-data="someScopeData">
Now in my directive, how can i render the custom template and use it with the passed data? When i try to use it to return in template directly it throws a ReferenceError: $scope is not defined Error. If i call it without scope, it says ReferenceError: myDirectiveCustomTemplate is not defined Error.
Where and how can i use my template if i do not just want to use it as a return directly?
EDIT: let's say, this is my directive:
(function() {
'use strict';
var Combobox = function() {
var displayInputField = elem.find('input.dropdown');
scope.$watch(scope.nsdComboboxModel,function(newVal){
/* search for newVal in given data object */
scope.setDisplayInputValue(newVal);
});
scope.setDisplayInputValue = function(value)
{
displayInputField.val(value);
};
scope.elementSelected = function (item, model, label) {
scope.ComboboxCallback(item);
scope.setDisplayInputValue(label);
};
}
return {
restrict: 'A',
transclude: true,
scope: {
Combobox: '#', /* used as HTML/CSS-id/name/path */
ComboboxModel: '=', /* the actual AngularJS model (ng-model) */
ComboboxAutocompleteData: '=', /* the data used for autoComplete (must be array of objects having id and value) */
ComboboxDropdownData: '=', /* data used by the dropdown template */
ComboboxCallback: '=', /* a callback function called with selected autocomplete data item on select */
ComboboxLabel: '#', /* label for the input field */
ComboboxDropdownTemplate: '#' /* label for the input field */
},
template:
'<section class="-combobox-directive container-fluid">' +
'<label for="{{Combobox}}" ng-if="ComboboxTranslation" translate="{{ComboboxLabel}}"></label>' +
'<div class="combobox input-group">' +
'<input type="text" ' +
'id="{{Combobox}}" ' +
'autocomplete="off" ' +
'ng-model="ComboboxDestinationDisplay" ' +
'data-toggle="dropdown" ' +
'typeahead="value as location.value for location in ComboboxAutocompleteData | filter:$viewValue" ' +
'typeahead-editable="false" ' +
'typeahead-on-select="elementSelected($item, $model, $label)" ' +
'class="form-control dropdown">' + // dropdown-toggle
'<span data-toggle="dropdown" class="input-group-addon dropdown-toggle">' +
'<span class="glyphicon glyphicon-globe"></span>' +
'</span>' +
//$compile(ComboboxDropdownTemplate) +
'</div>' +
'</section>',
link: link
};
};
angular.module('vibe.directives').directive('nsdCombobox', [NsdCombobox]);
})();
HTML
<script type="text/ng-template" id="myDirectivesCustomTemplate.html">
<ul>
<li ng-repeat="value in values">
<a ng-click="doSomething({id:value.id})">
{{value.name}}
</a>
</li>
</ul>
</script>
<div ng-controller="MainCtrl">
<div my-directive template="myDirectivesCustomTemplate.html" mydata="mydata" mycallback="doSomething(id)"></div>
</div>
JS
app.controller('MainCtrl',function($scope){
$scope.mydata = [{id:1,name:'One'},{id:2,name:'Two'},{id:3,name:'Three'}];
$scope.doSomething = function(id){
alert(id);
}
});
app.directive('myDirective', function($templateCache,$compile) {
return {
restrict: 'A',
scope:{
template : "#",
mydata : "=",
mycallback:"&"
},
link: function(scope,element) {
var template = $templateCache.get(scope.template);
scope.values = scope.mydata;
scope.doSomething = scope.mycallback;
element.append($compile(template)(scope));
}
}
});
Looking at your directive i can suggest try ng-include. Where you want to do
//$compile(ComboboxDropdownTemplate) +
'</div>'
change it to
<span ng-include='templateUrlPropertyOnScope'>
'</div>'
templateUrlPropertyOnScope property should point to a template either on server side or in script section created with type=text/ng-template.
I know this is 4 years later but if anyone still has this question, perhaps this answer may also be found useful.
With a simple directive such as this:
<my-directive template="custom-template.html"></my-directive>
You can then refer to the template attribute in your directive, like this:
(function() {
angular
.module('app')
.directive('myDirective', myDirective);
function myDirective() {
return {
restrict: 'E',
templateUrl: function(elem, attrs) {
return attrs.template;
}
}
}
}
you can use $http and $compile to achieve such a task.
app.directive('myDirective', function($http, $templateCache, $compile) {
return {
scope: {
// reference to your data.
// Just use `data.values` or `data.whatever` in your template
data: '=myDirectiveData'
},
link: function(scope, elm, attrs) {
// Load the template via my-directive-custom-template attribute
$http.get(attrs.myDirectiveCustomTemplate, {cache: $templateCache}).success(function(html) {
// update the HTML
elm.html(html);
// compile the html against the scope
return $compile(elm.contents())(scope);
});
}
};
});
I hope it gives you a good start

Two way databinding in AngularJs not working with async callback

So I'm trying to build an AngularJS app and am having some troubles with two way databinding between a controller and a directive when used with an async callback. I have a page controller that pulls data from a server and then uses multiple custom form directives to edit the data. Here is my setup:
function pageController($scope, $http) {
// this is what the data will look like
$scope.controllerModel = {
obj1: {},
obj2: {}
}
$http.get('get the data').then(function(data) {
$scope.controllerModel = data; // fill in the data
$scope.$broadcast('formDataReady'); // tell the forms the data is ready
});
}
The directive:
module('blah').directive('customForm', function() {
return {
restrict: 'E',
scope: { model: '=' },
transclude: true,
replace: true,
controller: function($scope, $attrs) {
$scope.cleanModel = $scope.model ? _.cloneDeep($scope.model) : {};
$scope.reset = function() {
$scope.model = _.cloneDeep($scope.cleanModel);
};
$scope.isClean = function() {
return _.isEqual($scope.model, $scope.cleanModel);
};
// Let page controllers tell the from when the model has been loaded
$scope.$on('formDataReady', function() {
console.log('custom-form: resetting the clean model');
$scope.reset();
console.log($scope);
console.log($scope.model);
});
$scope.reset();
},
template:
'<div>' +
'<form name="form" novalidate>' +
'<div ng-transclude></div>' +
'<div class="form-actions">' +
'<button class="btn btn-primary" ' +
'ng-click="save()" ' +
'ng-disabled="form.$invalid || isClean()">' +
'Save</button>' +
'<button class="btn" ' +
'ng-click="reset()" ' +
'ng-disabled=isClean()>' +
'Cancel</button>' +
'</div>' +
'</form>' +
'</div>'
};
});
And a bit of html:
<div ng-controller="pageController">
<custom-form model="controllerModel.obj1">
<!-- inputs with ng-model to edit the data -->
</custom-form>
<custom-form model="controllerModel.obj2">
<!-- inputs with ng-model to edit the data -->
</custom-form>
</div>
The problem is that the directive's model is not updated as a result of the async callback. The really strange thing is that in the event listener on the directive, those two console.log calls seem to give contradictory information:
console.log($scope):
...
model: { object with data inside it as expected }
...
console.log($scope.model):
Object {} // empty
So in the first log the scope has the model, but $scope.model is somehow empty.
Thanks so much for any help with this. It really, really appreciated.
If you get your data in a resolve before the controller is instantiated, then the directive should read it from it just fine:
app.config(function($routeProvider) {
$routeProvider.route('myRoute', {
url: '/myroute',
resolve: {
getData: function($http) {
return $http.get('get the data').then(function(data) {
return data;
}
}
}
});
});
function pageController($scope, getData) {
// getData from your $http call is now available before your controller was instantiated
// and can be used by your directive
$scope.controllerModel = getData;
}
I'm not sure why the console log is giving contradictory info though

Resources