ko.mapping (update array issue) - javascript

I have the problem with ko.mapping
1) On client ViewModel:
var self = {
books: ko.observableArray([ {id : 1, title: "A"},{id : 2, title: "B" } ])
};
2) From server (array of objects) :
var data = [ {id : 1, title: "C"} ]
3)I need to replace data only if it exists:
[ {id : 1, title: "C"},{number : 2, title: "B" } ]
3) I try to using ko.mapping plugin, but result [ {id : 1, title: "C"} ] => data replace self.books
var mappingOptions = {
key: function (data) {
return ko.utils.unwrapObservable(data.id);
}
};
ko.mapping.fromJS(data, mappingOptions, self.books);
Thx:)

You can (probably) use the ko.mapping plugin for this, but I think it'll benefit you if you try to solve this in plain javascript first. It's not much code, and at the very least you'll better understand what kinds of things the plugin does under the hood.
Your books array needs to be merged with an array of updates (data). You use the id property to check for equality. So, in short, you'll have to:
Loop through your updates
Find the old book that matches the id
Replace it with the new book
Set the books array with the updated list so knockout can figure out what has changed
A straightforward implementation of the update function:
var books = ko.observableArray([
{id : 1, title: "A"},
{id : 2, title: "B" }
]);
var updateBooks = function(updates) {
var oldBooks = books();
var newBooks = oldBooks.map(function(book) {
var update = updates.find(function(update) {
return update.id === book.id;
});
return update || book;
});
books(newBooks);
};
ko.applyBindings({
books: books,
updateBooks: updateBooks
});
updateBooks([{ id: 1, title: "C" }]);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<ul data-bind="foreach: books">
<li>
<span data-bind="text:id"></span>
<strong data-bind="text: title"></strong>
</ul>
This implementation will work if you don't mind performance requirements, and if you can be certain all of the ids in the updates are already available in the oldBooks (i.e. the updates do not contain new books). It also creates a new array with a combination of new and old book objects. This makes it harder for knockout to render the list efficiently.
With some more work, you can make a book viewmodel with an observable title property. By using an id based map, you can speed up the update cycle.
var Book = function(options) {
this.id = options.id;
this.title = ko.observable(options.title);
// Instead of creating new Books all the time,
// we update its observable property so only the changed values
// are re-rendered
this.update = function(options) {
this.title(options.title);
}.bind(this);
};
Book.create = function(options) { return new Book(options); };
var BookList = function(bookData) {
this.books = ko.observableArray(bookData.map(Book.create));
// Compute an object that stores each book by id.
// Whenever the books array changes, this object is updated.
// Access a book by calling: this.bookMap()[bookId]
this.bookMap = ko.pureComputed(function() {
return this.books().reduce(function(map, book) {
map[book.id] = book;
return map;
}, {});
}, this);
};
BookList.prototype.updateBooks = function(updates) {
// Apply each update
updates.forEach(function(newBook) {
// Find the book by id in our map
var bookRef = this.bookMap()[newBook.id];
if (bookRef) {
// The book has its own viewmodel, with an update method
bookRef.update(newBook);
}
}.bind(this));
};
var data = [
{id : 1, title: "A"},
{id : 2, title: "B" }
];
var vm = new BookList(data);
ko.applyBindings(vm);
vm.updateBooks([{ id: 1, title: "C" }]);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<ul data-bind="foreach: books">
<li>
<span data-bind="text:id"></span>
<strong data-bind="text: title"></strong>
</ul>

Related

How do I deal with an object sent into AngularJS controller?

In my controller I have a function that recieves an object from Java controller. My AngularJS variable is simple:
var self = this;
self.item = {};
And my function where I get the object:
function getItem() {
MyService.getItem(REST_SERVICE_URI)
.then(
function (d) {
self.item = d;
},
function (errResponse) {
console.error('Error while getting item');
}
);
}
Object that's received has rather complicated structure. It has id, name and list of child objects, who have also id and name fields. How do I get into this object's fields and list in the AngularJS controller? I tried loop though list using fucntion below to even count duplicate values but it didn't work. I tried even to include one more loop into it with outputing result in console, no effect. It only returns zeros.
var i = "SOME TEST NAME VALUE TO CHECK";
function getCount(i) {
var iCount = iCount || 0;
for (var el in self.item) {
console.log("let me see what are you: " + el);
if (el == i) {
iCount++;
}
}
return iCount;
}
The object I recieve is ok, I can see it content in Chrome using F12 - Network - Response or Preview.
added later:
On my page I test it like this
<tr class="my_item" ng-repeat="p in ctrl.item.children">
<span>getCount {{p.name}}: {{ctrl.getCount(p.name)}}</span>
</tr>
It displays p.name in the span btw. Java object structure is
public class Item {
private int id;
private String name;
List<Child> children = new ArrayList<>();
}
Child class is simple
public class Child {
private int id;
private String name;
}
As per your question, the content is complex and has recursive properties inside child content.
So you need to iterate on content recursively, inside one forEach loop.
See this example working Demo:
var myApp = angular.module('myApp', []);
myApp.controller('ExampleController', function() {
var vm = this;
vm.count = 0;
vm.searchTxt = "";
vm.getCount = function() {
vm.count = 0; //clear count before search
recursive(vm.content);
}
function recursive(dataArray) { //recursive function
dataArray.forEach(function(data) {
if (vm.searchTxt == data.name) { //match name property
vm.count = vm.count + 1;
}
if (data.child.length > 0) {
recursive(data.child); // call recursive function
}
});
}
vm.content = [{ //example content
id: 1,
name: 'one',
child: [{
id: 1.1,
name: 'new one',
child: [{
id: 1,
name: 'one',
child: []
}]
}]
}, {
id: 2,
name: 'two',
child: [{
id: 1.1,
name: 'new two',
child: []
}]
}]
});
<script src="https://code.angularjs.org/1.5.2/angular.js"></script>
<div ng-app="myApp" ng-controller="ExampleController as vm">
<input ng-model="vm.searchTxt" placeholder="ender search.." />
<br>
<button ng-click="vm.getCount()">Search</button>
<br>
<span>Match 'Name' count : {{vm.count}}</span>
</div>

Group the same `category` objects [duplicate]

This question already has answers here:
Group array items using object
(19 answers)
Closed 6 years ago.
I'm trying to group the raw data from:
items:
[
{
category: "blog",
id : "586ba9f3a36b129f1336ed38",
content : "foo, bar!"
},
{
category: "blog",
id : "586ba9f3a36b129f1336ed3c",
content : "hello, world!"
},
{
category: "music",
id : "586ba9a6dfjb129f1332ldab",
content : "wow, shamwow!"
},
]
to
[
{
category: "blog",
items:
[
{
id : "586ba9f3a36b129f1336ed38",
content : "foo, bar!"
},
{
id : "586ba9f3a36b129f1336ed3c",
content : "hello, world!"
},
]
},
{
category: "music",
items:
[
{
id : "586ba9a6dfjb129f1332ldab",
content : "wow, shamwow!"
}
]
}
]
The format like this helps me to print the same category data together in the frontend.
The content of the category field is dynamically, so I'm not sure how do I store it to a temporary object and sort them, any thoughts?
(I can't think a better title for the question, please edit if you got a better title.)
You can do it using Array#reduce in one pass:
var items = [{"category":"blog","id":"586ba9f3a36b129f1336ed38","content":"foo, bar!"},{"category":"blog","id":"586ba9f3a36b129f1336ed3c","content":"hello, world!"},{"category":"music","id":"586ba9a6dfjb129f1332ldab","content":"wow, shamwow!"}];
var result = items.reduce(function(r, item) {
var current = r.hash[item.category];
if(!current) {
current = r.hash[item.category] = {
category: item.category,
items: []
};
r.arr.push(current);
}
current.items.push({
id: item.id,
content: item.content
});
return r;
}, { hash: {}, arr: [] }).arr;
console.log(result);
Or the ES6 way using Map:
const items = [{"category":"blog","id":"586ba9f3a36b129f1336ed38","content":"foo, bar!"},{"category":"blog","id":"586ba9f3a36b129f1336ed3c","content":"hello, world!"},{"category":"music","id":"586ba9a6dfjb129f1332ldab","content":"wow, shamwow!"}];
const result = [...items.reduce((r, { category, id, content }) => {
r.has(category) || r.set(category, {
category,
items: []
});
r.get(category).items.push({ id, content });
return r;
}, new Map).values()];
console.log(result);
Personally, without any helper libraries, I'd just do this
var step1 = items.reduce((result, {category, id, content}) => {
result[category] = result[category] || [];
result[category].push({id, content});
return result;
}, {});
var result = Object.keys(step1).map(category => ({category, items: step1[category]}));
Which babel converts to
var step1 = items.reduce(function (result, _ref) {
var category = _ref.category,
id = _ref.id,
content = _ref.content;
result[category] = result[category] || [];
result[category].push({ id: id, content: content });
return result;
}, {});
var result = Object.keys(step1).map(function (category) {
return { category: category, items: step1[category] };
});
So I just solved the question with the following code (jsfiddle):
// Items
// var items = []
// Create an empty object, used to store the different categories.
var temporaryObject = {}
// Scan for each of the objects in the `items` array.
items.forEach((item) =>
{
// Create a category in the teporary object if the category
// hasn't been created.
if(typeof temporaryObject[item.category] === "undefined")
temporaryObject[item.category] = []
// Push the item to the its category of the `temporaryObject`.
temporaryObject[item.category].push({
id : item.id,
content: item.content
})
})
// Create a empty array used to stores the sorted, grouped items.
var newItems = []
// Scan for each of the category in the `temporaryObject`
for(var category in temporaryObject)
{
// Push the new category in the `newItems` array.
newItems.push({
category: category,
items : []
})
// Get the last category index of the `newItems` array,
// so we can push the related data to the related category.
var lastItem = newItems.length - 1
// Scan for the related category in the `temporaryObject` object.
temporaryObject[category].forEach((item) =>
{
// Push the related data to the related category of the `newItems` array.
newItems[lastItem].items.push(item)
})
}
// Prints the sorted, grouped result.
console.log(newItems)

ReactJs - How to update state in array?

I'm creating a Page Builder with React.
I have a component which contains the structure of the page.
var LayoutPage = React.createClass({
getInitialState: function getInitialState() {
return {
items: {
'78919613':{
id: '78919613',
component : 'OneColumn',
cols:{
'565920458':{
id: '565920458',
content:{
'788062489':{
id: '788062489',
component : 'Text',
params: 'Lorem ipsum'
},
'640002213':{
id: '640002213',
component : 'Text',
params: 'Lorem ipsum'
}
}
}
}
}
}
};
},
.....
});
I have a system with drag'n drop to put a new element on the page and it works. But when the new element is dropped I want to update the state to add a new item in the array.
So how can I push a new item ? I did a test with that :
this.state.items.push({.....});
But I have an error :
TypeError: this.state.items.push is not a function
Can you help me ?
Thank you.
Instead of using an object in your state you should change it to the array like below :
this.state = {
items: [ // items array
{
id: 1,
name: "Stack"
},
{
id: 2,
name: "Overflow"
}],
count: 3, // another state
textValue : '' // and this is state too
}
Where the items it's an array of objects. And then you will be able to add new items to an array.
const newItem = {
id : this.state.count,
name: this.state.textValue
};
const newArr = this.state.items.concat(newItem);
this.setState({
items: newArr,
textValue: '',
count: this.state.count + 1
})
The whole example is here.
I hope it will help you!
Thanks
You'll start to get headaches if you directly mutate the state of your application. If you forget to call this.setState then it won't re-render!
Assuming you can't use an array (which would be easier), then you'll have to generate a unique key if you want to add another item to the object.
// create a new copy of items based on the current state
var newItems = Object.assign({}, this.state.items),
newItem = { id: '', component: '', cols: {} },
uniqueId = generateUniqueId();
// safely mutate the copy
newItems[uniqueId] = newItem;
// update the items property in state
this.setState({ items: newItems });
This is even easier with ES7/Babel.
const newItem = { id: '', component: '', cols: {} },
uniqueId = generateUniqueId(),
items = { [uniqueId]: newItem, ...this.state.items };
this.setState({ items });
You can generate a similar unique ID to the one you have there using Math.random.
function generateUniqueId() {
// removing leading '0.' from number
return Math.random()
.toString()
.slice(3);
}

Update Value In textbox using Knockout + Typeahead Custom Binding

I am making an application using knockout + typeahead that will show suggestion list of entered character matches the list.
All thing working fine.
Only problem is when I select the item from list it stores the whole object.
I want to store only name is first textbox and as per it store related value in second textbox.
I am not getting how to do that ?
can do using subscribe ?
put subscribe on first textbox, it will get whole object then it will process the object then store values in related textbox.
HTML :
<input type="text" class="form-control" data-bind="typeahead: data, dataSource: categories"/> *
<input type="text" class="form-control" data-bind="value: item"/>
Java Script :
var ViewModel = function () {
var self = this;
self.categories = [
{ name: 'Fruit', items: 'abc' },
{ name: 'Vegetables', items: 'xyz' }
];
self.data = ko.observable();
self.item = ko.observable();
};
var viewModel = new ViewModel();
ko.bindingHandlers.typeahead = {
init: function (element, valueAccessor, allBindingsAccessor) {
var $e = $(element);
var accessor = valueAccessor();
var source = allBindingsAccessor().dataSource || [];
var names = new Bloodhound({
datumTokenizer: function (d) {
return Bloodhound.tokenizers.whitespace(d.name);
},
queryTokenizer: Bloodhound.tokenizers.whitespace,
local: source
});
names.initialize();
var substringMatcher = function() {
return function findMatches(q, cb) {
var matches, substrRegex;
substrRegex = new RegExp(q, 'i');
$.each(source, function(i, p) {
if (substrRegex.test(p.name)) {
matches.push({ value: p });
}
});
console.dir(matches);
cb(matches);
};
};
$e.typeahead({
hint: true,
highlight: true,
minLength: 1
},
{
name: 'name',
displayKey: 'name',
source: names.ttAdapter()
}).on('typeahead:selected', function (el, datum) {
console.dir(datum);
accessor(datum);
}).on('typeahead:autocompleted', function (el, datum) {
console.dir(datum);
console.log(accessor);
});
},
update: function (element, valueAccessor) {
var value = ko.utils.unwrapObservable(valueAccessor());
console.log(value);
$(element).val(ko.utils.unwrapObservable(valueAccessor()));
}
};
ko.applyBindings(viewModel);
Here is jsfiddle of it.
I have asked question in morning also but i don't get answer so I code myself by studying the tutorials, but now I am stuck Here.
Any Suggestion on this situation ??
There are certain limitations with the temporary workout like knockout extension for Bootstrap Typeahead. I used it before in my project but it lead to a lot of limitations like custom control over templating, custom control over selected object property and selected object value. So I moved on to plugin jqAuto which is based on JQuery-UI autocomplete.
This plugin has enormous range of customizations and is faster than typeahead. I replicated your code in the jqAuto code and it is faster and cleaner than bootstrap typeahead.
JavaScript
var ViewModel = function() {
self.categories = [
{ name: 'Fruit', items: 'abc' },
{ name: 'Vegetables', items: 'xyz' }
];
self.data = ko.observable("");
self.item = ko.observable();
};
ko.applyBindings(new ViewModel());
HTML
<input data-bind="jqAuto: { source: categories, value: data, dataValue : data, inputProp: 'name', valueProp: 'name', labelProp: 'name' }" />
You can control the label propery, input property as well as value property in granular level.
Fiddle demo : http://jsfiddle.net/rahulrulez/uGGb8/4/
Detailed documentation of jqAuto plugin is given here : https://github.com/rniemeyer/knockout-jqAutocomplete

Find object in an array

I want to implement some sort of hasObject function with underscore.js.
Example:
var Collection = {
this.items: [];
this.hasItem: function(item) {
return _.find(this.items, function(existingItem) { //returns undefined
return item % item.name == existingItem.name;
});
}
};
Collection.items.push({ name: "dev.pus", account: "stackoverflow" });
Collection.items.push({ name: "margarett", account: "facebook" });
Collection.items.push({ name: "george", account: "google" });
Collection.hasItem({ name: "dev.pus", account: "stackoverflow" }); // I know that the name would already be enough...
For some reason underscores find returns undefined...
What am I doing wrong?
It looks like you are reading underscore documentation too literally, where
they have:
var even = _.find([1, 2, 3, 4, 5, 6], function(num){ return num % 2 == 0; });
However, this doesn't make any sense for your case, you just want to see if the .name property is equal to
some other object's .name, like this:
var Collection = {
items: [],
hasItem: function(item) {
return _.find(this.items, function(existingItem) { //returns undefined
return item.name === existingItem.name;
});
}
};
You need to check both values for the name and the account.
var Collection = {
this.items: [];
this.hasItem: function(target) {
return _.find(this.items, function(item) {
return item.name === target.name && item.acount === target.account;
});
}
};
Have you considered using Backbone.js? It fulfills all your collection management needs and uses underscore's methods too.
// create a collection
var accounts = new Backbone.Collection();
// add models
accounts.add({name: 'dev.pus', account: 'stackoverflow'});
accounts.add({name: 'margarett', account: 'facebook'});
accounts.add({name: 'george', account: 'google'});
// getting an array.
var results = accounts.where({ name: 'dev.pus', account: 'stackoverflow' });

Categories

Resources