Angular - Run jQuery after data update - javascript

I'm using Angular 1.4.7. I didn't set up this site so I'm not sure what information I need to give for this. We're rendering some search filters, but whether or not they're enabled is configured by data retrieved from the server, for example:
$scope.searchFields = {
"date": true,
"price": true,
...
};
The HTML for a search field ($scope.filter being an individual set of vars for this particular filter, in this case it's just selecting a maximum numeric value):
<div class="limit-group clearfix">
<span class="detail">Filter Name:</span>
<div class="filter-select-small">
<select name="filter" class="custom-select" ng-if="searchFilters.code">
<option value="">No Max</option>
<option ng-repeat="(key, value) in filter" value="<%key%>"><%value%></option>
</select>
<span class="unavailable" ng-if="!searchFilters.code">Unavailable</span>
</div>
</div>
There are conditions where the available search fields will change. The select fields, for whatever reason, get replaced by a jQuery plugin called selectbox. I have a function resetFilterStyles() which reattaches the jQuery plugin to the fields, but I'm not sure where to fire it.
Inside of methods defined on $scope the data is being updated using $http.get() but running resetFilterStyles() inside of these anonymous functions does not work, presumably because Angular hasn't processed yet that the data has been updated, so the changes that would be performed by resetFilterStyles() are undone by Angular's updates to the DOM.
I've tried setting a $watch handler but this seems to be the wrong place to run my function as it doesn't appear to take, either (and additionally causes grievous errors within Angular). As someone who doesn't use Angular, I'm not sure where to go from here.
Edit
The reset function:
function resetFilterStyles() {
// desktop adv filters
var filterContainer = $('.advance-filter-dropdown');
$('select',filterContainer).selectbox('detach');
$('select',filterContainer).selectbox('attach');
$(".filter-select-small .sbOptions").niceScroll({cursorborder:"",cursorcolor:"#ccc",autohidemode: false});
// mobile adv filters
var filterContainer = $('.filter-wizard-mobile');
$('select',filterContainer).selectbox('detach');
$('select',filterContainer).selectbox('attach');
$(".filter-select-small .sbOptions").niceScroll({cursorborder:"",cursorcolor:"#ccc",autohidemode: false});
}

From the AngularJS perspective, you shouldn't do DOM modifications inside the controller. You should create a directive/component to handle that for you.
But if you really must have that jQuery (legacy projects, for example), you can
put your DOM modification into $timeout:
$http.get("url").then(function() {
$timeout(function() {
// updateDom
}, 0);
});

Related

AngularJS ng-repeat render once and reuse

A view of my AngularJS app makes heavy use of ng-repeat directive. It is done like this:
<div ng-repeat="branches in company">
<p>{{branches.name}}</p>
<p>{{branches.location}}</p>
<div>
<select ng-model="branches.officeInformationType">
<option ng-repeat="offices in branches">{{offices.type}}</option>
</select>
<select ng-model="branches.officeInformationMeters">
<option ng-repeat="offices in branches">{{offices.meters}}</option>
</select>
<select ng-model="branches.officeInformationColor">
<option ng-repeat="offices in branches">{{offices.color}}</option>
</select>
</div>
</div>
The fact is, the second ng-repeat and and the others after it (offices in branches) are actually the same everytime, so it wouldn't need to be recalculated for every branch. It would need to be binded to the row it belonges to, for saving it later, so the branches.officeInformation model should still be watched by angular, but I would like to make the whole program more performant.
I am using angular-ui-router and when I change the view between my "Choose your office" view and any other, the lag is tremendous, almost at a minute of wait time when you leave the "Choose your office" page. It renders fast enough, 2 seconds for the whole rendering, but when I leave the page it takes a ton of time to change to the other view.
Any ideas, taking into consideration that the ng-model binding "branches.officeInformation.." is of importance?
EDIT: I have tried remove the nested ng-repeats and for each ng-repeat that I removed, the transition between states got faster and faster. When I removed all the nested ng-repeats the transition became instantaneous, hence why I believe it has to do with the ng-repeats.
The ng-repeats are tracked by $index and where possible I used :: for one time binding.
Thanks.
We can lazy load a dropdown's options right before the user interacts with it.
First, we initialize each dropdown with only the selected option, so you can see it when the dropdown is closed.
Then we attach an ng-focus directive to each dropdown. When our callback fires we can:
fully populate the options for that dropdown
remove all but the selected option from the previously active dropdown
I wasn't entirely sure of the structure of your data (it looks like some arrays have additional properties on them). So I chose to create "view model" objects that represent the UI. You can adapt this to your own structure.
Controller:
// Set up some test office options (null for no selection)
var allOffices = [null];
for (var i = 0; i < 50; i++) {
allOffices.push(i);
}
// activeDropdown holds the dropdown that is currently populated with the full list
// of options. All other dropdowns are only populated with the selected option so
// that it shows when the dropdown is closed.
var activeDropdown;
$scope.company = [
// Branch 1
[
// These objects represent each dropdown
{
// Just the selected option until the user interacts with it
options: ["0"],
selected: "0"
}, {
// Just the selected option until the user interacts with it
options: ["1"],
selected: "1"
}, {
// Just the selected option until the user interacts with it
options: [null],
selected: null
}
],
// Branch 2
[
// These objects represent each dropdown
{
// Just the selected option until the user interacts with it
options: ["2"],
selected: "2"
}, {
// Just the selected option until the user interacts with it
options: ["3"],
selected: "3"
}, {
// Just the selected option until the user interacts with it
options: [null],
selected: null
}
]
];
// When the user interacts with a dropdown:
// - fully populate the array of options for that dropdown
// - remove all but the selected option from the previously active dropdown's
// options so that it still shows when the dropdown is closed
$scope.loadOffices = function (dropdown) {
if (activeDropdown === dropdown) {
return;
}
dropdown.options = allOffices;
if (activeDropdown) {
activeDropdown.options = [activeDropdown.selected];
}
activeDropdown = dropdown;
};
Template:
<div ng-repeat="branch in company">
<div ng-repeat="dropdown in branch">
Selected: {{ dropdown.selected }}
<select ng-focus="loadOffices(dropdown)" ng-model="dropdown.selected">
<option ng-repeat="o in dropdown.options">{{ o }}</option>
</select>
</div>
</div>
Note that ng-focus was the only directive I needed to apply to each dropdown when I tested this. But you may need to add ng-keydown, ng-mouseover, ng-click, or others to get it to work in all scenarios including mobile.
I also noticed a potential styling issue. When you focus on a dropdown, we load all of the options for that dropdown. This may cause the width of the dropdown to change, so if you can set the same width for all of them you should be good.
If the number of options in each dropdown is huge, we may be able to optimize even further by writing some custom directives that interact and allow the actual DOM element options to be shared. But I suspect we won't have to go that far for this example.
Have you tried 'track by $index' ? it will reduce angular watches overhead.
something like that:
div ng-repeat="branches in company track by $index">
<p>{{branches.name}}</p>
<p>{{branches.location}}</p>
<div>
<select ng-model="branches.officeInformationType">
<option ng-repeat="offices in branches track by $index">{{offices.type}}</option>
</select>
<select ng-model="branches.officeInformationMeters">
<option ng-repeat="offices in branches track by $index">{{offices.meters}}</option>
</select>
<select ng-model="branches.officeInformationColor">
<option ng-repeat="offices in branches track by $index">{{offices.color}}</option>
</select>
</div>
</div>
First and foremost, thanks to those that helped me find the answer.
The problem was that I nested too many ng-repeats with too many event handlers attached to each repeated element. ng-models, ng-changes and ng-clicks were really heavy, but the number of elements was also out of control.
I solved this by using a single select without any nested ng-repeats, this select (and the options) are in a modal view, so a different controller. From that controller I return the select results, having only one select for all the elements in the page. When the data is returned from the modal, I use it from the main controller of the view.
Thanks again.

AngularJS - how to add dynamic ng-model

I'm working with a form and would like to add ng-model on dynamic input elements. I have a scope variable defined as:
$scope.formData = {};
On the page there are a couple of drop-down lists that users can choose an option from and based on those options we are appending some input fields to form body.
formBody.append('<input ng-model="formData.'+obj.Title+'" type="number"></input></br>');
This is not working for me because I'm assuming once the controller runs it can't register any new ng-model. Is there way to add dynamic ng-model or there is a different approach to what I'm trying to do (i.e. build predefined views that can be loaded on the page)?
EDIT:
I have created a jsfiddle that outlines what I'm trying to do - http://jsfiddle.net/k5u64yk1/
If you need to dynamically add html with dynamic bindings that cannot be encapsulated into ng-repeat, ng-if, etc, you have to call $compile on the template once it has been modified to alert AngularJS that it has to reparse the template and initiate a new digest cycle. This will pick up any new ng-model bindings and appropriately tie them to your scope.
HTML:
<div ng-app="MyApp" ng-controller="MyCntrl">
<button ng-click="addInput()">Add Input</button>
<div id="form">
input would go here.
</div>
</div>
JS:
By placing your add input inside of a click event, you avoid an infinite compile loop. Note that this currently resets the state of your form, so if you wanted to work around that you'd need to capture your form state and restore it after compile.
$scope.addInput = function () {
var aForm = (angular.element(document.getElementById('form')));
if ($scope.data["Digital Conversation"][0].MetricType.Title === "Number") {
aForm.append(
'<input ng-model="formData.' +
$scope.data["Digital Conversation"][0].Title.Title +
'" type="number"></input>');
}
$compile(aForm)($scope);
}
You can find the working jsfiddle here: http://jsfiddle.net/k5u64yk1/

Watching for change in filtered items in angular

I have a simple Angular app which has some data, and an input box to filter that data.
<input class="form-control" ng-model="filters.generic" />
<h3>{{filteredUsers.length}} filtered users</h3>
<ul>
<li ng-repeat="user in filteredUsers = (data.userData | filter:filters.generic )">
{{user.registration.firstName}}
</li>
</ul>
The 'filteredUsers' variable stores the users with the applied filter. That all works absolutely fine. However, I'd like to be able to add a watch to execute a particular function whenever this 'filteredUsers' variable changes (whenever the user types anything in the input box).
I do not want to simply add a method to ng-change on the input, because I want to introduce other filters elsewhere.
I thought that I could do this in my controller with
$scope.$watch('filteredUsers', function () {
alert('something');
});
This executes once, when the application loads, and then never again. What must I do to make this watch for changes to filteredUsers?
You might need to use $watchCollection() instead of $watch()
Keep in mind that the filter on an ng-repeat could be running way more frequently than you need or expect it to, and adversely affect the performance of your code.
Even though you said you don't want to use ng-change, I think you should reconsider that and only update your array when needed.
<input class="form-control" ng-model="filters.generic" ng-change="genericFilterChange()" />
var updateFilteredUsers = function(){
$scope.filteredUsers = $filter('filter')($scope.data.userData, $scope.filters.generic);
alert('something');
};
var genericFilterChange = function(){
updateFilteredUsers();
updateOtherFilteredThings();
};

Making Meteor reactive for html elements

So I have a modal box that allows the user to edit / save some data.
I just want to add that unlike other Meteor apps, I don't want to save the data straight away - I want the user to fill in all the fields before hitting save where it will save to the database and send to server etc. This is mainly because I want the user to be able to hit the "cancel" button to revert all changes.
I have a drop down box at the start of the form where depending on the value, fields will be shown or hidden
<select class="form-control" id="ddlNewInputType" placeholder="Enter your input type">
<option value="input">Input</option>
<option value="formula">Formula</option>
</select>
And I have a handlebar around a field like this to determine whether I want to show it
{{#if isFormula }}
<div class="row">
<input type="text"
id="txtNewInputFormula" placeholder="Enter formula">
</div>
{{/if}}
With a helper looking like this
isFormula: ->
$('#ddlNewInputType').val() == 'formula'
However, this doesn't work. Aside from when it first loads onto the page, it never hits isFormula, probably because Meteor doesn't consider any of the HTML elements as reactive so it never re-evaluates when the HTML element changes.
What is a suitable way to get around this? Is it possible to make something reactive explicitly in Meteor? I was also considering putting the dropdown list value into a session variable but that just seems messy because I'm going to need to manage this session variable (remember to clear it when the modal box closes etc.)
Your analysis is correct - a reactive variable needs to be involved in order for your helper to reevaluate after changing the select element. The basic strategy looks like:
Initialize a reactive variable when the template is created.
Whenever the select changes, update the reactive variable.
Read the reactive variable in your helper.
Rather than use a session variable, let's use a ReactiveVar scoped to your template. Here's an example set of modifications:
Template.myTemplate.helpers({
isFormula: function() {
return Template.instance().isFormula.get();
}
});
Template.myTemplate.events({
'change #ddlNewInputType': function (e, template) {
var isFormula = $(e.currentTarget).val() === 'formula';
template.isFormula.set(isFormula);
}
});
Template.myTemplate.created = function() {
// in your code, default this to the current value from
// your database rather than false
this.isFormula = new ReactiveVar(false);
};
Remember that you'll need to also do:
$ meteor add reactive-var
See my post on scoped reactivity for a full explanation of this technique.

Angular ng-model bound from nested ng-repeat

This must have been already done, and I am missing something about how Angular works on that point.
In short, I have <select ng-model><option ng-repeat/></select> and I don't know how to give it a default value at page load.
View:
<div ng-ctrl="MyCtrl">
<form ng-submit="submitChoice()">
<select ng-model='choice'>
<option ng-repeat='choice in choices' value='{{choice.id}}'>{{choice.id}}</option>
</select>
</form>
</div>
Way 1: When user updates the view (select the 'choice' he wants), the $scope used in MyCtrl gets it and I can perform whatever I want with it:
Way 2: But if, the other way round, I want to programmatically set the default value for the choices from the controller (at start), it can't change the value displayed:
function MyCtrl ($scope) {
$scope.submitChoice = function() {return;}; // Way1: OK!
$scope.choice = choice4; // Way2: Doesn't change the view!!
}
I guess it's because each element in ng-repeat has its own scope (fyi if I hardcode options in view instead of a ng-repeat, it works well).
In way 1 the inner scope selected by the use emits choice to the upper scope, if I understand well. But I have no idea how to do way 2.
Is it doable in a simple way? Is a directive necessary?
You would be better off using the ng-options directive with the select directive. Using select + ngRepeat has all sort of limitations and is better avoided. Is there any particular reason for generating options with ngRepeat?
If you could share more code (especially the structure of your data model) I might be able to provide more info.

Categories