How do I Stop Angular from Clearing the Text Selection? - javascript

I'm trying to render and refresh every few seconds a list of complicated objects using Angular. An issue I've found is that when the refresh happens, even if a particular HTML subcomponent hasn't changed, the HTML is updated and if you'd selected some of the text (e.g. you were trying to copy it) the selection goes away.
I know there is a general issue with changing the html that contains a selection, but I'm wondering if Angular has some solution to the problem that I'm just not aware of. Basically what I'm looking for is for only the HTML that actually changed being updated. I could do that if I was writing view code manually in jQuery, but every other part of doing it manually is awful
JS:
angular.module('items', [])
.factory('itemList', ['$http', function($http) {
var items = [];
var refresh = function() {
// imagine that this makes an HTTP call to get the new list
// of items
items.length = 0;
for (var i = 0; i < 10; i++) {
items.push("item " + Math.random(1, 10))
}
}
refresh();
return {
items: items,
refresh: refresh
};
}]);
var app = angular.module('app', [
'items'
]);
app.controller('ItemListController',
['$scope', 'itemList', '$interval',
function($scope, itemList, $interval) {
this.items = itemList.items;
$interval(itemList.refresh, 2000)
}
]);
HTML:
<body ng-app="app">
<div ng-controller="ItemListController as controller">
<h3>Items</h3>
<div ng-model="active">
<div ng-repeat="item in controller.items">
<div class="header">Header</div>
<div>{{item}}</div>
<hr/>
</div>
</div>
</div>
</body>

As you're wholesale replacing itemList on each refresh, angular has no option but to re-create all the elements in the ng-repeat, which is fine if you don't mind losing the selection and the refresh isn't too large and expensive. To prevent this though, you could try writing a merge in that factory that diffs the previous against the new and adds/removes items without replacing the whole reference. Then only if the item you selected no longer exists would you lose the selection.
Also, if the list is long and the differences from refresh to refresh are small, then this will probably be more efficient.

$interval refreshes entire DOM So selection is get refreshed and new value will appear.
if you want to select then cancel the interval for particular time $interval.cancel(milliseconds) then restart the timer with selected range.
$scope.$watch("refresh", function(){
$interval.cancel(5000);
p = $interval(itemsList.refresh(), 2000);
})

I figured it out. Thor's answer was on the right track. The issue was that I wasn't using "track by FOO" on my ng-repeat, so it had no way of knowing that the elements in my actual case with complex objects were the same and thus it redrew them

Related

Dynamically creating buttons for paragraphs with Angularjs

I want to dynamically create a button for each paragraph that is created inside a contenteditable div. I've been thinking a lot and can't come up with a good solution. The things I've thought about are
putting the button and paragraph together into one directive and have the content editable add a new <button+p> tag each time the user hits return. This has the benefit of having both the button and the paragraph use the same controller, but it leaves the button in the content editable div so it can be deleted...
Use the Model to maintain an array of all paragraphs in the div, then create buttons for each of the paragraphs in this array. My question here is: if I update the model with new paragraphs, will the buttons automatically be generated? If I use ng-repeat?
I'm kind of at a loss of the best way to approach this. Should I try to build the button and the paragraph together? Or is there a better way of separating them but binding them together so that when the button is clicked I can change the styling of the paragraph?
Create a directive and associate it to your div.
Ex:
Define as binding a parameter with two way data binding, the ones that will keep track of the p elements created inside the div and that will be passed from the the controller associated to your view.
Inject inside your link function of the directive the $element.
Then bind to the div with contenteditable the input event in order to detect edits in the div.
Inside this code get the total number of p children of your div, and associate it to the variable allowed from the directive.
In this way your parameter is always sync with the number of p inside your div, and it can be accessed from outside scopes because you pass it from outside.
Then inside your view, use a ng-repeat iterating over this parameter you passed in the directive, and create your dynamic content inside the ng-repeat.
HTML Code:
<div ng-app="myApp">
<div ng-controller="Controller">
<div contenteditable="true" p-inspector p-elements="pElementsNumber">
TEST
</div>
{{pElementsNumber}}
<div ng-repeat="p in returnArrayFromNumber() track by $index">
P detected
</div>
</div>
</div>
Here the JS code:
angular.module('myApp', [])
.controller('Controller', ['$scope', function($scope) {
$scope.pElementsNumber = 0;
$scope.returnArrayFromNumber = function () {
return new Array($scope.pElementsNumber);
};
}])
.directive('pInspector', function($rootScope) {
return {
restrict: 'A',
scope: {
pElements: '='
},
link: function ($scope, $element, $attrs) {
$element.on("input", function(e) {
var htmlString = $element.text();
var regex = /<p>[^<p><\/p>]*<\/p>/gi, result, count = 0;
var count = 0;
while ( (result = regex.exec(htmlString)) ) {
count++;
}
$scope.pElements = count;
$rootScope.$apply();
});
}
};
});
Here the running example: https://jsfiddle.net/a0jwmpy4/81/
Just one recommendation: if you want to detect more elements, make this directive dynamic accepting the name of the elements in the parameters and detecting all of them. Please do not create a single directive for every element you want to detect inside the div :)
Hope this helps
Have you tried to use ng-repeat for each paragraph/modal then set all your code in each repeat something like below
<div>
<p ng-repeat="paragraph in paragraphs"> {{contentsOfParagraph}} <button ng-click="editParagraph(MayBeIDOfParagraph)">Edit</button></p>
</div>
now your js code will have a function editParagraph that pass the ParagraphID

AngularJS lazy rendering (not lazy loading views)

Let's say I have a lot (3000+) of items I want to render (in a ng-repeat) in a div with a fixed height and overflow: auto, so I'd get N visible items and a scrollbar for the rest of them.
I'm guessing doing a simple ng-repeat with so many items will probably take a lot of time. Is there a way I can make AngularJS render only those visible N items?
Edit:
An infinite scroll is not what I want. I want the user to be able to scroll to any point of the list, so I literally want a text editor-like behavior. Said with other words: I'd like the scroll to contain the "height" of all the items, but place in the DOM just a few ones.
This answer provides an approach for lazy-rendering only items currently in-view, as defined by the edit to the original question. I want the user to be able to scroll to any point of the list, so I literally want a text editor-like behavior. Said with other words: I'd like the scroll to contain the "height" of all the items, but place in the DOM just a few ones.
Install the angular-inview plugin before trying this.
In order to get your scrollheight you'd need something holding the space for your array items. So I'd start with an array of 3000 simple items (or combine with infinite scroll to whatever extent you want.)
var $app = angular.module('app', ['infinite-scroll']);
$app.controller('listingController', function($scope, $window) {
$scope.listOfItems = new Array($window._bootstrappedData.length);
$scope.loadItem = function($index,$inview) {
if($inview) {
$scope.listOfItems[$index] = $window._bootstrappedData[$index];
}
}
});
Since we're talking about flexible heights, I would create a placeholder for what your content looks like pre-render.
<div ng-controller="listingController">
<ul>
<li ng-repeat="item in listOfItems track by $index" in-view="loadItem($index,$inview)" style="min-height:100px"><div ng-if="item">{{item.name}}</div></li>
</ul>
</div>
Using ng-if will prevent rendering logic from being run unnecessarily. When you scroll an item into view, it'll automatically display. If you want to wait a second to see if the user is still scrolling you could set a timeout in the loadItem method that cancels if the same index gets pushed out of view within a reasonable time period.
Note: If you truly wanted to avoid putting anything in the DOM, you could set your scrollable area to a specific multiple of your "placeholder" height. Then you could create a directive that uses that height to determine the indexes of the items that should be displayed. As soon as you display new items, you'd need to add their heights to the total and make sure you position them at the right spot and make sure your directive knows how to interpret those heights into evaluating the next set of displayed elements. But I think that's way too radical and unnecessary.
Expanding on Grundy's point of using .slice().
I use ngInfiniteScroll when I need to lazy-render/lazy-load data.
I would keep those 3000 records out of your scope to prevent weighing down your digest performance unnecessarily and then append them to your scope data as you need them. Here's an example.
var $app = angular.module('app', ['infinite-scroll']);
$app.controller('listingController', function($scope, $window) {
/*
* Outside of this controller you should bootstrap your data to a non-digested array.
* If you're loading the data via Ajax, save your data similarly.
* For example:
* <script>
* window._bootstrappedData = [{id:1,name:'foo'},{id:2,name:'bar'},...];
* </script>
*/
var currentPage, pageLength;
$scope.listOfItems = [];
currentPage = 0;
pageLength = 100;
$scope.nextPage = function() {
// make sure we don't keep trying to slice data that doesn't exist.
if (currentPage * pageLength >= $window._bootstrappedData.length) {
return false;
}
// append the next data set to your array
$scope.listOfItems.push($window._bootstrappedData.slice(currentPage * pageLength, (currentPage + 1) * pageLength));
currentPage++;
};
/*
* Kickstart this data with our first page.
*/
return $scope.nextPage();
});
And your template:
<div ng-controller="listingController" infinite-scroll="nextPage()" infinite-scroll-distance="3">
<ul>
<li ng-repeat="item in listOfItems">{{item.name}}</li>
</ul>
</div>

create HTML element dynamically

I am very new to angular js. I want to create an input box on click of particular div. Here I need to create element on div which repeating.
<div><div ng-repeat ng-click="create();"></div><div>
What will be the best way to do so?
DOM manipulation in Angular is done via directives (There is paragraph on 'Creating a Directive that Manipulates the DOM' here)
First, read through this excellent article: How do i think in Angular if i have a jQuery background
The Angular Team also provides a pretty neat tutorial, which definetly is worth a look: http://docs.angularjs.org/tutorial
While Angular is pretty easy and fun to use once you have wrapped your head around the concepts, it can be quite overwhelming to dive into the cold. Start slow and do not try to use each and every feature from the beginning. Read a lot.
I strongly recommend egghead.io as a learning resource. The video-tutorials there are bite-sized and easy to watch and understand. A great place for both beginners and intermediates. Start from the bottom here.
Some folks have done great things with Angular. Take a look at http://builtwith.angularjs.org/ and check out some source code.
Use an array and ng-repeat to do that. Have a look at the following code.
I crated scope variable as an empty array. Then created a function to add values to that array.
app.controller('MainCtrl', function($scope) {
$scope.inputFields = [];
$scope.count = 0;
$scope.addField = function(){
$scope.inputFields.push({name:"inputText"+$scope.count++});
}
});
I used ng-repeat with this array. and called the function on the click event of a div.
<div ng-click="addField()">Click here to add</div>
<div ng-repeat="inputField in inputFields">
<input type="text" name="inputField.name">
</div>
Check this working link
Update - Show only one text box on click
I created addField() as follows.
$scope.addField = function(){
$scope.newTextField = "<input type='text' name='myTxt'>";
}
To render this html in my view file I created a new directive called compile as follows.
app.directive('compile', function($compile) {
// directive factory creates a link function
return function(scope, element, attrs) {
scope.$watch(
function(scope) {
// watch the 'compile' expression for changes
return scope.$eval(attrs.compile);
},
function(value) {
// when the 'compile' expression changes
// assign it into the current DOM
element.html(value);
// compile the new DOM and link it to the current
// scope.
// NOTE: we only compile .childNodes so that
// we don't get into infinite loop compiling ourselves
$compile(element.contents())(scope);
}
);
};
});
Then used this directive in my view.html file
<body ng-controller="MainCtrl">
<div ng-click="addField()">Click to Add</div>
<div compile="newTextField"></div>
</body>
click here to view the working link

AngularJS on top of server generated content

I'm looking for a way to integrate something like ng-repeat with static content. That is, to send static divs and to have them bound to JS array (or rather, to have an array constructed from content and then bound to it).
I realize that I could send static content, then remove and regenerate the dynamic bits. I'd like not to write the same divs twice though.
The goal is not only to cater for search engines and people without js, but to strike a healthy balance between static websites and single page applications.
I'm not sure this is exactly what you meant, but it was interesting enough to try.
Basically what this directive does is create an item for each of its children by collecting the properties that were bound with ng-bind. And after it's done that it leaves just the first child as a template for ng-repeat.
Directive:
var app = angular.module('myApp', []);
app.directive('unrepeat', function($parse) {
return {
compile : function (element, attrs) {
/* get name of array and item from unrepeat-attribute */
var arrays = $parse(attrs.unrepeat)();
angular.forEach(arrays, function(v,i){
this[i] = [];
/* get items from divs */
angular.forEach(element.children(), function(el){
var item = {}
/* find the bound properties, and put text values on item */
$(el).find('[ng-bind^="'+v+'."]').each(function(){
var prop = $(this).attr('ng-bind').split('.');
/* ignoring for the moment complex properties like item.prop.subprop */
item[prop[1]] = $(this).text();
});
this[i].push(item);
});
});
/* remove all children except first */
$(element).children(':gt(0)').remove()
/* add array to scope in postLink, when we have a scope to add it to*/
return function postLink(scope) {
angular.forEach(arrays, function(v,i){
scope[i] = this[i];
});
}
}
};
});
Usage example:
<div ng-app="myApp" >
<div unrepeat="{list:'item'}" >
<div ng-repeat="item in list">
<span ng-bind="item.name">foo</span>
<span ng-bind="item.value">bar</span>
</div>
<div ng-repeat="item in list">
<span ng-bind="item.name">spam</span>
<span ng-bind="item.value">eggs</span>
</div>
<div ng-repeat="item in list">
<span ng-bind="item.name">cookies</span>
<span ng-bind="item.value">milk</span>
</div>
</div>
<button ng-click="list.push({name:'piep', value:'bla'})">Add</button>
</div>
Presumable those repeated divs are created in a loop by PHP or some other backend application, hence why I put ng-repeat in all of them.
http://jsfiddle.net/LvjyZ/
(Note that there is some superfluous use of $(), because I didn't load jQuery and Angular in the right order, and the .find on angular's jqLite lacks some features.)
You really have only one choice for this:
Render differently for search engines on the server, using something like the approach described here
The problem is you would need to basically rewrite all the directives to support loading their data from DOM, and then loading their templates somehow without having them show up in the DOM as well.
As an alternative, you could investigate using React instead of Angular, which (at least according to their website) could be used to render things directly on the web server without using a heavy setup like phantomjs.

How do I update my html on Click in Angularjs Controllers

I have the html Structure that I need to update from the json data. My Json data is in a Controller. I need to write an expression for ng-click event that will read the json data and put the in the corresponding div in html. but I am not sure how to acheive this.
Below is what I have so far.
<body data-ng-app>
<div class="container" data-ng-controller="UpdateDataCtrl">
<div class="inner1"></div>
<div class="inner2"></div>
</div>
UPdate Controllers
</body>
function UpdateDataCtrl($scope) {
$scope.data = [
{
"USA":"Eglish",
"Pop":"232423432432"
},
{
"France":"French",
"Pop":"1212323432"
},
{
"Spain":"Spainish",
"Pop":"3432432"
}
]
}
On each click the 2 Div should get updated from the json. First div should have USA---English Pop---2342234232 and then on next click the div should have data from France and so on.
http://jsfiddle.net/MBFpD/1/
Thanks
It appears that you are unclear on the concept of AngularjS. You don't want to update the DIVs. You want to reference your model and then change the data in your model.
For example you can write the div like this:
<div class="inner1">Population: {{data[dataindex].Pop}}</div>
Then in the Controller you initialize the dataindex to 0, so that this will output the population from the first entry in the array:
$scope.dataindex = 0;
The click function (you must have the link with the ng:click inside the block governed by the Controller!) could then just increase the dataindex by one and by using modulo restart at 0 again when the end of the array was reached.
$scope.click = function() {
$scope.dataindex = ($scope.dataindex+1) % $scope.data.length;
Here is an updated and modified jsfiddle of your example which will show everything in action: http://jsfiddle.net/MBFpD/2/
Bind your data to your scope when you click on the link:
$scope.update = function() {
$scope.data = data; //your array defined locally to the scope
};
ng-repeat your data bound to the scope; display the container if the size of the array is > 0.
Use {{index}} to get the iteration variable inside the loop.
Above all, move your ng-controller declarative at the top to enclose both your ng-repeat and your ng-click; otherwise, AngularJS cannot guess what you want to achieve.
http://jsfiddle.net/MBFpD/5/

Categories

Resources