Custom elements inside AngularJS directive? - javascript

I'd like to avoid repeating myself with my page header as much as possible. So, I'm thinking doing something like this:
<header>
<title>Forms</title>
<actions>
<action ng-click="showNewFormModal()">New Form</action>
<action ng-click="showHelp()">?</action>
</actions>
</header>
which would desirably result in
<div class="page-header">
<div class="left">
<h3>Forms</h3>
</div>
<div class="right">
<a class="btn btn-default" role="button" ng-click="showNewFormModal()">New Form</a>
<a class="btn btn-default" role="button">?</a>
</div>
<div class="clearfix"></div>
</div>
As you can see, <header> contains custom elements, <title>, <actions>, and <action>, which are specific to only the <header> element.
Is such a thing possible in Angular? How would I accomplish this?

See this jsbin I put together which shows how it can be done with custom directives. I had trouble using the names header and title (though I'm almost positive it can be done), for the example, I've user my-header and my-title
Anyways, I've put together the first two elements, the rest will follow the same convention. You can also set up dependencies and scope on each directive, i.e. an action must be inside of an actions parent element
app.directive('myHeader', function() {
return {
restrict: "E",
transclude: true,
template: "<div class='page-header'><div ng-transclude></div></div>"
};
});
app.directive('myTitle', function() {
return {
restrict: "E",
transclude: true,
template: "<div class='left'><div ng-transclude></div></div>"
};
});
Your HTML will look like:
<my-header>
<my-title>Forms</my-title>
</my-header>
And the generated HTML will look like:
<my-header>
<div class="page-header">
<div ng-transclude="">
<my-title class="ng-scope">
<div class="left">
<div ng-transclude="">
<span class="ng-scope">Forms</span>
</div>
</div>
</my-title>
</div>
</div>
</my-header>

Related

Dynamically add element to DOM in angular

This is how my page looks like on initial load
<body>
<div class="col-md-12" id="dataPanes">
<div class="row dataPane"> Chunk of html elements </div>
</div>
<div class"col-md-12 text-right">
<input type="button" class="btn btn-primary" value="Add dynamic row" ng-click="addElementChunk()" />
</body>
I am in need to add rows to div#dataPanes on button click
If I was using jQuery,addElementChunk() function would have looked as below
var addElementChunk = function()
{
var html = "<div class='row dataPane'> Chunk of html elements </div>";
$("#dataPanes").append(html);
}
but how do I implement the same in angular??
You need to use $compile
Compiles an HTML string or DOM into a template and produces a template function, which can then be used to link scope and the template together.
and $sce
Strict Contextual Escaping (SCE) is a mode in which AngularJS constrains bindings to only render trusted values. Its goal is to assist in writing code in a way that (a) is secure by default, and (b) makes auditing for security vulnerabilities such as XSS, clickjacking, etc. a lot easier.
addElementChunk = function(){
var html = '<div class="row dataPane"> Chunk of html elements </div>';
var trustedHtml = $sce.trustAsHtml(html);
var compiledHtml = $compile(trustedHtml)($scope);
angular.element(document.getElementById('dataPanes')).append(compiledHtml);
}
you can append new div using angular ng-repeat directive
lets say you have an array that contain one element and every time you click the button you add another element to the array, while you are repeating it in your "dataPane" div
so you code could be:
HTML
<div ng-app="myApp" ng-controller="myCtr">
<div class="col-md-12" id="dataPanes">
<div class="row dataPane" ng-repeat="element in added_elements"> Chunk of html elements ( {{element}} ) </div>
</div>
<div class="col-md-12 text-right">
<input type="button" class="btn btn-primary" value="Add dynamic row" ng-click="addMoreElements()" />
</div>
</div>
JS
angular
.module('myApp', [])
.controller('myCtr', ['$scope', function($scope) {
$scope.added_elements = ["elem 1"];
$scope.addMoreElements = function(){
$scope.added_elements.push("elem "+ ($scope.added_elements.length+1));
}
}])
so you can add whatever data you want about your repeated row and bind it in html in simple way without having to repeat the whole html code
Working Demo
You can also append a new html element in this way. I think its very easy to write and also understand. hope it will help you.
angular.element used to access the html element.
Here is the html code:
angular.module('myApp',[]).controller('myCtrl', function($scope){
$scope.addElementChunk = function()
{
var htmlStr = '<div class="row dataPane"> Chunk of html elements </div>';
debugger;
angular.element(document.getElementById('dataPanes')).append(htmlStr);
}
});
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/angular.js/1.0.3/angular.min.js"></script>
<div ng-app="myApp" ng-controller="myCtrl">
<div class="col-md-12" id="dataPanes">
<div class="row dataPane"> Chunk of html elements </div>
</div>
<div class="col-md-12 text-right">
<input type="button" class="btn btn-primary" value="Add dynamic row" ng-click="addElementChunk()" />
</div>
</div>
Here is the fiddle link

Directive to externalize view not working

I'm using pageslide directive in order to display a lateral sliding menu when I click on an icon.
This is the usual way to do it, and its working :
<div class="navbar-header">
<span ng-controller="slideController as s">
<a class="navbar-brand" href="javascript:void(0)"
ng-click="s.toggle()"><i class="fa fa-bars fa-lg"></i></a>
<div pageslide ps-open="s.isActive" ps-side="left">
<div style="padding:20px">
<h2>Hello Pageslide</h2>
<p>Put here whatever I want in the lateral menu</p>
<a ng-click="s.toggle()" class="button" >Close</a>
</div>
</div>
</span>
<a class="navbar-brand text-danger" href="javascript:void(0)">Balrog</a>
</div>
But I would lilke to make the menu content in an external template, so I'm trying this :
<div class="navbar-header">
<span ng-controller="slideController as s">
<a class="navbar-brand" href="javascript:void(0)"
ng-click="s.toggle()"><i class="fa fa-bars fa-lg"></i></a>
<menu-item isActive="s.isActive"></menu-item>
</span>
<a class="navbar-brand text-danger" href="javascript:void(0)">Balrog</a>
</div>
with this as menu.html :
<div pageslide ps-open="s.isActive" ps-side="left">
<div style="padding:20px">
<h2>Hello Pageslide</h2>
<p>Put here whatever I want in the lateral menu</p>
<a ng-click="s.toggle()" class="button" >Close</a>
</div>
</div>
also, here is the related controller :
'use strict';
angular.module('BalrogApp').controller('slideController', function(){
this.isActive= false; // This will be binded using the ps-open attribute
this.toggle = function(){
this.isActive= !this.isActive
}
});
and the <menu-item> directive :
angular.module('BalrogApp').directive('menuItem', function () {
return {
restrict: 'E',
templateUrl: "views/menu.html",
scope: {
isActive: '='
},
controller: 'slideController',
controllerAs: 's',
bindToController: true
}
});
So it's not working this way, maybe it is because the main view (including the <menu-item>) is itself included in the main page thanks to <ng-view> ?
Or maybe the pageslide-directive requires the <div pageslide ...> and its parent element to be on the same file ?
I can tell from the console that toggle() is called and changes isActive in both cases but it's not opening the menu with the directive version.
And also, adding ng-controller="slideController as s" to the root <div> with the pageslide attribute didn't change anything.
So how can I make this work with the menu in another file ?
Did you try to replace <div ng-include="menu.html"></div> with <div ng-include="'menu.html'"></div>. According to https://docs.angularjs.org/api/ng/directive/ngInclude, you have to wrap string constants in single quotes.
I highly recommend you not use ng-include. You can get the same approach using directive instead.
For example:
Menu html
<div pageslide ps-open="vm.checked" ps-side="left">
<div style="padding:20px">
<h2>Hello Pageslide</h2>
<p>Put here whatever I want in the lateral menu</p>
<a ng-click="vm.toggle()" class="button" >Close</a>
</div>
</div>
Directive:
function Controller() {
}
Controller.prototype.toggle = function() {
this.checked = !this.checked;
}
angular
.module('BalrogApp')
.directive('menuItem', function() {
return {
templateUrl: 'urltomenu.html',
restrict: 'E',
scope: {
checked: '='
},
controller: Controller,
controllerAs: 'vm',
bindToController: true
}
})
Main template
<div class="navbar-header">
<span ng-controller="slideController as s">
<a class="navbar-brand" href="javascript:void(0)"
ng-click="s.toggle()"><i class="fa fa-bars fa-lg"></i></a>
<menu-item checked="s.checked"><menu-item>
</span>
<a class="navbar-brand text-danger" href="javascript:void(0)">Balrog</a>
</div>
This approach give you more power. All the logic is inside the directive controller and you could reuse this element each time. The scope attribute checked is the "bridge" between your main template if change outside also change inside and also the other way.

Creating Directive for two views

I am trying to create a directive for two views. basically we have a modal, with header and footer. When we click on continue button in footer, the modal body should be alone changed displaying question 2 as a new view with header and footer fixed. How would i achieve this using directives?
Do i need to create individual directives for each view?
Here is my code
modal.html -
<section>
<header class="modal-header">
</header>
<div class="modal-body">
question 1
</div>
<footer>
<button>{{'BACK' | translate}}</button>
<button type="submit" ng-click="showQuestion(2)">{{'CONTINUE' | translate}}</button>
</footer>
</section>
directive.js
'use strict';
angular.module('OrderApp').directive('modalQuestions', [function () {
function getQuestions() {
return {
restrict: 'A',
templateUrl: 'modules/common/modals/modal.html',
scope: true
};
}
]);
It sounds like you may want to use the ngSwitch directive:
<section>
<header class="modal-header">
</header>
<div class="modal-body" ng-switch="questionNumber">
<div ng-switch-when="1">
question 1
</div>
<div ng-switch-when="2">
question 2
</div>
</div>
<footer>
<button>{{'BACK' | translate}}</button>
<button type="submit" ng-click="showQuestion(2)">{{'CONTINUE' | translate}}</button>
</footer>
</section>
Then on your controller:
$scope.questionNumber = 1;
$scope.showQuestion = function (questionNumber) {
$scope.questionNumber = questionNumber;
}
When $scope.questionNumber is updated, ngSwitch will change which of the divs it shows. Everything else on the page will remain the same.

Directive link function does not have access to the entire template DOM

I have a directive which has a template that recursively include a template. In my directive link function, I am unable to get the complete DOM with a selector.
Here is my directive. Notice that my directive try to call dropdown() function on all .ui.dropdown divs constructed so nested dropdown will be activated.
.directive("floatingDropdown", function() {
return {
restrict: 'E',
templateUrl: "scripts/Ui/FloatingDropdown.html",
replace: true,
scope: {
uiClass: '#',
model: '=ngModel',
optionTree: '='
},
link: function(scope, elem, attrs) {
scope.elemClass = scope.uiClass || "ui floating dropdown icon button";
$(elem).dropdown();
$(elem).find(".ui.dropdown").dropdown();
}
}
})
The scripts/Ui/FloatingDropdown.html contains a nested include. This creates multiple levels of dropdowns
<div class="{{elemClass}}">
<script type="text/ng-template" id="node_template.html">
<div class="ui dropdown" ng-if="option.options">
<span ><i class="dropdown icon"></i> {{option.value}}</span>
<div class="menu" ng-if="data.options">
<div class="item" ng-repeat="option in data.options" ng-include="'node_template.html'"></div>
</div>
</div>
<span ng-if="!option.options" ng-click="model=option">{{option}}</span>
</script>
<i class="dropdown icon"></i>
<div class="menu">
<div class="item" ng-repeat="option in optionTree.options" ng-include="'node_template.html'">
</div>
</div>
</div>
My problem is $(elem).find(".ui.dropdown") will not find the recursively generated divs by ng-include
By attempting to do DOM manipulation in a directive's link() method like that, you're trying to query/modify a part of the DOM that hasn't been rendered yet.
You need to defer those jquery calls until later. You can do this using:
$scope.$evalAsync(function() {
// DOM code
});
or
$timeout(function() {
// DOM code
}, 0);
Using $evalAsync will run the expression during the next $digest cycle, will allow you to modify HTML before it's rendered in the browser. Using $timeout will wait until all $digest cycles are complete.

Directive template with Input + ng-model = magic?

I'm quite frustrated and apologize in advance for poorly formulated question.
I've created derictive for simple list editing:
angular.module('myApp').
directive('variableList', function () {
return {
restrict: 'AE',
templateUrl: 'variableList.html',
replace: true,
scope: {
value: '='
},
controller: [
'$scope', '$element', '$attrs', '$transclude',
function($scope) {
$scope.removeListItem = function (index) {
$scope.value.splice(index, 1);
};
$scope.addListItem = function () {
$scope.value.push($scope.nextListItem);
$scope.nextListItem = null;
};
}
]
};
});
and template
<div class="variable-list">
<div class="variable-list-items">
<div class="row collapse variable-list-item" ng-repeat="(index, val) in value">
<div class="small-11 columns variable-list-item-value">
<input type="text" ng-model="val" />
</div>
<div class="small-1 columns">
<button class="button alert prefix no-margin icon-minus"
ng-click="removeListItem(index)"></button>
</div>
</div>
</div>
<div class="row collapse variable-list-controls">
<div class="small-11 columns">
<input type="text" ng-model="nextListItem" />
</div>
<div class="small-1 columns">
<button ng-class="{disabled: !nextListItem}"
ng-click="addListItem()"
class="button success prefix no-margin icon-plus"></button>
</div>
</div>
</div>
the important part of template if
<input type="text" ng-model="val" />
In the end I have quite working ui
But inputs for existings items doesnt work! Nothing happen when I try to edit them. Input for new item, add and remove buttons works as intended.
Any ideas?
Edit
I've tried to bind model like this
<input type="text" ng-model="value[key]" />
I was able to edit input but it caused even more magic, after first keypress input loses focus.
Found answer here https://github.com/angular/angular.js/issues/1267
Basically you have to have a . in ng-model or the revers data binding does not work on primitives.

Categories

Resources