angularjs scope not accessable in directive - angularjs

I am trying to load a chart using angularjs directive. I want to load the chart after data comes from server response. But scope is empty. My code is
<div class="span4" ui-if="loadingIsDone">
<article id="piChart2">
<section id="toolBox1">
<h3 id="toolH1"></h3>
<p id="toolD1"></p>
</section>
<article id="toolTip1"></article>
<canvas id="canvas1" width="250" height="250" draw-chart>
</canvas>
</article>
</div>
and my controller is
controller(){
$scope.teamStrengths = {};
$scope.loadingIsDone = false;
$http({
url: "url",
method: "POST",
data: {"userUids":uids}
}).success(function(response){
$scope.teamStrengths = response.resource.strengths;//data loads successfully
$scope.loadingIsDone = true;
//rest of code is skipped
}
and my directive is
Directives.directive('drawChart', function() {
return function(scope, element, attrs) {
console.debug("element :"+element);
console.debug("attrs :"+attrs);
graphDraw('canvas1',scope.teamStrengths.value1,scope.teamStrengths.value2,scope.teamStrengths.value3)
//grap loads successfully with static data
};
});
kindly help me, i shall be thankful

$http call is asynchronous. Directive need to $watch changes in the scope. Add this in your directive:
scope.$watch('teamStrengths', function(newValue, oldValue) {
if (newValue)
graphDraw('canvas1',scope.teamStrengths.value1,scope.teamStrengths.value2,scope.teamStrengths.value3)
}, true);
Or watch for $scope.loadingIsDone change:
scope.$watch('loadingIsDone', function(newValue, oldValue) {
if (newValue == true)
graphDraw('canvas1',scope.teamStrengths.value1,scope.teamStrengths.value2,scope.teamStrengths.value3)
}, true);

Related

Directive causing $scope to go undefined when it fires off

Today I tried my hand at writing a directive in angular purely for means to make my own "check if this email exists" validation.
However, when the directive runs, it clears out the scope - rendering it undefined and I have no cooking-clue why. disabling the directive causes my scope not to go lost when I try to submit my form. Can anyone Explain to me why it would do this?
my html:
<form class='form-horizontal' name="userForm" novalidate>
<div class='form-group' ng-class="{ 'has-error' : userForm.emailAddress.$invalid && !userForm.emailAddress.$pristine }">
<div class="col-sm-3">
<label class="control-label" for='emailAddress'>Email Address: </label>
</div>
<div class="col-sm-6">
<input class='form-control' type='email' id='emailAddress' name='emailAddress' ng-model='letMeKnowEmail' email-exists required/>
<p ng-show="userForm.emailAddress.$error.required && !userForm.emailAddress.$pristine" class=".input-error">Your email is required.</p>
<p ng-show="userForm.emailAddress.$error.email && !userForm.emailAddress.$pristine" class=".input-error">Your email is in an invalid format.</p>
<p ng-show="userForm.emailAddress.$error.emailExists && !userForm.emailAddress.$pristine" class=".input-error">This email already exists.</p>
</div>
<div class="col-sm-3">
<button class='btn btn-theme' ng-disabled="userForm.$invalid" ng-click='userForm.$invalid || addEmail(letMeKnowEmail)'>Let Me Know!</button>
</div>
</div>
</form>
my angular JS file:
/* dependancies */
import angular from 'angular';
import angularMeteor from 'angular-meteor';
import uiRouter from 'angular-ui-router';
/* templates */
import template from './applicationProcessTemp.html';
class ApplicationProcessTempCtrl {
constructor($scope, $reactive) {
$reactive(this).attach($scope);
$scope.letMeKnowEmail = '';
$scope.addEmail = function(letMeKnowEmail) {
if (this.userForm.$valid) {
SiteInterestShown.insert({
email: letMeKnowEmail
});
}
}
}
}
const name = 'applicationProcessTemp';
ApplicationProcessTempCtrl.$inject = ['$scope', '$reactive'];
export default angular.module(name, [
angularMeteor
]).component(name, {
template,
controllerAs: name,
controller: ApplicationProcessTempCtrl
}).config(config)
.directive('emailExists', directive);
function config($stateProvider) {
'ngInject';
$stateProvider.state('applicationTemp', {
url: '/applicationTemp',
template: '<application-process-temp></application-process-temp>'
});
}
config.$inject = ['$stateProvider'];
function directive($timeout, $q) {
return {
restrict: 'AE',
require: 'ngModel',
link: function(scope, elm, attr, model) {
model.$asyncValidators.emailExists = function() {
var defer = $q.defer();
$timeout(function() {
var exists = SiteInterestShown.findOne({
'email': model.$viewValue
}) == undefined;
model.$setValidity('emailExists', exists);
defer.resolve;
}, 1);
return defer.promise;
}
}
}
}
directive.$inject = ['$timeout', '$q'];
The environment is a meteor environment, however I have severe doubts that meteor is causing my scope to go undefined at the point the directive fires off to validate whether or not the email exists. I have my suspicions that minimongo might be involved in this matter (SiteInterestShown variable is a mongo collection set in the collections folder on the root of the project)
model.$asyncValidators.emailExists = function(modelValue, viewValue) {
var deferred = $q.defer();
var value = modelValue || viewValue;
var checkVal = model.$viewValue;
var exists = SiteInterestShown.findOne({
'email': value
}) != undefined;
if (!exists) {
deferred.resolve(); <-- resolve was a method. called it as a method on validation success.
} else {
deferred.reject(); <-- reject would destroy the binding value and as far as I see, it gets auto-called if resolve() isn't fired.
}
return deferred.promise;
}
The issue that this code segment faced was that I called resolve as a property, not as a method. I also removed some extra code (the $timeout) as I deemed it unnecessary - but not incorrect - in this sample. the moment I called resolve(), when the deferral's promise was returned, angular did not scrap the value from my $scope - which was another thing I misunderstood. the "$scope" that was destroyed wasn't the entire scope, but only the Scope variable bound to the element as it's model because resolve() wasn't called - and theory here, it auto called reject() which scrapped the value.
Long story short - I have access to my scope variable again :)

How to disable buttons until http request is processed/loaded in AngularJS?

I want to write a directive that keeps a button and page disabled for the duration of the http request.
If I update or submit a form, the button will disable until the http
response is complete.
When a page is loading, it will disable until the entire data is
loaded from the server.
After 10 seconds, the button will be active and the user can click
multiple times.
app.js
<script>
var angModule = angular.module("myApp", []);
angModule.controller("myCtrl", function ($scope, $timeout) {
$scope.isSaving = undefined;
$scope.btnVal = 'Yes';
$scope.save = function()
{
$scope.isSaving = true;
$timeout( function()
{
$scope.isSaving = false;
}, 1000);
};
});
</script>
index.html
<div ng-app="myApp">
<ng-form ng-controller="myCtrl">
Saving: {{isSaving}}
<button ng-click="save()" ng-disabled="isSaving">
<span ng-hide="isSaving">Save</span>
<span ng-show="isSaving">Loading...</span><i class="fa fa-spinner fa-spin" ng-show="isSaving"></i>
</button>
</ng-form>
</div>
I am new to AngularJS, please help me write a directive for this.
here a basic example :
<button ng-click="save()" loading="Loading..." notloading="save" disableonrequest>
myApp.directive("disableonrequest", function($http) {
return function(scope, element, attrs) {
scope.$watch(function() {
return $http.pendingRequests.length > 0;
}, function(request) {
if (!request) {
element.html("<span >"+attrs.notloading+"</span>");
} else {
element.html("<span >"+attrs.loading+"</span><i class='fa fa-spinner fa-spin'></i>");
}
});
}
});
A WORKING EXAMPLE
Depending on your needs, you may not necessarily need a custom directive for this simple task.
You can simply set the $scope.isSaving property inside the callback for $http.
For example
$http({
url: 'http://path/to/the/api/',
method: 'GET',
headers: {
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
}
})
.success(function(data, status){
$scope.isSaving = false;
})
.error(....);

Angular: append html in dom element

I need to write directive to append html in div
Here i want to append html which i get it from server usinh $http post request
<div id="invoice_template_preview"
ng-bind-html-unsafe="invoice_html_template"
class="span6"
style="background: rgba(242, 230, 205, 0.95);margin: -100px 0 0 0;border: 1px solid #ddd; height: auto;padding: 18px;position: relative;width: 50% !important;">
</div>
This is my angular function to get html from db
$scope.getInvoiceTemplate = function() {
$scope.invoiceTemplate = [];
var request = $http({
method: "post",
url: "/c_make_invoice/",
data: {
action: 'getInvoiceTemplate',
id:$scope.our_company_id
},
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
});
request.success(function (data) {
$scope.invoiceTemplate = data.result;
$scope.invoice_html_template = $scope.invoiceTemplate.invoice_html_template;
$scope.invoice_num_format = $scope.invoiceTemplate.invoice_num_format;
});
};
I try this
$scope.invoice_html_template = $scope.invoiceTemplate.invoice_html_template;
but its not a proper way to solve this.
I return json from server, how i can append that html in #invoice_template_preview
Updated directive to watch the scope variable. When it changes the template html is changed to whatever is in the variable.
.directive('CustomDirective', function(){
return {
restrict: 'E',
link: function(scope, element, attrs){
scope.$watch('invoice_html_template', function(){
var template = scope.invoice_html_template;
element.html(template);
$compile(element.contents())(scope);
});
},
template: '<div>{{$scope.invoiceTemplate}}</div>'
});
and it can be used:
<div id="invoice_template_preview">
<custom-directive></custom-directive>
</div>
You should also be using $sce when getting hold of the HTML. see sce
request.success(function (data) {
$scope.invoiceTemplate = $sce.trustAsHtml(data.result.invoice_html_template);
Whan i try $compile I get that $compile is not defined, I fixed that whan I add $compile param in controller initialization.
After that I simple add this code
$('#invoice_template_preview').html($compile($scope.invoiceTemplate.invoice_html_template)($scope));

Refactoring. How to decouple Calls to Services and $scope values modification?

We are working with two models: Weeks and Days. Every Day belongs to a Week.
An we got two views: week-detail and day-detail
How could we decouple the Calls to Service and the $scope values modification?
(maybe using AngularJS Directives)
.
Week Detail View has the following template, there are listed all Days that belong to the specific Week
<!-- file 1: week-detail.tpl.html -->
<table>
<tr ng-repeat="day in weekdetail.days">
<td>
<span ng-click="toggleActive(day.id, $index)">
{{ day.active }}
</span>
</td>
</tr>
</table>
The Controller that commands this View is the following:
// file 2: week-detail.controller.js
function WeekDetailCtrl ($scope, $routeParams, Week, Days, DayToggleActive) {
this.week = Week.get({ id: $routeParams.id });
this.days = Days.query({ id: $routeParams.id });
// #param dayId: id to be processed by DayToggleActvive.toggleActive() service
// #param position: $index from ng-repeat, to modify this specific DOM element
$scope.toggleActive = function(dayId, position) {
// call to service
(DayToggleActive.toggleActive({ id: dayId }))
.$promise
.then(function(data) {
// $scope values manipulation
$scope.weekdetail.week = data;
$scope.weekdetail.days[position].active = !$scope.weekdetail.days[position].active;
});
};
}
Day Detail View has the following template:
<!-- file 3: day-detail.tpl.html -->
<div ng-click="toggleDetailActive(daydetail.day.id)">
{{ daydetail.day.active }}
</div>
The Controller that commands this View is the following:
// file 4: day-detail.controller.js
function DayDetailCtrl ($scope, $routeParams, Day, DayToggleActive) {
this.day = Day.get({ id: $routeParams.id });
this.ods = Ods.query({ id: $routeParams.id });
// #param dayId: id to be processed by DayToggleActvive.toggleActive() service
$scope.toggleDetailActive = function(dayId) {
// call to same service
(DayToggleActive.toggleActive({ id: dayId }))
.$promise
.then(function(data) {
// $scope value manipulation is different in Day Detail View
$scope.daydetail.day.active = !$scope.daydetail.day.active;
});
};
}
.
Thank you in advance for your help
UPDATE
Thanks to help of Oddman, we have proceeded with an advance
We have added a Custom Directive Tag to HTML
<!-- file 3: day-detail.tpl.html -->
<div toggleActive="daydetail.day">
{{ daydetail.day.active }}
</div>
And added the directive to module
// file 4: day-detail.controller.js
module.directive('toggleActive', toggleActive);
function toggleActive() {
return {
scope: {day: '=toggleActive'},
restrict: "A",
link: function(scope, element, attrs ) {
element.bind("click", function() {
scope.day.active = !scope.day.active;
});
}
}
}
That looks better, but... what we need is to toggle scope.day.active after successfully call to Service, so...
// file 4: day-detail.controller.js
// inject DayToggleActive service dependency
module.directive('toggleActive', ['DayToggleActive', toggleActive]);
function toggleActive(DayToggleActive) {
return {
scope: {day: '=toggleActive'},
restrict: "A",
link: function(scope, element, attrs ) {
element.bind("click", function() {
(DayToggleActive.toggleActive({ id: scope.day.id }))
.$promise
.then(function(data) {
scope.day.active = !scope.day.active;
})
.catch(function(response) {
console.log(response);
});
});
}
}
}
Previously, the reference was in controller declaration, so let's remove it
// file 4: day-detail.controller.js
function DayDetailCtrl ($scope, $routeParams, Day) {
...
NOW
Week Detail View can be refactored the same way
<!-- file 1: week-detail.tpl.html -->
<table>
<tr ng-repeat="day in weekdetail.days">
<td>
<span toggleDayActive="day">
{{ day.active }}
</span>
</td>
</tr>
</table>
And Day Detail Controller will include a similar directive to which we defined for Week Detail Controller
// file 2: week-detail.controller.js
// inject DayToggleActive service dependency
module.directive('toggleDayActive', ['DayToggleActive', toggleDayActive]);
function toggleDayActive(DayToggleActive) {
return {
scope: {day: '=toggleDayActive'},
restrict: "A",
link: function(scope, element, attrs ) {
element.bind("click", function() {
(DayToggleActive.toggleActive({ id: scope.day.id }))
.$promise
.then(function(data) {
scope.day.active = !scope.day.active;
// HOW COULD WE ACCESS PREVIUSLY REFERENCED AS $scope.weekdetail.week ?
})
.catch(function(response) {
console.log(response);
});
});
}
}
}
How could we regain access to previously referenced as $scope.weekdetail.week ?
Do we need to pass that as parameter in an way?
Is there a way to reach that $scope?
I would setup directives for your toggling, and just pass the day or week to that. For example:
<div toggle-week="week"></div>
Then in your directive, something like:
module.directive('toggleWeek', function() {
return {
scope: {week: '=toggleWeek'},
link: function(scope, element, attrs) {
// add your scope function here
}
};
});
You can do a similar thing for your daily toggle. Then you just can just deal with the week/day binding in your directive rather than working directly on the $scope.
Hope that helps.

Angular : how to re-render compiled template after model update?

I am working on an angular form builder which generate a json.
Everything works fine except one thing.
You can find an example here : http://jsfiddle.net/dJRS5/8/
HTML :
<div ng-app='app'>
<div class='formBuilderWrapper' id='builderDiv' ng-controller="FormBuilderCtrl" >
<div class='configArea' data-ng-controller="elementDrag">
<h2>drag/drop</h2>
<form name="form" novalidate class='editBloc'>
<div data-ng-repeat="field in fields" class='inputEdit'>
<data-ng-switch on="field.type">
<div class='labelOrder' ng-class='{column : !$last}' drag="$index" dragStyle="columnDrag" drop="$index" dropStyle="columnDrop">{{field.type}}
</div>
<label for="{{field.name}}" data-ng-bind-html-unsafe="field.caption"></label>
<input data-ng-switch-when="Text" type="text" placeholder="{{field.placeholder}}" data-ng-model="field.value" />
<p data-ng-switch-when="Text/paragraph" data-ng-model="field.value" data-ng-bind-html-unsafe="field.paragraph"></p>
<span data-ng-switch-when="Yes/no question">
<p data-ng-bind-html-unsafe="field.yesNoQuestion"></p>
<input type='radio' name="yesNoQuestion" id="yesNoQuestion_yes" value="yesNoQuestion_yes" />
<label for="yesNoQuestion_yes">Oui</label>
<input type='radio' name="yesNoQuestion" id="yesNoQuestion_no" value="yesNoQuestion_no"/>
<label for="yesNoQuestion_no">Non</label>
</span>
<p data-ng-switch-when="Submit button" class='submit' data-ng-model="field.value">
<input value="{{field.name}}" type="submit">
</p>
</data-ng-switch>
</div>
</form>
</div>
<div id='previewArea' data-ng-controller="formWriterCtrl">
<h2>preview</h2>
<div data-ng-repeat="item in fields" content="item" class='templating-html'></div>
</div>
</div>
</div>
The JS :
var app = angular.module('app', []);
app.controller('FormBuilderCtrl', ['$scope', function ($scope){
$scope.fields = [{"type":"Text/paragraph","paragraph":"hello1"},{"type":"Yes/no question","yesNoQuestion":"following items must be hidden","yes":"yes","no":"no"},{"type":"Text/paragraph","paragraph":"hello2"},{"type":"Submit button","name":"last item"}] ;
}]);
app.controller('elementDrag', ["$scope", "$rootScope", function($scope, $rootScope, $compile) {
$rootScope.$on('dropEvent', function(evt, dragged, dropped) {
if($scope.fields[dropped].type == 'submitButton' || $scope.fields[dragged].type == 'submitButton'){
return;
}
var tempElement = $scope.fields[dragged];
$scope.fields[dragged] = $scope.fields[dropped];
$scope.fields[dropped] = tempElement;
$scope.$apply();
});
}]);
app.directive("drag", ["$rootScope", function($rootScope) {
function dragStart(evt, element, dragStyle) {
if(element.hasClass('column')){
element.addClass(dragStyle);
evt.dataTransfer.setData("id", evt.target.id);
evt.dataTransfer.effectAllowed = 'move';
}
};
function dragEnd(evt, element, dragStyle) {
element.removeClass(dragStyle);
};
return {
restrict: 'A',
link: function(scope, element, attrs) {
if(scope.$last === false){
attrs.$set('draggable', 'true');
scope.dragStyle = attrs["dragstyle"];
element.bind('dragstart', function(evt) {
$rootScope.draggedElement = scope[attrs["drag"]];
dragStart(evt, element, scope.dragStyle);
});
element.bind('dragend', function(evt) {
dragEnd(evt, element, scope.dragStyle);
});
}
}
}
}]);
app.directive("drop", ['$rootScope', function($rootScope) {
function dragEnter(evt, element, dropStyle) {
element.addClass(dropStyle);
evt.preventDefault();
};
function dragLeave(evt, element, dropStyle) {
element.removeClass(dropStyle);
};
function dragOver(evt) {
evt.preventDefault();
};
function drop(evt, element, dropStyle) {
evt.preventDefault();
element.removeClass(dropStyle);
};
return {
restrict: 'A',
link: function(scope, element, attrs) {
if(scope.$last === false){
scope.dropStyle = attrs["dropstyle"];
element.bind('dragenter', function(evt) {
dragEnter(evt, element, scope.dropStyle);
});
element.bind('dragleave', function(evt) {
dragLeave(evt, element, scope.dropStyle);
});
element.bind('dragover', dragOver);
element.bind('drop', function(evt) {
drop(evt, element, scope.dropStyle);
var dropData = scope[attrs["drop"]];
$rootScope.$broadcast('dropEvent', $rootScope.draggedElement, dropData);
});
}
}
}
}]);
app.controller('formWriterCtrl', ['$scope', function ($scope){
}]);
app.directive('templatingHtml', function ($compile) {
var previousElement;
var previousIndex;
var i=0;
var inputs = {};
var paragraphTemplate = '<p data-ng-bind-html-unsafe="content.paragraph"></p>';
var noYesQuestionTemplate = '<p data-ng-bind-html-unsafe="content.yesNoQuestion"></p><input id="a__index__yes" type="radio" name="a__index__"><label for="a__index__yes" />{{content.yes}}</label><input id="a__index__no" class="no" type="radio" name="a__index__" /><label for="a__index__no">{{content.no}}</label>';
var submitTemplate = '<p class="submit"><input value="{{content.name}}" type="submit" /></p>';
var getTemplate = function(contentType, contentReplace, contentRequired) {
var template = '';
switch(contentType) {
case 'Text/paragraph':
template = paragraphTemplate;
break;
case 'Yes/no question':
template = noYesQuestionTemplate;
break;
case 'Submit button':
template = submitTemplate;
break;
}
template = template.replace(/__index__/g, i);
return template;
}
var linker = function(scope, element, attrs) {
i++;
elementTemplate = getTemplate(scope.content.type);
element.html(elementTemplate);
if(previousElement == 'Yes/no question'){
element.children().addClass('hidden');
element.children().addClass('noYes'+previousIndex);
}
if(scope.content.type == 'Yes/no question'){
previousElement = scope.content.type;
previousIndex = i;
}
$compile(element.contents())(scope);
}
return {
restrict: "C",
link: linker,
scope:{
content:'='
}
};
});
On the example there are 2 areas :
- the first one does a ngRepeat on Json and allow to reorder items with drag and drop
- the second area also does a ngRepeat, it is a preview templated by a directive using compile function. Some elements are hidden if they are after what I called "Yes/no question"
Here is an example of Json generated by the form builder :
$scope.fields =
[{"type":"Text/paragraph","paragraph":"hello1"},{"type":"Yes/no question","yesNoQuestion":"following items must be hidden","yes":"yes","no":"no"},
{"type":"Text/paragraph","paragraph":"hello2"},{"type":"Submit button","name":"last item"}] ;
When the page load everything is ok, Hello1 is visible and Hello2 is hidden.
But when I drop Hello1 after "Yes/no question", dom elements are reorganised but Hello1 is not hidden.
I think it comes from $compile but I don't know how to resolve it.
Could you help me with this please?
Thank you
I only see you setting the 'hidden' class on the element based on that rule (after a yes/no) in the link function. That's only called once for the DOM element - when it's first created. Updating the data model doesn't re-create the element, it updates it in place. You would need a mechanism that does re-create it if you wanted to do it this way.
I see three ways you can do this:
In your linker function, listen for the same dropEvent that you listen for above. This is more efficient than you'd think (it's very fast) and you can re-evaluate whether to apply this hidden class or not.
Use something like ngIf or literally re-creating it in your collection to force the element to be recreated entirely. This is not as efficient, but sometimes is still desirable for various reasons.
If your use case is actually this simple (if this wasn't a redux of something more complicated you're trying to do) you could use CSS to do something like this. A simple rule like
.yes-no-question + .text-paragraph { display: none; }
using a sibling target could handle this directly without as much work. This is much more limited in what it can do, obviously, but it's the most efficient option if it covers what you need.

Resources