ion-toggle ng-change does not trigger properly with controller + service structure - javascript

I've created a list of toggles to select bluetooth devices and connect it, and I've created a controller.js and service.js for them. However, the ng-changeonly triggers when ion-toggles first loaded in view, and were not triggering for changes afterwards.
Here is my snippet of setting.html:
<ion-toggle ng-repeat="item in devicesList"
ng-model="item.checked"
ng-change="connectDevice(devicesList)">
{{ item.address }}
</ion-toggle>
and here is my settingController.js:
(function () {
'use strict';
angular.module('i18n.setting').controller('SettingController', SettingController);
SettingController.$inject = ['$scope','SettingService'];
function SettingController($scope, SettingService) {
$scope.devicesList = [];
$scope.scanDevices = SettingService.scanDevices($scope.devicesList);
$scope.connectDevice = SettingService.connectDevice($scope.devicesList);
};
})();
and here is my settingService.js:
(function(){
'use strict';
angular.module('i18n.setting').service('SettingService', SettingService);
SettingService.$inject = [];
function SettingService(){
var self = this;
this.scanDevices = function(devicesCollection) {
window.bluetoothSerial.discoverUnpaired(function(devices) {
console.log("in discoverUnpaired callback, setting page");
devices.forEach(function(device) {
console.log(device.name);
device.checked = false;
devicesCollection.push(device);
});
}, function(error) {
console.log("in discoverUnpaired error function");
console.log(error);
});
};
this.connectDevice = function(devicesCollection) {
console.log(devicesCollection);
console.log("In connectDevice");
for (var i =0; i<devicesCollection.length; i++)
{
console.log(devicesCollection[i].id + " " + devicesCollection[i].checked);
if (devicesCollection[i].checked == true)
{
console.log(devicesCollection[i].name);
window.bluetoothSerial.connect(devicesCollection[i].address,
function(data) {console.log("This device is"+macaddress); console.log(data);},
function(error) {console.log(error);});
}
}
};
}
})();
And if I define this function directly in settingController.js, it's working properly and can detect every change of the toggle. Also, I noticed that my scanDevices function will be triggered even if I don't click the button. I don't know why is it. Will there be someone tell me what cause is it? Any help, thanks

The controller should put the service functions on the scope; not invocations of the service functions.
(function () {
'use strict';
angular.module('i18n.setting').controller('SettingController', SettingController);
SettingController.$inject = ['$scope','SettingService'];
function SettingController($scope, SettingService) {
$scope.devicesList = [];
//$scope.scanDevices = SettingService.scanDevices($scope.devicesList);
$scope.scanDevices = SettingService.scanDevices;
//$scope.connectDevice = SettingService.connectDevice($scope.devicesList);
$scope.connectDevice = SettingService.connectDevice;
};
})();
Since the scanDevices function has no return statement, scanDevices($scope.deviceList) returns undefined. Thus $scope.scanDevices was getting set to undefined.
Update
Can you also explain why scanDevices get invoked before I press the Scan button?
Which was bind to:
<button class="button button-stable"
ng-click="scanDevices(devicesList)">Scan Devices
</button>
By binding an invocation of the function to the scope variable instead of the function itself:
//ERRONEOUS
$scope.scanDevices = SettingService.scanDevices($scope.devicesList);
//CORRECT
$scope.scanDevices = SettingService.scanDevices;
The expression SettingService.scanDevices($scope.devicesList) invokes the function before the button is clicked. And assigns undefined to the $scope.scanDevices variable.

Related

KnockoutJs call function in other viewmodel of applyBindings

On a page I'm calling ko.applyBindings twice to iniate 2 view models. When viewModelOne saves successfully, I want to reload the other view model as some data is added in the backend as they are loosely linked.
Now I'm trying to call viewModelTwo.reloadData in saveSuccess() but I keep getting the error that it can't find the function whatever I try.
(Uncaught TypeError: viewModelTwo.reloadData is not a function)
What is the correct way of calling a function from the other viewmodel in KnockoutJs? Could anyone point me in the right direction?
var viewModelOne = (function () {
function reloadData(url) {
...
}
function saveSuccess(){
viewModelTwo.reloadData('');
}
});
var viewModelTwo = (function () {
function reloadData(url) {
...
}
});
ko.applyBindings(viewModelOne, document.getElementById("modelOneContainer"));
ko.applyBindings(viewModelTwo, document.getElementById("modelTwoContainer"));
You could use a constructor function:
function ViewModelOne() {
var vm = this;
vm.reloadData = function() {
console.log('vm1 reloaddata');
}
}
var vm1 = new ViewModelOne();
function ViewModelTwo() {
var vm = this;
vm.reloadData = function() {
vm1.reloadData();
console.log('vm2 reloaddata');
}
}
var vm2 = new ViewModelTwo();
ko.applyBindings(vm1, document.getElementById("modelOneContainer"));
ko.applyBindings(vm2, document.getElementById("modelTwoContainer"));
vm2.reloadData();
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<div id="modelOneContainer"></div>
<div id="modelTwoContainer"></div>

how to pass data from service to directive in angular.js

I have an alert service which shows alerts on top of the page. I have written a service and a directive which feeds off of the data coming from the service.
However, when i add a service using teh alert service and pass it to the directive, it does not show up, the alert
here is my code
The template
<div class="alert alert-{{alert.type}}">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true" ng-click="close()">×</button>
<div ng-bind="::alert.message" ></div>
</div>
Alert Service and directive
angular.module('test')
.service('alertService', function() {
var alerts = [];
this.add = function(type, msg) {
var self = this;
var alert = {
type: type,
msg: msg,
close: function() {
return self.closeAlert(alert);
}
};
return alerts.push(alert);
};
this.closeAlert = function(alert) {
return this.closeAlertIdx(alerts.indexOf(alert));
};
this.closeAlertIdx = function(index) {
return alerts.splice(index, 1);
};
this.clear = function() {
alerts = [];
};
this.getAlerts = function() {
return alerts;
};
})
.directive('alertList', ['alertService', function(alertService) {
return {
restrict: 'EA',
templateUrl: 'templates/alert/alert.html',
replace: true,
link: function(scope) {
scope.alerts = alertService.getAlerts();
}
};
}]);
In the index.html , i have referenced the alert-list directive
<div>
<alert-list ng-repeat="alert in alerts">
</alert-list>
</div>
In my controller i have,
alertService.add('info', 'This is a message');
I see that the alertService adds the alert to the array, but when i put a breakpoint in the link function of the directive, it never gets called
services are function that return an object, so you had to modify your service to be more or less like this:
.service('alertService', function() {
var alerts = [];
return{
add : function(type, msg) {
var self = this;
var alert = {
type: type,
msg: msg,
close: function() {
return self.closeAlert(alert);
}
};
return alerts.push(alert);
},
closeAlert: function(alert) {
return this.closeAlertIdx(alerts.indexOf(alert));
},
closeAlertIdx : function(index) {
return alerts.splice(index, 1);
},
clear: function() {
alerts = [];
},
getAlerts: function() {
return alerts;
}
})
The link function is only called once, when the directive element is created. When your app starts up, the link function will be called, and the scope.alerts will be set to an empty list.
I think you need to move the ng-repeat to the outer div of the alert template, rather than on the alert-list element.
Since the link function is only called once, and the identity of the array can change if you call alertService.clear, you'll probably have better luck putting a watch in your alert's link statement:
link: function(scope) {
scope.$watchCollection(alertService.getAlerts, function(alerts) {
scope.alerts = alerts;
});
}
Since this method doesn't directly do any DOM manipulation, modern angular best-practice would probably be to implement this as a component instead.

Angular - removing class after time

I am building a sort of (faux) loader in Angular. Currently, I have this:
const app = angular.module('app', []);
app.controller('loaderCtrl', ($scope, $timeout) => {
let loading = $scope.loading,
loaded = $scope.loaded;
$scope.reset = () => {
$timeout(() => {
loading = false;
loaded = false;
console.log(loaded);
}, 500);
}
});
HTML:
<main ng-app="app">
<div ng-controller="loaderCtrl as loader" >
<div class="loader" ng-class="{ '-loading' : loader.loading === true, '-loaded' : loader.loaded === true }"></div>
<button ng-click="loader.loading = true;">loading</button>
<button ng-click="loader.loaded = true; reset();">loaded</button>
</div>
</main>
CodePen: http://codepen.io/tomekbuszewski/pen/WrXXdp
My problem is, both loading and loaded aren't being set up for my view, so the classes are permanently there. What can I do?
So, this is a problem of scope. Basically when you do this
let loading = $scope.loading,
loaded = $scope.loaded;
You get the "value" of the variables inside Angular scope. Therefore Angular does not know anything about changes made to those
The fix is simple, don't do that, but instead
$scope.reset = () => {
$timeout(() => {
$scope.loading = false;
$scope.loaded = false;
}, 500);
}
Why not using an object and change its content? It is possible to do that as #beaver pointed out, but then you have another problem, you need to trigger the digest cycle yourself via $apply. And somewhere in your code, you might accidentally change the content of the object and it might affect other part of the system
Having said that I do not know Babel and so I worked on the JS compiled version, I noticed that you assigned loader.loading and loader.loaded to variables and then used those "references" in $timeout function.
As in javascript
Primitives are passed by value, Objects are passed by "copy of a
reference"
you have to use $scope.loader.loading and $scope.loader.loaded
app.controller('loaderCtrl', function ($scope, $timeout) {
$scope.loader = {};
var loading = $scope.loader.loading, loaded = $scope.loader.loaded;
$scope.reset = function () {
$timeout(function () {
$scope.loader.loading = false;
$scope.loader.loaded = false;
}, 500);
};
});
Here I forked your CodePen: http://codepen.io/beaver71/pen/wMPprm

AngularJS $emit does not fire the event after added code to unregister

I just found out how to communicate between controllers using $broadcast and $emit, tried it in my POC and it worked, sort of, the original problem described in this other post is still not solved but now I have another question, the event is being registered multiple times so I am trying to unregister it the way I've seen it in multiple posts here on SO but now the event won't fire. The code is as follows:
tabsApp.controller('BasicOverviewController', function ($scope, $location, $rootScope) {
var unbind = $rootScope.$on('displayModal', function (event, data) {
if (data.displayModal) {
alert("I want to display a modal!");
var modal = $('#basicModal');
modal.modal('toggle');
}
});
$scope.$on('$destroy', function () {
unbind();
});
});
tabsApp.controller('SportsController', function SportsController($scope, $location, $rootScope) {
$scope.goToOverview = function (showModal) {
$location.path("overview/basic");
$rootScope.$emit('displayModal', { displayModal: showModal })
};
});
If I remove the
var unbind = ...
the event fires and I can see the alert. As soon as I add the code to unregister the event, the code is never fired. How can the two things work together?
Could you just pull out unbind into its own function, and use it in both like this?
tabsApp.controller('BasicOverviewController', function ($scope, $location, $rootScope) {
var unbind = function (event, data) {
if (data.displayModal) {
alert("I want to display a modal!");
var modal = $('#basicModal');
modal.modal('toggle');
}
};
$rootScope.$on('displayModal', unbind);
$scope.$on('$destroy', unbind);
});
I could be wrong but my guess would be that the BasicOverviewController isn't being persisted and it's scope is being destroyed before the SportsController gets a chance to utilize it. Without a working example, I can't deduce much more. If you want to maintain this on $rootScope then a possible pattern would be:
if (!$rootScope.displayModalDereg) {
$rootScope.displayModalDereg = $rootScope.$on('displayModal', function (event, data) {
if (data.displayModal) {
alert("I want to display a modal!");
var modal = $('#basicModal');
modal.modal('toggle');
}
});
This also allows you to check and see if there is an event registered so you can dereg it if needed.
if ($rootScope.displayModalDereg) {// this event has been registered
$rootScope.displayModalDereg();
$rootScope.dispalyModalDereg = undefined;
}
I would heavily suggested creating a displayModal directive that persists all of this instead of maintaining it on $rootScope. Obviously you would still $emit, or better yet, $broadcast from $rootScope, just not persist the dereg function there.
Here is an example of a modal directive I once wrote:
/**
*
* Modal Directive
*/
'use strict';
(function initModalDrtv(window) {
var angular = window.angular,
app = window.app;
angular.module(app.directives).directive('modalDrtv', [
'$rootScope',
function modalDrtv($rootScope) {
return {
restrict: 'A',
scope: {},
templateUrl: '/templates/modal.html',
replace: true,
compile: function modalCompileFn(tElement, tAttrs) {
return function modalLinkFn(scope, elem, attrs) {
scope.show = false;
scope.options = {
'title': '',
'message': '',
'markup': undefined,
'buttons': {
showCancel: false,
showSecondary: false,
secondaryAction: '',
primaryAction: 'Ok'
},
'responseName': ''
};
scope.respond = function(response) {
var r = '';
if (response === 1) {
r = scope.options.buttons.primaryAction;
} else if (response === 2) {
r = scope.options.buttons.secondaryAction;
} else {
r = response;
}
$rootScope.$broadcast(scope.options.responseName, r);
scope.show = false;
};
scope.$on('initIrpModal', function(event, data) {
if (angular.isUndefined(data)) throw new Error("Data missing from irp modal event");
scope.options.title = data.title;
scope.options.message = data.message;
scope.options.buttons.showCancel = data.buttons.showCancel;
scope.options.buttons.showSecondary = data.buttons.showSecondary;
scope.options.buttons.secondaryAction = data.buttons.secondaryAction;
scope.options.buttons.primaryAction = data.buttons.primaryAction;
scope.options.responseName = data.responseName;
scope.show = true;
});
}
}
}
}
]);
})(window);
This directive utilizes one modal and let's anything anywhere in the app utilize it. The registered event lives on its isolate scope and therefore is destroyed when the modal's scope is destroyed. It also is configured with a response name so that if a user response is needed it can broadcast an event, letting the portion of the app that initialized the modal hear the response.

AngularJS : How to run JavaScript from inside Directive after directive is compiled and linked

I have a responsive template that I am trying to use with my Angularjs app. This is also my first Angular app so I know I have many mistakes and re-factoring in my future.
I have read enough about angular that I know DOM manipulations are suppose to go inside a directive.
I have a javascript object responsible for template re-sizes the side menu and basically the outer shell of the template. I moved all of this code into a directive and named it responsive-theme.
First I added all the methods that are being used and then I defined the App object at the bottom. I removed the function bodies to shorten the code.
Basically the object at the bottom is a helper object to use with all the methods.
var directive = angular.module('bac.directive-manager');
directive.directive('responsiveTheme', function() {
return {
restrict: "A",
link: function($scope, element, attrs) {
// IE mode
var isRTL = false;
var isIE8 = false;
var isIE9 = false;
var isIE10 = false;
var sidebarWidth = 225;
var sidebarCollapsedWidth = 35;
var responsiveHandlers = [];
// theme layout color set
var layoutColorCodes = {
};
// last popep popover
var lastPopedPopover;
var handleInit = function() {
};
var handleDesktopTabletContents = function () {
};
var handleSidebarState = function () {
};
var runResponsiveHandlers = function () {
};
var handleResponsive = function () {
};
var handleResponsiveOnInit = function () {
};
var handleResponsiveOnResize = function () {
};
var handleSidebarAndContentHeight = function () {
};
var handleSidebarMenu = function () {
};
var _calculateFixedSidebarViewportHeight = function () {
};
var handleFixedSidebar = function () {
};
var handleFixedSidebarHoverable = function () {
};
var handleSidebarToggler = function () {
};
var handleHorizontalMenu = function () {
};
var handleGoTop = function () {
};
var handlePortletTools = function () {
};
var handleUniform = function () {
};
var handleAccordions = function () {
};
var handleTabs = function () {
};
var handleScrollers = function () {
};
var handleTooltips = function () {
};
var handleDropdowns = function () {
};
var handleModal = function () {
};
var handlePopovers = function () {
};
var handleChoosenSelect = function () {
};
var handleFancybox = function () {
};
var handleTheme = function () {
};
var handleFixInputPlaceholderForIE = function () {
};
var handleFullScreenMode = function() {
};
$scope.App = {
//main function to initiate template pages
init: function () {
//IMPORTANT!!!: Do not modify the core handlers call order.
//core handlers
handleInit();
handleResponsiveOnResize(); // set and handle responsive
handleUniform();
handleScrollers(); // handles slim scrolling contents
handleResponsiveOnInit(); // handler responsive elements on page load
//layout handlers
handleFixedSidebar(); // handles fixed sidebar menu
handleFixedSidebarHoverable(); // handles fixed sidebar on hover effect
handleSidebarMenu(); // handles main menu
handleHorizontalMenu(); // handles horizontal menu
handleSidebarToggler(); // handles sidebar hide/show
handleFixInputPlaceholderForIE(); // fixes/enables html5 placeholder attribute for IE9, IE8
handleGoTop(); //handles scroll to top functionality in the footer
handleTheme(); // handles style customer tool
//ui component handlers
handlePortletTools(); // handles portlet action bar functionality(refresh, configure, toggle, remove)
handleDropdowns(); // handle dropdowns
handleTabs(); // handle tabs
handleTooltips(); // handle bootstrap tooltips
handlePopovers(); // handles bootstrap popovers
handleAccordions(); //handles accordions
handleChoosenSelect(); // handles bootstrap chosen dropdowns
handleModal();
$scope.App.addResponsiveHandler(handleChoosenSelect); // reinitiate chosen dropdown on main content resize. disable this line if you don't really use chosen dropdowns.
handleFullScreenMode(); // handles full screen
},
fixContentHeight: function () {
handleSidebarAndContentHeight();
},
setLastPopedPopover: function (el) {
lastPopedPopover = el;
},
addResponsiveHandler: function (func) {
responsiveHandlers.push(func);
},
// useful function to make equal height for contacts stand side by side
setEqualHeight: function (els) {
var tallestEl = 0;
els = jQuery(els);
els.each(function () {
var currentHeight = $(this).height();
if (currentHeight > tallestEl) {
tallestColumn = currentHeight;
}
});
els.height(tallestEl);
},
// wrapper function to scroll to an element
scrollTo: function (el, offeset) {
pos = el ? el.offset().top : 0;
jQuery('html,body').animate({
scrollTop: pos + (offeset ? offeset : 0)
}, 'slow');
},
scrollTop: function () {
App.scrollTo();
},
// wrapper function to block element(indicate loading)
blockUI: function (ele, centerY) {
var el = jQuery(ele);
el.block({
message: '<img src="./assets/img/ajax-loading.gif" align="">',
centerY: centerY !== undefined ? centerY : true,
css: {
top: '10%',
border: 'none',
padding: '2px',
backgroundColor: 'none'
},
overlayCSS: {
backgroundColor: '#000',
opacity: 0.05,
cursor: 'wait'
}
});
},
// wrapper function to un-block element(finish loading)
unblockUI: function (el) {
jQuery(el).unblock({
onUnblock: function () {
jQuery(el).removeAttr("style");
}
});
},
// initializes uniform elements
initUniform: function (els) {
if (els) {
jQuery(els).each(function () {
if ($(this).parents(".checker").size() === 0) {
$(this).show();
$(this).uniform();
}
});
} else {
handleUniform();
}
},
updateUniform : function(els) {
$.uniform.update(els);
},
// initializes choosen dropdowns
initChosenSelect: function (els) {
$(els).chosen({
allow_single_deselect: true
});
},
initFancybox: function () {
handleFancybox();
},
getActualVal: function (ele) {
var el = jQuery(ele);
if (el.val() === el.attr("placeholder")) {
return "";
}
return el.val();
},
getURLParameter: function (paramName) {
var searchString = window.location.search.substring(1),
i, val, params = searchString.split("&");
for (i = 0; i < params.length; i++) {
val = params[i].split("=");
if (val[0] == paramName) {
return unescape(val[1]);
}
}
return null;
},
// check for device touch support
isTouchDevice: function () {
try {
document.createEvent("TouchEvent");
return true;
} catch (e) {
return false;
}
},
isIE8: function () {
return isIE8;
},
isRTL: function () {
return isRTL;
},
getLayoutColorCode: function (name) {
if (layoutColorCodes[name]) {
return layoutColorCodes[name];
} else {
return '';
}
}
};
}
};
});
Originally the App.init() object method would be called at the bottom of any regular html page, and I have others that do certain things also that would be used on specific pages like Login.init() for the login page and so forth.
I did read that stackoverflow post
"Thinking in AngularJS" if I have a jQuery background? and realize that I am trying to go backwards in a sense, but I want to use this template that I have so I need to retro fit this solution.
I am trying to use this directive on my body tag.
<body ui-view="dashboard-shell" responsive-theme>
<div class="page-container">
<div class="page-sidebar nav-collapse collapse" ng-controller="SidemenuController">
<sidemenu></sidemenu>
</div>
<div class="page-content" ui-view="dashboard">
</div>
</div>
</body>
So here is my problem. This kinda sorta works. I don't get any console errors but when I try to use my side menu which the javascript for it is in the directive it doesn't work until I go inside the console and type App.init(). After that all of the template javascript works. I want to know how to do responsive theme stuff in these directives. I have tried using it both in the compile and link sections. I have tried putting the code in compile and link and calling the $scope.App.init() from a controller and also at the bottom after defining everything. I also tried putting this in jsfiddle but can't show a true example without having the console to call App.init().
My end design would be having some way to switch the pages through ui-router and when a route gets switched it calls the appropriate methods or re-runs the directive or something. The only method that will run on every page is the App.init() method and everything else is really page specific. And technically since this is a single page app the App.init() only needs to run once for the application. I have it tied to a parent template inside ui-router and the pages that will switch all use this shell template. There are some objects that need to access other to call their methods.
Im sorry in advance for maybe a confusing post. I am struggling right now trying to put together some of the ways that you do things from an angular perspective. I will continue to edit the post as I get responses to give further examples.
You said I have read enough about angular that I know DOM manipulations are suppose to go inside a directive but it sounds like you missed the point of a directive. A directive should handle DOM manipulation, yes, but not one directive for the entire page. Each element (or segment) of the page should have its own directive (assuming DOM manip needs to be done on that element) and then the $controller should handle the interactions between those elements and your data (or model).
You've created one gigantic directive and are trying to have it do way too much. Thankfully, you've kinda sorta designed your code in such a way that it shouldn't be too hard to break it up into several directives. Basically, each of your handle functions should be its own directive.
So you'd have something like:
.directive('sidebarMenu', function(){
return {
template: 'path/to/sidebar/partial.html',
link: function(scope, elem, attrs){
// insert the code for your 'handleSidebarMenu()' function here
}
};
})
.directive('horizontalMenu', function(){
return {
template: 'path/to/horizontal/partial.html',
link: function(scope, elem, attrs){
// insert the code for your 'handleHorizontalMenu()' function here
}
};
})
and then your view would look something like:
<body ui-view="dashboard-shell" responsive-theme>
<div class="page-container">
<div class="page-sidebar nav-collapse collapse">
<horizontal-menu></horizontal-menu>
<sidebar-menu></sidebar-menu>
</div>
<div class="page-content" ui-view="dashboard">
</div>
</div>
</body>
And then you don't need a SidebarmenuController because your controller functions shouldn't be handling DOM elements like the sidebar. The controller should just handling the data that you're going to display in your view, and then the view (or .html file) will handle the displaying and manipulation of that data by its use of the directives you've written.
Does that make sense? Just try breaking that huge directive up into many smaller directives that handle specific elements or specific tasks in the DOM.

Categories

Resources