AngularJS: set target (similar to focus) to any $scope property - javascript

I have a layout with multiple elements which are able to gain target. I need to target only one element at the time.
Is it possible to define a function on the $scope that receives an object from the model (for example a line item belonging to an invoice) and tell Angular to add a css class wherever the view of this model is?
If I use the ng-class directive, it would force me to add ng-class to all "targetable" elements in the html and each element should know if it is the current target or not. I don't want to add an isTarget() function to each possible element because it will dirty the model.
Example:
This is the html:
<p>{{document.shipFrom}}</p>
<p>{{document.shipTo}}</p>
<ul>
<li ng-repeat="item in document.items">{{item.description}}</li>
</ul>
And this is the controller:
angular.module('myApp').controller('DocumentCtrl', function($scope){
$scope.document = {
shipFrom: 'Origin',
shipTo: 'Destination',
items: [
{description:'item1'},
{description:'item2'}
]
};
})
Is there a way to define $scope.setTarget($scope.document.items[0]) so that it adds a class "on-target" to the element? Note that all the document properties (the items and the shipFrom/To) can gain target.
Edit: Solved
I found a way to get a model's attribute value in my directive's linking function. If I use the $parse service then I can evaluate the model's property attached to the directive simply by instantiating a getter function:
link: function postLink ($scope, $iElement, $iAttrs) {
var valueGetter = $parse($iAttrs.ngModel);
//Then, I can subscribe the directive to a custom event:
$scope.$on('set.target', function (event, elem) {
$iElement.removeClass('on-target alert-info');
//Now it gets the actual value of the model related to the directive
var value = valueGetter($scope);
//If both the model and the event's value are the same, then focus the element.
if (value == elem) {
$iElement.addClass('on-target alert-info');
$scope.setTarget(valueName, elem);
$scope.$apply();
}
return;
});
}//end link function
When I need something to gain target from the controller, then I just do $scope.$broadcast('set.target', $scope.document.shipFrom)

HTML Part :
<p>{{document.shipFrom}}</p>
<p>{{document.shipTo}}</p>
<ul>
<li ng-repeat="item in document.items" ng-click="setTarget(item.description)" ng-class="{'active' : selectedTarget == item.description}">{{item.description}}</li>
</ul>
Controller:
$scope.document = {
shipFrom: 'Origin',
shipTo: 'Destination',
items: [
{description:'item1'},
{description:'item2'}
]
};
$scope.selectedTarget = '';
$scope.setTarget = function(data) {
$scope.selectedTarget = data;
};
DEMO

If you don't want to add an isTarget() function to each possible item, you could add isTarget method on document.
isTarget: function(item){
return this.target === item;
}
and change the html to
<li ng-repeat="item in document.items" ng-class="{'on-target': document.isTarget(item)}">{{item.description}}</li>

Related

AngularJS: Add inline custom code using a directive

Here's the scenario.
In the app, you can add inline custom code (HTML attributes ex. style="", onclick="alert('Test')") in an element (ex. input texts, divs). The custom code is binded to the main model and loaded to the element using a custom directive I've created. I'm doing this to control dynamically generated fields that I want to hide and show based on different inputs.
This is my custom directive that loads inline attributes on the element:
app.directive('addCustomHtml', function() {
return {
scope: {
customHtml: "="
},
link: function(scope, element, attributes){
scope.$watch('customHtml', function(newVal, oldVal) {
if (newVal) {
var attrs = newVal.split('\n');
for (var i = 0; i < attrs.length; i++) {
var result = attrs[i].split('=');
var attr = result.splice(0,1);
attr.push(result.join('='));
if (attr[1]) {
element.attr(attr[0], attr[1].replace(/^"(.*)"$/, '$1'));
}
}
} else {
if (oldVal) {
var attrs = oldVal.split('\n');
for (var i = 0; i < attrs.length; i++) {
var attr = attrs[i].split('=');
if (attr[0]) {
element.removeAttr(attr[0]);
}
}
}
}
})
}
}
});
It is binded to the element like this:
<input type="checkbox" add-custom-html custom-html="checkbox1.customHtml">Yes
To see it in action, you can check the plunkr here: https://plnkr.co/edit/xjjMRPY3aE8IVLIeRZMp?p=preview
Now my problem is, when I try to add AngularJS directives (ex. ng-show, ng-if) using my custom directive, AngularJS doesn't seem to recognize them and the model scope I'm passing inside.
Another problem is when I try to add vanilla Javascript event functions (ex. onclick="", onchange=""), it does work but sometimes AngularJS does not read them especially when the element has an ng-change, ng-click attributes.
Again, I am doing this approach on the app because I have generic fields and I want to control some of them by adding this so called "custom codes".
Any help would be highly appreciated!!
If you want to add HTML code and compile it within current $scope, you should use the $compile service:
let someVar = $compile(yourHTML)($scope);
// you can now append someVar to any element and
// angular specific markup will work as expected
Being a service, you'll need to inject it into current controller (or pre/post link function) to be able to use it.

Scope value not updating when service variable changes

I've a component (parms-bar.component.js) which is supposed to update when I click on the button (box.component.js), but it's not happening. I'm trying to let them communicate using the "selected" variable in the service (main.service.js).
When you launch the app "test node" is displayed by my "parms-bar" component. On the button click it should change to "Box", but it's not.
Here you can see a live example
I've also read the answer to this question which says that I'm probably replacing the memory location that my selected is associated to every time I assign it a new value while the scope is stuck pointing to the old location.
But even trying to modify only its name property, rather than assign a new object, I got no joy.
You're having object reference issues. The easiest way to fix this is to change your service to return a setter and getter.
main.service.js
angular.module('app').service('mainService', function(){
var selected = {name: "test node"};
var service = {
get selected(){
return selected;
},
set selected(value){
selected = value;
}
}
return service;
});
Now you can change your other modules to get and set this object directly.
box.component.js
angular.module('box').component('box', {
templateUrl: 'box.template.html',
controller: function boxController(mainService){
this.addBox = function () {
var box = mainService.selected;
//Set custom properties
box.name = "Box";
//Set as selected
//mainService.selected = box; <-- This is not needed
}
}
});
parms-bar.component.js
angular.module('parms-bar').component('parmsbar', {
templateUrl: 'parms-bar.template.html',
controller: function parmsController(mainService){
this.selected = mainService.selected;
}
});
And then use the object in your parms-bar.template.html
<div id="parms-bar">
<a>
{{$ctrl.selected.name}}
</a>
</div>

AngularJS doesn't parse dynamically loaded data from directive

I am trying to create N number of Select controls dynamically from directive based on array that is passed in from the attribute (where N is the length of the array).
Structure of an object of the array is as such:
selectDescription = {
array: arrayObject, //ng-options, a string as 'item as item.name for item in selectArray[0]'
change: methodName, //ng-change, actionname
level: levelNumber //level number
}
So the number of select controls inside span tag depends on the number of selectDescription(s) that I get from the attribute.
First select control is rendered successfully. Subsequent select controls should have been rendered on select of an option from previous rendered select controls. But it's not happening in my case. Although I am successfully appending angular elements in the current inputEl(on select of an option), it is not being rendered in the UI. I guess I am missing something very crucial.
On change of selectDescriptions, a flipped attribute is set, through which I am able to call scope.$editable.render() from link, which in turn runs render function to re-append elements after clearing the previous HTML inside span.
My Code:
app.directive('editableLocation', function(editableDirectiveFactory) {
var createElement = function(el, index){
var newElement = angular.element("<select/>");
newElement.attr('ng-model','$data'+index);
newElement.attr('ng-options',el.array);
newElement.attr('ng-change',el.change.substring(0, el.change.length - 1)+", $data"+index+")");
return newElement;
}
var descriptions = [] ;
var dir = editableDirectiveFactory({
directiveName: 'editableLocation',
inputTpl: '<span></span>',
render: function() {
this.parent.render.call(this);
this.inputEl.html("");
for(var i = 0 ; i < descriptions.length ; i ++){
this.inputEl.append(createElement(descriptions[i], i));
}
}
});
var linkOrg = dir.link;
dir.link = function(scope, el, attrs, ctrl) {
console.log(el);
descriptions = scope.$eval(attrs.description);
scope.$watch('flipped',function(newValue,oldValue){
if(newValue != 0){
scope.$editable.render();
}
});
return linkOrg(scope, el, attrs, ctrl);
};
return dir;
});
Since you are adding the dynamic HTML content in the link function of the Angular directive, Angular will not auto compile/parse it. You need to do it manually using $compile directive. So after you appended all the HTML, do the following (inject $compile in your code)
$compile(element.contents())(scope);
Where element is your any parent element where you are generating dynamic HTML and scope is the scope of the directive or any other scope which you want it to be attached to the dynamic HTML.
Looking at xeditable.js I have found that xeditable renders the UI by calling a show method defined in its editableController.
It is defined as:
self.show = function() {
self.setLocalValue();
self.render(); //calls 'render' function of 'editableDirectiveFactory'; that' where my custom UI lies
$element.after(self.editorEl); //attaches newelement(especially whole <form/> element)
$compile(self.editorEl)($scope); //renders whole UI(and also the newly attached one)
self.addListeners();
$element.addClass('editable-hide');
return self.onshow();
};
So what I felt is, I need to call this show method from my link function, which receives the controller.
This is what I did:
dir.link = function (scope, el, attrs, ctrl) {
$element = el;
scope.$watch(attrs.flipped, function (newValue, oldValue) {
//re-render element if flipped is changed; denoting description of select controls have been altered
if (newValue != 0) {
ctrl[0].show(); //this will call render function and also $compile({{content/html/element}})(scope)
}
});
return linkOrg(scope, el, attrs, ctrl);
};
And also you need to hide the previous <form/> element(which contains previous rendered UI), so that only one forms get displayed.
This is how I hid that previous <form/> element in render' function ofeditableDirectiveFactory`:
var prevForm = $element[0].nextElementSibling; //hide previous form element which would already contain previous select
if (prevForm)
prevForm.classList.add('editable-hide');
That solved my problem at least :)

Angularjs watch for change in parent scope

I'm writing a directive and I need to watch the parent scope for a change. Not sure if I'm doing this the preferred way, but its not working with the following code:
scope.$watch(scope.$parent.data.overlaytype,function() {
console.log("Change Detected...");
})
This it logged on window load, but never again, even when overlaytype is changed.
How can I watch overlaytype for a change?
Edit: here is the entire Directive. Not entirely sure why I'm getting a child scope
/* Center overlays vertically directive */
aw.directive('center',function($window){
return {
restrict : "A",
link : function(scope,elem,attrs){
var resize = function() {
var winHeight = $window.innerHeight - 90,
overlayHeight = elem[0].offsetHeight,
diff = (winHeight - overlayHeight) / 2;
elem.css('top',diff+"px");
};
var watchForChange = function() {
return scope.$parent.data.overlaytype;
}
scope.$watch(watchForChange,function() {
$window.setTimeout(function() {
resize();
}, 1);
})
angular.element($window).bind('resize',function(e){
console.log(scope.$parent.data.overlaytype)
resize();
});
}
};
});
If you want to watch a property of a parent scope you can use $watch method from the parent scope.
//intead of $scope.$watch(...)
$scope.$parent.$watch('property', function(value){/* ... */});
EDIT 2016:
The above should work just fine, but it's not really a clean design. Try to use a directive or a component instead and declare its dependencies as bindings. This should lead to better performance and cleaner design.
I would suggest you to use the $broadcast between controller to perform this, which seems to be more the angular way of communication between parent/child controllers
The concept is simple, you watch the value in the parent controller, then, when a modification occurs, you can broadcast it and catch it in the child controller
Here's a fiddle demonstrating it : http://jsfiddle.net/DotDotDot/f733J/
The part in the parent controller looks like that :
$scope.$watch('overlaytype', function(newVal, oldVal){
if(newVal!=oldVal)
$scope.$broadcast('overlaychange',{"val":newVal})
});
and in the child controller :
$scope.$on('overlaychange', function(event, args){
console.log("change detected")
//any other action can be perfomed here
});
Good point with this solution, if you want to watch the modification in another child controller, you can just catch the same event
Have fun
Edit : I didn't see you last edit, but my solution works also for the directive, I updated the previous fiddle ( http://jsfiddle.net/DotDotDot/f733J/1/ )
I modified your directive to force it to create a child scope and create a controller :
directive('center',function($window){
return {
restrict : "A",
scope:true,
controller:function($scope){
$scope.overlayChanged={"isChanged":"No","value":""};
$scope.$on('overlaychange', function(event, args){
console.log("change detected")
//whatever you need to do
});
},
link : function(scope,elem,attrs){
var resize = function() {
var winHeight = $window.innerHeight - 90,
overlayHeight = elem[0].offsetHeight,
diff = (winHeight - overlayHeight) / 2;
elem.css('top',diff+"px");
};
angular.element($window).bind('resize',function(e){
console.log(scope.$parent.data.overlaytype)
resize();
});
}
};
});
You should have the data property on your child scope, scopes use prototypal inheritance between parent and child scopes.
Also, the first argument the $watch method expects is an expression or a function to evaluate and not a value from a variable., So you should send that instead.
If you're looking for watching a parent scope variable inside a child scope, you can add true as second argument on your $watch. This will trigger your watch every time your object is modified
$scope.$watch("searchContext", function (ctx) {
...
}, true);
Alright that took me a while here's my two cents, I do like the event option too though:
Updated fiddle
http://jsfiddle.net/enU5S/1/
The HTML
<div ng-app="myApp" ng-controller="MyCtrl">
<input type="text" model="model.someProperty"/>
<div awesome-sauce some-data="model.someProperty"></div>
</div>
The JS
angular.module("myApp", []).directive('awesomeSauce',function($window){
return {
restrict : "A",
template: "<div>Ch-ch-ch-changes: {{count}} {{someData}}</div>",
scope: {someData:"="},
link : function(scope,elem,attrs){
scope.count=0;
scope.$watch("someData",function() {
scope.count++;
})
}
};
}).controller("MyCtrl", function($scope){
$scope.model = {someProperty: "something here");
});
What I'm showing here is you can have a variable that has two way binding from the child and the parent but doesn't require that the child reach up to it's parent to get a property. The tendency to reach up for things can get crazy if you add a new parent above the directive.
If you type in the box it will update the model on the controller, this in turn is bound to the property on the directive so it will update in the directive. Within the directives link function it has a watch setup so anytime the scope variable changes it increments a counter.
See more on isolate scope and the differences between using = # or & here: http://www.egghead.io/

Change div content dynamically on other div click (AngularJS)

So, I'm new to AngularJS and I'm trying to change a div content after another is clicked (this one holds a div with the content that I want to put on the first one).
HTML
<div ng-controller="dCtrl">
<ul ng-repeat="product in products">
<li change>
{{product.name}}
<div class="hide">{{product.description}}</div>
</li>
</ul>
</div>
<div id="test"></div>
Javascript
var app = angular.module("dt", []);
app.directive("change", function() {
return function(scope, element) {
element.bind("click", function() {
var message = element.children("div").text();
console.log("breakpoint");
angular.bind("#test", function() {
this.text(message);
});
})
}
})
app.controller("dCtrl", function($scope) {
$scope.products = [
{ "name" : "Escova XPTO", "description": "Lava tudo num instante"},
{ "name" : "Pasta de Dentes YMZ", "description": "Dentifrico do camandro"}
];
})
I know that I could just say:
$("#test").html(message);
But I'm still confused about mixing jQuery and AngularJS, I dont know if that is a correct way of doing it
Thanks
Setup ng-click:
ngClick is for doing things such as the scary jQuery-esque stuff you have going on in your change directive. Place ng-click in your clickable div's attributes and pass in a method that changes the $scope variable accepted by...
ngShow and ngHide.
When true these directives, as the name states, show or hide the associated html object. You can pass in $scope variables determine the boolean value. When the $scope updates these methods automatically update the DOM to show/hide the element.

Categories

Resources