Directive causing $scope to go undefined when it fires off - angularjs

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 :)

Related

How do I integrate Stripe credit card field with `ng-disabled` attribute of a `button`?

I have a form in my page with AngularJS and Stripe JS.
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.6/angular.min.js"> <script type="text/javascript">
var app= angular.module("app",[]);
app.config(function($interpolateProvider){
$interpolateProvider.startSymbol("[[[");
$interpolateProvider.endSymbol("]]]");
});
app.controller("Ctrl",function(stripeService, $scope, $rootScope){
$scope.name= "World";
$scope.stripeCompleted= false;
stripeService.start();
$rootScope.on("stripedone", function(e,stripeEvent){
$scope.stripeCompleted= stripeEvent.complete;
$scope.$apply();
});
});
app.service("stripeService", function($window,$rootScope){
function start(){
var btn= document.querySelectorAll("#test")[0];
var displayError= document.getElementById('card-errors');
var stripe= Stripe("{{ stripe_key }}");
var elements= stripe.elements();
var style= {
base: {
fontSize: "1.1875rem",
fontSmoothing: "always",
fontWeight: "600"
}
};
var card= elements.create("card", {style:style});
card.mount("#card-element");
card.addEventListener('change', function(event) {
if (event.error) {
displayError.textContent = event.error.message;
} else {
displayError.textContent = '';
}
if (event.complete) {
$rootScope.$broadcast("stripedone",event);
} else {
$rootScope.$broadcast("stripedone",event);
}
});
var formID= "register-form";
var form= document.getElementById(formID);
form.addEventListener("submit",function(event){
event.preventDefault();
stripe.createToken(card).then(function(result){
if(result.error) {
displayError.textContent= result.error.message;
} else {
stripeTokenHandler(result.token, formID);
}
});
});
return {"start":start};
}
});
// tut https://stripe.com/docs/stripe-js/elements/quickstart#create-form
function stripeTokenHandler(token, formID) {
// Insert the token ID into the form so it gets submitted to the server
var form = document.getElementById(formID);
var hiddenInput = document.createElement('input');
hiddenInput.setAttribute('type', 'hidden');
hiddenInput.setAttribute('name', 'stripeToken');
hiddenInput.setAttribute('value', token.id);
form.appendChild(hiddenInput);
// Submit the form
form.submit();
}
</script> <form id="register-form" name="regForm" method="post>
<input ng-model="reg.email" type="email" name="username">
<div id="stripe-wrapper">
<div id="card-element"></div>
</div>
<small id="card-errors" class="text-danger" role="alert">{{ ccErrMsg }}</small>
<br>
<button type="submit" ng-model="reg.btn" ng-disabled="!regForm.username.$valid>Register</button>
</form>
I want for my button to be un-clickable unless the user fills out the Stipe credit card section correctly. How do I make it so my button is disabled unless the the Stripe credit card fields are filled out correctly?
Update: Following karthick's answer gives me a new error:
angular.js:13642 TypeError: stripeService.start is not a function
at Object.<anonymous> ((index):135)
at Object.invoke (angular.js:4708)
at P.instance (angular.js:10177)
at n (angular.js:9096)
at g (angular.js:8459)
at angular.js:8339
at angular.js:1782
at m.$eval (angular.js:17378)
at m.$apply (angular.js:17478)
at angular.js:1780
You should use a AngularJS directive in this case. This is nothing to put in a controller, service, factory or component. Once you use a directive your code will be much smarter and become fully supported by AngularJS diggest cycles and DOM bindings. This is how the documentation introduces directives:
What are Directives? At a high level, directives are markers on a DOM element (such as an attribute, element name, comment or CSS class) that tell AngularJS's HTML compiler ($compile) to attach a specified behavior to that DOM element (e.g. via event listeners), or even to transform the DOM element and its children.
> Demo fiddle
Your solution could be smart like this one by using a nice directive:
View
<body ng-app="angularjs-starter">
<script src="https://js.stripe.com/v3/"></script>
<div ng-controller="MainCtrl">
<form name="regForm" id="register-form">
<label>Mail</label>
<input ng-model="reg.email" type="email" name="username">
<div stripe-validator
stripe-complete="stripeCompleted"
stripe-form-id="register-form"></div>
<br>
<button ng-model="reg.btn" ng-disabled="stripeCompleted === false || !regForm.username.$valid">Register</button>
</form>
</div>
</body>
AngularJS Application / Stripe.js card validation directive
var app = angular.module('angularjs-starter', []);
app.controller('MainCtrl', function($scope, $rootScope) {
//Init stripe state via controller
$scope.stripeCompleted = false;
});
app.directive('stripeValidator', function() {
return {
restrict: 'A',
template: `
<div id="stripe-wrapper">
<div id="card-element"></div>
</div>
<small id="card-errors" class="text-danger" role="alert">{{ ccErrMsg }}</small>
<input type="hidden" name="stripeToken" ng-value="stripeToken" />`,
scope: {
"stripeComplete": '=',
"stripeFormId": '#',
"stripeError": '=',
"stripeToken": '=',
},
link: function(scope, element, attrs) {
//Init
var stripe = Stripe("pk_test_6pRNASCoBOKtIshFeQd4XMUh");
var elements = stripe.elements();
var card = elements.create("card");
var form = document.getElementById(scope.stripeFormId);
//mount card element https://stripe.com/docs/stripe-js/reference#element-mount
card.mount("#card-element");
//add event listener
card.addEventListener('change', function(event) {
//check for errors
if (event.error) {
scope.ccErrMsg = event.error.message;
} else {
scope.ccErrMsg = '';
}
//check for complete
scope.stripeComplete = event.complete ? true : false;
//apply scope
scope.$apply();
});
//inject form submit event
form.addEventListener("submit", function(event) {
//prevent form submit
event.preventDefault();
//handle token, handle error and form submit forward
stripe.createToken(card).then(function(result) {
if (result.error) {
scope.ccErrMsg = event.error.message;
scope.stripeToken = '';
} else {
scope.ccErrMsg = '';
scope.stripeToken = result.token;
}
//apply scope
scope.$apply();
//forward submit
form.submit();
})
});
}
}
});

Changing expression provided to ngShow attribute using controller

I am using ng-show and ng-hide to display/hide content. I would like to change the showme status from true to false within the controller. But when I use the code below, it doesn't work. I'm using the Controller As syntax. Any suggestions on how to get this working right?
HTML:
<h1 ng-show="showme">Confirm Order</h1>
<h4 ng-hide="showme">Contact Information</h4>
Javascript:
.controller('ContactFormCtrl',
function ($http, serviceF, $scope) {
var contactForm = this;
$scope.$watch(serviceF.get, function(valid)
{
if (valid === 'yes') {
contactForm.showme=true;
}
else
{
contactForm.showme=false;
}
});
});
Service:
.service('serviceF', function() {
var valid = 'true';
return {
get: function () {
return valid;
},
set: function (value) {
valid = value;
}
};
UI Router:
.state('payment', {
url: '/payment',
views: {
// . . .
'top': {
templateUrl: 'views/clientinfo.html',
controller: 'ContactFormCtrl as contactForm'
// . . .
}
})
I'm not sure what you're trying to do, but the Controller As syntax goes this way in HTML:
<div ng-controller="ContactFormCtrl as contactForm">
<h1 ng-show="contactForm.showme">Confirm Order</h1>
<h1 ng-show="contactForm.showme">Confirm Order</h1>
</div>
Note the 'as contactForm' thingy passed in the ng-controller directive
Now you know that showme is actually a property of contactForm which is essentially an "alias" of the ContactFormCtrl controller
From there, whenever the showme property changes in the controller, the view will behave accordingly.
// In your controller
var contactForm = this; // aliasing this
contactForm.showme = true; //or false
UPDATE:
Since you're using ui-router, you should be good without ng-controller in your view. I'm noticing you are not passing $scope to your controller, that could be a reason why $scope.$watch isn't working, thus not updating the view.
.controller('ContactFormCtrl', function ($scope, $http, serviceF) {
var contactForm = this;
$scope.$watch(serviceF.get, function(valid) {
if (valid === 'yes') {
contactForm.showme = true;
}else{
contactForm.showme = false;
}
});
});

How to change select functions in Angular Directive?

http://plnkr.co/edit/pJRzKn2v1s865w5WZBkR?p=preview
I have a large select dropdown form which is repeated in 2 places. The only thing that changes is the first select tag, which has a different function.
<!--
On simple, change ng-change function to functionOne
On advanced, change ng-change function to functionTwo
-->
<select name="name1" ng-change="functionOne('function1')" id="the-id-1">
<select name="name2" ng-change="functionTwo('function2)" id="the-id-2">
<option value="aaa">aaa</option>
<option value="bbb">bbb</option>
<option value="ccc">ccc</option>
</select>
I tried using ng-hide ng-show however there must be a different way to accomplish this.
var app = angular.module('myApp', [])
.directive('termsForm', function() {
return {
templateUrl : "termsForm.html",
restrict : "E",
scope : false,
controller : 'TermsFormController'
}
})
.directive('selectOptions', function() {
return {
templateUrl : "form.html",
restrict : "E",
scope : false
}
})
.controller('TermsFormController',
['$scope',
function($scope) {
var vs = $scope;
vs.hello = "This is the form.";
vs.showingSimple = true;
vs.showingAdvanced = false;
vs.showForm = function(type) {
if (type === 'simple') {
vs.showingSimple = true;
vs.showingAdvanced = false;
} else if (type === 'advanced') {
vs.showingSimple = false;
vs.showingAdvanced = true;
}
}
vs.functionOne = function(msg) {
alert(msg);
}
vs.functionTwo = function(msg) {
alert(msg);
}
}]);
termsForm.html
<ul class="nav nav-tabs">
<button class="btn btn-info" ng-click="showForm('simple')">Simple</button>
<button class="btn btn-info" ng-click="showForm('advanced')">Advanced</button>
</ul>
<p>The select:</p>
<div ng-show="showingSimple" class="simple-form">
<p>Simple</p>
<select-options></select-options>
</div>
<div ng-show="showingAdvanced" class="advanced-form">
<p>Advanced</p>
<select-options></select-options>
</div>
You already have a directive created for your select, that gets you half way there. Now you just need to pass the function in through whats known as the isolated scope.
.directive('selectOptions', function() {
return {
templateUrl : "form.html",
restrict : "E",
scope : {
changeFunc: '&'
}
}
})
This allows you to pass in the function you want to call on the ng-change event:
<select-options changeFunc="function1"></select-options>
<select-options changeFunc="function2"></select-options>
And then in your form.html you simply put
<select name="name2" ng-change="changeFunc()" id="the-id-2">
This way you are basically passing the funciton in as a parameter. Read this blog for a great guide on isolated scopes.
I would just refactor your markup and controller to adapt based on the simple/advanced context.
In your controller, you'd expose a 'generic' on change function for the dropdown, first...
(function () {
'use strict';
angular.module('app').controller('someCtrl', [someCtrl]);
function someCtrl() {
var vm = this;
vm.isSimple = true;
vm.nameChange = function () {
if(vm.isSimple)
functionOne('function1');
else
functionTwo('function2');
}
// Other things go here.
}
})();
...Then, on your view, your select would change to this*:
<select id="someId" name="someName" ng-change="vm.nameChange()" />
*: Assuming you're using controllerAs syntax, that is. If you're not, don't prepend the vm. on the select.

Passing data from ionic/angular modal using separate template html file

I'm developing a simple Ionic mobile app although the answer likely lies with Angular. The app is really simple, displays a list of employees with an Add button which displays a modal, lets the user enter some details, click Save and it's persists the data to a back-end Firebase store. It has 1 controller and a simple service. Initially I had the template html for the modal inside script tags inside the index.html and it all worked fine. When I decided to structure things out and put the modal template in a separate html file, suddenly the data object assigned to ng-modal via the input boxes no longer passes any data to the event handler to save the data, instead it's always undefined. Everything else works as it should, the modal displays ok, the event handlers are calling the right functions etc. The only change is moving the input template to a separate file. I know it's likely something really simple but can't for the life of me work out why and can't find any info about it anywhere else.
Template HTML file for the modal :
<ion-list>
<h1>Add Employee</h1>
<div class="list list-inset">
<ion-item>
<label class="item item-input">
<input type="text" placeholder="Employee Name" ng-model="data.employeeName">
</label>
<label class="item item-input">
<input type="text" placeholder="Employee Age" ng-model="data.employeeAge">
</label>
</ion-item>
<button class="button button-outline button-block button-balanced"
ng-click="addEmployee(true, data)">
Save & Add Another
</button>
<button class="button button-outline button-block button-positive"
ng-click="addEmployee(false, data)">
Save
</button>
<button class="button button-outline button-block button-assertive"
ng-click="closeAddModal()">
Cancel
</button>
</ion-list>
</ion-modal-view>
addEmployee event - data parameter is now always undefined. Worked fine with embedded template :
$scope.addEmployee = function(retainModal, data) {
var employee = {employeeName:data.employeeName,
employeeAge:data.employeeAge};
employeeService.saveEmployee(employee);
if (! retainModal) {
$scope.closeAddModal();
};
data.employeeName = "";
data.employeeAge = "";
};
Based on this question and other needs I create a service that can be useful.
See this post: Ionic modal service or see in operation: CodePen
(function () {
'use strict';
var serviceId = 'appModalService';
angular.module('app').factory(serviceId, [
'$ionicModal', '$rootScope', '$q', '$injector', '$controller', appModalService
]);
function appModalService($ionicModal, $rootScope, $q, $injector, $controller) {
return {
show: show
}
function show(templateUrl, controller, parameters) {
// Grab the injector and create a new scope
var deferred = $q.defer(),
ctrlInstance,
modalScope = $rootScope.$new(),
thisScopeId = modalScope.$id;
$ionicModal.fromTemplateUrl(templateUrl, {
scope: modalScope,
animation: 'slide-in-up'
}).then(function (modal) {
modalScope.modal = modal;
modalScope.openModal = function () {
modalScope.modal.show();
};
modalScope.closeModal = function (result) {
deferred.resolve(result);
modalScope.modal.hide();
};
modalScope.$on('modal.hidden', function (thisModal) {
if (thisModal.currentScope) {
var modalScopeId = thisModal.currentScope.$id;
if (thisScopeId === modalScopeId) {
deferred.resolve(null);
_cleanup(thisModal.currentScope);
}
}
});
// Invoke the controller
var locals = { '$scope': modalScope, 'parameters': parameters };
var ctrlEval = _evalController(controller);
ctrlInstance = $controller(controller, locals);
if (ctrlEval.isControllerAs) {
ctrlInstance.openModal = modalScope.openModal;
ctrlInstance.closeModal = modalScope.closeModal;
}
modalScope.modal.show();
}, function (err) {
deferred.reject(err);
});
return deferred.promise;
}
function _cleanup(scope) {
scope.$destroy();
if (scope.modal) {
scope.modal.remove();
}
}
function _evalController(ctrlName) {
var result = {
isControllerAs: false,
controllerName: '',
propName: ''
};
var fragments = (ctrlName || '').trim().split(/\s+/);
result.isControllerAs = fragments.length === 3 && (fragments[1] || '').toLowerCase() === 'as';
if (result.isControllerAs) {
result.controllerName = fragments[0];
result.propName = fragments[2];
} else {
result.controllerName = ctrlName;
}
return result;
}
} // end
})();
Usage:
appModalService
.show('<templateUrl>', '<controllerName> or <controllerName as ..>', <parameters obj>)
.then(function(result) {
// result from modal controller: $scope.closeModal(result) or <as name here>.closeModal(result) [Only on template]
}, function(err) {
// error
});
You can use another service to centralize the configuration of all modals:
angular.module('app')
.factory('myModals', ['appModalService', function (appModalService){
var service = {
showLogin: showLogin,
showEditUser: showEditUser
};
function showLogin(userInfo){
// return promise resolved by '$scope.closeModal(data)'
// Use:
// myModals.showLogin(userParameters) // get this inject 'parameters' on 'loginModalCtrl'
// .then(function (result) {
// // result from closeModal parameter
// });
return appModalService.show('templates/modals/login.html', 'loginModalCtrl as vm', userInfo)
// or not 'as controller'
// return appModalService.show('templates/modals/login.html', 'loginModalCtrl', userInfo)
}
function showEditUser(address){
// return appModalService....
}
}]);
You need to attach your models to the scope:
$scope.data.employeeName = "";
$scope.data.employeeAge = "";
...and similar every time you reference them.

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