Link scopes of element and attribute directives - angularjs

I have element directive, let's say "card". It has value. Also i have attribute directive, let's call it "clickable", that should bind to existing directive scope and add +1 to it's value.
The problem is that scopes of element and attribute directives are different and i need to do scope.$apply() after click.
el.on('click', (event) => {
ctrl.value++;
console.log('clicked', ctrl.value);
// i need scope.$apply(); here to update <card>'s value
});
I want to attribute directive to not have it's own scope, relying on parent's instead.
try to click on those inputs: http://codepen.io/Fen1kz/pen/GZrgBx?editors=0010

Related

Setting attrs dynamically for ui-bootstrap tooltip / popover

I'm trying to programmatically toggle tooltips (like mentioned here: https://stackoverflow.com/a/23377441) and got it fully functional except for one issue. In order for it to work I must have tooltip-trigger and tooltip attributes hardcoded as follows:
<input type="text" tooltip-trigger="show" tooltip="" field1>
In my working directive, I'm able to change the tooltip attributes and trigger a tooltip, but if I try to leave those two attributes out and attempt to set them dynamically, ui-bootstrap doesn't pick them up and no tooltip gets displayed.
html
<input type="text" field2>
js
myApp.directive('field2', function($timeout) {
return {
scope: true,
restrict: 'A',
link: function(scope, element, attrs) {
scope.$watch('errors', function() {
var id = "field2";
if (scope.errors[id]) {
$timeout(function(){
// these attrs dont take effect...
attrs.$set('tooltip-trigger', 'show');
attrs.$set('tooltip-placement', 'top');
attrs.$set('tooltip', scope.errors[id]);
element.triggerHandler('show');
});
element.bind("click", function(e){
element.triggerHandler('hide');
});
}
});
},
};
});
I'd prefer not to hardcode these attributes in the html, so how do I go about setting these attributes dynamically and get ui-bootstrap to pick them up?
Here is a plunker that has a working (field1) and non working (field2) directive: http://plnkr.co/edit/mP0JD8KHt4ZR3n0vF46e
You can do this, but you have to change a couple of things in your approach.
Plunker Demo
Directive
app.directive("errorTooltip", function($compile, $interpolate, $timeout) {
return {
scope: true,
link: function($scope, $element, $attrs) {
var errorObj = $attrs.errorTooltip;
var inputName = $attrs.name;
var startSym = $interpolate.startSymbol();
var endSym = $interpolate.endSymbol();
var content = startSym+errorObj+'.'+inputName+endSym;
$element.attr('tooltip-trigger', 'show');
$element.attr('tooltip-placement', 'top');
$element.attr('tooltip', content);
$element.removeAttr('error-tooltip');
$compile($element)($scope);
$scope.$watch(errorObj, function() {
$timeout(function(){
$element.triggerHandler('show');
});
}, true);
$element.on('click', function(e){
$element.triggerHandler('hide');
});
}
};
});
The super long detailed explanation:
Okay, so from the top: #Travis is correct in that you can't just inject the attributes after the fact. The tooltip attributes that you place on the element are directives themselves, so the tooltip needs to be compiled when it's appended. That's not a problem, you can use the $compile service to do this, but you need to do it just once for the element.
Also, you need to bind the tooltip text (the value given to the tooltip attribute) to an expression. I do that by passing in a concatenated value of $interpolate.startSymbol() + the scope value that you want to display (in the demo it is the fieldx property of the errors object) + the $interpolate.endSymbol(). This basically evaluates to something like: {{error.field1}}. I use the $interpolate service start and end symbols because it just makes the directive more componentized, so you can use it on other projects where you might have multiple frameworks and be using something other than double curly-braces for your Angular expressions. It's not necessary though and you could instead do: '{{'+errorObj+'.'+inputName+'}}'. In this case, you don't have to add the $interpolate service as a dependency.
As you can see, to make the directive truly reuseable, rather than hard-coding the error field, I set the value given to the directive attribute to the name of the object that will be watched and use the input name value as the object property.
The chief thing you need to remember is that before you compile, you have to remove the error-tooltip attribute from the element because if you don't you'll wind up in an infinite loop and crash hard! Basically, the compile service is going to take the element that the directive is attached to and compile it with all of the attributes your directive added, if you leave the error-tooltip attribute, it's going to try and recompile that directive too.
Lastly, you can take advantage of the fact that the tooltip will not display if its text value is empty or undefined (see line 192). That means you only have to watch the errors object not the individual property on the error associated with the tooltip. Make sure that you set the equality operator on the $watch to true, so that it will trigger if any of the object's properties are changed:
$scope.$watch('errors', function() {
$timeout(function(){
$element.triggerHandler('show');
});
}, true); //<--equality operator
In the demo, you can see the effect of changing the errors object. If you click the Set Errors button the tooltip will display for both the first and second inputs. Click the Change Error Values and the tooltip displays for the first and third inputs.
TL;DR:
Add the directive to your markup by setting the value to be the name of the object that will contain all of the errors. Make sure to give the field a name attribute that corresponds to the property key name in the object that will contain the errors for that input, such as:
<input class="form-control" ng-model="demo.field1" name="field1" error-tooltip="errors" />

How to include data/scope from controller in a dynamically added directive?

I'm trying to figure out how to include scope with a directive that I add to the dom on a click event in a controller.
Step 1. On a click event, I call a function in my controller that adds a directive like this
$scope.addMyDirective = function(e, instanceOfAnObjectPassedInClickEvent){
$(e.currentTarget).append($compile("<my-directive mydata='instanceOfAnObjectPassedInClickEvent'/>")($scope));
}
//I'm trying to take the `instanceOfAnObjectPassedInClickEvent` and make it available in the directive through `mydata`
The above, part of which I got from this SO answer, successfully adds the directive (and the directive has a template that gets added to the dom), however, inside the directive, I'm not able to access any of the scope data mydata it says it's undefined.
My directive
app.directive('myDirective', function(){
return {
restrict: 'AE',
scope: {
mydata: '='
//also doesn't work if I do mydata: '#'
},
template: '<div class="blah">yippee</div>',
link: function(scope,elem,attrs) {
console.log(scope) //inspecting scope shows that mydata is undefined
}
}
}
Update
I changed the name of datafromclickedscope in the OP to make it more clear. In the controller action addMyDirective (see above) instanceOfAnObjectPassedInClickEvent is an instance of an object passed into the controller method on a click event that I try to pass into the directive as mydata='instanceOfAnObjectPassedInClickEvent'. However, even if I change = to # in the directive and I try to access scope.mydata in the link function of the directive, it just shows a string like this "instanceOfAnObjectPassedInClickEvent", not the actual object data that is available to me in my method that handles the click event
When you use mydata='instanceOfAnObjectPassedInClickEvent' in a template you need instanceOfAnObjectPassedInClickEvent to defined in $scope. So before compiling you should assign a variable in $scope. I will rename this variable in code below, so that same names would not confuse you and it would be clear that a formal parameter of a function cannot be visible in a template.
$scope.addMyDirective = function(e, instanceOfAnObjectPassedInClickEvent){
$scope.myEvent = instanceOfAnObjectPassedInClickEvent;
$(e.currentTarget).append($compile("<my-directive mydata='myEvent'/>")($scope));
}
EDIT: slightly adapted jsfiddle not using JQuery no manipulate DOM

Two-way binding within nested directives and ui-select

Problem I have been working on:
The original problem (which only addressed one-way binding) can be found here:
Unable to pass/update ngModel from controller to directive
I have managed to get the one-way binding from inside the directive out to the view working. But if I want push in a selection when the page loads, for example, I cannot get the two-way binding to function. I can see that the ng-model inside the first directive is getting the data, but I tried various scope settings with the child directive and either it breaks it or it does the same thing as before - nothing.
I have a basic $watch function set up, so that when I push a simple object into the binding that is attached to ng-model in the view, the watcher assigns the $viewValue to the directive's scope object. It does this, and the directive responds only by having any existing selection wiped out, even though I can clearly see the objects inside ng-model binding assigned to ui-select.
Here is the watch function:
scope.$watch(function() {
return ngModel.$viewValue;
}, function(newVal) {
console.log(newVal, scope.options);
scope.options.selected = newVal;
});
I use this function to update the view whenever we interact with the ui-select (which works fine):
scope.changer = function() {
ngModel.$setViewValue(scope.options.selected);
console.log(scope.options.selected);
};
A Plunker for tinkering
So the expected behavior is:
selections from the ui-select should be displayed in the select, and also get passed to the view
by clicking the 'Change' button, data should be displayed in the view, and get passed to the ui-select
The situation was that I had a directive with ui-select inside it. The way I was able to get data from the main controller scope through the directive scope and down to ui-select's scope was by putting a watch function on ngModel.$viewValue in the link function of the directive.
I then assigned that new value, whenever there was a change, to an object on that directive's scope, to hold it for me. Then I set a watch function in the link function of ui-select to watch scope.$parent.myVal so that if anything was ever pushed to that variable from the main controller, ui-select would see it. When that happened, I would assign that new value to $select.selected which is the container for selected items in the ui-select directive.
It should be noted that ui-select uses scope: true in it's directive, and thus becomes a child of whatever scope it is instantiated in, which allows you to access the scope of it's parent with $parent.
Watch function for the directive:
scope.$watch(function() {
return scope.$parent.myVar;
}, function(newVal) {
$select.selected = newVal;
})
Watch function to be added to ui-select:
scope.$watch(function() {
return ngModel.$viewValue;
}, function(newVal) {
scope.myVar = newVal;
})
Plunker Demo

Updating Scope from a Directive

I have defined a scope in my directive.
scope.selection = 3
When I apply the directive to an element,
<div myDirective></div>
its template would print the scope var correctly.
<span>{{selection}}</span> //prints: 3
but when I update the scope variable in the directive it does not update in the view.
element.bind("keydown", function(event){
if(event.which === 38){ //up arrow
scope.selection--;
console.log(scope.selection); //logs the updated value but view stays the same
}
}
Can I bind scopes from a directive?
EDIT:
If I view a different browser tab then go back to the app the view is then updated.

AngularJS : ng-repeat list not updating - directives

I am new to angular and I have the following code:
Here is my html:
<drawer title="'Add a new Item'"
visible="showAddItemDrawer"
on-close="showAddApplicationItem = false">
<!-- items list refresh does not work from inside the drawer. -->
<add-item></add-item>
</drawer>
<!--list refresh works if i place the code out here.
<add-item></add-item> -->
<div ng-repeat="item in itemList">
<item-list></item-list>
</div>
Here is my directive.js:
directive("additem", function () {
return {
restrict:"E",
transclude:true,
controller:"ItemsCtrl",
templateUrl:"partials/add-item.html" //contains a form to add item
};
})
.directive("itemlist", function () {
return {
restrict:"E",
transclude:true,
templateUrl:"partials/item-list.html" //contains code to display the items in a list
};
})
I have a form in add-item.html to add an item. The form shows up when you click on add button(like an accordion). I call the push() to add a new item to the scope.
The list update works if i place the directive call outside the drawer..
If i place it inside the drawer,the scope is not getting updated until i hit refresh.
Can anyone point me what I am doing wrong with the directives? Thanks a lot!
Edit: Added additional code :
In the form to add an item:
<button type="submit"
ng-click="addItemService()">
Add Item
</button>
addItemService() code:
$scope.addItemService = function () {
var data = {
"name": $scope.itemName,
};
ItemService.addItem(data, $scope.listgroupid)
.success(function (result) {
$scope.itemName = "";
viewList(); //The function that sets the scope of the list
})
.error(function () {
});
};
viewlist() code:
var viewList = function () {
ListService.getList($scope.listgroupid)
.success(function (result) {
$scope.itemList = result;
//In the angular inspector, I am able to see the new item in the variable result
})
.error(function () {
});
};
EDIT: function scope is different from variable scope, use $scope.$parent.itemList when referencing new variable value
i had an issue where the directive scope (while debugging) and the batarang inspected scope (when selecting an element in the dev tools and typing $scope.varname in the console) were not matching after a successfully resolved promise.
the promise updates a list for a ng-repeat which contains a directive.
the debugged scope held the values from the last data before the promise was loading new data.
so i added a timeout around the data-loaded event broadcast which pushes the broadcast into the next digest cycle and angular gets informed about the update.
$scope.$apply most often throws the "apply already in progress" error.
the timeout solves this a little more softly...
$timeout( function () {
// broadcast down to all directive children
$scope.$broadcast('dataLoaded');
}, 1);
need to see the code wherein you are calling push for item.
In the absence of that I suspect this is scope issue. Remember that ngRepeat and drawer reside in difference scopes by default and until you have explicitly made them share certain variables , changes inside drawer won't be reflected in ngRepeat. For more on scopes inside directives read http://docs.angularjs.org/api/ng.$compile.
The 'isolate' scope takes an object hash which defines a set of local
scope properties derived from the parent scope. These local properties
are useful for aliasing values for templates. Locals definition is a
hash of local scope property to its source:
# or #attr - bind a local scope property to the value of DOM
attribute. The result is always a string since DOM attributes are
strings. If no attr name is specified then the attribute name is
assumed to be the same as the local name. Given and widget definition of scope: { localName:'#myAttr' },
then widget scope property localName will reflect the interpolated
value of hello {{name}}. As the name attribute changes so will the
localName property on the widget scope. The name is read from the
parent scope (not component scope).
= or =attr - set up bi-directional binding between a local scope property and the parent scope property of name defined via the value
of the attr attribute. If no attr name is specified then the attribute
name is assumed to be the same as the local name. Given and widget definition of scope: {
localModel:'=myAttr' }, then widget scope property localModel will
reflect the value of parentModel on the parent scope. Any changes to
parentModel will be reflected in localModel and any changes in
localModel will reflect in parentModel. If the parent scope property
doesn't exist, it will throw a NON_ASSIGNABLE_MODEL_EXPRESSION
exception. You can avoid this behavior using =? or =?attr in order to
flag the property as optional.
& or &attr - provides a way to execute an expression in the context of
the parent scope. If no attr name is specified then the attribute name
is assumed to be the same as the local name. Given and widget definition of scope: {
localFn:'&myAttr' }, then isolate scope property localFn will point to
a function wrapper for the count = count + value expression. Often
it's desirable to pass data from the isolated scope via an expression
and to the parent scope, this can be done by passing a map of local
variable names and values into the expression wrapper fn. For example,
if the expression is increment(amount) then we can specify the amount
value by calling the localFn as localFn({amount: 22}).
You can either use a $rootScope.$broadcast() to fire an event that your directive would capture, and then let the directive update the list with ng-repeat or you could call a $scope.$apply() with the updation wrapped in it within the controller to indicate angular that he list is updated, but when using $scope.$apply() be careful, and make sure it that it is delayed/deferred until the next digest cycle.
A library called Underscore.js lets you delay $apply until the next digest cycle.

Resources