Trouble with executing Angular directive method on parent - angularjs

I have a directive inside a directive, and need to call a parent method using the child directive. I'm having a bit of trouble passing the data around, and thought y'all might have an idea.
Here's the setup:
My parent directive is called screener-item. My child directive is called option-item. Inside of every screener-item, there might be n option-items, so they're dynamically added. (Essentially, think of this as dynamically building a dropdown: the user gives it a title, then a set of options available)
Here's how this is set up:
screener-item.directive.js
angular.module('recruitingApp')
.directive('screenerItem', function(Study, $compile) {
return {
templateUrl: 'app/new-study/screener-item/screener-item.html',
scope: {
study: '='
},
link: function(scope, el, attrs) {
var options = [];
scope.addOptionItem = function(item) {
options.push(item);
}
scope.saveScreenerItem = function() {
if (scope.item._id) {
var isEdit = true;
}
Study.addScreenerQuestion({id:scope.study._id},{
_id: scope.item._id,
text: scope.item.text,
type: scope.item.type
}, function(item){
scope.mode = 'show';
scope.item._id = item._id;
if (!isEdit) {
el.parent().append($compile('<screener-item study="newStudy.study"></screener-item')(scope.$parent));
}
});
}
}
}
});
screener-item.html
<div class="screener-item row" ng-hide="mode == 'show'">
<div class="col-md-8">
<input type="text" placeholder="Field (e.g., name, email)" ng-model="item.text">
</div>
<div class="col-md-3">
<select ng-model="item.type">
<option value="text">Text</option>
<option value="single_choice">Single Select</option>
<option value="multi_choice">Multi Select</option>
</select>
<div ng-show="item.type == 'single_choice' || fieldType == 'multi_choice'">
<h6>Possible answers:</h6>
<option-item item-options="options" add-option-item="addOptionItem(value)"><option-item>
</div>
</div>
<div class="col-md-1">
<button ng-click="saveScreenerItem()">Save</button>
</div>
</div>
<div class="screener-item-show row" ng-model="item" ng-show="mode == 'show'">
<div class="col-md-8">{{item.text}}</div>
<div class="col-md-3">({{item.type}})</div>
<div class="col-md-1">
<a ng-click="mode = 'add'">edit</a>
</div>
</div>
You'll notice option-item which is included there in them middle. That's the initial option offered to the user. This may be repeated, as the user needs it to be.
option.item.directive.js
angular.module('recruitingApp')
.directive('optionItem', function($compile) {
return {
templateUrl: 'app/new-study/screener-item/option-item.html',
scope: {
addOptionItem: '&'
},
link: function(scope, el, attrs) {
scope.mode = 'add';
scope.addItem = function(value) {
console.log("Value is ", value);
scope.addOptionItem({item:value});
scope.mode = 'show';
var newOptionItem = $compile('<option-item add-option-item="addOptionItem"></option-item')(scope);
el.parent().append(newOptionItem);
}
}
}
});
option-item.html
<div ng-show="mode == 'add'">
<input type="text" ng-model="value">
<button ng-click="addItem(value)">Save</button>
</div>
Here's what I want to happen: When the user enters a value in the option-item textbox and saves it, I want to call addItem(), a method on the option-item directive. That method, then, would call the parent method - addOptionItem(), passing along the value, which gets pushed into an array that's kept on the parent (this array keeps track of all the options added).
I can get it to execute the parent method, but for the life of me, I can't get it to pass the values - it comes up as undefined each time.
I'm trying to call the option-item method instead of going straight to the parent, so that I can do validation if needed, and so I can dynamically add another option-item underneath the current one, once an item is added.
I hope this makes sense, please let me know if this is horribly unclear.
Thanks a ton!
EDIT: Here's a jsFiddle of it: http://jsfiddle.net/y4uzbapz/1/
Note that when you add options, the logged out array of options on the parent is undefined.

Got this working. All the tutorials have this working by calling the parent method on ng-click, essentially bypassing the child controller. But, if you need to do validation before passing the value up to the parent, you need to call a method on the child directive, then invoke the parent directive's method within that call.
Turns out, you can access it just the same way that you can as if you were putting the expression inside of ng-click.
Here's a fiddle showing this working: http://jsfiddle.net/y4uzbapz/3/
Notice that the ng-click handler is actually on the child directive, which calls the parent directive's method. This lets me do some pre/post processing on that data, which I couldn't do if I'd invoked the parent directive directly from ng-click.
Anyway, case closed :)

Related

Update a scope variable from a directive (angularJS)

I have a directive I would like to apply to multiple input elements to change their value. I've been successful in applying it to the input elements value, but for some reason that is not being reflected in the scope. I'm kinda new to Angular and apologize if I'm missing some kind of obvious answer.
http://jsfiddle.net/hmko75td/ JS Fiddle
<div ng-app="myApp">
<div ng-controller="MyCtrl">
<select ng-model='Factor'>
<option value=1>1</option>
<option value=2>2</option>
<option value=5>5</option>
</select>
<br />
<input convert-input ng-model="myNumber">
{{myNumber}}
<br />
<input convert-input ng-model="myNumber2">
{{myNumber2}}
<br />
<input convert-input ng-model="myNumber3">
{{myNumber3}}
<br />
</div>
</div>
var app = angular.module('myApp',[]);
app.controller('MyCtrl', function($scope) {
$scope.myNumber = 1;
$scope.myNumber2 = 2;
$scope.myNumber3 = 3;
$scope.Factor = 1;
});
app.directive("convertInput", function () {
return {
require: 'ngModel',
restrict: "A",
link: function (scope, element, attrs) {
scope.$watch('Factor', function () {
if(scope.Factor){
element[0].value = scope.Factor * element[0].value;
}
});
}
};
});
This simplified example shows the crux of my problem. When changing the value of the droplist it correctly updates the element's value on the page, but that does not get translated correctly back into the scope variable.
Any ideas how to either 1) tell the directive which scope variable needs to be updated or 2) force the model to update based on the inputs value?
Thanks!
One constraint I see with your snippet is that you need all uses of your convert-input directive to share Factor, which exists in an enclosing parent scope.
In such a case, one viable approach would be to use the attrs passed into the directive to extract the name of the ng-model binding, and then to update the corresponding binding via scope.
scope[attrs.ngModel] *= scope.Factor;
Here's a fork of your JSFiddle demonstrating this:
http://jsfiddle.net/m4hvre2y/
Another approach to having directives update an ng-model in a parent scope would be to declare two-way binding (e.g. scope: { ngModel: '='), but it isn't applicable in your case due to the constraint I mentioned above. If you did this, the isolated scope means you lose access to Factor unless it's specifically passed into the directive.

Change vm variable value after clicking anywhere apart from a specific element

When I click anywhere in the page apart from ul element (where countries are listed) and the suggestion-text input element (where I type country name), vm.suggested in controller should be set to null. As a result ul element will be closed automatically. How can I do this?
I've seen Click everywhere but here event and AngularJS dropdown directive hide when clicking outside where custom directive is discussed but I couldn't work out how to adapt it to my example.
Markup
<div>
<div id="suggestion-cover">
<input id="suggestion-text" type="text" ng-model="vm.countryName" ng-change="vm.countryNameChanged()">
<ul id="suggest" ng-if="vm.suggested">
<li ng-repeat="country in vm.suggested" ng-click="vm.select(country)">{{ country }}</li>
</ul>
</div>
<table class="table table-hover">
<tr>
<th>Teams</th>
</tr>
<tr ng-if="vm.teams">
<td><div ng-repeat="team in vm.teams">{{ team }}</div></td>
</tr>
</table>
<!-- There are many more elements here onwards -->
</div>
Controller
'use strict';
angular
.module('myApp')
.controller('readController', readController);
function readController() {
var vm = this;
vm.countryNameChanged = countryNameChanged;
vm.select = select;
vm.teams = {.....};
vm.countryName = null;
vm.suggested = null;
function countryNameChanged() {
// I have a logic here
}
function select(country) {
// I have a logic here
}
}
I solved the issue by calling controller function from within the directive so when user clicks outside (anywhere in the page) of the element, controller function gets triggered by directive.
View
<ul ng-if="vm.suggested" close-suggestion="vm.closeSuggestion()">
Controller
function closeSuggestion() {
vm.suggested = null;
}
Directive
angular.module('myApp').directive('closeSuggestion', [
'$document',
function (
$document
) {
return {
restrict: 'A',
scope: {
closeSuggestion: '&'
},
link: function (scope, element, attributes) {
$document.on('click', function (e) {
if (element !== e.target && !element[0].contains(e.target)) {
scope.$apply(function () {
scope.closeSuggestion();
});
}
});
}
}
}
]);
This is just an example but you can simply put ng-click on body that will reset your list to undefined.
Here's example:
http://plnkr.co/edit/iSw4Fqqg4VoUCSJ00tX4?p=preview
You will need on li elements:
$event.stopPropagation();
so your html:
<li ng-repeat="country in vm.suggested" ng-click="vm.select(country); $event.stopPropagation()">{{ country }}</li>
and your body tag:
<body ng-app="myWebApp" ng-controller="Controller01 as vm" ng-click="vm.suggested=undefined;">
UPDATE:
As I said it's only an example, you could potentially put it on body and then capture click there, and broadcast 'closeEvent' event throughout the app. You could then listen on your controller for that event - and close all. That would be one way to work around your problem, and I find it pretty decent solution.
Updated plunker showing communication between 2 controllers here:
http://plnkr.co/edit/iSw4Fqqg4VoUCSJ00tX4?p=preview
LAST UPDATE:
Ok, last try - create a directive or just a div doesn't really matter, and put it as an overlay when <li> elements are open, and on click close it down. Currently it's invisible - you can put some background color to visualize it.
Updated plunker:
http://plnkr.co/edit/iSw4Fqqg4VoUCSJ00tX4?p=preview
And finally totally different approach
After some giving it some thought I actually saw that we're looking at problem from the totally wrong perspective so final and in my opinion best solution for this problem would be to use ng-blur and put small timeout on function just enough so click is taken in case someone chose country:
on controller:
this.close = function () {
$timeout(()=>{
this.suggested = undefined;
}, 200);
}
on html:
<input id="suggestion-text" type="text" ng-model="vm.countryName" ng-change="vm.countryNameChanged()" ng-blur="vm.close()">
This way you won't have to do it jQuery way (your solution) which I was actually trying to avoid in all of my previous solutions.
Here is plnker: http://plnkr.co/edit/w5ETNCYsTHySyMW46WvO?p=preview

extending angularjs form controller

I have multiple child ng-form which are part of parent ng-form, I want to set the $submitted status of all the child forms, when I set the parent ng-form to $submitted status.
As of now no such method is available on the form-controller, checked here
Lets say, if I want to extend the current form controller to do this, how should I do that? how do I add a new method $setChildFormsToSubmittedState() ? of course I want to do it without disturbing/touching the angular code.
Is it possible? I think it should be given all the child forms hook into parent form using $addControl();.
No idea from where to start.
You can create a directive that appends a functionality to the form controller. Simply create a method that iterates over all the controls by checking if an item has a $$parentForm property that is equal to the form object it belongs to.
DEMO
Javascript
.directive('myForm', function() {
return {
require: 'form',
link: function(scope, elem, attr, form) {
form.__setSubmitted = function() {
setSubmitted(form);
};
function setSubmitted(form) {
form.$submitted = true;
angular.forEach(form, function(item) {
if(item && item.$$parentForm === form) {
setSubmitted(item);
}
});
}
}
};
});
HTML
<form name="myForm" my-form ng-submit="myForm.__setSubmitted()">
<ng-form name="mySubForm1">
<input type="text" ng-model="data.something1" name="something">
{{mySubForm1.$submitted}}
</ng-form>
<br>
<ng-form name="mySubForm2">
<input type="text" ng-model="data.something2" name="something">
{{mySubForm2.$submitted}}
</ng-form>
<br>
<button type="submit">Button</button>
{{myForm.$submitted}}
</form>

Creating a Reusable Component in AngularJS

I am new to Stackoverflow. I'm also new to AngularJS. I apologize if I'm not using this correctly. Still, I'm trying to create a UI control with AngularJS. My UI control will look like this:
+---------------------+
| |
+---------------------+
Yup. A textbox, which has special features. I plan on putting it on pages with buttons. My thought is as a developer, I would want to type something like this:
<ul class="list-inline" ng-controller="entryController">
<li><my-text-box data="enteredData" /></li>
<li><button class="btn btn-info" ng-click="enterClick();">Enter</button></li>
</ul>
Please note, I do not want buttons in my control. I also want the enteredData to be available on the scope associated with child controls. In other words, when enterClick is called, I want to be able to read the value via $scope.enteredData. In an attempt to create my-text-box, I've built the following directive:
myApp.directive('myTextBox', [function() {
return {
restrict:'E',
scope: {
entered: '='
},
templateUrl: '/templates/myTextBox.html'
};
}]);
myApp.controller('myTextBoxController', ['$scope', function($scope) {
$scope.doSomething = function($item) {
$scope.entered = $item.name;
// Need to somehow call to parent scope here.
};
}]);
myTextBox.html
<div ng-controller="myTextBoxController">
<input type="text" class="form-control" ng-model="query" placeholder="Please enter..." />
</div>
entryController.js
myApp.controller('entryController', ['$scope', function($scope) {
$scope.enteredData = '';
$scope.enterClick = function() {
alert($scope.enteredData);
};
});
Right now, I have two issues.
When enterClick in entryController is called, $scope.enteredData is empty.
When doSomething in myTextBoxController is called, I do not know how to communicate to entryController that something happened.
I feel like I've setup my directive correctly. I'm not sure what I'm doing wrong. Can someone please point me in the correct direction.
Three suggestions for you.
1) You really shouldn't create a directive with a template that references a controller defined elsewhere. It makes the directive impossible to test in isolation and is generally unclear. If you need to pass data into a directive from a parent scope use the isolate scope object on your directive to bind to that data (Note how the directive template doesn't have a controller) http://jsfiddle.net/p4ztunko/
myApp.directive('myTextBox', [function () {
return {
restrict: 'E',
scope: {
data: '='
},
template: '<input type="text" class="form-control" ng-model="data" placeholder="Please enter..." />'
};
}]);
myApp.controller('entryController', ['$scope', function ($scope) {
$scope.enteredData = 'Stuff';
$scope.enterClick = function () {
alert($scope.enteredData);
};
}]);
<div>
<ul class="list-inline" ng-controller="entryController">
<li>{{enteredData}}
<my-text-box data="enteredData" />
</li>
<li>
<button class="btn btn-info" ng-click="enterClick();">Enter</button>
</li>
</ul>
</div>
2) Don't obfuscate HTML when you don't need to. One of the goals of angular is to make the markup more readable, not replace standard elements with random custom elements. E.g. If you want to watch the value of the input and take action depending on what it is you could do that in the linking function (Note: still not referencing an external controller) http://jsfiddle.net/Lkz8c5jo/
myApp.directive('myTextBox', function () {
return {
restrict: 'A',
link: function(scope, element, attrs){
function doSomething (val) {
alert('you typed ' + val);
}
scope.$watch(attrs.ngModel, function (val) {
if(val == 'hello'){
doSomething(val);
}
});
}
};
});
myApp.controller('entryController', ['$scope', function ($scope) {
$scope.enteredData = 'Stuff';
$scope.enterClick = function (data) {
alert('You clicked ' + data);
};
}]);
<div>
<ul class="list-inline" ng-controller="entryController">
<li>{{enteredData}}
<input type="text" ng-model="enteredData" my-text-box />
</li>
<li>
<button class="btn btn-info" ng-click="enterClick(enteredData)">Enter</button>
</li>
</ul>
</div>
3) Pass data into controller functions from the UI instead of referencing the $scope object in the function like in ng-click="enterClick(enteredData)" It makes testing easier because you remove the $scope dependency from that method
$scope.enteredData is empty because you're not using the correct scope. The $scope in entryController is not the same $scope as in myTextBoxController. You need to specify one of these controllers on your directive, and then use that to reference the proper scope.
It seems like you should move the enterClick and corresponding button into your directive template. Then move the enter click into your text box controller and you will be able to reference $scope.enteredData.
You can notify a parent scope of a change by using $emit. (This is in reference to your comment "// Need to somehow call to parent scope here.")
Furthermore, you may have an issue of not using the proper variable. In myTextBox directive, you declare $scope.entered, yet you are effectively setting $scope.data equal to the value of enteredData in the html.

Ng-controller on same element as ng-repeat - no two-way-data-binding

I can't get two-way-data-binding to work in an Angular js ng-repeat.
I have an ng-controller specified on the same element that has the ng-repeat specified -
I just learnt that by doing this, I can get a hold of each item that is being iterated over by ng-repeat. Here is the HTML:
<div ng-controller="OtherController">
<div id="hoverMe" ng-controller="ItemController" ng-mouseenter="addCaption()"
ng-mouseleave="saveCaption()" ng-repeat="image in images">
<div class="imgMarker" style="{{image.css}}">
<div ng-show="image.captionExists" class="carousel-caption">
<p class="lead" contenteditable="true">{{image.caption}}</p>
</div>
</div>
</div>
</div>
And here is the ItemController:
function ItemController($scope){
$scope.addCaption = function(){
if($scope.image.captionExists === false){
$scope.image.captionExists = true;
}
}
$scope.saveCaption = function(){
console.log($scope.image.caption);
}
}
And the OtherController:
function OtherController($scope){
$scope.images = ..
}
When I hover the mouse over the #hoverMe-div - the caption-div is added correctly. But when I input some text in the paragraph and then move the mouse away from the #hoveMe-div, the $scope.image-variables caption value is not updated in the saveCaption-method. I understand I'm missing something. But what is it?
You don't need a ng-controller specified on the same element that has the ng-repeat to be able to get each item.
You can get the item like this:
<div ng-repeat="image in images" ng-mouseenter="addCaption(image)" ng-mouseleave="saveCaption(image)" class="image">
And in your controller code:
$scope.addCaption = function (image) {
if(!image.captionExists){
image.captionExists = true;
}
};
To get contenteditable to work you need to use ng-model and a directive that updates the model correctly.
Here is a simple example based on the documentation:
app.directive('contenteditable', function() {
return {
require: 'ngModel',
link: function(scope, element, attrs, controller) {
element.on('blur', function() {
scope.$apply(function() {
controller.$setViewValue(element.html());
});
});
controller.$render = function(value) {
element.html(value);
};
}
};
});
Note that the directive probably needs more logic to be able to handle for example line breaks.
Here is a working Plunker: http://plnkr.co/edit/0L3NKS?p=preview
I assume you are editing the content in p contenteditable and are expecting that the model image.caption is update. To make it work you need to setup 2 way binding.
2 way binding is available for element that support ng-model or else data needs to be synced manually. Check the ngModelController documentation and the sample available there. It should serve your purpose.

Resources