modalService cannot talk with controller and view - javascript

The code at this plnkr has a modal which pops up when a user clicks on a "Click to take quiz" button which calls a controller method that in turn calls a modal service. To get the plnkr to work, click anywhere in the code and press the space bar to add white space in a way that does not effect syntax. This will trigger plnkr to re-initialize the app and make the modal pop up after you click the button.
The problem is that the text printed in the modal does not update dynamically when timeLeft variable counts down. And also, the user's button click does not update the quizAnswer variable. In short, the modal is not able to talk interactively with the calling controller and view.
What specific changes need to be made to the plnkr to get the modal text to show the dynamic countdown, and to get the modal buttons to change the value of the $scope.quizAnswer variable?
Also, I have been carefully reading the documentation at this link. I think that the answer may be related to:
1.) $uibModal's options parameter passed in open(options) contains the parameter scope that defines the parent scope to be used for the modal's content, and also property bindToController which, when set to true, binds the scope property to a specific controller defined by controllerAs.
2.) The open(options) method returns a modal instance, which includes close(result) and dismiss(reason).
I suspect that the solution lies in these methods and parameters, but I am looking for good examples and would appreciate some experienced eyes looking at this problem.
NOTE: The solution to this came in the comments below the accepted answer, especially the link to another posting that contains 2 lines of code for emitting the modal button click's results back to the parent controller.

You have a number of issues.
First, takeQuiz at navigation.js - line 16, should be attached to $scope, not this, since this will mutate depending on context.
Second, $scope.$apply and $scope.$digst(); at navigation.js - lines 29/30 are unnecessary since you will already be in a digest cycle. They should be removed else they'll trigger an error.
Finally (and this is the meat of your issue), you are misunderstanding how modal options are bound across when creating a modal instance. It is NOT two-way binding; it is a single extends from one object to another. As a result, trying to bind to the options (or creating a concatenated string with the timeRemaining) will not update once it's bound across.
Instead, one possibility is to create an event handler inside of the modal and broadcast on each tick, updating the modal. In addition, if you pass the body text as prepend and append text, it is easier to insert your timestamp value:
You will need to inject (and broadcast from) $rootScope in your navigationController, since the modalService is registered somewhere very high in the scope chain.
On each tick, broadcast the time remaining navigation.js:
$rootScope.$broadcast('timeRemainingTick', $scope.timeRemaining);
In your modalService.js, register to receive the event inside of the controller assignment:
var timeRemainingUnbind = $scope.$on('timeRemainingTick', function(event, newTick) {
$scope.modalOptions.timeRemaining = newTick;
});
Finally, make sure that you unbind the event by calling timeRemainingUnbind() in the close events of your modal to prevent memory leaks:
$scope.modalOptions.ok = function (result) {
timeRemainingUnbind();
$modalInstance.close(result);
};
$scope.modalOptions.close = function (result) {
timeRemainingUnbind();
$modalInstance.dismiss('cancel');
};
See my working forked plunker here

Related

AngularJS: function is not a function

I have an AngularJS app, and on one of the pages, I have a number of widgets, each one displaying some information about the status of a part of the system.
I am currently working on adding the functionality to allow the user to 'hide' the heading of a given widget.
There is a 'Settings' button on the page where the widgets are displayed, which, when clicked, overlays a toolbar on top of each of the widgets. The toolbar has a number of buttons- one of which is another 'Settings' button, which opens up a dialog that allows the user to change the settings for that particular widget.
I have added a checkbox to the dialog, to enable the user to 'hide' the heading for that particular widget from view:
When the checkbox is selected on the dialog, and the user clicks 'Preview', I am expecting (eventually- I'm still working on the implementation of the feature) the heading for that particular widget to be hidden. However, currently, when the user clicks 'Preview', whether the checkbox is selected or not, I am getting an error in the console that says:
TypeError: $scope.widget.toggleWidgetHeading is not a function
This error is coming from the $scope.preview function in ctrl.js called when the 'Preview' button is clicked on the dialog:
}).controller('WidgetPickerCtrl', function($scope, $timeout, fxTag, gPresets, appWidget) {
...
$scope.preview = function(e) {
$scope.widget.toggleWidgetHeading();
...
};
...
});
I don't understand why I'm getting this console error, since toggleWidgetHeading() clearly is a function...
If I right-click on the function call above in Sublime, and select 'Go to definition', I am taken to the directive.js file where the function is defined:
.directive('appWidget', function($timeout, fxTag, appColorFilter) {
return {
...
link: function($scope, $element){
...
var toggleWidgetHeading = function(){
...
}
...
}
}
})
Also, clicking the 'Preview' button on the dialog no longer closes the dialog...
Why is it that I'm being told that this function call is not a function when it is clearly defined as one? Is the issue here something to do with the scope (i.e. the fact that I'm calling the function from ctrl.js, even though it's defined in directive.js)?
The definition of your directive, where you added ..., is actually a really relevant part about directives scopes.
Directives can implement several kind of scopes. You can actually inherit and access the parent scope, or you can have an isolated scope for example.
You may read about that in the official documentation where is well explained:
https://docs.angularjs.org/guide/directive
However, whatever will be the scope you use, by default AngularJS implements the inheritance of scopes, as usually inheritance works: children can access parent methods, but parent cannot access children's methods.
Here it seems that from the parent scope (the controller) you are trying to access the directive's scope, which is actually no possible. (even if in the link function you define the toggleWidgetHeading as private variable, and not associated to the $scope itself - but it won't work either).
So you have few options in these cases:
Define your "visible properties" inside a service and inject the service inside the directive and the controller. Then use the service in order to access and change these values, so that they will be sync between the controller and the directive
Add a scope parameter to the directive as callback & and provide a function from the controller which returns the chosen visibility of the widget, so from the directive you can call that function and get back the value of widget's visibility
Add a scope parameter as two way data binding = in the directive, which is bound to the widget's visibility of the controller, so that you have that always sync between your controller and your directive
Use events in order to communicate between the controller and the directive, broadcasting an event from the controller when the visibility changes, and reading the event from the directive getting the widget's visibility value
I hope it makes sense

Angular directives - watching directive's parent controller (require) doesn't trigger $watch callback

I'm trying to build a custom angular table directive, and came upon a curious problem. I have a parent directive parentDir, which has multiple child elements childDir inside it. When a childDir is clicked, it calls the parent's sortByKey function, which changes the sorting variable of the controller, and later on is supposed to sort the data depending on the clicked column.
The problem is, in childDirs, I'm $watching this the parent's sorting variable, but the callback function doesn't trigger when the variable value changes. Instead, it seems to queue the callback until something happens in either the parent directive controller's $scope or its parent $scope - so for example, changing an input with an ng-model triggers all of the queued $watch callbacks.
So far I've tried to call $scope.$apply() in the sortByKey function (which doesn't work) and in the watch callback (which obviously doesn't work since it's not getting called). I'm kinda stumped on why the callback isn't triggering since it's the first time I've ever encountered this behaviour.
Here's a link to the plunker demonstrating the behaviour. Open the console, and then click on the "Hello", "Hello 2" and "Hello 3" texts. You can see in the console that the sortByKey function is being called properly, but there's no logging from the $watch callback. Then change the input's value, and you can see the callback function console.logs.
To summarize:
$watch() callback function isn't being triggered in a child directive until parent scope is updated
adding $scope.$apply calls doesn't help
The wanted outcome is for the show text to appear after the clicked text as the text is clicked, and to disappear from all the other texts

Alter shared mdToolbar from view

I have a SPA in Angular Material which is displaying an mdToolbar element with a hamburger menu + left sidenav. That menu at the moment resides in my index.html where I have also set-up a <ui-view> element to render the view.
Now I have a view with a mdList in it. When the user selects some items, I want a delete icon to appear in the toolbar. That delete icon should be linked to the delete action of my controller which is of course specific to the view loaded, not to index.html.
I want to know what a recommended pattern for this would be. I can think of some ways to do it, but those are ugly. I was thinking in the direction of being able to have some placeholder area in the toolbar which I can replace with contents from my view, where the element actions (ngClick) are linked to the actions of the view controller. Does anyone know a good tutorial or codepen-like example of how to do this?
Update
I've now got something implemented that I'm happy with, but it's not quite there yet. What I did is create a menuService which is injected in the controller behind my menu (it's not a separate view, could be but doesn't make a difference in this scenario). The menu controller binds to this service and other services can inject stuff in it. In my test scenario, I inject a string which is then displayed in the toolbar, all ok.
The only thing I now need to do is instead of a string, inject a button with an event handler which goes back to the controller of the view. I'm not quite certain how to do that yet.
Another update
So I created this class:
export class CommandButton {
svgSrc: string;
click: () => void;
}
which I can inject into my menuService and then binds to the menu controller. Works fine for the icon (the button appears) but as one might expect (I did), the click function doesn't work. I set this in the view controller as follows:
var deleteButton = new Services.CommandButton();
deleteButton.svgSrc = 'icons/ic_delete_24px.svg';
deleteButton.click = this.deleteLogs;
this.menuService.setButtons([deleteButton]);
And the code for this.deleteLogs is simply:
deleteLogs() {
console.log('deleting logs');
}
Code for the buttons:
<div ng-repeat="button in ctrl.buttons">
<md-button ng-click="button.click">
<md-icon md-svg-src="{{button.svgSrc}}" class="md-icon md-24"></md-icon>
</md-button>
</div>
What I hoped for is that this would trigger the deleteLogs method in the view controller, but that's not the case. I need data from the view as that is where the items are selected. As far as I can see there's just nothing happening so the binding fails somewhere. What would be a good way to make sure the click event makes to to the view controller function? I could do a $rootscope.$broadcast but that feels hacky.
Last update
Never mind, I found my own bug. The binding of the event was incorrect, should have been (note the parenthesis):
<md-button ng-click="button.click()">
In the meantime I've figured out a nice way to do this. The post itself now also contains the answer.

Update scope from directive and watch with another one

Here is the problem :
I try to do things like this :
Click on a directive triggers an event
Event sends data to a rootScope function which sets a scope variable value with new data
Another directive has a ng-model in its template which is supposed to reflect the previously changed var
Data should be updated in the view AND model. Model is ok (update done), but view update does not seem to trigger automatically, it only updates when I click on the "show button" (see fiddle)
Here is the corresponding jsfiddle I made, with comments :
http://jsfiddle.net/wkmm576m/7/
Here is the code which I think is making trouble... :
app.directive('addEditInput', function() {
return {
restrict : 'E',
template : '<div id="editForm"><input type="text" ng-model="currentVar.name" /><br/></div>'
}
});
In fact, I return a template, with ng-model. But the ng-model is not updated when its value changes from anywhere else...
Tbh, I'm a bit confused with directives and scopes communications - data....
Thanks for reading / help
Short answer - you need to call $scope.$apply in top controller:
http://plnkr.co/edit/QEjzlsJj4fh1eeqBXSvt?p=preview
Long answer: you bind usual js function to usual js event, so when user clicks on element - your function executes. Thats all. When you use ng-click - it will trigger digest cycle and your bindings will be updated. So you need to trigger it manually, so you call $scope.$apply.
Note: better do not use jquery where you do not need to. I.e. use $element.bind('click',function(){ instead of $().on...

What is the proper way to extend the behavior of a custom Angular directive?

I've written my own Angular directive called <my-table> since I use it multiple times, passing in different data to display. All of the tables have a button that when clicked, a popup form appears, like so:
However, for one of the <my-table> directives, I want to extend the behavior so that it acts slightly different from the other <my-table>s. For example, let's say that for the form that pops up, an alert box will appear when you click Submit, displaying data present in that <my-table>'s scope.
My question is, what is the best way extend the behavior of a given directive while still being able to access the directive's scope? Is this possible, or am I simply using directives incorrectly?
You can do something to this extent:
<my-table on-submit="doSomething(message)"></my-table>
In your my-table directive definition you would bind your callback to the scope:
scope: {
'submit': '&onSubmit'
},
Then in your controller you define your function:
$scope.doSomething = function(message) {
alert(message);
}
In your template, where you define the submit button, you would do:
<button ng-click="submit({message: 'bye!'})"></button>
You can reference angular's documentation for more info:
https://docs.angularjs.org/guide/directive

Categories

Resources