I’m hoping there are some Angular 1.x experts who can show me what I’m doing wrong. I have a simple function to update which of 3 buttons in a “tab group” is the current one. This function is called whenever any of the buttons is clicked.
$scope.updateFilter = function (type, value) {
// Additional unrelated code here ...
document.getElementsByClassName('active')[0].className = document.getElementsByClassName('active')[0].className.replace(' active', '');
document.getElementById('tabButton_' + value).className += ' active';
$scope.$apply();
};
The background color of the current button is indeed highlighted but only AFTER one clicks elsewhere on the screen. In other words, it’s not updated instantly like it should.
Any ideas how to correct this?
It's hard to diagnose the issue without seeing some more code or a reproduction of your existing issue. However, from the above, you are certainly not doing the "angularjs" way. A more angular approach would be to use bindings and update the model as the user clicks different button options. A very basic (and ugly styled) example:
angular.module('myApp', [])
.controller('MainController', function () {
var self = this;
self.$onInit = function $onInit() {
// These will be ng-repeated over for the example
self.buttons = [
'Option 1',
'Option 2',
'Option 3'
];
// This is the model binding that will drive the active style
self.activeIndex = 0;
};
self.setActiveIndex = function setActiveIndex(index) {
// This is called on button click and updates the model used
// for the active button styling
self.activeIndex = index;
};
});
.active {
background: blue;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.7.5/angular.min.js"></script>
<!-- repeat the buttons. when clicked, call controller method to update model's active index -->
<div ng-app="myApp" ng-controller="MainController as $ctrl">
<button ng-repeat="b in $ctrl.buttons" type="button" ng-class="{active: $ctrl.activeIndex===$index}" ng-click="$ctrl.setActiveIndex($index)">{{::b}}</button>
</div>
Take aways:
You probably shouldn't be doing DOM manipulation. Use existing directives and model binding, else you are losing many of the benefits you are supposed to get from angularjs.
Don't call $scope.$apply(). Angular will do this for you if you are using an ng-click in your template (which you probably should instead of building the event listeners yourself).
Related
Angular UI Bootstrap: Make accordion save state
How can I make the accordion in Angular UI Boot strap save its state?
The behaviour I'd like is when a user clicks on a link inside the accordion, then later clicks back, the same groups are expanded as before.
If it helps, I'm using an SPA with ui-router, and I'm happy to save the state it a cookie.
I've not got very far because I haven't figured out how to read the accordion's state, let alone save it.
Plunker
I added a status property to hold the state of the accordion elements
{
title: 'Dynamic Group Header - 1',
content: 'Dynamic Group Body - 1',
status: true
}
Set the directive to track the state of the accordion with is-open="group.status":
<accordion close-others="oneAtATime">
<accordion-group heading="{{group.title}}" ng-repeat="group in groups" is-open="group.status">
{{group.content}}
</accordion-group>
</accordion>
I built a service to keep the accordion data in:
app.factory('accordionKeeper', function() {
var accordionState = {};
return {
get: function() {
return accordionState;
},
set: function(stateObj) {
accordionState = stateObj;
}
}
});
I set a watch on the dataset which the accordion is built from, and if the data changes, save the value in the service:
$scope.$watchGroup(function() {
return $scope.groups;
}, function(newVal, oldVal) {
if (newVal !== oldVal) {
accordionKeeper.set($scope.groups)
console.log('Accordion changed. Value set')
}
});
Now if if you ever need to set the accordion to the saved state, just assign $scope.groups = accordionKeeper.get();
In the plunker I added some buttons that operate on the service, to set different values to the data that is stored in the service. The third button actually pulls the data from the service and applies to the controllers' data. This is just to simulate situations you might encounter where you are traversing back and forth in your routes and your controller gets re-instantiated when a user visits that part of the site again.
I have a simple onsen-ui sample page.
It's built in Monaca (http://monaca.mobi) with Onsen UI 1.0.4.
The page contains two onsen-ui buttons (ons-button), their visibility is bound a javascript method via angular js. The buttons are mutually exclusive, meaning when button 1 is visible, button 2 must be hidden - and the other way around.
When either button is clicked, an internal flag is changed and the other button is shown.
Problem is: the visibility of the buttons is not applied correctly when the page first loads.
It only works when the user manually clicks one of the buttons.
As a counter example, there are also two normal HTML buttons on the page - these buttons work correctly as soon as the page is loaded.
Can you give me any advice? Do I have to manually force a refresh when the page is loaded?
Thank you very much in advance!
HTML code:
<div ng-controller="AppCtrl">
<strong>Click To Toggle</strong> <br>
<button ng-click="startTracking()" ng-hide="isTrackingRunning()"><strong>On</strong></button>
<button ng-click="stopTracking()" ng-show="isTrackingRunning()"><strong>Off</strong></button>
<ons-button ng-click="startTracking()" ng-hide="isTrackingRunning()">Start Tracking</ons-button>
<ons-button ng-click="stopTracking()" ng-show="isTrackingRunning()">Stop Tracking</ons-button>
</div>
JS code:
angular.module('SotraMon', ['onsen.directives'])
.controller('AppCtrl',['$scope', function($scope){
var trackingRunning = false;
$scope.isTrackingRunning = function() {
console.log("getter called, returning " + trackingRunning);
return trackingRunning;
}
$scope.startTracking = function() {
trackingRunning = true;
}
$scope.stopTracking = function() {
trackingRunning = false;
}
}]);
I can reproduce in Onsen UI 1.0.4. One solution is to use external span tag s.t.
<span ng-hide="isTrackingRunning()"><ons-button ng-click="startTracking()">Start Tracking</ons-button></span>
<span ng-show="isTrackingRunning()"><ons-button ng-click="stopTracking()">Stop Tracking</ons-button></span>
Firstly, a similar question has been answered before, however the aforementioned does not resolve my issue.
I'd like to - from within my js, not within my html - be able to close the current accordion and open the next. Please note that this action will be triggered from the js from within a controller which is not the accordion controller (yes, I can use a factory function and make $scope available to other controllers, which I'm doing already). Also important is that the accordions are hard-coded so, they're not within a loop.
EDIT: Adding code
Ok, on my accordionCtrl is empty (at the moment as I don't need to add any code it for now), so all the action is happening on another controller:
var CustomerFormCtrl = function($scope, mainObj, $http, $timeout) {
$scope.saveCustomer = true;
$scope.master = {};
$scope.update = function(customer) {
$scope.master = angular.copy(customer);
mainObj.customer[customer.name] = customer;
// Saving customer
if($scope.saveCustomer === true) {
$http({
method: 'POST',
url: '/customers/create',
data: $.param(mainObj.customer),
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
})
.success(function(data) {
$scope.SavedCustomer = true;
$scope.Message = "Customer saved";
$timeout(function() { $scope.SavedCustomer = false }, 2000);
});
}
};
$scope.reset = function() {
$scope.customer = angular.copy($scope.master);
};
$scope.reset();
}
And here's my accordion (in jade rather than html)
div(ng-controller="accordionCtrl")
accordion(close-others="false")
// Customer accordion
accordion-group(heading="Step 1 - Customer details" is-open="true")
div.col-md-6
h4 Search a customer
div(class="container-fluid", ng-controller="SearchCustomerCtrl")
input(type="text", ng-model="asyncSelected", placeholder="Search customer", typeahead="address for address in getLocation($viewValue) | filter:$viewValue" typeahead-loading="loadingLocations" class="form-control")
i(ng-show="loadingLocations" class="glyphicon glyphicon-refresh")
div.col-md-6
h4 Customer details
div(ng-controller="CustomerFormCtrl")
div(ng-show="SavedCustomer")
alert(type="success") {{Message}}
form(name="CustomerDetails", class="", role="form", novalidate)
div.form-group
// my form here
// Order accordion
accordion-group(heading="Step 2 - Placing the order")
p Order
// Checkout accordion
accordion-group(heading="Step 3 - Checkout")
p Checkout
On $http({...}).success(function(data) {...} I'd like to add some code that'd close the Step 1 accordion and open Step 2.
If I was using jQuery (which I can do, but I'd rather not) I could select the aforementioned accordion through it' id/class something along these lines:
$('.boot-tab').find('li.active')
.next()
.find('a[data-toggle="tab"]')
.click();
But with Angular I've no idea how to make this work. Thanks for your help.
Sure - the easiest way is instead of setting is-open to true, set it to a property on your scope.
accordion-group(heading="Step 1 - Customer details" is-open="$parent.step1open")
if you want to, you can put the init inline there too:
accordion-group(heading="Step 1 - Customer details" is-open="$parent.step1open" ng-init="step1open = false")
Then in your JS, set $scope.step1open = true in your success function. I assume you're doing this in the accordianCtl - if you're not, you'll soon have followup questions about scope visiblity and inheritance.
Here's a plunker with an example.
There seems to be some issues with the accepted answer so for anyone else that's still having trouble let me help. Here is a way to not only programatically show the content, but also programatically show/hide increments of the groups, kind of like pagination.
Here's a quick example accordion:
<accordion close-others="false">
<accordion-group ng-repeat="item in myAjaxedItems" is-open="accordionIndexViewable($index)">
... some accordion content / heading / what-have-you
</accordion-group>
</accordion>
Notice the "accordionIndexViewable" function. You can pass the index of the accordion's ng-repeat using the $index variable. Then check if that index should be viewable based on some other scope "viewable item limit". You can have a "Show more" button that in this case would call some other function to increment the "viewable item limit".
And here's the controller:
myApp.controller('MyCustomAccordionController', function($scope){
$scope.itemLimit = 1;
$scope.accordionIndexViewable = function(index){
return index < $scope.itemLimit;
};
// another function which will trigger on some event to change the itemLimit. Can do a variety of different things for paginating down/up. But in this case it's super simple.
$scope.onClickingSomething = function() {
if($scope.itemLimit <= 10)
$scope.itemLimit += 2;
};
});
The is-open attribute may not be desirable on the actual model item itself. For me, there was an update taking place on the model (via ajax) and I didn't want an "open" property on the actual DB model item. The accordionIndexViewable function is a good alternative and idempotent. There could even be a more efficient way of doing it using an object hashmap, but that's beyond being necessary to answer this question here.
I have a list of model objects that are presented in the view with an ng-repeat directive on input elements, being editable. There is an Add new button to add a new item to the list, which creates a new empty input. But as soon as the new empty input is created in the view, I want it to be focused and ready to be edited. I'm not sure how to go about this, since I have little access to the DOM in the controller and the controller is where I am adding the new empty model to the list.
I'm still new with AngularJS, writing this app to learn. So, do let me know any other things I've done wrongly in the implementation.
Plunker: http://plnkr.co/Ig4OtuUtFatIknO1Kfxi
Interesting pieces of code:
HTML:
<body ng-controller=PlanetCtrl>
<div>
<input ng-repeat='it in planets' type=text ng-model=it.name edit-spotlight>
</div>
<button ng-click=addNew() class='btn btn-primary'>+ Add new item</button>
<div id=spotlight-shadow></div>
</body>
Javascript:
angular.module('planet-man', []).
// ... editSpotlight directive ...
function PlanetCtrl ($scope) {
$scope.planets = [
{name: 'Mercury'},
{name: 'Venus'},
{name: 'Earth'},
{name: 'Mars'}
];
$scope.addNew = function () {
$scope.planets.push({name: ''});
};
}
As you can see from the plunker, when the Add button is clicked, a new input is added to the view, but one has to manually focus it. I want it to receive focus (and have the spotlight effect appear) immediately on clicking the Add button.
Thank you.
tried to do this in a bit more 'angular' way: http://plnkr.co/edit/8vtgfTqBh3sxqO0KLk4y
Here is modified version of your plunker: plnkr.co/edit/QScU5GE1EeGskbmrZCIc?p=preview
I've started working with angular js and have a problem that requires getting the current state of the DOM inside of my controller. Basically I'm building a text editor inside of an contenteditable div. Revisions to the text in the div can come from an external service(long polling pushes from the server) as well as the user actually typing in the field. Right now the revisions from the server are manipulating my angular model, which then updates the view through an ng-bind-html-unsafe directive. The only problem with this is that this blows away the users current cursor position and text selection.
I've figured out a way around the problem, but it requires directly manipulating dom elements in my controller, which seems to be discouraged in angular. I'm looking for either validation of my current method, or reccomendations on something more "angulary".
Basically what I've done is added two events to my model, "contentChanging" and "contentChanged". The first is fired right before I update the model, the second right after. In my controller I subscribe to these events like this.
//dmp is google's diff_match_patch library
//rangy is a selection management library http://code.google.com/p/rangy/wiki/SelectionSaveRestoreModule
var selectionPatch;
var selection;
scope.model.on("contentChanging", function() {
var currentText = $("#doc").html();
selection = rangy.saveSelection();
var textWithSelection = $("#doc").html();
selectionPatch = dmp.patch_make(currentText, textWithSelection);
});
scope.model.on("contentChanged", function() {
scope.$apply();
var textAfterEdit = $("#doc").html();
$("#doc").html(dmp.patch_apply(selectionPatch, textAfterEdit)[0]);
rangy.restoreSelection(selection);
});
So basically, when the content is changing I grab the current html of the editable area. Then I use the rangy plugin which injects hidden dom elements into the document to mark the users current position and selection. I take the html without the hidden markers and the html with the markers and I make a patch using google's diff_match_patch library(dmp).
Once the content is changed, I invoke scope.$apply() to update the view. Then I get the new text from the view and apply the patch from earlier, which will add the hidden markers back to the html. Finally I use range to restore the selection.
The part I don't like is how I use jquery to get the current html from the view to build and apply my patches. It's going to make unit testing a little tricky and it just doesn't feel right. But given how the rangy library works, I can't think of another way to do it.
Here's a simple example of how you would start:
<!doctype html>
<html ng-app="myApp">
<head>
<script src="http://code.angularjs.org/1.1.2/angular.min.js"></script>
<script type="text/javascript">
function Ctrl($scope) {
$scope.myText = "Here's some text";
}
angular.module("myApp", []).directive('texteditor', function() {
return {
restrict: 'E',
replace: true,
template: '<textarea></textarea>',
scope: {
text: '=' // link the directives scopes `text` property
// to the expression inside the text attribute
},
link: function($scope, elem, attrs) {
elem.val($scope.text);
elem.bind('input', function() {
// When the user inputs text, Angular won't know about
// it since we're not using ng-model so we need to call
// $scope.$apply() to tell Angular run a digest cycle
$scope.$apply(function() {
$scope.text = elem.val();
});
});
}
};
});
</script>
</head>
<body>
<div ng-controller="Ctrl">
<texteditor text="myText"></texteditor>
<p>myText = {{myText}}</p>
</div>
</body>
</html>
It's just binding to a textarea, so you would replace that with your real text editor. The key is to listen to changes on the text in your text editor, and update the value on your scope so that the outside world know that the user changed the text inside the text editor.