How should I integrate my directive and scope object in the view? - angularjs

In my controller I have an array of objects. The object is called Well has a few properties, one of which is Location, which stores a string like "A1", "B4", "B13", etc. The location indicates a position on a grid. The letter represents the row, and the number represents the column.
Now that I have this nice list of objects, I would like to display them all on a grid in my view. When I say grid, I mean that loosely. The grid I have come up with is a series of divs, each with an id equal to a location name.
I have created a directive called tile that will display the properties of a single object. The directive looks like so:
<div class="row">
<div class="col-md-3" ng-repeat="well in wellArray">
<ul><li ng-repeat="prop in well">{{ prop }}</li></ul>
</div>
</div>
Great! And then I can create a tile in my view for a specific Well in the list of Well objects like so:
<div tile name="{{my.getName()}}" dil="{{my.getDilution()}}"></div>
If this list of objects was ordered by the location property, I could simply turn it into an array of arrays, one array for each row, and then use a double ng-repeat in my view. Unfortunately they are not in order, and I do not want to create a sorting method given the format that the location property is in. If i were to do the double ng-repeat on this list as it is now, I would end up with a grid of tiles that are in no particular order.
Given my limited exposure to javascript, I thought of using jquery's .append() method.(note: i have referenced jquery before angular, so angular.element() will use the jquery library instead of jqlite so I can use jquery selectors) In my view I created a bunch of divs in the following format:
<div id="A1"></div>
<div id="A2"></div>
etc.
And then in my controller I created a method that attempts to append a single Well which has a location of "A1" to the element on the view that has an id="A1". My code looks like so:
angular.element('#A1').append('<div tile name="{{my.getName()}}" dil ="{{my.getDilution()}}"></div>');
I thought it would append the div with the tile directive, to the div with id="A1", however, it does nothing. In fact, there are no errors at all.
Surely my psuedo jquery approach is not the best way to go about this. Not only is it not working (no idea why, maybe because angular needs to compile something somehow), but it's also not a very Angular approach. I keep reading in tutorials and introductions to "not use jquery at all for the first few weeks" and "90% of the things you'll waste lines of code in jquery, can be done suceinctly in Angular". Someone please lend this poor excuse of a programmer a hand!!

Just following your example in comments with .append, instead of iterating over your array and appending elements to a container element, create a conceptual representation of the data, and then use it in the view.
In controller, do something like the following:
$scope.wellData = {};
for (well in wellArray){
var key = wellArray[well].getLocation();
$scope.wellData[key] = well;
}
Then in the view, do ng-repeat over wellData:
<div id="item.getLocation()" ng-repeat="item in wellData">
<div tile name="{{item .getName()}}" dil="{{item .getDilution()}}"></div>
</div>
You definitely should stay away from jQuery in controllers. Just assume that there is no DOM in controllers whenever you get the urge to do anything related to DOM. Controller deals with ViewModels which are conceptual representation of the view, but it is view-independent. Whenever you break that separation, you make your controllers harder to test, and you make your view more difficult to change. And, by going against MVVM principles, you will keep bumping into issues with AngularJS.

Related

How can my template include an element whose type is determined by an expression in angularjs?

It's 2022 and sadly I'm learning AngularJS (already past end of life!)
I need need to use what might be called a dynamic element/component. Pseudocode example:
In controller:
this.theElementName = 'b';
In the template:
<{{$ctrl.theElementName}}>this is some text</{{$ctrl.theElementName}}>
I would want this to create <b>this is some text</b>.
The reason is that I want to generate an array of different directives to render, and I don't want code like:
<b ng-if="$ctrl.theElementName === 'b'">this is some text</b>
<div ng-if="$ctrl.theElementName === 'div'">this is some text</div>
<directive-abc ng-if="$ctrl.theElementName === 'directive-abc'">this is some text</directive-abc>
...
In Svelte, it's
<svelte:element this={theElementName} />
In Vue it's
<div :is="theElementName" />
EDIT: in response to the reluctant 'that person', clarifying the use-case
Consider a user-configurable UI. The result of the configuration might be an array list of components desired. I would then need to loop and output those different components in my template. Of course the components would need a standard interface for properties passesd in, events emitted etc. but that can all be designed for.
My code could do a big switch statement, but that requires prior knowledge of every possible component that might be used now or in the future. By doing it the way I intend to, however, a future person could add a component without needing to touch this code.
You can write directive my-directive to use:
<div my-directive="$ctrl.theElementName">...
to generate:
<div><component-a>...
<div><component-b>...
<div><component-c>...
All directive should do is to generate html string and compile it:
element.append($compile('<' + scope.myDirective + '>...')(scope))
(also remember to update content in onChanges if you want to support it)
Directive may also copy certain/all attributes from original element etc.
P.S. you should be cautious e.g. if component name comes from database that may allow injections.
Not a brilliant solution, but documenting what is more of a workaround.
ng-include can be used to source another template file. That file can contain the component you need to include.
<ng-include src="'/path/to/' + theElementName + '.html'"></ng-include>

Angular render markup that is nested

I get JSON like this
{
"lots of":"keys"
"description" : {
"key":"Some sample key",
"value":"This is the markup™"
}
}
from server and I ultimately iterate the description objects and populate table rows with two columns: one for the key and one for the value.
I have tried putting on my <td> tag ng-bind-html as well as injecting $sce into my controller and using trustAsHtml but so far the string always displays as it is in the JSON. Not every value will be HTML but I can easily detect based on the key if HTML is a possibility. It seemed when I put in the directive on the td it did not display anything if no HTML was present. I am interested in using something that can allow HTML in the value but not require it so I can display either
HTML fragment
<tr ng-repeat="(key, val) in record.description">
<td>{{key}}:</td>
<td>{{val}}</td>
</tr>
I created a quick fiddle here:
https://jsfiddle.net/frishi/mvw97s3q/6/
I used angular-sanitize, which I am not sure you mentioned injecting in your module dependency list. Either way, the example works simply by using ng-bind-html
Relevant docs page: https://docs.angularjs.org/api/ng/directive/ngBindHtml
It works by using the directive ng-bind-html on the element you want to display the HTML string in. You use it like so:
<p ng-bind-html="data.firstName"></p>
assuming that data.firstName = "<strong>FeeFee</strong>" or something like that.
I would also like to add that Angular does not allow this natively because of legitimate security concerns. That and the fact that allowing arbitrary HTML to be rendered might not always produce desirable results. Your page layout could quite possibly break because of some HTML you allowed to be passed through.
Angular was designed with security in mind, and will prevent you from displaying HTML from raw strings whenever possible - to prevent various injection attacks.
Here is workarround for your problem: AngularJS: Insert HTML from a string. Generally you should use ng-bind-html insted of ng-bind (this is used by curly braces).

Why is my angular grid so slow?

So, I have made some custom directive which draws kind of a data-grid, based on floated divs (because nested flex implementation in FF sucks - but it's not the point).
How it works :
I pass some data collection to the directive via something like <the-grid data-list="parentController.displayedRows">
Inside this first directive, I have columns via something like <a-grid-column data-value="row.value"></a-grid-column> with many attributes I won't precise here.
The data-value value can be a direct expression, bound to the row on which the the-grid directive controller is ng-repeating in order to display each columns, or a function which have to be $eval-uated in order to display the intended value from the parentController.
In my <the-grid> directive controller, I have the rendering template of my grid which make a nested ng-repeat div (first one on the rows of the data-collection, second one on the columns, created in the directive), it looks like :
<div data-ng-repeat="row in list">
<div data-ng-repeat="cell in theGridColumns"
data-ng-bind-html="renderCell(row, cell)">
</div>
</div>
I have some keyboard nav in order to quickly select a row or navigate within pagination or many tabs, which does nothing more than applying some class on the selected row, in addition to update some "selectedRowIndex".
I'm using angular-vs-repeat in order to have the minimum of row divs in my DOM (because the app is running on slow computers). This works well.
The problem is that every time I'm hitting some "up" or "down" key on my keyboard, Angular is "redrawing" EVERY cells of the list.
So, let's suppose I've 200 rows in my data list, and 7 columns for each rows. First load of the page, it passes ~3000 times in the renderCell() function. Ok , let's accept that (don't really understand why, but ok).
I hit the down key in order to go to the second line of my list. It passes ~1100 times in the renderCell() function.
So yes, the result is very slow (imagine if I let the down arrow key pressed in order to quick navigate within my rows)... I can't accept that. If someone could explain that to me... Any help would be greatly accepted :)
If I make the same thing without a directive (direct DOM manipulation, with columns made by hand and not in a ng-repeat within a ng-repeat), every thing is smooth and clean.
Yes, I've look into every angular grid on the market. No one is satisfying me for my purpose, that's why I've decided to create my own one.
And no, I really can't give you some JSFiddle or anything for the moment. The whole app is really tentacular, isolating this is some kind of complicated).
Try to use bind once (angular 1.3+)
<div data-ng-repeat="row in ::list">
<div data-ng-repeat="cell in ::theGridColumns"
data-ng-bind-html="::(renderCell(row, cell))">
</div>
</div>

AngularJS: how put correct model to repeated radio buttons

I think I have some sort of special code here as all I could google was "too simple" for my problem and it also didn't helped to come to a solution by myself, sadly.
I got a radio button group of 2 radios. I am iterating over "type" data from the backend to create the radio buttons.
My problem is the data binding: When I want to edit an object its "type" is set correctly, but not registered by the view so it doesn't select the desired option.
Follwing my situation:
Backend providing me this as "typeList":
[
{"text":"cool option","enumm":"COOL"},
{"text":"option maximus","enumm":"MAX"}
]
HTML Code:
<span ng-repeat="type in typeList track by type.enumm">
<input
type="radio"
name="type" required
ng-model="myCtrl.object.type"
ng-value="type">
{{type.text}}
</span>
Some Explanation
I don't want to use "naked" texts, I want to use some sort of identifier - in this case it is an enum. The chosen value shall be the entire "type", not only "type.text" as the backend expects type, and not a simple String.
So all I do with this is always a package thingy, the type.text is for like formatted/internationlized text etc.
A Pre-Selection works by setting this in the controller: this.object.type = typeList[0];
The first radio button is already selected, wonderful.
But why isn't it selected when editing the object. I made a "log" within the HTML with {{myCtrl.object.type}} and the result is {"text":"cool option","enumm":"COOL"}. The very same like when pre selecting. I already work with the same "technique" using select inputs, and it works fine. I also found some google results saying "use $parent because of parent/child scope". But 1) I didn't get that straight and 2) think it is not the problem here, as I use a controllers scope and not the $scope, or is this thinking wrong?
It might be explained badly, sorry if so, but I hope someone 1) get's what I want and 2) knows a solution for it.
Thank you!
If you're trying to bind to elements from an array, I believe you need to assign the actual elements from the array to your model property.
So this creates a new obj and sets it to $scope.selectedType (not what you want):
$scope.selectedType = {"text":"cool option","enumm":"COOL"};
whereas this assigns the first element of the array (which is what you want)
$scope.selectedType = $scope.typeList[0];
So to change the model, you can lookup the entry from the array and assign it to your model with something like this
$scope.selectedType = $scope.typeList.filter(...)
Here's a quick example of this approach http://plnkr.co/edit/wvq8yH7WIj7rH2SBI8qF

Stop AngularJS inserting <span class="ng-scope"></span> using ng-include

I'm using the Foundation layout framework, which automatically floats the last sibling of .column to the right and I really appreciate this is a behaviour. However, AngularJS takes it upon itself to insert span.ng-scope after every div.column, which somehow causes browsers to consider the last span the last sibling of .column (even though it is not).
Specifically the css in Foundation responsible for this is:
[class*="column"] + [class*="column"]:last-child { float: right; }
As I understand it, [attribute*="substring"] should select only siblings that match, so, for the above, only elements whose class attribute contains column (including columns). I would think a span tag whose class attribute that does not contain column should not match (and thus be ignored by :last-child). However, this does not seem to be the case.
Regardless, the span is causing the problem:
Angular buggering it up (jsfiddle)
Works fine without Angular (same jsfiddle, no ng-include)
Is there a way to configure angular to stop inserting those span tags? I would, begrudgingly, modify the css selector to somehow ignore all span tags; however I might eventually need/want to use a span tag.
Since you indicated the div can be moved inside, this works:
<ng-include src="'main.tmpl'"></ng-include>
Then in your template:
<div class="row">
<article id="sidepanels" class="four columns">
...
</div>
I'm not aware of any way to prevent angular from inserting the span tags (I think it keeps track of scopes that way -- for garbage collection).
Also you can try my version of include directive that does not creates a scope: Gist source.
As no scopes are created, AngularJS should not create additional element to mainain scope (it actually use data attributes to store link to scope).

Resources