Angular 1.5: dynamically load a component - angularjs

I am trying to create some sort of generic gridview using AngularJS 1.5 and its components. A (pseudocode) version of what I've got going right now:
// inside <my-grid-component data="data" metadata="metadata">
<div ng-repeat="item in $ctrl.data">
<my-row-component item="item" metadata="$ctrl.metadata"></my-row-component>
</div
// inside <my-row-component item="item" metadata="metadata">
<div ng-repeat="column in $ctrl.metadata.columns">
<my-cell-component value="$ctrl.item[column]"></my-cell-component>
</div>
Now <my-cell-component>could have some basic ng-switchstatement that handles the obvious cases, like if the value is text or an image or something, but since this will be used by many people and on many projects, it is possible that someone will want to do something fancy and/or highly specific inside a cell. They could just ammend <my-cell-component> with more ng-switches, but then they are messing with base framework code which hurts maintainability.
So, ideally, I'd want to make something where a developer can optionally provide his own custom template for a specific field in the metadata, e.g. metadata.columns[3].customCellComponentName = 'some-custom-template';
Then <my-row-component> would look something like this:
<div ng-repeat="column in $ctrl.metadata.columns">
<div ng-if="!column.isCustomCellComponent">
<my-cell-component value="$ctrl.item[column]"></my-cell-component>
</div>
<div ng-if="column.isCustomCellComponent">
??? --> <column.customCellComponentName value="$ctrl.item[column]"></column.customCellComponentName>
</div>
</div>
The project automatically puts all templates in $templateCache, so resolving the template should not be a problem, but other than that, the marked line with the "???" obviously does not work. It demonstrates what I would like to achieve, but I have no idea how to actually do something like this. I looked into transclusion, ng-include and other solutions, but none seem to provide the option to dynamically load a template AND model-bind some data to it.
Any and all ideas very much welcome. I would like to stay as far away from overly complex directives as possible. While they allow you to do many things, in my experience they are also a debugging and maintainability nightmare.
Thanks.

You can create a directive for your <my-cell-component> such a way that it either accepts a templateUrl or it determines the templateUrl and uses it. Let me give you an example for the latter,
angular.module('myApp')
.directive('myCellComponent', ['CELL_TYPE', function (CELL_TYPE) {
return {
restrict: 'E',
scope:{
cellType: '='
},
template: '<div ng-include="templateUrl"></div>',
link: function (scope) {
function setTemplate(cellType) {
scope.templateUrl = CELL_TYPE[cellType.value].templateUrl;
// or figure out some other way to determine templateUrl
}
scope.$watch(function () {
return scope.cellType;
}, function (newVal) {
if(newVal) {
setTemplate(scope.cellType);
}
});
}
};
}]);
So, we've got the directive's template having an ng-include that uses the templateUrl determined on the basis of some constants, say CELL_TYPE.
Now you've got a directive that dynamically loads its template based on your attributes!
You can (obviously) get rid of $watch if dynamically changing the templateUrl isn't applicable to your use-case.

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

Multiple use of a template in the same page in AngularJS

I have some repetitive components on my AngularJS page such as billingAddress, shippingAddress and primaryAddress. I have created a separate template file for address components and expected to be able to use ng-include to include the template thrice on my page. I am unable to find documentation around passing models to templates. What I am looking for is something like
<div ng-include="address.tpl.html" ng-model="{address: primaryAddress}"></div>
<div ng-include="address.tpl.html" ng-model="{address: billingAddress}"></div>
<div ng-include="address.tpl.html" ng-model="{address: shippingAddress}"></div>
Is this even possible as of now?
his is for what directives are made.
angular.module('docsSimpleDirective', [])
.directive('myAddress', function() {
return {
scope: {
address : '='
},
templateUrl: 'address.tpl.html'
};
});
Then in your template simple use the $scope.address.
On declaring the directive you should use it like this.
<my-address address="primaryAddress"><my-address>
<my-address address="billingAddress"><my-address>
<my-address address="shippingAddress"><my-address>

How to avoid "sausage" type binding when having nested model

I have nested model and I am trying to avoid vm.someObject.someChild.ChildOfChild.name type of situations. Is there a way to set the model for outer <div> so that I can instead do ChildOfChild.name or even name. In Silverlight this was called DataContext. I put "vm" on the $scope, but in html I would like to avoid having to type the full path to attribute.
For example:
<div>
{{someObject.Id}}
<div>
{{someObject.name.first}}
{{someObject.name.last}}
</div>
<div>
{{someObject.someChild.name.first}}
</div>
</div>
I would like to do something like this
<div datacontext = someObject>
{{Id}}
<div datacontext = name>
{{first}}
{{last}}
</div>
<div datacontext = someChild.name>
{{first}}
</div>
</div>
You can do this with a custom directive.
HTML:
<div ng-app="myApp" ng-controller="myCtrl as ctrl">
<div>
Access from deepObj: {{ctrl.deepObj.one.two.three.four}}
</div>
<div scope-context="ctrl.deepObj.one.two.three">
Access from third level: {{four}}
</div>
</div>
JS:
var myApp = angular.module('myApp', []);
var myCtrl = function() {
this.deepObj = {one: {two: {three: {four: "value"}}}};
};
myApp.directive('scopeContext', function () {
return {
restrict:'A',
scope: true,
link:function (scope, element, attrs) {
var scopeContext = scope.$eval(attrs.scopeContext);
angular.extend(scope, scopeContext);
}
};
});
See the documentation on $compile for information on what scope: true does.
Make sure you don't call the directive something like data-context as an attribute starting with data- has a special meaning in HTML5.
Here is the plunker: http://plnkr.co/edit/rMUQlaNsH8RTWiRrmohx?p=preview
Note that this can break two-way bindings for primitive values on the scope context. See this plunker for an example: http://plnkr.co/edit/lCuNMxVaLY4l4k5tzHAn?p=preview
You could try/abuse ng-init
Try ng-init, you'll have one more ., but it's better than the other answer I've seen proposed:
<div ng-init="x = foo.bar.baz">
{{x.id}}
{{x.name}}
</div>
BUT Be warned, doing this actually creates a value on your scope, so doing this with something like ng-model if you're reusing the same name, or in a repeater, will produce unexpected results.
Why a custom directive for this probably isn't a good idea
What #rob suggests above is clever, and I've seen it suggested before. But there are issues, which he touches on, in part at least:
Scope complexity: Adding n-scopes that need to be created (with prototypical inheritence) whenever views are compiled.
View processing complexity: Adding an additional directive (again for no real functional benefit) that needs to be checked on each node when the view is compiled.*
Readability? The next Angular developer will likely less readable because it's different.
Forms Validation: If you're doing anything with forms in Angular, this might break things like validation.
ng-model woahs: Setting things with ng-model this way will not be at all intuitive. You'll have to use $parent.whatever or $parent.$parent.whatever depending on how may contexts deep you are.
* For reference, views are $compiled more than you think: For every item in a repeater, whenever it's changed for example.
A common idea that just doesn't jive with Angular
I feel like this question comes up frequently in StackOverflow, but I'm unable to find other similar questions ATM. ... regardless, if you look at the approaches above, and the warnings given about what the side effects will be, you should be able to discern it's probably not a good idea to do what you're trying to do just for the sake of readability.

How to write re-usable HTML components with 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!

Resources