I have two observable arrays, and I need to remove elements from the first one and push to the second one and vice versa. But when I do so, the alphabetical sorting is messed up.
self.allCourses = ko.observableArray([]);
self.selectedCourses = ko.observableArray([]);
I will interchange courses between the two arrays, and using this :
self.sortArrays = function(){
self.allCourses.sort(function (l, r) {
return l.code() < r.code() ;
});
self.selectedCourses.sort(function (l, r) {
return l.code() < r.code() ;
});
}
not only is it not efficient, but also doesnt work as expected ;I call the function each time I call one of these functions
self.addCourse = function(course){
self.selectedCourses.push(course);
self.allCourses.remove(course);
self.sortArrays();
};
self.removeCourse = function(course){
self.allCourses.push(course);
self.selectedCourses.remove(course);
self.sortArrays();
};
I would consider two approaches.
Keep your data always sorted. Instead of calling .sort(), search for the right location to put the element, and call .splice() to insert it in the right place. This is a O(n) algorithm, but should be fast in practice.
Use something like https://libraries.io/npm/dsjslib to maintain a sorted data structure at all times. This makes insert/delete a O(log(n)) operation. However every operation now has extra complexity.
Which one to use will depend on whether your operations are dominated by the effort of insert/delete, or by running through the list and displaying it. My best guess is that running through the list and displaying it matters more.
Furthermore the next question is whether it is better to do the search by scanning through the array, or by binary search. Scanning is O(n) but branch prediction mistakes cost so much that I've seen it be faster than binary search for inserting into lists of hundreds of elements.
Using knockout, u can also create computed based on your observable array, so you always will have sorted array
self.allCoursesSorted = ko.computed(function(){
return this.allCourses.sort(function (l, r) {
return l.code() < r.code() ;
});
}, this);
for selected courses you can use same approach but with filter
self.allCoursesSelected = ko.computed(function(){
return ko.utils.arrayFilter(this.allCoursesSorted(),
function (item) {
return item.selected === true;
});
}, this);
When removing an item from an array, you will never have to do a re-sort.
Instead of pushing and re-sorting, you could insert an item using your sort definition.
You'll only need to define the sorted inject function, since knockout observable arrays already have a remove method:
const sorter = (a, b) => a > b ? 1 : a < b ? -1 : 0;
const leftNumbers = ko.observableArray(
[3,5,1,2].sort(sorter)
);
const rightNumbers = ko.observableArray(
[4,1,3,5].sort(sorter)
);
// There are many ways to write this function, which you can probable
// find on stack overflow. The destructuring probably makes this slower
// than just re-sorting. I'll leave it up to you to optimize for performance.
const injectSorted = (sorter, arr, nr) => {
const pos = arr.findIndex(x => sorter(x, nr) > -1);
if (pos === -1) return arr.concat(nr);
return [
...arr.slice(0, pos),
nr,
...arr.slice(pos)
];
};
// Notice how we don't need to re-sort
const moveFromTo = (arr1, arr2) => x => {
arr2(injectSorted(sorter, arr2(), arr1.remove(x)));
};
ko.applyBindings({ leftNumbers, rightNumbers, moveFromTo });
div { display: flex; justify-content: space-around; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<p>Click numbers to move between lists</p>
<div>
<ul data-bind="foreach: leftNumbers">
<li data-bind="click: moveFromTo(leftNumbers, rightNumbers), text: $data"></li>
</ul>
<ul data-bind="foreach: rightNumbers">
<li data-bind="click: moveFromTo(rightNumbers, leftNumbers), text: $data"></li>
</ul>
</div>
Related
TLDR: I'm mapping over an array of arrays and mapping over each of those arrays to render data. React is rendering the each element of each array in order, so that the entire first array is rendered before the second array begins rendering. I would like to render the first element of each array, then the second element of each array, etc.
I'm trying to build a masonry component! From what I can tell, these are traditionally made by calculating the length of each column, and appending new items to the shortest column, then re-calculating and doing it again.
This is an obviously expensive process, and a hacky solution I came up with is:
Sort data (images with known heights) from lowest to highest
Split these evenly into a number of arrays equal to the desired number of columns (end result may have columns of different lengths, but it will approximate close enough for my needs)
Randomize the arrays to give variation in heights
Display data from arrays in flexbox columns
Here's the relevant code for that:
const { columns, height, width } = useStore()
const [ content, setContent ] = useState<any[]>([])
useEffect(() => {
// Sort data by image height
data.sort((a: item, b: item) =>
a.images.downsized.height - b.images.downsized.height
)
// Create array of arrays
let columnsArray: any[] = []
for (let i = 0; i < columns; i++) {
let newArray: item[] = []
columnsArray.push(newArray)
}
// Populate each array with images
let index = 0
data.forEach((item: item) => {
columnsArray[index].push(item)
if (index == columnsArray.length - 1) { index = 0 }
else { index++ }
})
// Randomize each array to give masonry appearance
const shuffle = (array: any[]) => {
// Fisher-Yates algorithm
let m = array.length, t, i;
while (m) {
i = Math.floor(Math.random() * m--)
t = array[m]
array[m] = array[i]
array[i] = t
}
return array
}
columnsArray.forEach(array => shuffle(array))
// Move new data structure to state
setContent(columnsArray)
}, [data])
This works really well as it performs pretty light computations and leverages CSS and flexbox to make up the difference.
The problem is that it renders each column in order. If I'm rendering two columns of 100 items each, it doesn't make sense to load the 100th item of the first column before the 1st item of the second.
Here's the code for rendering:
{content.length > 0 && content.map((contentArray: item[]) =>
<Col>
{contentArray.map((item: item) =>
<MasonryItem
src={item.images.downsized.url}
alt={item.title}
/>
)}
</Col>
)}
Instead, I'd like React to render the first item of the first array, then the first item of the second, etc.
Is there a way to do this? Thanks in advance!
I have a Vue component that builds the below into a blog form field. The writer is allowed to creatively add/slot any field of choice in between each other when building a blog post ...(like: title, paragraph, blockquote, image) in an object like:
{"post":{"1":{"title":{"name":"","intro":""}},"2":{"paragraph":{"text":"","fontweight":"default-weight","bottommargin":"default-bottom-margin"}},"3":{"image":{"class":"default-image-class","creditto":""}},"4":{"subheading":{"text":"","size":"default"}}}};
I've tried using jQuery each to iterate and add it up into a makedo "dataObj" object and inject it back on the data:
data: { treeData: myUserData.post },
injectFieldType: function(type, position){
var storeObj = {};
var dataObj = this.treeData;
var crntKey;
$.each( dataObj, function( key, value ) {
if(key < position)
{
//remain same as key is not to change
}
else if(key == position)
{
dataObj[''+(parseInt(key)+1)] = dataObj[key]; /*push key further right with +1*/
dataObj[key] = /*add injected field here*/;
}
else if(key > position)
{
dataObj[''+(parseInt(key)+1)] = dataObj[key]; /*push the rest*/
}
});
and inject it back with (this.treeData = dataObj;) when it has injected the desired key and has shifted the rest by adding 1 to their keys when this is clicked:
<button type="button" v-on:click="injectFieldType('image','2')">
I need to have {"post":{"1":{"title":{"name":"","intro":""}},"2":{"image":{"class":"default-image-class","creditto":""}},"3":{"paragraph":{"text":"","fontweight":"default-weight".... When I try to inject the image field in-between the existing "name" and "paragraph" fields and make the paragraph key now 3 (instead of the old 2).
I want "{1:{foo}, 2:{bar}"} to become => {"1:{foo}, 2:{moo}, 3:{bar}" }(notice 3 changed key)
NOTE: the number order is needed to align them reliably in publishing. And data: { treeData: myUserData.post } needs to agree with the changes to allow creating the field and updating each form "name" attribute array.
There are a few problems to address here.
Firstly, trying to use var dataObj = this.treeData; and then this.treeData = dataObj isn't going to help. Both dataObj and this.treeData refer to the same object and that object has already been processed by Vue's reactivity system. You could address the reactivity problems by creating a totally new object but just creating an alias to the existing object won't help.
Instead of creating a new object I've chosen to use this.$set in my example. This isn't necessary for most of the properties, only the new one added at the end really needs it. However, it would have been unnecessarily complicated to single out that one property given the algorithm I've chosen to use.
Another potential problem is ensuring all numbers are compared as numbers and not as strings. In your example you're passing in the position as the string '2'. Operators such as < will give you the expected answer for numbers up to 9 but once the number of items in treeData reaches 10 you may start to run into problems. For string comparision '2' < '10' is false.
The next problem is the order you're moving the entries. In your current algorithm you're overwriting entry key + 1 with entry key. But that means you've lost the original value for entry key + 1. You'll end up just copying the same entry all the way to the end. There are two ways you could fix this. One would be to use a new object to hold the output (which would also help to address the reactivity problem). In my solution below I've instead chosen to iterate backwards through the keys.
new Vue({
el: '#app',
data () {
return {
newEntry: 'Yellow',
newIndex: 4,
treeData: {
1: 'Red',
2: 'Green',
3: 'Blue'
}
}
},
computed: {
treeDataLength () {
return Math.max(...Object.keys(this.treeData))
}
},
methods: {
onAddClick () {
const newIndex = Math.round(this.newIndex)
if (newIndex < 1 || newIndex > this.treeDataLength + 1) {
return
}
this.injectFieldType(this.newEntry, newIndex)
},
injectFieldType (type, position) {
const list = this.treeData
for (let index = this.treeDataLength + 1; index >= position; --index) {
if (index === position) {
this.$set(list, index, type)
} else {
this.$set(list, index, list[index - 1])
}
}
}
}
})
<script src="https://unpkg.com/vue#2.6.10/dist/vue.js"></script>
<div id="app">
<ul>
<li v-for="index in treeDataLength">
{{ index}}. {{ treeData[index] }}
</li>
</ul>
<input v-model="newEntry">
<input v-model="newIndex">
<button #click="onAddClick">Add</button>
</div>
The decision to use an object with number keys seems very strange. This would all be a lot easier if you just used an array.
I'm writing a tiny reactive framework where I need to find out which subscriber needs updating. I'm implementing deep binding and I'm running into a wall how to find subscribers in an effective manner.
A stored variable can be an object, so for example
{
"user": {
"preferences": {
"food": "vegetarian"
}
}
}
You can get content to any level of this variable like this
getVar("user_preferences_food");
getVar("user_preferences");
However, you can also update it like that
setVar("user_preferences_food", "meat");
setVar("user_preferences", {"food": "meat"});
But in case of the first setVar (user_preferences_food) how can I find the subscriber using getVar("user_preferences"); or even getVar("user"); most effectively.
I already got it working by splitting the var on _ and then one by one concatting the next level and merging all the resulting arrays. But this is very resource intensive. Especially if there are a lot of subscribers. There must be a better way to find them that is less resource intensive.
Edit: I left out part of the explanation.
There is a subscribe method too
subscribe("user", cb);
subscribe("user_preferences", cb);
subscribe("user_preferences_food", cb);
These subscriptions are stored in an array in the framework.
As soon as "user_preferences_food" is updated for example, all subscriptions above should be triggered. But obviously not subscribe('othervar');
simplification of the subscribe method:
var subscriptions = [];
function subscribe(var, callback){
subscriptions.push({var: var, cb: callback});
}
Simplification of getVar
vars = {};
getVar(var){
// find var in vars with this logic: https://stackoverflow.com/a/18937118/249710
// current exact match on subscribers, but need the "parents, grandparents etc here
var toUpdate = _.where(subscriptions, {
"var" : var
});
_.each(toUpdate, function(sub){ sub.cb();});
}
Storing or getting data as part of the key I've already got covered. It is just finding the subscribers in the most effective manner
ps: this is in an environment where I cannot rely on ES6 yet (not all users have it enabled), there is no DOM but I do have underscore included. (Titanium app development platform)
I would try to make a list for the callbacks, so you loop trough one list so you dont have to search, because you know the list is there with all the callbacks.
So if you call setVar('user_prefs') you set a seperate list with the root var. in this case its the user.
if any object is changed with setVar (in depth or not) you go to you're root var, get the list and loop trough this list with the callbacks.
The beauty of this is you can set a list with the root var,
var cbList[FIRSTVAR] this contains all the callbacks. No searching just loop.
Its the mongoDb principle, the data is ready to go, you don't search because you know the list is already there.
You could split the string and use it for reduceing the object.
function getVar(object, path) {
return path
.split('_')
.reduce(function (o, k) {
return (o || {})[k];
}, object);
}
function setVar(object, path, value) {
var keys = path.split('_'),
last = keys.pop();
keys.reduce(function (o, k) {
return o[k] = o[k] || {};
}, object)[last] = value;
}
var object = { user: { preferences: { food: "vegetarian" } } };
console.log(getVar(object, "user_preferences_food"));
console.log(getVar(object, "user_preferences"));
setVar(object, "user_preferences_food", "meat");
console.log(object);
setVar(object, "user_preferences", {"food": "meat"});
console.log(object);
.as-console-wrapper { max-height: 100% !important; top: 0; }
I ended up doing this:
var options = [];
var parts = key.split('_');
var string = parts[0];
_.each(parts, function(p, i){
if (i > 0) string += '_' + p;
options.push(string);
});
var toUpdate = _.filter(subscribers, function(sub){
if (sub.var.indexOf(key + '_') === 0) return true;
if (options.indexOf(sub.var) > -1) return true;
return false;
});
So checking with indexOf on the string to see if there are children. And building an array with parents so any layer is a match, and doing an indexOf on that as well. I think this is the least complicated method of implementing it
I am building a project using React.js as a front-end framework. On one particular page I am displaying a full data set to the user. I have an Array which contains this full data set. It is an array of JSON objects. In terms of presenting this data to the user, I currently have it displaying the whole data set by returning each item of data using Array.map().
This is a step in the right direction, but now I need to display only a portion of the data-set, not the whole thing, I also want some control in terms of knowing how much of the total data set has been displayed, and how much of the data set is yet to be displayed. Basically I am building something like a "view more" button that loads more items of data to the user.
Here is what I am using now where 'feed' represents my Array of JSON objects. (this displays the whole data set.)
return (
<div className={feedClass}>
{
feed.map((item, index) => {
return <FeedItem key={index} data={item}/>
})
}
</div>
);
I am wondering if it is possible to use .map() on only a portion of the array without having to break up the array before hand? I know that a possible solution would be to hold the full data set, and break it off into portions, and then .map() those portions, but is there a way to .map() a portion of the array without having to break it up?
Any and all feedback is appreciated. Thanks!
Do not try to solve this problem with a hack in your mapping step.
Instead, slice() the list to the right length first before the mapping:
class Feed extends React.Component {
constructor(props) {
super(props)
this.handleShowMore = this.handleShowMore.bind(this)
this.state = {
items: ['Item A', 'Item B', 'Item C', 'Item D'],
showItems: 2
}
}
handleShowMore() {
this.setState({
showItems:
this.state.showItems >= this.state.items.length ?
this.state.showItems : this.state.showItems + 1
})
}
render() {
const items = this.state.items.slice(0, this.state.showItems).map(
(item) => <div>{item}</div>
)
return (
<div>
{items}
<button onClick={this.handleShowMore}>
Show more!
</button>
</div>
)
}
}
ReactDOM.render(
<Feed />,
document.getElementById('root')
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id='root'></div>
The easiest way in my head is just to use a filter and map
const feed = [1,2,3,4,5,6,7,8,9,10,11]
feed.filter((item, index) => index < 5).map((filteredItem) => //do somthing with filtred item here//)
where 5 is just a number of items you want to get
you could use the slice function before to map the array, it looks like you want to do some pagination there.
var fruits = ['Banana', 'Orange', 'Lemon', 'Apple', 'Mango'];
var citrus = fruits.slice(1, 3);
// fruits contains ['Banana', 'Orange', 'Lemon', 'Apple', 'Mango']
// citrus contains ['Orange','Lemon']
Array.reduce should do what you're asking for. Just change the if statement depending on which range you want.
var excludeAfterIndex = 5;
feed.reduce((mappedArray, item, index) => {
if (index > excludeAfterIndex) { // Whatever range condition you want
mappedArray.push(<FeedItem key={index} data={item}/>);
}
return mappedArray;
}, []);
If you just want to map a portion of an array, you should first filter() your array to obtain the expected portion according to conditions :
array.filter(item => <condition>).map();
Yes, you can map portion of array, based on index. For example:
yourArray = yourArray.map(function (element, index, array) {
if (array.indexOf(element) < yourIndex) {
return {
//logic here
};
} else {
return {
//logic here
};
}
});
You can use slice to get portion of an array:
const data = [1,2,3,4,5,6,7,8,9,10,11]
var updatedData = data.slice(0, 3);
Array#map iterates over all items.
The map() method creates a new array with the results of calling a provided function on every element in this array.
You could use Array#filter
The filter() method creates a new array with all elements that pass the test implemented by the provided function.
for the wanted items and then apply map for the wanted format.
There is no version of the map() function that only maps a partial of the array.
You could use .map() in conjunction with .filter().
You get the index of the current element as the second arg of map and if you have a variable for current page and page size you can quite easily filter the right page from your array without having to really slice it up.
var currentPage = 1;
var pageSize = 25;
dataArray.filter(function(elt, index) {
var upperThreshold = currentPage * pageSize;
var lowerThreshold = currentPage * pageSize - pageSize;
return index < upperThreshold && index > lowerThreshold;
});
Using slice() is better than adding a condition to your map or reduce function, but it still creates an additional, unused copy of that segment of the array. Depending on what you're doing, that might not be desired. Instead, just use a custom map function:
function sliceMap(fn, from, toExclusive, array) {
const len = toExclusive - from;
const mapped = Array(len);
for (let i = 0; i < len; i++) {
mapped[i] = fn(array[i + from], i);
}
return mapped;
};
Note that fn receives the array value and the (now) zero-based index. You might want to pass the original index (i + from). You might also want to pass the full array as a third parameter, which is what Array.map does.
Use this, easy approach
const [limit, setLimit] = useState(false);
const data = [{name: "john}, {name: 'Anna'}]
Here we will have 2 cases:
Display only first data which is John
Display all
data.slice(0, extended ? data.length : 1).map((item, index) => <Text>{item.name}</Text>)
....
I've been searching online and it seems that I cannot make sort work on my observable array. I am definitely wrong somewhere but not sure where, here is the code:
var availableProducts = [{"Id":"1","Description":"Product 1","Rate":2956.00},{"Id":"3","Description":"Product 2","Rate":1518.00},{"Id":"2","Description":"Product 3","Rate":750.00}];
function productViewModel() {
var self = this;
self.products = ko.observableArray();
}
var productDetails = new productViewModel();
$(document).ready(function () {
productDetails.products = (availableProducts);
ko.applyBindings(productDetails, document.getElementById("product-edit"));
}
And HTML looks like this:
<tbody data-bind="foreach: sortedProducts">
<tr>
<td><span data-bind="text: Description"></span></td>
<td><span data-bind="currency: Rate"></span></td>
</tr>
</tbody>
So as it can be seen IDs are in order 1, 3, 2 and I would like to sort them and show them in order, but I cannot seem to sort products. I tried putting following in my ViewModel
self.sortedProducts = function () {
return self.products().sort(function (a,b) {
return a.Id < b.Id ? -1 : a.Id > b.Id ? 1 : 0;
});
This did not work, I tried then adding same code to "$(document).ready(...)" with exception of replacing self with productDetails but did not work. I do not want to make button to be called to sort my data, I want them sorted before presenting them.
The sort function looks OK, although you should check - preferrably with unit tests - whether your supported browsers actually understand the compareFunction handed to Array.prototype.sort. It's just an issue I ran into a while ago.
1) You are overwriting the ko.observableArray() with the native array availableProducts- this should give you an error when accessing the property with the getter syntax self.products().
2) The foreach binding in your template shouldn't work because you're handing it a plain function rather than an observable array, I'd guess that it actually iterates over the properties of the function object itself (.length, .prototype, .hasOwnProperty etc.).
3) I'd recommend using the Knockout Projections library that adds efficient observable handling to arrays in case you plan on handling larger arrays. In my experience with over more than say 1000 items in a collection the UI is not as fluid as you'd like it to be without the projections feature.
My take on this would be:
var availableProducts = [{"Id":"1","Description":"Product 1","Rate":2956.00},{"Id":"3","Description":"Product 2","Rate":1518.00},{"Id":"2","Description":"Product 3","Rate":750.00}];
function productViewModel() {
var self = this;
// In case you know the products at this stage you could just
// specify it as the first argument
self.products = ko.observableArray(/* [{...}] */);
// The sorted products are computed from the original ones
self.sortedProducts = ko.computed(function () {
// I'd probably use explicit parens for better
// readability - I had to look twice to get the comparer :-)
return self.products().sort(function (a, b) {
return a.Id < b.Id ? -1 : (a.Id > b.Id ? 1 : 0);
});
//// With ko-projections this would become:
//// (note the missing parens on `products`)
//return self.products.sort(function (a, b) {
// return a.Id < b.Id ? -1 : a.Id > b.Id ? 1 : 0;
//});
});
}
var productDetails = new productViewModel();
$(document).ready(function () {
// Set the observable array value
productDetails.products(availableProducts);
ko.applyBindings(productDetails, document.getElementById("product-edit"));
});