Mixing Angular with MVC partial views - javascript

What I need
A chain of screens that each open the next screen on a button click. Each previous screen must be collapsed and the new screen must be added by loading a partial view from the MVC backend.
What I have
An AngularJS controller with the following function:
self.AddChild = function (uri, targetContainerId, collapseTitle, breadCrumbContainerId) {
var target = $("#" + targetContainerId);
if (target != 'undefined' && target != undefined && target.length > 0) {
apiService.Get(uri).then(function (viewData) {
self.CollapsePreviousChild(self.ChildCount);
// Increase childcount by 1
self.ChildCount += 1;
// Set HTML data
var html = '<div id="collapsibleScreen-"' + self.ChildCount + ' class="open">' + viewData + '</div>';
target.html(html);
// Update screens collapse status
self.UpdateScreenBreadCrumb(collapseTitle, breadCrumbContainerId);
});
};
}
The UpdateScreenBreadCrumb function works and is otherwise unrelated.
It is called (for instance) like this:
self.AddChild("/Partials/View1", "targetContainer", "View", "breadCrumbContainer");
What it does
The content of the View, a form, is loaded, the breadcrumb is updated correctly.
What I need fixed
On the partial view that was loaded, there is a button defined like this:
<button class="btn btn-primary" ng-click="AddPartialView()">Add partial view</button>
Clicking that button has no effect whatsoever. If I add a console.log('Code was here.') to the
AddPartialView(), it is not logged. Setting the ng-click value directly to alert('hello') has no effect either.
There are no errors of any kind visible.
Any suggestions on how to make this button work?

In regards to your question, you are adding HTML that isn't compiled by Angular. You need to use $compile on your newly added HTML element and then bind it to a scope. The $compile() function returns a link() function which you use to bind a scope to. Example:
$compile(new-element)(scope-to-bind-to)
NOTE: You should not be manipulating the DOM via a controller. This is considered bad practice. You should be using a custom directive or some combo of Angular directives (ngIf, ngSwitch, ngInclude). I recommend watching AngularJS best practices.

I've looked into $compile, as suggested by Itamar L. and got it to work. The samples I found were using Directives as well, so I implemented them anyway:
angular.module('directives.api').directive("PartialViewLoader", [
'$compile',
'chainedScreensService',
function (
$compile,
chainedScreensService) {
return {
restrict: 'A',
scope: {
view: '=',
parent: '='
},
controller: function() {
},
link: function (scope, element, attrs) {
chainedScreensService.GetPartialView(attrs.view).then(function (viewData) {
var linkFunc = $compile(viewData);
var content = linkFunc(scope);
element.append(content);
if (attrs.parent != 'undefined' && attrs.parent != undefined && attrs.parent.length > 0) {
chainedScreensService.CollapsePartialByIdentifier(attrs.parent);
}
});
}
}
}
]);
I use it like this:
<div ng-controller="collapseController">
<div id="breadCrumbContainer" style="display: inline"></div>
<div id="mainContainer">
<div id="personContainer" partial-view-loader view="persoon" parent="" class="open"></div>
</div>
</div>
That in itself displays the first page, which has a button to the next, as mentioned. The associated function, found in the collapseController, is this:
self.AddNextScreen = function (parentViewIdentifier, targetContainerId, breadCrumbContainerId) {
self.AddChildByDirective("NextScreen", parentViewIdentifier, targetContainerId, breadCrumbContainerId);
}
The code for AddChildByDirective:
self.AddChildByDirective = function (viewIdentifier, parentViewIdentifier, targetContainerId, breadCrumbContainerId) {
var html = '<div id="' + viewIdentifier + 'Container" fvl-partial-view-loader view="' + viewIdentifier + '" parent="' + parentViewIdentifier + '" class="open"></div>';
var target = $('#' + targetContainerId);
var linkFunc = $compile(html);
var content = linkFunc($scope);
target.append(content);
self.UpdateScreenBreadCrumb(viewIdentifier, breadCrumbContainerId);
}
At this point I still need to test actual chaining, but this works to load a new screen and collapse the previous.

Related

AngularJS: Add inline custom code using a directive

Here's the scenario.
In the app, you can add inline custom code (HTML attributes ex. style="", onclick="alert('Test')") in an element (ex. input texts, divs). The custom code is binded to the main model and loaded to the element using a custom directive I've created. I'm doing this to control dynamically generated fields that I want to hide and show based on different inputs.
This is my custom directive that loads inline attributes on the element:
app.directive('addCustomHtml', function() {
return {
scope: {
customHtml: "="
},
link: function(scope, element, attributes){
scope.$watch('customHtml', function(newVal, oldVal) {
if (newVal) {
var attrs = newVal.split('\n');
for (var i = 0; i < attrs.length; i++) {
var result = attrs[i].split('=');
var attr = result.splice(0,1);
attr.push(result.join('='));
if (attr[1]) {
element.attr(attr[0], attr[1].replace(/^"(.*)"$/, '$1'));
}
}
} else {
if (oldVal) {
var attrs = oldVal.split('\n');
for (var i = 0; i < attrs.length; i++) {
var attr = attrs[i].split('=');
if (attr[0]) {
element.removeAttr(attr[0]);
}
}
}
}
})
}
}
});
It is binded to the element like this:
<input type="checkbox" add-custom-html custom-html="checkbox1.customHtml">Yes
To see it in action, you can check the plunkr here: https://plnkr.co/edit/xjjMRPY3aE8IVLIeRZMp?p=preview
Now my problem is, when I try to add AngularJS directives (ex. ng-show, ng-if) using my custom directive, AngularJS doesn't seem to recognize them and the model scope I'm passing inside.
Another problem is when I try to add vanilla Javascript event functions (ex. onclick="", onchange=""), it does work but sometimes AngularJS does not read them especially when the element has an ng-change, ng-click attributes.
Again, I am doing this approach on the app because I have generic fields and I want to control some of them by adding this so called "custom codes".
Any help would be highly appreciated!!
If you want to add HTML code and compile it within current $scope, you should use the $compile service:
let someVar = $compile(yourHTML)($scope);
// you can now append someVar to any element and
// angular specific markup will work as expected
Being a service, you'll need to inject it into current controller (or pre/post link function) to be able to use it.

Appending a list iterating in AngularJS to the HTML DOM

So I have an AngularJS function that when called should add a list element to a div in the DOM, and the list should be ng-repeat so that it could iterate on objects from a list.
These objects in the list contain a few properties which I want to print out.
Part of the AngularJS program
var el = angular.element(document.getElementById('categories'));
el.append('<ul id="' + categoryItemStr + '"><li ng-repeat="item in categories[currentCategory]"><h4 class="h4">{{item.itemName}}</h4>{{item.itemDescription}}<br><span style="font-size:11px;">{{item.itemPrice}}.00 $</span></li></ul>');
But when I run it it's not ng-repeatable and it looks like the javascript hasn't rendered. It's important to note that I have the same HTML already in the document when the page loads and it works fine when it doesn't come out of an angular function but is written inside the HTML document.
How do I fix this?
But when I run it it's not ng-repeatable and it looks like the javascript hasn't rendered.
If you want to let to AngularJs to know about your DOM with directive ng-repeat you need compile it first by using $compile.
Lets say this is your root:
<div id="categories" some-dir></div>
where some-dir directive is:
app.directive('someDir', function($compile) {
return {
restrict: 'A',
link: function(scope, elm, attrs) {
scope.categories = [{
itemName: "itemName",
itemPrice: 11
}];
var categoryItemStr = 'someId';
var el = angular.element(document.getElementById('categories'));
el.append('<ul id="' + categoryItemStr + '"><li ng-repeat="item in categories[currentCategory]"><h4 class="h4">{{item.itemName}}</h4>{{item.itemDescription}}<br><span style="font-size:11px;">{{item.itemPrice}}.00 $</span></li></ul>');
var e = angular.element(el);
$compile(e.contents())(scope);
elm.replaceWith(e);
}
};
});
Some simple Demo in Fiddle

AngularJS doesn't parse dynamically loaded data from directive

I am trying to create N number of Select controls dynamically from directive based on array that is passed in from the attribute (where N is the length of the array).
Structure of an object of the array is as such:
selectDescription = {
array: arrayObject, //ng-options, a string as 'item as item.name for item in selectArray[0]'
change: methodName, //ng-change, actionname
level: levelNumber //level number
}
So the number of select controls inside span tag depends on the number of selectDescription(s) that I get from the attribute.
First select control is rendered successfully. Subsequent select controls should have been rendered on select of an option from previous rendered select controls. But it's not happening in my case. Although I am successfully appending angular elements in the current inputEl(on select of an option), it is not being rendered in the UI. I guess I am missing something very crucial.
On change of selectDescriptions, a flipped attribute is set, through which I am able to call scope.$editable.render() from link, which in turn runs render function to re-append elements after clearing the previous HTML inside span.
My Code:
app.directive('editableLocation', function(editableDirectiveFactory) {
var createElement = function(el, index){
var newElement = angular.element("<select/>");
newElement.attr('ng-model','$data'+index);
newElement.attr('ng-options',el.array);
newElement.attr('ng-change',el.change.substring(0, el.change.length - 1)+", $data"+index+")");
return newElement;
}
var descriptions = [] ;
var dir = editableDirectiveFactory({
directiveName: 'editableLocation',
inputTpl: '<span></span>',
render: function() {
this.parent.render.call(this);
this.inputEl.html("");
for(var i = 0 ; i < descriptions.length ; i ++){
this.inputEl.append(createElement(descriptions[i], i));
}
}
});
var linkOrg = dir.link;
dir.link = function(scope, el, attrs, ctrl) {
console.log(el);
descriptions = scope.$eval(attrs.description);
scope.$watch('flipped',function(newValue,oldValue){
if(newValue != 0){
scope.$editable.render();
}
});
return linkOrg(scope, el, attrs, ctrl);
};
return dir;
});
Since you are adding the dynamic HTML content in the link function of the Angular directive, Angular will not auto compile/parse it. You need to do it manually using $compile directive. So after you appended all the HTML, do the following (inject $compile in your code)
$compile(element.contents())(scope);
Where element is your any parent element where you are generating dynamic HTML and scope is the scope of the directive or any other scope which you want it to be attached to the dynamic HTML.
Looking at xeditable.js I have found that xeditable renders the UI by calling a show method defined in its editableController.
It is defined as:
self.show = function() {
self.setLocalValue();
self.render(); //calls 'render' function of 'editableDirectiveFactory'; that' where my custom UI lies
$element.after(self.editorEl); //attaches newelement(especially whole <form/> element)
$compile(self.editorEl)($scope); //renders whole UI(and also the newly attached one)
self.addListeners();
$element.addClass('editable-hide');
return self.onshow();
};
So what I felt is, I need to call this show method from my link function, which receives the controller.
This is what I did:
dir.link = function (scope, el, attrs, ctrl) {
$element = el;
scope.$watch(attrs.flipped, function (newValue, oldValue) {
//re-render element if flipped is changed; denoting description of select controls have been altered
if (newValue != 0) {
ctrl[0].show(); //this will call render function and also $compile({{content/html/element}})(scope)
}
});
return linkOrg(scope, el, attrs, ctrl);
};
And also you need to hide the previous <form/> element(which contains previous rendered UI), so that only one forms get displayed.
This is how I hid that previous <form/> element in render' function ofeditableDirectiveFactory`:
var prevForm = $element[0].nextElementSibling; //hide previous form element which would already contain previous select
if (prevForm)
prevForm.classList.add('editable-hide');
That solved my problem at least :)

Nested directives/controllers in angular

Just getting my head around Angular - failing to understand a few concepts as I come from the Backbone school of thought.
I've picked a random project to get started: a card game.
Let's say that I wanted to define a hand controller and a card controller. For simplicity, I want to have them as directives.
Here is the card directive:
app.directive('card', function(){
return {
restrict:'E',
templateUrl:'card.html',
controller:function($scope){
this.suit = 'clubs';
this.rank = 'a';
this.suitClass = function(){
return this.suit + '-' + this.rank;
}
},
controllerAs:'card'
};
});
And here is the hand directive:
app.directive('hand', function(){
return {
restrict:'E',
template:'hand.html',
controller:function($scope){
this.cards = [
{suit:'clubs', rank:'a'},
{suit:'spades', rank:'10'},
{suit:'hearts', rank:'2'},
{suit:'diamonds', rank:'k'}
];
},
controllerAs:'hand'
}
});
With the following plunker, I was expecting to be able to simply drop in the <hand></hand> element and have angular do all the work for me. In my minds eye there should be cards representing different suits nested within the <hand> directive. What am I missing? Currently, as you can tell in the plunker, the nested controller/directive does not instantiate the view properly.
Am I thinking in too much of an MVC way? Is OOP haunting me? Or is angular just badly designed?
I am not 100% sure that I understand your question but I think that this is a better way to write it:
var app = angular.module('app', []);
app.directive('card', function(){
return {
restrict:'E',
templateUrl:'card.html',
replace: true,
link: function ($scope, element, attrs){
$scope.suit = 'clubs';
$scope.rank = 'a';
$scope.suitClass = function(){
return this.suit + '-' + this.rank;
}
}
};
});
app.directive('hand', function($compile){
return {
restrict:'E',
templateUrl:'hand.html',
link:function($scope, element, attrs){
$scope.cards = [
{suit:'clubs', rank:'a'},
{suit:'spades', rank:'10'},
{suit:'hearts', rank:'2'},
{suit:'diamonds', rank:'k'}
];
}
}
});
And the html can be something like these:
(hand directive template)
<div>
<card ng-repeat="card in cards"></card>
</div>
And (card directive template)
<div ng-class="card.suitClass()">
{{ suit }}
</div>
I will explain the problem by going top down through the order of elements/objects that will be called:
hand directive:
The directive is ok so far. But the $compile parameter and the $scope parameter are not used an should be removed. To be more clear I applied this to a variable hand, but it does not change the behaviour of the application.
app.directive('hand', function(){
return {
restrict:'E',
templateUrl:'hand.html',
controller:function() {
var hand = this;
hand.cards = [
{suit:'clubs', rank:'a'},
{suit:'spades', rank:'10'},
{suit:'hearts', rank:'2'},
{suit:'diamonds', rank:'k'}
];
},
controllerAs:'hand'
}
});
hand.html:
You never passed the current card of the ng-repeat to the card directive.
That way you only produce the card templates times the number of card but never using the actual values.
I removed the obsolete div tag and enhanced the hand.html to this:
<card ng-repeat="card in hand.cards" card-model="card"></card>
This way I get every card from the hand view in the card directive.
card directive:
First I remove the $scope variable because it is never used and won't be used here.
This function is rather incomplete. At least it is missing the card values you want to use. But a major problem in here is that the context of this is bound to the caller. To be more precise, you are using this inside of the suitClass function, but you want to use the suit and rank values of the controller. this does not point to the controller function but to the newly created suitClass function which doesn't know any of these values. For that problem you should introduce a variable that holds the context and access the values that way. And I add the scope variable cardModel that is bound to the element attribute to get the desired values. And I add the bindToController: true to access the passed in model as card.cardModel instead of the pure cardModel:
app.directive('card', function(){
return {
restrict:'E',
scope: {
cardModel: '='
},
templateUrl:'card.html',
controller:function(){
var card = this;
console.log(card.cardModel)
card.suitClass = function(){
return card.cardModel.suit + '-' + card.cardModel.rank;
}
},
controllerAs:'card',
bindToController: true
};
});
card.html:
This view is okay. I only applied my changes:
<div ng-class="card.suitClass()">{{ card.cardModel.rank }}</div>
I hope it is still useful for anybody.

Data from directive not displaying within ng-repeat

I have broken this problem down into it's simplest form. Basically I have a directive that, for the demo, doesn't yet really do anything. I have a div with the directive as an attribute. The values within the div, which come from an object array, are not displayed. If I remove the directive from the div, they are displayed OK. I am clearly missing something really obvious here as I have done this before without any problems.
Here's the Plunk: http://plnkr.co/edit/ZUXD4qW5hXvB7y9RG6sB?p=preview
Script:
app.controller('MainCtrl', function($scope) {
$scope.tooltips = [{"id":1,"warn":true},{"id":2,"warn":false},{"id":3,"warn":true},{"id":4,"warn":true}];
});
app.directive("cmTooltip", function () {
return {
scope: {
cmTooltip: "="
}
};
});
HTML
<div ng-repeat="tip in tooltips" class="titlecell" cm-tooltip="true">
A div element: {{ tip.id }}
</div>
<br><br>
Just to prove it works without the directive:
<div ng-repeat="tip in tooltips" class="titlecell">
A div element: {{ tip.id }}
</div>
There is a hack to make it working in earlier versions of angular by making use of transclusion, like that:
app.directive("cmTooltip", function () {
return {
scope: {
cmTooltip: "="
},
transclude: true,
template : '<div ng-transclude></div>'
};
});
PLNKR
As by Beyers' comment above and below, the behaviour the question is about no longer exists in at least 1.2.5
To be clearer; this has nothing to do with ng-repeat, you can remove it and there still will be no tip ( or tooltips ).
See this question on what the = and other configs mean and what it is doing for you.
Basically for your situation when you use = the scope of the directive will be used in the underlying elements, you no longer have your controller's scope. What this means for you is that there is no {{ tip.id }} or not even tip. Because the directive doesn't supply one.
Here's a plunker that demonstrates what you can do with it.
Basically all i did was
app.directive("cmTooltip", function () {
return {
scope: {
cmTooltip: "="
},
link: function($scope){ // <<
$scope.tip = { id: 1 }; // <<
} // <<
};
});
This creates the tip object on the scope so it has an id.
For your situation you would probably just not use = and look at this question for your other options depending on what you want.
In my opinion this isn't the way to go.
I would use Objects.
JS code:
function tooltip(id,warn){
this.id = id;
this.warn = warn;
}
tooltip.prototype.toString = function toolToString(){
return "I'm a tooltip, my id = "+this.id+" and my warn value = "+this.warn;
}
$scope.tooltips = [new tooltip(1,true),new tooltip(2,false),new tooltip(3,true),new tooltip(4,true)];
HTML:
<div ng-repeat="tip in tooltips" class="titlecell">
A div element: {{ tip.toString() }}
</div>

Categories

Resources