AngularJS SmartTable persistent state not held with custom search (scope issue?) - angularjs

Using AngularJS and SmartTable...
I have a Persistent State which works fine when filters are applied within the table.
I then added a custom search field which searches all columns.
This also works.
However the filter this applied is not held by my persistent state directive.
I believe its a scope issue but cannot seem to line the two up to meet.
Here is a Plunker
http://plnkr.co/edit/2qt7f6NxKH2blJ5GudNl?p=preview
Is my scope written incorrectly?
Here's my directive for the custom search
// Text Search for all columns in stTable
// USAGE: <input type="text" ng-model="queryAllColumns"/>
// There HAS TO BE a way to implement this into the stPersist
.directive('searchWatchModel',function(){
return {
require:'^stTable',
scope:{
searchWatchModel:'='
},
link:function(scope, ele, attr, ctrl){
var table=ctrl;
scope.$watch('searchWatchModel',function(val){
ctrl.search(val);
});
}
};
})
And here is my Persistent Table directive
// Create a Persistant Table Display
// Allows you to close the browser and return with filters still applied
// USAGE: on the st-able ADD: st-persist="myTable" <--myTable can be anything
.directive('stPersist', function () {
return {
require: '^stTable',
link: function (scope, element, attr, ctrl) {
var nameSpace = attr.stPersist;
//save the table state every time it changes
scope.$watch(function () {
return ctrl.tableState();
}, function (newValue, oldValue) {
if (newValue !== oldValue) {
localStorage.setItem(nameSpace, JSON.stringify(newValue));
}
}, true);
//fetch the table state when the directive is loaded
if (localStorage.getItem(nameSpace)) {
var savedState = JSON.parse(localStorage.getItem(nameSpace));
var tableState = ctrl.tableState();
angular.extend(tableState, savedState);
ctrl.pipe();
}
}
};
})

I believe there is already a "global search" available with smart-table.
<input st-search placeholder="global search" class="input-sm form-control" type="search"/>
so why do you have to create your own custom search ? maybe i'm missing something.
By the way, you should add a throttle / delay to your stPersist Directive to avoid unnecessary setItem on localStorage.

Related

AngularJS multiple select box not being filled in

I have a multiple select box with a model attached to it. Here is a JSFiddle of my basic setup.
As you can see, everything works as it should - values that are in my tags dataset automatically appear in my multiple select box (which has been made into a 'tag box' using jQuery Chosen).
The problem arises when I change the source of my tags data from being hardcoded into the JS, to being retrieved via an API call.
If I replace everything inside myController with the below code, the correct data is still send to the view, but it no longer automatically fills/selects the items in the multiple select.
var path = 'http://example.com/api/tags';
$http.get(path).success(function(data) {
$scope.tags = data;
$scope.createForm = {};
$scope.createForm.tags = [];
for(var i = 0; i < $scope.tags.length; i++)
{
$scope.createForm.tags[i] = $scope.tags[i].id;
}
});
The data I am retrieving from the API call is in the exact same format as I had for the hardcoded value. What could the problem be? I want the behaviour to be the same whether the tags data is being hardcoded or loaded form an external source.
Once chosen is initialised, you have to trigger the update whenever the model is changed:
$(elem).trigger("chosen:updated");
And watch for any changes to your model, inside the directive:
myApp.directive("ngChosen", function ($timeout) {
return {
require: 'ngModel',
restrict: 'A',
link: function (scope, elem, attrs, ngModel) {
scope.$watch(function () {
return ngModel.$modelValue;
}, function (newValue) {
$timeout(function () {
// TRIGGER CHOSEN UPDATE
$(elem).trigger("chosen:updated");
});
}, true);
// INIT CHOSEN
$timeout(function () {
$(elem).chosen({
width: "240px"
});
});
}
};
});
DEMO

Isolate ng-model in child directives

I'm looking for the best solution to create a form, built from the small functional blocks.
What I actually do:
Main controller loads the object and passes it to directive:
$scope.context= {model object};
<form><employee-context ng-model="context"></form>
employeeContext directive has it owns template built with another directives (That's what I call small functional blocks):
<base-section ng-model="baseModel"></base-section>
<address-section ng-model="addressModel"></address-section>
<details-section ng-model="detailsModel"></details-section>
employeeContext link function:
link: function(scope, elem, attrs, ngModel) {
ngModel.$render = function() {
scope.model = ngModel.$modelValue; //this is main model
if (scope.model) {
scope.baseModel = {
name: scope.model.name,
photoKey: scope.model.photoKey
};
scope.addressModel = scope.model.addresses;
scope.detailsModel = scope.model.details;
}
};
scope.$watch('addressModel', function(newVal, oldVal) {
if (newVal !== oldVal){
scope.model.addresses = newVal;
ngModel.$setViewValue(scope.model);
}
});
scope.$watch('detailsModel', function(newVal, oldVal) {
if (newVal !== oldVal){
scope.model.details = newVal;
ngModel.$setViewValue(scope.model);
}
});
}
Above code is my solution to simplify building new context directives. Each time developer want to create new context, he must create similar directive, split the model and set his own html template. But this solution has some problems that I don't know how to resolve.
The problem:
I've expected that ng-model in each directive will be isolated from each other. But it is not.
It means that each change inside lower level directives are automatically propagated to main model. I'd like to avoid this behaviour and update main model only with calling ngModel.$setViewValue(updatedModel) (for example only after button click).
Example functional block code:
link: function(scope, elem, attrs, ngModel) {
ngModel.$render = function() {
scope.adr = ngModel.$modelValue;
};
scope.updateModel = function(){
_.each(scope.adr,function(m){
m.city = m.city.toUpperCase();
});
ngModel.$setViewValue(scope.adr);
};
}
I'm also confused why this watches in context link function are fired only once - at initialization. They don't react with later model changes.
My question is:
What is the best solution to create some isolated scopes with a model passed as ngModel and with the possibility to update this model only on demand.
Here is the plunkr http://plnkr.co/edit/vBUCwQHOT6GDDfJLNN5N?p=preview

Model not updating in Angular.js directive for bootstrap-multiselect

I have the following directive:
app.directive('scMultiselect', [function() {
return {
link: function(scope, element, attrs) {
element = $(element[0]);
element.multiselect({
enableFiltering: true,
// Replicate the native functionality on the elements so
// that Angular can handle the changes for us
onChange: function(optionElement, checked) {
optionElement.prop('selected', false);
if (checked)
optionElement.prop('selected', true);
element.change();
}
});
scope.$watch(function () {
return element[0].length;
}, function () {
element.multiselect('rebuild');
});
scope.$watch(attrs.ngModel, function() {
element.multiselect('refresh');
});
}
};
}]);
And the following element in my partial:
<select
id="level_teachers"
class="multiselect col-sm-10"
multiple="multiple"
ng-model="level.teachers"
ng-options="teacher.id as teacher.name for teacher in teachers"
sc-multiselect>
</select>
The bootstrap-multiselect control initializes and displays correctly, however when I select entries in it, my model (level.teachers) remains empty.
Had same problem and this worked for me :
First you add ngModel as 4th parameter of link function. Its very useful - more about it here: https://docs.angularjs.org/api/ng/type/ngModel.NgModelController
Then you basically have to add/delete 'by hand' the option in onChange method to/from your ngModel.
ngModel.$setViewValue() updates the value, ngModel.$render and scope.$apply() are making it visible and spread new model further :)
If you have only single selection then its much easier - less code because of no array control - just use $setViewValue(), $render() and $apply().
app.directive('scMultiselect', function() {
return {
require: 'ngModel',
link: function(scope, element, attrs, ngModel) {
element = $(element[0]);
element.multiselect({
enableFiltering: true,
onChange: function(optionElement, checked) {
optionElement.prop('selected', false);
var modelValue = ngModel.$modelValue; // current model value - array of selected items
var optionText = optionElement[0].text; // text of current option
var optionIndex = modelValue.indexOf(optionText);
if (checked) {
if ( optionIndex == -1) { // current option value is not in model - add it
modelValue.push(optionText)
}
optionElement.prop('selected', true);
} else if ( optionIndex > -1 ) { // if it is - delete it
modelValue.splice(optionIndex,1);
}
ngModel.$setViewValue(modelValue);
ngModel.$render();
scope.$apply();
}
});
scope.$watch(element[0].length, function () {
element.multiselect('rebuild');
});
}
};
});
Hope it will work for you too :)
IT's probably because Bootstrap components aren't built in a way that allow them to be used by Angularjs. They don't really have a way to update themselves after you instantiate them, and more importantly they don't participate in Angularjs's update process. Anyway the good news is someone has taken the initiative to rewrite those bootstrap components in Angularjs so we can use them.
http://angular-ui.github.io/bootstrap/
If you are using Bootstrap 3.x then get the latest. If you're stuck on 2.3.x v0.8 is the last version that supported 2.3.x

JQuery UI Spinner is not updating ng-model in angular

Angular's ng-model is not updating when using jquery-ui spinner.
Here is the jsfiddle http://jsfiddle.net/gCzg7/1/
<div ng-app>
<div ng-controller="SpinnerCtrl">
<input type="text" id="spinner" ng-model="spinner"/><br/>
Value: {{spinner}}
</div>
</div>
<script>
$('#spinner').spinner({});
</script>
If you update the text box by typing it works fine (you can see the text change). But if you use the up or down arrows the model does not change.
Late answer, but... there's a very simple and clean "Angular way" to make sure that the spinner's spin events handle the update against ngModel without resorting to $apply (and especially without resorting to $parse or an emulation thereof).
All you need to do is define a very small directive with two traits:
The directive is placed as an attribute on the input element you want to turn into a spinner; and
The directive configures the spinner such that the spin event listener calls the ngModel controller's $setViewValue method with the spin event value.
Here's the directive in all its clear, tiny glory:
function jqSpinner() {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, element, attrs, c) {
element.spinner({
spin: function (event, ui) {
c.$setViewValue(ui.value);
}
});
}
};
};
Note that $setViewValue is intended for exactly this situation:
This method should be called when an input directive wants to change
the view value; typically, this is done from within a DOM event
handler.
Here's a link to a working demo.
If the demo link provided above dies for some reason, here's the full example script:
(function () {
'use strict';
angular.module('ExampleApp', [])
.controller('ExampleController', ExampleController)
.directive('jqSpinner', jqSpinner);
function ExampleController() {
var c = this;
c.exampleValue = 123;
};
function jqSpinner() {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, element, attrs, c) {
element.spinner({
spin: function (event, ui) {
c.$setViewValue(ui.value);
}
});
}
};
};
})();
And the minimal example template:
<div ng-app="ExampleApp" ng-controller="ExampleController as c">
<input jq-spinner ng-model="c.exampleValue" />
<p>{{c.exampleValue}}</p>
</div>
Your fiddle is showing something else.
Besides this: Angular can not know about any changes that occur from outside its scope without being aknowledged.
If you change a variable of the angular-scope from OUTSIDE angular, you need to call the apply()-Method to make Angular recognize those changes. Despite that implementing a spinner can be easily achieved with angular itself, in your case you must:
1. Move the spinner inside the SpinnerCtrl
2. Add the following to the SpinnerCtrl:
$('#spinner').spinner({
change: function( event, ui ) {
$scope.apply();
}
}
If you really need or want the jQuery-Plugin, then its probably best to not even have it in the controller itself, but put it inside a directive, since all DOM-Manipulation is ment to happen within directives in angular. But this is something that the AngularJS-Tutorials will also tell you.
Charminbear is right about needing $scope.$apply(). Their were several problems with this approach however. The 'change' event only fires when the spinner's focus is removed. So you have to click the spinner then click somewhere else. The 'spin' event is fired on each click. In addition, the model needs to be updated before $scope.$apply() is called.
Here is a working jsfiddle http://jsfiddle.net/3PVdE/
$timeout(function () {
$('#spinner').spinner({
spin: function (event, ui) {
var mdlAttr = $(this).attr('ng-model').split(".");
if (mdlAttr.length > 1) {
var objAttr = mdlAttr[mdlAttr.length - 1];
var s = $scope[mdlAttr[0]];
for (var i = 0; i < mdlAttr.length - 2; i++) {
s = s[mdlAttr[i]];
}
s[objAttr] = ui.value;
} else {
$scope[mdlAttr[0]] = ui.value;
}
$scope.$apply();
}
}, 0);
});
Here's a similar question and approach https://stackoverflow.com/a/12167566/584761
as #Charminbear said angular is not aware of the change.
However the problem is not angular is not aware of a change to the model rather that it is not aware to the change of the input.
here is a directive that fixes that:
directives.directive('numeric', function() {
return function(scope, element, attrs) {
$(element).spinner({
change: function(event, ui) {
$(element).change();
}
});
};
});
by running $(element).change() you inform angular that the input has changed and then angular updates the model and rebinds.
note change runs on blur of the input this might not be what you want.
I know I'm late to the party, but I do it by updating the model with the ui.value in the spin event. Here's the updated fiddle.
function SpinnerCtrl($scope, $timeout) {
$timeout(function () {
$('#spinner').spinner({
spin: function (event, ui) {
$scope.spinner = ui.value;
$scope.$apply();
}
}, 0);
});
}
If this method is "wrong", any suggestions would be appreciated.
Here is a solution that updates the model like coder’s solution, but it uses $parse instead of parsing the ng-model parameter itself.
app.directive('spinner', function($parse) {
return function(scope, element, attrs) {
$(element).spinner({
spin: function(event, ui) {
setTimeout(function() {
scope.$apply(function() {
scope._spinnerVal = = element.val();
$parse(attrs.ngModel + "=_spinnerVal")(scope);
delete scope._spinnerVal;
});
}, 0);
}
});
};
});

Angular ng-blur not working with ng-hide

Using a directive focus-me="inTextModeInput" in a text input
app.directive('focusMe', function($timeout) {
/*focuses on input
<input type="text" focus-me="focusInput">
*/
return {
scope: { trigger: '=focusMe' },
link: function(scope, element) {
scope.$watch('trigger', function(value) {
if(value === true) {
$timeout(function() {
element[0].focus();
scope.trigger = false;
});
}
});
}
};
});
Actually having 2 inputs, both uses focus-me
When i programatically set the value to focus on an input the ng-blur of other is not called.
NOTE : i am also using this in an ng-repeat.
Isolated scope
The blur is called, but you're not seeing that because you've created a directive with an isolated scope. The ng-blur is executed on the $parent scope. You should only use an isolated scope when the directive is implementing re-useable templates.
Two way binding on trigger
The line 'scope.trigger = false' is also setting a different boolean value because it's on a different scope. If you want to assign a value to a variable from a directive you should always wrap the value inside another object: var focus = { me: true } and set it like trigger=focus.me.
A better solution
But I wouldn't set the trigger to false at all. AngularJS is a MVC/MVVM based framework which has a model state for the user interface. This state should be idempotent; meaning that if you store the current state, reload the page and restore the state the user interface should be in the exact same situation as before.
So what you probably need is a directive that
Has no isolated scope (which allows all other directives to work: ng-blur, ng-focus, ...)
Keeps track of a boolean, which indicates the focus state
Sets this boolean to false when the element has lost focus
It's probably easier to see this thing in action: working plunker.
Maybe this (other) plunker will give you some more insight on scopes and directives.
Code
myApp.directive('myFocus', function($parse, $timeout) {
return {
restrict: 'A',
link: function myFocusLink($scope, $element, $attrs, ctrls) {
var e = $element[0];
// Grab a parser from the provided expression so we can
// read and assign a value to it.
var getModel = $parse($attrs.myFocus);
var setModel = getModel.assign;
// Watch the parser -- and focus if true or blur otherwise.
$scope.$watch(getModel, function(value) {
if(value) {
e.focus();
} else {
e.blur();
}
});
function onBlur() {
$timeout(function() {
setModel($scope, false);
});
}
function onFocus() {
$timeout(function() {
setModel($scope, true);
});
}
$element.on('focus', onFocus);
$element.on('blur', onBlur);
// Cleanup event registration if the scope is destroyed
$scope.$on('$destroy', function() {
$element.off('focus', onFocus);
$element.off('blur', onBlur);
});
}
};
});

Resources