I'm building a simple app with AngularJS. The app make a async AJAX call to the server and the server returns an array like this:
{
paragraphs: [
{content: "content one"},
{content: "cnt two"},
{content: "random three"},
{content: "last one yeeaah"}
]
}
So I'm setting this content to the StorageService factory via my set method. Everything is fine here.
I'm using ng-repeat to render the results and JQuery UI sortable to be able to change the order of the elements. When an item is swapped my script is calling the StorageService.swap method and the element order in StorageService is updated, BUT ng-repeat isn't rerendering the change, but if I remove/add or change the content it's working. How I can force the angular to rerender the ng-repeat?
= JSFIDDLE =
http://jsfiddle.net/6Jzx4/3/
= Example =
When a swap occurs the ng-repeat should be rerendered, so the IDs are consecutive
= Code =
HTML
<div ng-controller="Test" sortable>
<div ng-repeat="item in data().paragraphs" class="box slide_content" id="{{$index}}">
{{item.content}}, ID: {{$index}}
</div>
<input type="button" ng-click="add()" value="Add">
</div>
JS
var App = angular.module("MyApp", []);
App.controller("Test", function($scope, StorageService) {
StorageService.set({
paragraphs: [
{content: "content one"},
{content: "cnt two"},
{content: "random three"},
{content: "last one yeeaah"}
]
});
$scope.data = StorageService.get;
$scope.add = StorageService.add;
});
App.directive("sortable", function(StorageService) {
return {
link: function(scope, element, attrs) {
$(element[0]).sortable({
cancel: ".disabled",
items: "> .slide_content:not(.disabled)",
start: function(e, t) {
t.item.data("start_pos", t.item.index());
},
stop: function(e, t) {
var r = t.item.data("start_pos");
if (r != t.item.index()) {
StorageService.sort($(this).sortable("toArray"));
}
}
});
}
};
});
App.factory('StorageService', function() {
var output = {};
return {
set: function(data) {
angular.copy(data, output);
return output;
},
get: function() {
return output;
},
add: function() {
output.paragraphs.push({
content: 'Content'
});
},
sort: function(order) {
var localOutput = [];
for (var j in order) {
var id = parseInt(order[j]);
localOutput.push(output.paragraphs[id]);
}
console.log('new order', localOutput);
output.paragraphs = localOutput;
return output;
}
};
});
Angular doesn't know you've changed the array. Executing your sort inside a scope.$apply() will address that.
Note that I've added a that variable since this changes meaning inside the apply.
var that = this;
scope.$apply(function() {
StorageService.sort($(that).sortable("toArray"));
}
But that fix uncovers other problems that appear to be caused by the interaction between the jQuery sortable and Angular (here's a fiddle that shows an attempt at resolving the problems but still has issues). These issues have been solved in Angular UI Sortable. So a good path forward may be to switch to this.
Related
I'm trying to keep all my model functionality in one place. I'd like to be able to call its methods within it:
JS
/**
* This app prints "OK" a number of times, according to which button is pressed
*/
angular.module('myApp', [])
.controller('MyCtrl', ['$scope', function MyCtrl($scope) {
$scope.okModel = {
oks: [],
addOne: function(){
this.oks.push(["OK"]);
},
addTwo: function(){
this.addOK();
this.addOK();
},
buttons: [
{name:"addOK", action: this.addOne}, // THIS is where the issue is I think
{name:"add2OKs", action: this.addTwo}
]
};
}]);
HTML
<div ng-controller="MyCtrl">
<!-- print the object holding all the "OK"s -->
oks: {{okModel.oks}}
<!-- show the buttons -->
<button ng-repeat="item in okModel.buttons" ng-click="item.action()">
{{item.name}}
</button>
<!-- print the "OK"s -->
<div ng-repeat="ok in okModel.oks">
{{ok[0]}}
</div>
</div>
I'm not getting an error, but it's not working either. No "OK"s are being added to the model. It seems like the issue may be with the okModel.buttons action property.
Here's a plunker: https://plnkr.co/edit/mDk43yEKSQB37QSmiKJn?p=preview
TL;DR: I realize that the issue is probably with this in buttons, what should I use instead?
Bonus question: I'm new to angular and realize I may be using models incorrectly. If you know of a better way to do models, please let me know.
Since you asked, the "angular" way to do this is to have a service provide the model, rather than defining it in your controller:
var app = angular.module('myApp', []);
app.factory('modelService', function() {
var okModel = {
oks: [],
}
okModel.addOne = function() {
okModel.oks.push(["OK"]);
};
okModel.addTwo = function() {
okModel.addOne();
okModel.addOne();
};
okModel.buttons = [{
name: "addOK",
action: okModel.addOne
},
{
name: "add2OKs",
action: okModel.addTwo
}
];
return okModel;
});
app.controller('MyCtrl', ['$scope', 'modelService', function MyCtrl($scope, modelService) {
$scope.okModel = modelService;
}]);
Here is a plunk.
#reeverd answer seems to be correct. Here is a little more clean up.
$scope.okModel = {
oks: [],
addOne: function(){
oks.push(["OK"]); // you don't have to push an array with the OK text. You can just push in the OK text itself.
},
addMulti: function(cnt){
for (var ii = 0; cnt < ii; ii++) {
// this.addOK(); this may also be your issue addOK function is not defined.
$scope.addOne(); // calls the addOne and pushes OK into the oks array.
}
},
buttons: [
{name:"addOK", action: $scope.addOne}, // THIS is where the issue is I think
{name:"add2OKs", action: $scope.addMulti(2)}
]
};
The problem is that this inside the {name:"addOK", action: this.addOne} in the array is the object itseld, and not the object wrapping the array. In this case you could do something like this:
angular.module('myApp', [])
.controller('MyCtrl', ['$scope', function MyCtrl($scope) {
// Here you declare the functions which are going to modify
// $scope.okModel.oks
var addOne = function() {
if ($scope.okModel) {
$scope.okModel.oks.push("OK");
}
},
addTwo = function() {
addOne();
addOne();
};
$scope.okModel = {
oks: [],
addOne: addOne, // Here you assign okModel.addOne to the functions declared above
addTwo: addTwo, // Same here
buttons: [{
name: "addOK",
action: addOne // Same here
}, {
name: "add2OKs",
action: addTwo // Same here
}]
};
}]);
First you declare the functions which will modify $scope.okModel.oks array and then you use that same funcions both in your model methods and in your model buttons.
EDIT: Here you have a working plunker: https://plnkr.co/edit/sKUxjzUyVsoYp3S7zjTb?p=preview
Instead of using this, try using $scope.okModel. The actual object where this refers to is not always what you expect. It depends on how the function is invoked.
EDIT:
You can pull the definition of the functions out of the okModel like this:
angular.module('myApp', [])
.controller('MyCtrl', ['$scope', function MyCtrl($scope) {
var addOne = function() {
$scope.okModel.oks.push(["OK"]);
};
var addTwo = function() {
addOne();
addOne();
};
$scope.okModel = {
oks: [],
addOne: addOne,
addTwo: addTwo,
buttons: [{
name: "addOK",
action: addOne
}, {
name: "add2OKs",
action: addTwo
}]
};
}]);
I have been trying to subscribe to when a dropdown value changes. I have the following logic however I cannot seem to get it working.
HTML
<div id="case-pin-#modelItem.CaseID" data-caseid="#modelItem.CaseID" class="row hidden popovercontainer pinBinding">
<select data-bind="options:userPins,
value:selectedPin,
optionsCaption:'-- please select --',
optionsText: 'Name',
optionsValue: 'Id'"></select>
</div>
JS
function UserPinViewModel(caseId) {
var self = this;
self.selectedPin = ko.observable();
self.userPins = ko.observableArray([]);
self.caseId = caseId;
self.selectedPin.subscribe(function (newValue) {
console.log(newValue);
//addCaseToPin(newValue, self.caseId);
});
}
var pinObjs = [];
$(function () {
pinObjs = [];
$(".pinBinding").each(function () {
var caseId = this.getAttribute("data-caseid");
var view = new UserPinViewModel(caseId);
pinObjs.push(view);
ko.cleanNode(this);
ko.applyBindings(view, this);
});
})
The userPins array is populated by an AJAX call to the server as the values in the dropdown are dependent upon another section of the website which can change the values in the dropdown - here the logic I have used to populate the array.
function getPins() {
$.ajax({
type: 'POST',
url: '/Home/GetPins',
success: function (data) {
for (var i = 0; i < pinObjs.length; i++) {
pinObjs[i].userPins(data);
}
},
error: function (request, status, error) {
alert("Oooopppppsss! Something went wrong - " + error);
}
});
}
The actual values in the dropdowns all change to match what is returned from the server however whenever I manually change the dropdown, the subscription event is not fired.
You're using both jQuery and Knockout to manipulate the DOM, which is not a good idea. The whole idea of Knockout is that you don't manipulate the DOM, it does. You manipulate your viewModel.
Using cleanNode is also a code smell, indicating that you're doing things the wrong way. Knockout will handle that if you use the tools Knockout provides.
In this case, I was going to suggest a custom binding handler, but it looks like all you really want is to have a UserPinViewModel object created and applied to each instance of your .pinBinding element in the HTML. You can do that using the with binding, if you expose the UserPinViewModel constructor in your viewModel.
function UserPinViewModel(caseId) {
var self = this;
self.selectedPin = ko.observable();
self.userPins = ko.observableArray([]);
self.caseId = caseId;
self.selectedPin.subscribe(function(newValue) {
console.log(newValue);
//addCaseToPin(newValue, self.caseId);
});
// Pretend Ajax call to set pins
setTimeout(() => {
self.userPins([{
Name: 'option1',
Id: 1
}, {
Name: 'option2',
Id: 2
}, {
Name: 'option3',
Id: 3
}])
}, 800);
// Later, the options change
setTimeout(() => {
self.userPins([{
Name: 'animal1',
Id: 'Elephant'
}, {
Name: 'animal2',
Id: 'Pony'
}, {
Name: 'animal3',
Id: 'Donkey'
}])
}, 4000);
}
ko.bindingHandlers.pin = {
init: () => null,
update: () => null
};
ko.applyBindings({
pinVm: UserPinViewModel
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div id="case-pin-#modelItem.CaseID" data-bind="with: new pinVm('someCaseId')" class="row hidden popovercontainer pinBinding">
<select data-bind="options:userPins,
value:selectedPin,
optionsCaption:'-- please select --',
optionsText: 'Name',
optionsValue: 'Id'"></select>
</div>
Your getPins function suggests that the .pinBinding elements should correspond to the data being received. In that case, pinObjs should really be a part of your viewModel, and the elements should be generated (perhaps in a foreach) from the data, rather than being hard-coded. I don't know how that works with what I presume is the server-side #modelItem.CaseID, though.
I am relatively new to Angular and got stuck on a custom directive.
I am trying to create a dynamic grid as a custom directive.
I already got that part working as in this example:
working grid as custom directive
There are certain scenarios where I need to set attributes on some of the elements of the grid.
This part has got me stumped.
I am planning on including the attributes as an array inside the object and then just putting it in the html tag of the associated entry.
This part is demonstrated here:
broken grid as custom directive with dynamic attributes
If you look at the "entries" array in the controller, I have now changed it to include an "attributes" array which will contain objects specifying the attribute name and property. These attributes should then be applied to the associated column.
e.g.
(First entry of the array)
col1: {
text: 'Obj1.col1',
attributes: [{
attr: 'ng-class',
attrVal: 'propVal == "" ? "someClass" : "someOtherClass"'
}, {
attr: 'id',
attrVal: '{{propName}}{{$index}}'
}]
},
...Truncated for brevity
This array entry should then be translated to:
<td ng-class="propVal == '' ? 'someClass' : 'someOtherClass'" id="col11">Obj1.col1</td>
I have read a couple of articles about the execution order of compile, controller, pre-link and post-link functions and have played around with different orders and trying to invoke compiling myself, but it all has failed.
Probably because I lack a deeper understanding of how it all ties together.
If someone can help me out or point me in the right direction if I'm heading down the wrong path, I would greatly appreciate that.
Okay, I finally figured out how to generate the grid dynamically using embedded custom directives inside a parent custom directive.
Here is a plunker showing how I did it:
Plunker with working dynamic grid
I have the Html templates defined as:
<div ng-grid ng-collection="entries" ng-collection-headings="headings" ng-button-click="theAction(inp)">
<div ng-checkbox-column></div>
</div>
and then the ng-grid directive as:
.directive("ngGrid", function () {
return {
restrict: "A",
scope: {
ngCollectionHeadings: "=",
ngCollection: "=",
ngButtonClick: "&"
},
template: function (element, attrs) {
var children = element.html();
children = children.trim().replace(/div/g, "td");
var htmlText = "<input type='button' ng-click='buttonClicked()' value='From the grid directive' /><table class='table table-bordered'><thead><tr><th ng-repeat='heading in ngCollectionHeadings'>{{heading}}</th></tr></thead><tbody><tr id='item{{$index}}' ng-repeat='item in ngCollection'>" + children + "</tr></tbody></table>";
return htmlText;
},
controller: function ($scope, $element) {
$scope.buttonClicked = function (inp) {
if (typeof inp != 'undefined')
inp = inp + ", through the grid directive.";
else
inp = "From the grid directive.";
$scope.ngButtonClick({ inp: inp });
};
}
};
})
and finally the ng-checkbox-column directive:
.directive("ngCheckboxColumn", function () {
return {
restrict: "A",
template: function (element, attributes) {
var htmlText = "<td><label><input type='checkbox' ng-model='item.checked' ng-click='tempButtonClicked()' /> From the checkbox directive.</label></td>";
return htmlText;
},
controller: function ($scope, $element) {
$scope.tempButtonClicked = function () {
var val = "From the checkbox directive";
$scope.buttonClicked(val);
};
}
};
})
My data collections are pretty straight forward:
$scope.headings = {
head1: 'Heading 1',
head2: 'Heading 2',
head3: 'Heading 3'
};
$scope.entries = [{
col1: 'Obj1.col1',
col2: 'Obj1.col2',
col3: 'Obj1.col3',
checked: false
}, {
col1: 'Obj2.col1',
col2: 'Obj2.col2',
col3: 'Obj2.col3',
checked: false
}, {
col1: 'Obj3.col1',
col2: 'Obj3.col2',
col3: 'Obj3.col3',
checked: false
}, {
col1: 'Obj4.col1',
col2: 'Obj4.col2',
col3: 'Obj4.col3',
checked: false
}];
This is still not entirely completed, but you should get the basic idea.
How does one pass {{row.getProperty(col.field)}} into ng-click? What happens is the id does not get propagated back, but the grid render correctly with the id.
code:
var app = angular.module('testing',['ngGrid']);
app.config(['$locationProvider', function($locationProvider)
{
$locationProvider.html5Mode(true);
}]);
app.controller('TestCtrl',function($scope)
{
$scope.details = []; //whatever dummy data
$scope.loadById = function(id)
{
$window.location.href= 'newPage/?id='+id;
};
$scope.gridOptions =
{
data: 'details',
columnDefs:[{field:'id',DisplayName:'id',
cellTemplate:'<div class="ngCellText" ng-class="col.colIndex()"><a ng-click="loadById({{row.getProperty(col.field)}})">{{row.getProperty(col.field)}}</a></div>'
}]
};
});
You can just pass row in ng-click, instead of only the value outputted by the double brackets expression.
Your cellTemplate becomes this
cellTemplate:'<div class="ngCellText" ng-class="col.colIndex()"><a ng-click="loadById(row)">{{row.getProperty(col.field)}}</a></div>'
Your function becomes this
$scope.loadById = function(row) {
window.console && console.log(row.entity);
$window.location.href= 'newPage/?id='+ row.entity.id;
};
I am attempting to create a custom table element like this:
<datatable items='tableItems' columns='columnsConfig' />
Here 'tableItems' is my array of items and 'columnsConfig' is configuration for column rendering, something like :
$scope.tableItems = [...];
$scope.columnsConfig = [
{
name: 'check',
with: '20px',
renderer: function (rowItem, cellValue) {
return '<input ng-click="clickHandler()" type="checkbox"/>';
}
},
{name: "person.fullName", text: "Name", visible: true, width: '150px'},
{
name: "person.age",
text: "Age",
renderer: function(rowItem, cellValue) {
return cellValue + ' years old';
}
}
];
Inside renderer function I can specify some additional data processing or templating.
In my directive template I have this:
<tbody>
<tr ng-repeat="item in items">
<td ng-repeat="column in columns"
ng-show="column.visible"
ng-bind-html-unsafe="getCellValue(item, $index)">
</td>
</tr>
</tbody>
where inside 'getCellValue' function I invoking my renderer function. Here is directive code:
angular.module('components', [])
.directive('datatable', function () {
return {
restrict: 'E',
templateUrl: '../pages/component/datatable.html',
scope: {
items: "=",
columns: "="
},
controller: function ($scope, $element) {
$scope.getCellValue = function (item, columnIndex) {
var column = $scope.columns[columnIndex];
// return render function result if it has been defined
if (column.renderer) {
return column.renderer(item, getItemValueByColumnName(item, column.name));
}
// return item value by column
return getItemValueByColumnName(item, column.name);
};
}
}
});
All works fine except ng-... directives. I think I have to do some additional processing of 'renderer' function results via $compile or something but yet I can't figure out how to achieve this. So the question is how make ng directives work when I specify them via my renderer function ?
Thanks.
After some investigations I have found next solution:
//after all DOM manipulations we should recompile parts that has been modified
setTimeout(function () {
applyAfterRenderDOMChanges();
}, 0);
var applyAfterRenderDOMChanges = function () {
var cells = $('td', element).children();
$compile(cells)(scope);
scope.$apply();
};
I have some concerns about efficiency of this solition but it works well so far.