How to write re-usable HTML components with AngularJS - angularjs

New to Angular and, so far, I'm loving it but the learning curve seems pretty steep. What I want to do is wrap up a bit of simple business logic and build some re-usable DOM components to template common areas of my system. Specifically I am writing a simple survey application that has different question types. My goal is to get to the point that while I am in an ng-repeat block I can do something like this:
<div ng-repeat="question in questions">
<show-question></show-question>
</div>
Ideally I want to wrap all of the logic into that one statement to switch on question type and then pull from templateUrl for different HTML sets. So if a question.type = "text" it would pull the templateUrl of "templates/textQuestion.html" and be able to inject scope into that template file as it produces the DOM element.
Big question is, am I going about this correctly AT ALL? Is a directive the way to go, should I even try to do this all in one directive/tag? I am open to being schooled on this!
Small question is, if I am going the right direction, what is the correct implementation?
I have already tried putting some logic inside my directives like IF and SWITCH, but that doesn't appear to be valid.
Any and all (constructive) help is welcome.
Thanks all!

It's called a directive. There's a complete guide here: http://docs.angularjs.org/guide/directive
It allows you to make custom attributes, elements, CSS classes, and comments that turn into components.
I wouldn't pull separate templates for each question type, I'd use a different directive for each question type. Then you can switch between them using a parent directive.
Here is what a directive that loads different directives might look like:
app.directive('question', function($compile){
"use strict";
return{
restrict: 'E',
replace: true,
link: function(scope, element, attrs){
var render = function(){
var template = "";
switch(attrs.type){
case "truefalse":
template = '<truefalse></truefalse>';
break;
case "multiplechoice":
template = '<multiplechoice></multiplechoice>';
break;
case "essay":
template = '<essay></essay>';
break;
}
element.html(template);
$compile(element.contents())(scope);
}
attrs.$observe('type', function(value) {
render();
});
render();
}
};
});
Now you could use this as such:
<question ng-repeat="question in questions" type="question.type" ></question>
Assuming you had a directive for each type of question, you'd get different directives rendered. This is sort of similar to using ng-if or different templates or whatever but I like it more because I also get re-usable one-off components.

So if your scope variable questions has all the info for each question like
$scope.questions = [
{ type: 'input',
prompt: 'name'
}
];
Then you might have some html that looks like
<div question="question" ng-repeat="question in questions"></div>
And have a directive that looks something like
app.directive('question', function() {
return {
scope: {
question: '=' // Creates 2 way data binding with the object you passed in as attribute by the same name
},
link: function($scope, $element, $attrs) {
$scope.question; // This will be equal the object you passed in
// { type: 'input', prompt: 'name' }
// You can modify the dom or whatever here
}
};
});
If you want to have different prepared templates then you can inject the $templateCache into your directive
app.directive('question', function($templateCache) {
and then call them in your link function
link: function($scope, $element, $attrs) {
var template = $templateCache.get('path/to/template.html');
// and append it to the element
$element.append(template);
}
You'll have to play around with it a bit, but that's half the fun. Good luck!

Related

Angular directive from array element in ngRepeat

I am working on a project that will have any number of HTML cards, and the format and positioning of the cards is the same, but the content of them will differ.
My plan was to implement the content of each card as an angular directive, and then using ngRepeat, loop through my array of directives which can be in any order, displaying each directive in a card. It would be something like this:
inside my controller:
$scope.cards = [
'directice-one',
'directive-two',
//etc..
]
my directive:
.directive('directiveOne', function () {
return {
restrict: "C",
template: '<h1>One!!</h1>'
};
})
.directive('directiveTwo', function () {
return {
restrict: "C",
template: '<h1>Two!!</h1>'
};
})
//etc..
my html:
<div class="card" ng-repeat="item in cards">
<div class="{{item}}"></div>
</div>
But.. the directives don't render. So I just get a div with class="directive-one".
This might be a design flaw on my part, some sort of syntax error, or just a limitation of angular. I'm not sure.
I've also considered making a directive <card>, and then passing the templateUrl: into it, but that would cause me to lose my access to $scope and the javsacript capabilities that I would have if each card was it's own directive.
So, advise, code help, anything would be very helpful!
I choose directives only when I need to use them in HTML mark up. For example, assuming cards layout is same and it takes different information based on user preference.
HTML File
<my-card Name="First" Option="Myoptions"></Card>
<my-card Name="Second" Option="GenOptions"></Card>
Directive
angular.module("testapp").directive("MyCard", function() {
scope: {
name: '#',
Option: '#'
Controller: "myCardController",
templateURL: "~/myCard/myCardTemplate.html"
}
});
In Template you can implement the information passed from HTML page via the directive.
Hope this helps.
Do take note that the above approach is preferred when you are developing a framework sort of things. For example you develop a web framework and the header takes 5 parameters and these 5 parameters needs to be passed via mark up. Most important thing is that the framework/header is independent
In your controller, you need to require the directive modules. Then assign them to a scope variable which would be that array you have. Will update with code when I get to desktop, tried doing with phone kinda tuff.

How to not refer to the controllerAs name in directive

Here is the minimal code to describe the issue. On the page, I have:
<div ng-controller='AController as a'>
<div a-directive></div>
</div>
In js, I have:
app.directive("aDirective", function($compile){
return {
scope: true,
link: function(scope, element, attrs) {
var template = "<h1>{{a.label}}</h1>";
element.append($compile(template)(scope));
}
}
});
app.controller("AController", function($scope){
self = this;
self.label = "some text";
});
That works, but the issue is that {{a.label}}, which made the view and controller/model tightly coupled. Is there any way to get rid of that a., and not to mention the controllerAs-name in the directive code at all? (just like what I did in the controller code)
To improve this you can pass the text to display as a parameter to the directive. Something like this is the initial idea:
<div a-directive="a.label"></div>
However, I DO recommend using an alias for the controller, so I made a Plunker where you can see all of this working together with some improvements.
Check it out here: http://plnkr.co/edit/1hBSBxwSEPXoj9TULzRQ?p=preview
I would also recommend to use template instead of link and restricting the directive to an element instead of using it as attribute, since it is modifying the DOM. But yeah, you could keep improving it till the end of the times :)

AngularJS : directives which take a template through a configuration object, and show that template multiple times

I'm looking to create a custom directive that will take a template as a property of a configuration object, and show that template a given number of times surrounded by a header and footer. What's the best approach to create such a directive?
The directive would receive the configuration object as a scope option:
var app = angular.module('app');
app.directive('myDirective', function() {
return {
restrict: 'E',
scope: {
config: '=?'
}
...
}
}
This object (called config) is passed optionally to the directive using two way binding, as show in the code above. The configuration object can include a template and a number indicating the number of times the directive should show the template. Consider, for example, the following config object:
var config = {
times: 3,
template: '<div>my template</div>'
};
It would, when passed to the directive, cause the directive to show the template five times (using an ng-repeat.) The directive also shows a header and a footer above and below the template(s):
<div>the header</div>
<div>my template</div>
<div>my template</div>
<div>my template</div>
<div>the footer</div>
What's the best way to implement this directive? Note: When you reply, please provide a working example in a code playground such as Plunker, as I've run into problems with each possible implementation I've explored.
Update, the solutions I've explored include:
The use of the directive's link function to append the head, template with ng-repeat, and footer. This suffers from the problem of the template not being repeated, for some unknown reason, and the whole solutions seems like a hack.
The insertion of the template from the configuration object into middle of the template of the directive itself. This proves difficult because jqLite seems to have removed all notion of a CSS selector from its jQuery-based API, leading me to wonder if this solution is "the Angular way."
The use of the compile function to build out the template. This seems right to me, but I don't know if it will work.
You could indeed use ng-repeat but within your directive template rather than manually in the link (as that wouldn't be compiled, hence not repeated).
One question you didn't answer is, should this repeated template be compiled and linked by Angular, or is it going to be static HTML only?
.directive('myDirective', function () {
return {
restrict: 'E',
scope: {
config: '=?'
},
templateUrl: 'myTemplate',
link: function(scope) {
scope.array = new Array(config.times);
}
}
}
With myTemplate being:
<header>...</header>
<div ng-repeat="item in array" ng-bind-html="config.template"></div>
<footer>...</footer>
I'd think to use ng-transclude in this case, because the header & footer wrapper will be provided by the directive the inner content should change on basis of condition.
Markup
<my-directive>
<div ng-repeat="item in ['1','2','3']" ng-bind-html="config.template| trustedhtml"><div>
</my-directive>
Directive
var app = angular.module('app');
app.directive('myDirective', function($sce) {
return {
restrict: 'E',
transclude: true,
template: '<div>the header</div>'+
'<ng-transclude></ng-transclude>'+
'<div>the footer</div>',
scope: {
config: '=?'
}
.....
}
}
Filter
app.filter('trustedhtml', function($sce){
return function(val){
return $sce.trustedHtml(val);
}
})

AngularJS - Generate HTML on the fly based on a directive

Say I want to generate some Html on the fly but based on the same definition in my directive, how would I achieve this?
My directive is currently expecting JSON:
app.directive('myDirective', [function () {
return {
template: '<div>A far more impressive template lives here...</div>',
restrict: 'AE',
scope: {
eventData: '='
},
link: function ($scope, $element, $attrs) {
//some cool functions...
}
};
}]);
Within my ng-repeat element, I have this kinda thing, where the event-data contains the named scope item from the ng-repeat, clause.
<div ng-repeat="event in myCtrl.events">
<my-directive event-data="event"></my-directive>
</div>
So how do I go about manually passing in some JSON to use as the data attribute so I can then generate fragments elsewhere, e.g. outside a scoped ng-repeat?
What I am hoping to do is add a function to a controller of mine which just returns a new instance of my directive and then I can plant that somewhere else (I am actually hoping to use as the content of a Google Maps info window).
I hope I have made some sort of sense here!
Thanks

Angular binding inside an inline ckeditor

I'm using inline editing with CKEditor, and I'd like to bind an element to an angular scope value.
<div contentEditable="true">
<p>Here is the value: {{testval}}</p>
</div>
testval should update in the same manner as it would outside the editor.
To protect this text in the editor, I'd like to do something similar to the placeholder plugin. In other words I plan to have a placeholder, dynamically displaying the final text rather than just the placeholder.
I've seen several examples of how to bind the entire contents with angular, but not individual elements. I'm still fairly new to both angular and ckeditor, so any help or pointers would be much appreciated.
It sounds to me like you will need to use a directive for what you want. I might be soewhat off because I'm not completely familiar, but goig by what you've provided, let's assume this example.
html
<body ng-app="myApp">
<div content-editable content="content"></div>
</body>
javascript
angular.module('myApp', [])
.directive('contentEditable', function() {
restrict: 'A',
replace: true,
scope: {
// Assume this will be html content being bound by the controller
// In the controller you would have:
// $scope.content = '<div>Hello World</div>'
content: "="
},
template: '<div contentEditable="true"><p>Here is the value {{ content }}</p></div>'
});
Still not sure if I completely comprehend, but let me know if I'm getting closer.
I assume that you want to bind the HTML text in model to the element. I used ng-bind-html to render what is in the model and I created the directive ck-inline to add the inline feature and bind the model to the changes that happen in the inline editor. This directive requires a ng-bind-html to work and you also need to have ngSanitize added to your module. Add directive ck-inline to your element and
I also use $timeout because I noticed that if I don't the text is rendered and then ckeditor somehow deletes all the values which messes up the model (this does not happen with the non-inline option). Here is the code.
yourModule.directive('ckInline', ['$sce', '$timeout', function($sce, $timeout){
return{
require : '?ngBindHtml',
scope:{value:"=ngBindHtml"},
link : function(scope, elm, attr, ngBindHtml)
{
$timeout(function()
{
var ck_inline;
elm.attr("contenteditable", "true");
CKEDITOR.disableAutoInline = true;
ck_inline = CKEDITOR.inline(elm[0]);
if (!attr.ngBindHtml)
return;
ck_inline.on('instanceReady', function()
{
ck_inline.setData(elm.html());
});
function updateHtml()
{
scope.$apply(function()
{
scope.value = $sce.trustAsHtml(ck_inline.getData());
});
}
ck_inline.on('blur', updateHtml);
ck_inline.on('dataReady', updateHtml);
});
}
};
}]);

Resources