Using AngularJs dynamic directives for custom view - angularjs

I'm using a server side generated JSON to populate a custom view using different directives with Angular 1.2.29. I have a couple of questions regarding what is the proper way a doing this considering performance and good practices.
5 different types of directive will be involved for about 30 items
The JSON will stay the same about 90% and it's a bit bad to have to regenerate all the DOM elements between user tab switch.
I want to avoid creating watches but in since I'm using 1.2.X should I consider using angular-once
Since I'm going to reuse the same directive couple of time should I consider cloneAttachFn
function processItems(items) {
angular.forEach(items, function(item) {
switch(item.type) {
case 'directive1':
var newDirective = angular.element('<directive-one></directive-one>');
newDirective.attr('value', item.value);
var compiledHtml = $compile(newDirective)(scope);
element.append(compiledHtml);
break;
case 'directive2':
var newDirective = angular.element('<directive-two></directive-two>');
newDirective.attr('value', item.value);
var compiledHtml = $compile(newDirective)(scope);
element.append(compiledHtml);
break;
}
})
}
I created a Plunker to show you guys my current approach. Comments and answers are very welcome! https://plnkr.co/edit/Za4ANluUkXYP5RCcnuAb?p=preview

I have been through this problem many times when generating dynamic filter type functionality. Your code works but I would argue it's over engineered and not very readable. GenericItems directive isn't needed. I would try and move functionality to the view and make it clear what happens as the type changes. Here is my solution as a Plunker
Controller
<div ng-controller="appCtrl as app">
<p>{{app.name}}</p>
<button ng-click="app.add1()">Directive 1</button>
<button ng-click="app.add2()">Directive 2</button>
<button ng-click="app.remove()">Remove</button>
<div ng-repeat="item in app.items">
<directive-one value="item.value" ng-if="item.type==='directive1'"></directive-one>
<directive-two value="item.value" ng-if="item.type==='directive2'"></directive-two>
</div>
</div>
app.js
app.controller('appCtrl', function() {
var vm = this;
vm.items = [];
vm.name = 'Dynamic directive test';
vm.add1 = function() {
vm.items.push({type: 'directive1', value: Math.random()})
};
vm.add2 = function() {
vm.items.push({type: 'directive2', value: Math.random()})
};
vm.remove = function() {
vm.items.pop();
};
});
app.directive('directiveOne', function() {
return {
scope: {
value: '='
},
restrict: 'E',
template: '<p>d1: {{value}}</p>'
}
});
app.directive('directiveTwo', function() {
return {
scope: {
value: '='
},
restrict: 'E',
template: '<p>d2: {{value}}</p>'
}
});

Related

How can I use isolated scope with a component and a directive?

The goal here is to let MainCtrl know when there is an error(s) found in the directive. The error must be displayed here:
<div ng-if="errors[0]">Error 1: {{errors[0]}}</div>
How can I get isolated scope with a directive inside a component? The following application works if you uncomment the 2 lines mentioned below. As it is, I get error:
Multiple Directive Resource Contention
I can read the causes. I need to know how to fix this while still allowing the directive to have isolated scope. I may have 3-4 of these directives on a page and each one needs it's own unique of errors that is also available to the parent.
(working case example on codepen)
var app = angular.module('app', []);
app.controller('MainCtrl', function($scope) {
$scope.errors = [false, false];
$scope.text = "bobby";
});
app.directive('testDirective', function(){
return {
restrict: 'A',
scope: {
errors: '=',
text: '#'
},
link: function($scope, $element, $attr, ngModel) {
console.log('link fired');
console.log('errors: ', $scope.errors);
console.log('scope.text', $scope.text);
$attr.$observe('text', function (val) {
if($scope.text === 'bobby'){
$scope.errors[0] = true;
}else{
$scope.errors[0] = false;
}
});
},
template: '<p>text: {{ text }} </p>'
+ '<p>errors: {{errors}}</p>'
+ '<p><input type="text" ng-model="errors" /></p>'
};
});
app.component('panel', {
bindings: {
},
template: [
'<div>',
'</div>'
].join(''),
controller: function() {
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.11/angular.min.js"></script>
<section ng-app="app" ng-controller="MainCtrl">
<h3>Parent Scope</h3>
<p>errors: {{errors}}</p>
<input type="text" ng-model="text"></div>
<div ng-if="errors[0]">Error 1: {{errors[0]}}</div>
<div ng-if="errors[1]">Error 2: {{errors[1]}}</div>
<!-- UNCOMMENT THE FOLLOWING 2 LINES AND THIS APP WILL WORK
<h3>Directive by itself</h3>
<div test-directive text="{{text}}" errors="errors"><div>
-->
<h3>Directive in component</h3>
<panel test-directive text="{{text}}" errors="errors"></panel>
</section>
After researching, I noticed Angular only returns bool from $validators (as opposed to object). At this point I decided my approach was wrong. I decided to create a unique $valiators for each unique error message. Then use ng-message for the output.
In order to work with multiple components on the same page, I also have to check the ngModel.$error as part of validation. This blog covers the basic approach.

AngularJS directive with ng-show not toggling with change in variable

I'm trying to create a simple html5 video playlist app. I've got an overlay div on top of the html5 video that should appear/disappear when stopping and starting the video.
I've got ng-show and a variable to trigger it, but it's not changing when I look using ng-inspector.
My events might not be quite correct, either - but I can't seem to find much information on putting events on different elements within the same directive. Is this a clue that I should break this up into multiple directives?
(function() {
'use strict';
angular
.module('app')
.controller('campaignController', campaignController)
.directive('myVideo', myvideo);
function campaignController($log,Campaign) {
var vm = this;
vm.overlay = true;
Campaign.getCampaign().success(function(data) {
vm.campaign = data[0];
vm.item = vm.campaign.videos[0];
});
vm.select = function(item) {
vm.item = item;
};
vm.isActive = function(item) {
return vm.item === item;
};
};
function myvideo() {
return {
restrict: 'E',
template: ['<div class="video-overlay" ng-show="vm.overlay">',
'<p>{{ vm.campaign.name}}</p>',
'<img class="start" src="play.png">',
'</div>',
'<video class="video1" controls ng-src="{{ vm.item.video_mp4_url | trusted }}" type="video/mp4"></source>',
'</video>' ].join(''),
link: function(scope, element, attrs) {
scope.video = angular.element(document.getElementsByClassName("video1")[0]);
scope.startbutton = angular.element(document.getElementsByClassName("start")[0]);
scope.startbutton.on('click', function() {
scope.vm.overlay = false;
scope.video[0].play();
});
scope.video.on('click', function() {
scope.video[0].pause();
scope.vm.overlay = true;
});
}
};
}
})();
From my personal experience angular expression evaluation does not work as javascript. so try ng-show="vm.overlay==true".
Furthermore you bind click using native javascript.
Either don't do that and use ng-click or call scope.$apply() in the click event t callbackas last intruction (even though i'm not sure if it's really important).

AngularJS : How to add a new object into array while using factory and directives

Trying to simulate a hotel cart.
Newbie here
Questions
1. How to add an item to orders when clicked on corresponding Add button
2. Is it correct to use a factory for serving both menuitems for menu directive and orderItems for cart directive
3. On click of add button, where should the to be called add function be written, in the factory or in the directive's controller
4. Is there any way to better this code and its logic?
For those who wish to see the plunkr demo can view the same here
HTML snippet
<menu></menu>
JS snippet
angular.module('myApp',[])
.factory('menuItems',function(){
return {
list:function(){
var items = [{'name':'kabab'},
{'name':'chicken'},
{'name':'egg'},
{'name':'noodles'}];
return items
}
};
})
.factory('cartItems',function(){
var orders = [];
return orders;
})
.directive('menu',function(){
return {
restrict:'E',
template:"<ul><li ng-repeat='item in menuItems'>"+
'{{item.name}}' +
"</li></ul>",
scope:{},
controllerAs:'menuCtrl',
controller:function($scope, menuItems){
console.log("I am in menuDirective and outputting menuItems")
console.log(menuItems);
$scope.menuItems = menuItems.list();
},
link:function(){
}
}
})
.directive('cart',function(){
return{
restrict:'E',
template:"<ul><li ng-repeat='order in cartItems'>"+
'{{order.name}}' +
"</li></ul>",
scope:{},
controller:function($scope,cartItems){
$scope.cartItems = cartItems.list();
},
link:function(){
}}
})
Plunker Demo
I think this is a fine way to do it. Since you need to access the same dataset from multiple directives, it makes sense to me to put the methods and data into a factory.
HTML:
<body ng-controller="MainCtrl">
<menu></menu>
<br />Cart:<br />
<cart></cart>
</body>
JS:
angular.module('plunker',[])
.controller('MainCtrl', function() {
})
.factory('menuItems',function(){
return {
list:function(){
var items = [{'name':'kabab'},
{'name':'chicken'},
{'name':'egg'},
{'name':'noodles'}];
return items
}
};
})
.factory('cartItems',function(){
return {
orders: [],
add: function(item) {
this.orders.push(item)
}
}
//return this.orders;
})
.directive('menu',function(cartItems){
return {
restrict:'E',
template:"<ul><li ng-repeat='item in menuItems'>"+
'{{item.name}}' +
"<button ng-click='addItem(item.name)'>Add</button></li></ul>",
scope:{},
controllerAs:'menuCtrl',
controller:function($scope, menuItems){
console.log("I am in menuDirective and outputting menuItems")
console.log(menuItems);
$scope.menuItems = menuItems.list();
$scope.addItem = function(item) {
cartItems.add(item);
console.log(cartItems.orders)
}
},
link:function(){
}
}
})
.directive('cart',function(){
return{
restrict:'E',
template:"<ul><li ng-repeat='order in cartItems track by $index'>"+
'{{order}}' +
"</li></ul>",
scope:{},
controller:function($scope,cartItems){
$scope.cartItems = cartItems.orders;
},
link:function(){
}}
})
Any methods relating to the orders array should be put in the factory. Your factory should encapsulate all logic related to that particular 'thing'. Keeping the logic out of your directives gives you good abstraction, so if you need to change something, you know where all the logic is kept, rather than spread out in a bunch of directives. So if you want to add an 'empty cart' method, or a 'delete from cart' method, I would put those into the factory, operate directly on the orders array, and return the updated array to whatever is calling the methods.

Angularjs create directive dynamically with ng-repeat

I'd like to know is there any way to create dynamic directive with ng-repeat. Something, like this:
<div ng-repeat="document in documents">
<div document.name></div>
</div>
You could use attributes to do that. For example:
<div ng-repeat="document in documents">
<document-viewer name="document.name"></document-viewer>
</div>
and
var app = angular.module("documents", []);
app.controller("AppCtrl", function($scope) {
$scope.documents = [
{id:1, name:'document1'},
{id:2, name:'document2'}
];
});
app.directive("documentViewer", function() {
return {
scope: {
//attribute name will match the name passed from your view,
//and you will be able to use it on your template or logic
name: "="
},
//Add your template here:
template: '<div><button ng-click="openDocument(name);"></div>',
//Add your logic here:
link: function(scope, element, attrs){ ... }
}
});
I don't know if that answers your question, but you are not making clear what is it you are trying to achieve.
Have a look at the following videos as they might come handy for you when dealing with directives and isolate scope:
http://egghead.io/lessons/angularjs-directive-to-directive-communication
http://egghead.io/lessons/angularjs-isolate-scope-expression-binding
http://egghead.io/lessons/angularjs-isolate-scope-attribute-binding
http://egghead.io/lessons/angularjs-isolate-scope-two-way-binding

Reset form to pristine state (AngularJS 1.0.x)

A function to reset form fields to pristine state (reset dirty state) is on the roadmap for AngularJS 1.1.x. Unfortunately such a function is missing from the current stable release.
What is the best way to reset all form fields to their initial pristine state for AngularJS 1.0.x.?
I would like to know if this is fixable with a directive or other simple workaround. I prefer a solution without having to touch the original AngularJS sources. To clarify and demonstrate the problem, a link to JSFiddle. http://jsfiddle.net/juurlink/FWGxG/7/
Desired feature is on the Roadmap - http://blog.angularjs.org/2012/07/angularjs-10-12-roadmap.html
Feature request - https://github.com/angular/angular.js/issues/856
Proposed solution Pull request - https://github.com/angular/angular.js/pull/1127
Updated with possible workaround
Good enough workaround?
I just figured out I can recompile the HTML part and put it back into the DOM. It works and it's fine for a temporarily solution, but also as #blesh mentioned in the comments:
Controllers should be used for business logic only, not for DOM!
<div id="myform">
<form class="form-horizontal" name="form">
</form>
</div>
And in my Controller on resetForm():
Save the original untouched HTML
Recompile the saved original HTML
Remove the current form from the DOM
Insert the new compiled template into the DOM
The JavaScript:
var pristineFormTemplate = $('#myform').html();
$scope.resetForm = function () {
$('#myform').empty().append($compile(pristineFormTemplate)($scope));
}
I think it's worth mentioning that in later versions of Angular (e.g. 1.1.5), you can call $setPristine on the form.
$scope.formName.$setPristine(true)
This will set all the form controls to pristine state as well.
FormController.$setPristine
Solution without a workaround
I came up with a solution which uses AngularJS without any workaround. The trick here is to use AngularJS ability to have more than one directive with the same name.
As others mentioned there is actually a pull request (https://github.com/angular/angular.js/pull/1127) which made it into the AngularJS 1.1.x branch which allows forms to be reset. The commit to this pull request alters the ngModel and form/ngForm directives (I would have liked to add a link but Stackoverflow doesn't want me to add more than two links).
We can now define our own ngModel and form/ngForm directives and extend them with the functionality provided in the pull request.
I have wrapped these directives in a AngularJS module named resettableForm. All you have to do is to include this module to your project and your AngularJS version 1.0.x behaves as if it was an Angular 1.1.x version in this regard.
''Once you update to 1.1.x you don't even have to update your code, just remove the module and you are done!''
This module also passes all tests added to the 1.1.x branch for the form reset functionality.
You can see the module working in an example in a jsFiddle (http://jsfiddle.net/jupiter/7jwZR/1/) I created.
Step 1: Include the resettableform module in your project
(function(angular) {
// Copied from AngluarJS
function indexOf(array, obj) {
if (array.indexOf) return array.indexOf(obj);
for ( var i = 0; i < array.length; i++) {
if (obj === array[i]) return i;
}
return -1;
}
// Copied from AngularJS
function arrayRemove(array, value) {
var index = indexOf(array, value);
if (index >=0)
array.splice(index, 1);
return value;
}
// Copied from AngularJS
var PRISTINE_CLASS = 'ng-pristine';
var DIRTY_CLASS = 'ng-dirty';
var formDirectiveFactory = function(isNgForm) {
return function() {
var formDirective = {
restrict: 'E',
require: ['form'],
compile: function() {
return {
pre: function(scope, element, attrs, ctrls) {
var form = ctrls[0];
var $addControl = form.$addControl;
var $removeControl = form.$removeControl;
var controls = [];
form.$addControl = function(control) {
controls.push(control);
$addControl.apply(this, arguments);
}
form.$removeControl = function(control) {
arrayRemove(controls, control);
$removeControl.apply(this, arguments);
}
form.$setPristine = function() {
element.removeClass(DIRTY_CLASS).addClass(PRISTINE_CLASS);
form.$dirty = false;
form.$pristine = true;
angular.forEach(controls, function(control) {
control.$setPristine();
});
}
},
};
},
};
return isNgForm ? angular.extend(angular.copy(formDirective), {restrict: 'EAC'}) : formDirective;
};
}
var ngFormDirective = formDirectiveFactory(true);
var formDirective = formDirectiveFactory();
angular.module('resettableForm', []).
directive('ngForm', ngFormDirective).
directive('form', formDirective).
directive('ngModel', function() {
return {
require: ['ngModel'],
link: function(scope, element, attrs, ctrls) {
var control = ctrls[0];
control.$setPristine = function() {
this.$dirty = false;
this.$pristine = true;
element.removeClass(DIRTY_CLASS).addClass(PRISTINE_CLASS);
}
},
};
});
})(angular);
Step 2: Provide a method on your controller which resets the model
Please be aware that you must reset the model when you reset the form. In your controller you can write:
var myApp = angular.module('myApp', ['resettableForm']);
function MyCtrl($scope) {
$scope.reset = function() {
$scope.form.$setPristine();
$scope.model = '';
};
}
Step 3: Include this method in your HTML template
<div ng-app="myApp">
<div ng-controller="MyCtrl">
<form name="form">
<input name="requiredField" ng-model="model.requiredField" required/> (Required, but no other validators)
<p ng-show="form.requiredField.$errror.required">Field is required</p>
<button ng-click="reset()">Reset form</button>
</form>
<p>Pristine: {{form.$pristine}}</p>
</div>
</dvi>
EDIT... I'm removing my old answer, as it was not adequate.
I actually just ran into this issue myself and here was my solution: I made an extension method for angular. I did so by following a bit of what $scope.form.$setValidity() was doing (in reverse)...
Here's a plnkr demo of it in action
Here's the helper method I made. It's a hack, but it works:
angular.resetForm = function (scope, formName, defaults) {
$('form[name=' + formName + '], form[name=' + formName + '] .ng-dirty').removeClass('ng-dirty').addClass('ng-pristine');
var form = scope[formName];
form.$dirty = false;
form.$pristine = true;
for(var field in form) {
if(form[field].$pristine === false) {
form[field].$pristine = true;
}
if(form[field].$dirty === true) {
form[field].$dirty = false;
}
}
for(var d in defaults) {
scope[d] = defaults[d];
}
};
Hopefully this is helpful to someone.
Your form fields should be linked to a variable within your $scope. You can reset the form by resetting the variables. It should probably be a single object like $scope.form.
Lets say you have a simple form for a user.
app.controller('Ctrl', function Ctrl($scope){
var defaultForm = {
first_name : "",
last_name : "",
address: "",
email: ""
};
$scope.resetForm = function(){
$scope.form = defaultForm;
};
});
This will work great as long as your html looks like:
<form>
<input ng-model="form.first_name"/>
<input ng-model="form.last_name"/>
<input ng-model="form.address"/>
<input ng-model="form.email"/>
<button ng-click="resetForm()">Reset Form</button>
</form>
Maybe I'm not understanding the issue here, so if this does not address your question, could you explain why exactly?
Here I have found a solution for putting the from to its pristine state.
var def = {
name: '',
password: '',
email: '',
mobile: ''
};
$scope.submited = false;
$scope.regd = function (user) {
if ($scope.user.$valid) {
$http.post('saveUser', user).success(function (d) {
angular.extend($scope.user, def);
$scope.user.$setPristine(true);
$scope.user.submited = false;
}).error(function (e) {});
} else {
$scope.user.submited = true;
}
};
Just write angular.extends(src,dst) ,so that your original object is just extends the blank object, which will appear as blank and rest all are default.
Using an external directive and a lot of jquery
app.controller('a', function($scope) {
$scope.caca = function() {
$scope.$emit('resetForm');
}
});
app.directive('form', function() {
return {
restrict: 'E',
link: function(scope, iElem) {
scope.$on('resetForm', function() {
iElem.find('[ng-model]').andSelf().add('[ng-form]').each(function(i, elem) {
var target = $(elem).addClass('ng-pristine').removeClass('ng-dirty');
var control = target.controller('ngModel') || target.controller('form');
control.$pristine = true;
control.$dirty = false;
});
});
}
};
});
http://jsfiddle.net/pPbzz/2/
The easy way: just pass the form into the controller function. Below the form "myForm" is referenced by this, which is equivalent to $scope.
<div ng-controller="MyController as mc">
<ng-form name="myform">
<input ng-model="mc.myFormValues.name" type="text" required>
<button ng-click="mc.doSometing(this.myform)" type="submit"
ng-disabled="myform.$invalid||myform.$pristine">Do It!</button>
</ng-form>
</div>
The Controller:
function MyController(MyService) {
var self = this;
self.myFormValues = {
name: 'Chris'
};
self.doSomething = function (form) {
var aform = form;
MyService.saveSomething(self.myFromValues)
.then(function (result) {
...
aform.$setPristine();
}).catch(function (e) {
...
aform.$setDirty();
})
}
}

Resources