I have a map with features on it that are also listed in a v-data-table. When the user clicks a row, the feature is highlighted on the map. When the user clicks a map feature, the corresponding grid row is selected. So I am programmatically setting the selected row like this:
selectRow(id) {
this.selected = [this.getRowFromId(id)]
},
getRowFromId(id) {
for (let site of this.sites) {
if (site.id === id) return site;
}
return []
},
Works fine with one UX problem: The table is not scrolled to the selected row.
I am using a vertically scrolling data-table with all rows in the grid rather than pagination.
Any ideas on how to programmatically scroll the data table?
Here is my solution. I found most of what I was looking for in this post: Plain JavaScript - ScrollIntoView inside Div
I had to do a couple of v-data-grid specific things to make it work. I am using a watcher in my context but you could to this anywhere you needed to:
watch: {
selectedId: function(newVal) { // watch it
// Select the row
this.selectRow(newVal)
// Scroll the item into view
// First we need to let the component finish selecting the row in the background
setTimeout( () => {
// We get the selected row (we assume only one or the first row)
const row = document.getElementsByClassName("v-data-table__selected")[0]
// Then we get the parent. We need to give the -v-data-table a ref
// and we actually take the child of the table element which
// has the scrollbar in my case.
const parent = this.$refs.detailGrid.$el.firstChild
// Finally call the scroll function
this.scrollParentToChild(parent, row )
}, 100)
}
},
methods: {
scrollParentToChild(parent, child) {
// Where is the parent on page
var parentRect = parent.getBoundingClientRect();
// What can you see?
var parentViewableArea = {
height: parent.clientHeight,
width: parent.clientWidth
};
// Where is the child
var childRect = child.getBoundingClientRect();
// Is the child viewable?
var isViewable = (childRect.top >= parentRect.top) && (childRect.top <= parentRect.top + parentViewableArea.height);
// if you can't see the child try to scroll parent
if (!isViewable) {
// scroll by offset relative to parent
parent.scrollTop = (childRect.top + parent.scrollTop) - parentRect.top - childRect.height
}
},
}
Related
I need to open the first card and verify that everything inside matches the hashtag 'Fashion' and then do the same for the next 3 cards and then press the 'next' button and do the same for next 4 cards. how would I do it? I tried the regular way by clicking on the element.eq(0) and verifying everything inside and then cy.go('back') and so on but that's so much code duplication. how would I do it the other way?
First 4 cards:
Second 4 cards:
The CSS selector for all of them is the same [class="discover-card"]. please help:) thank you
You can use Cypress's .each() functionality to iterate through elements with the same CSS selector.
cy.get('.discover-card').each(($card, index) => {
// cy.go('back') can cause the list to become detached, so find element by index of original list.
cy.get('.discover-card').eq(index).click();
// validations after clicking the card
// unsure on this exact function, but was provided in question
cy.go('back').then(() => {
// if this is the fourth item checked, we need to press the next button.
if ((index + 1) % 4 === 0) {
cy.get('.next-btn').click(); // this selector is the next button on the carousel
}
});
});
If the data between the cards is unique, I'd advise creating a data object you can use to store the data and reference it in your test. You can do this by having each data object have a unique key equal to the text on the card, or by storing them in an array.
// unique keys
const data = { fashion: { foo: 'bar' }, beauty: { foo: 'bar2' }, ... };
// array
const data = [{ foo: 'bar' }, { foo: 'bar2' }, ...];
...
// unique keys
cy.wrap($card).should('have.attr', 'foo', data[$card.text()].foo);
// array
cy.wrap($card).should('have.attr', 'foo', data[index].foo);
If you are concerned about code duplication, put the common code in a function
const expectedData [
{ index: 1, title:'Fashion', ... } // anything you want to check
{ index: 2, title:'Beauty', ... }
]
const checkCard = (cardIndex) => {
const data = expectedData[cardIndex]
cy.get('.discover-card')
.should('contain', data.title)
.click() // open card
// test card internals
}
Cypress._.times(3, (pageNo) => { // loop 3 times between pages
Cypress._.times(4, (cardNo) => { // loop 4 times for cards
const cardIndex = ((pageNo+1) * (cardNo+1)) -1
checkCard(cardIndex)
cy.go.back() // revert to menu
})
cy.get('.next-btn').click()
})
I have a paginated record set from a http response and want to further implement client side pagination on return paginated record set, thus I have the following component markup
<td v-for="item in items">....</td> // only print 5 at a time
and in the default....
{
data: { return {
itemsData:[] // populated from RESTful data in increments of 20
, offset: 0 // for internal pagination
} },
computed: {
items: function(){
return this.itemsData.slice(this.offset, 5); // re-populate over time as offset changes
}
},
methods: {
getItems: function() {
this.$http.get('/api/items/?page=' + this.page).then(response=>
{
this.itemsData = response.data.data; // json array and I al get back meta data
// for which i use in a mixin to calculate offset and page etc.
// for both client side and server side pagination
}) // fetches records 20 at a time
}
}
.........
If itesmData is populated and then offset is dynamically changed. Shouldn't the component's template re-rendered with a new items collection?
Or should I be using a method instead? e.g.
<td v-for="item in paginated(itemData)">....</td>
{
....
methods: {
paginated: function(items){
var arr=[];
for( var i = this.offset; i < this.offset + 5; i++)
arr.push(item[i]);
return arr;
}
}
How would the template be updated with the new array? Would I need to implement a watcher? on the computed data? or would the offset do?
UPDATE:
I tried to implement the pagination via competed and while I get the template to render the first 5..... in trying to re-render after updating offset does not fire.... arr seems to return empty even thought i am on the second page and offset is yet to reach itemsData.length
Can you iterate through a data array property OUTSIDE of the template? i.e. loop through this.itemsData[i] or this.$data.itemsData[i]???
You need to make following changes in your code:
computed: {
items: function(){
return this.itemsData.slice(this.offset, this.offset + 5); // re-populate over time as offset changes
}
}
As you can see from documentation, slice takes two argument start and end, it will return a portion of an array into a new array object selected from start to end (end not included).
I am dynamically building a TableView in my controller, which works fine: The initial table displays the initial collections data as expected.
The problem is that the TableView's 'click' event listener is ignored when I click on the table rows. I am testing in the browser, and I never even see the console event file (see comments in controller file). All relevant code snippets below:
In my alloy.js I setup a backbone collection:
function defaultTodo(name) { return {name: name, done: false}; }
function doneTodo(name) { return {name: name, done: true}; }
Alloy.Collections.todos = new Backbone.Collection();
Alloy.Collections.todos.reset([
defaultTodo('Apples'), // create not yet done todo
defaultTodo('Banana'),
defaultTodo('Paper Towels'),
defaultTodo('Broccoli'),
doneTodo('Beans'), // create already done todo
doneTodo('Water'),
doneTodo('Blueberries'),
doneTodo('Stir Fry')
])
Here is my index.js controller:
var todos = Alloy.Collections.todos;
function redrawTable() {
// clear all the old data
// See http://developer.appcelerator.com/question/49241/delete-all-rows-in-a-tableview-with-a-single-click
$.table.setData([]);
// Create and add the TableViewSections
var alreadyDone = Ti.UI.createTableViewSection({ headerTitle: "Already Done" });
var needsDoing = Ti.UI.createTableViewSection({ headerTitle: "Needs Doing" });
$.table.appendSection(needsDoing);
$.table.appendSection(alreadyDone);
// Add the todo to the appropriate sections
todos.forEach(function(todo) {
var section = todo.get('done') ? alreadyDone : needsDoing;
addEntry(todo, section);
});
// Add the click listener
// THIS LISTENER IS IGNORED ********************************
$.table.addEventListener('click', function(e) {
console.log(e);
todos.at(e.index).set('done', true);
todos.trigger('change');
});
// Helper function to add a row to a section
function addEntry(todo, section) {
var row = Ti.UI.createTableViewRow({
title: todo.get('name'),
className: "row"
});
section.add(row);
}
}
// Redraw our table each time our todos collection changes
todos.on('change', redrawTable);
// Trigger a change event to draw the initial table
todos.trigger('change');
$.index.open();
And here is index.xml view file:
<Alloy>
<Window class="container">
<Label id="test" class="header">My Grocery List</Label>
<TextField id="newItem"/>
<TableView id="table">
</TableView>
</Window>
</Alloy>
UPDATE: Working Code
In addition to the changes below, I also added onClick="markDone" to the xml.
function markDone(e) {
console.log(e.row.todo);
e.row.todo.set('done', true);
todos.trigger('change');
}
function redrawTable() {
// clear all the old data
// See http://developer.appcelerator.com/question/49241/delete-all-rows-in-a-tableview-with-a-single-click
$.table.setData([]);
var rows = [];
var done = [];
var doing = [];
// Add the todo to the appropriate sections
todos.forEach(function(todo) {
var row = Ti.UI.createTableViewRow({
title: todo.get('name'),
className: "row"
});
row.todo = todo;
todo.get('done') ? done.push(row) : doing.push(row);
});
// Create and add the TableViewSections
rows.push(Ti.UI.createTableViewSection({ headerTitle: "Needs Doing" }));
rows = rows.concat(doing);
rows.push(Ti.UI.createTableViewSection({ headerTitle: "Already Done" }));
rows = rows.concat(done);
$.table.setData(rows);
};
I created brand new project using files which you provided and eventListener is working perfectly fine. However there are couple other bugs:
Creating listener inside redrawTable() function, which is executed every time you click on something in TableView. As a result at the beginning you have one eventListener but after every click all listeners are duplicated.
Using index property in event handler to find index of Backbone model object to update. index property is indicating at which place given row was displayed in your TableView. When you are moving rows between sections their index are changing. In your case it's better to check e.row.name property and use Backbone.Collection.findWhere() method. If user can have two items with the same name on the list, then you have to create additional property to determine which object in model should be changed.
You should add rows to section before section are added to table. In your case table is very simple so instead of doing loops you can just create one simple array of objects (with title and optional header properties) and pass it to $.table.setData().
It's good to wait for postlayout event triggered on main view before triggering any custom events to make sure that the whole view was created and all objects are initiated.
Check rewrote index.js code below to see how it could be done.
var todos = Alloy.Collections.todos;
function redrawTable() {
var done = todos.where({done: true}).map(function(elem) {
return { title: elem.get('name') };
});
if (done.length > 0) {
done[0].header = 'Already Done';
}
var notdone = todos.where({done: false}).map(function(elem) {
return { title: elem.get('name') };
});
if (notdone.length > 0) {
notdone[0].header = 'Needs Doing';
}
$.table.setData( done.concat(notdone) );
}
$.table.addEventListener('click', function(e) {
todos.where({ name: e.row.title }).forEach(function(elem){
elem.set('done', !elem.get('done'));
});
todos.trigger('change'); // Redraw table
});
// Redraw our table each time our todos collection changes
todos.on('change', redrawTable);
// Trigger a change event to draw the initial table
$.index.addEventListener('postlayout', function init(){
todos.trigger('change');
// Remove eventListener, we don't need it any more.
$.index.removeEventListener('postlayout', init);
})
$.index.open();
redrawTable() could have a little more refactor but I left it so it's easier to read.
To read more about manipulating TableView check this Appcelerator documentation page.
We are using Durandal for our SPA application and came to a, in my opinion, common use case. We have two pages: one page is a list of entities (with filters, sorting, virtual scroll) and another is detail preview of an entity. So, user is on list page and set a filter and a list of results comes out. After scrolling a little bit down user notice an entity which he/she would like to see details for. So clicking on a proper link user is navigated to details preview page.
After "work finished" on preview page user click back button (in app itself or browser) and he/she is back on the list page. However, default 'entrance' transition scroll the page to the top and not to the position on list where user pressed preview. So in order to 'read' list further user have to scroll down where he/she was before pressing preview.
So I started to create new transition which will for certain pages (like list-search pages) keep the scroll position and for other pages (like preview or edit pages) scroll to top on transition complete. And this was easy to do however, I was surprised when I noticed that there are strange behavior on preview pages when I hit navigateBack 'button'. My already long story short, after investigation I found out that windows.history.back is completing earlier then the transition is made and this cause that preview pages are scrolled automatically down to position of previous (list) page when back button is hit. This scrolling have a very unpleasant effect on UI not mentioning that it is 'total catastrophe' for my transition.
Any idea or suggestion what could I do in this case?
Here is the code of transition. It is just a working copy not finished yet as far as I have this problem.
define(['../system'], function (system) {
var fadeOutDuration = 100;
var scrollPositions = new Array();
var getScrollObjectFor = function (node) {
var elemObjs = scrollPositions.filter(function (ele) {
return ele.element === node;
});
if (elemObjs.length > 0)
return elemObjs[0];
else
return null;
};
var addScrollPositionFor = function (node) {
var elemObj = getScrollObjectFor(node);
if (elemObj) {
elemObj.scrollPosition = $(document).scrollTop();
}
else {
scrollPositions.push({element: node, scrollPosition: $(document).scrollTop()});
}
};
var scrollTransition = function (parent, newChild, settings) {
return system.defer(function (dfd) {
function endTransition() {
dfd.resolve();
}
function scrollIfNeeded() {
var elemObj = getScrollObjectFor(newChild);
if (elemObj)
{
$(document).scrollTop(elemObj.scrollPosition);
}
else {
$(document).scrollTop(0);
}
}
if (!newChild) {
if (settings.activeView) {
addScrollPositionFor(settings.activeView);
$(settings.activeView).fadeOut(fadeOutDuration, function () {
if (!settings.cacheViews) {
ko.virtualElements.emptyNode(parent);
}
endTransition();
});
} else {
if (!settings.cacheViews) {
ko.virtualElements.emptyNode(parent);
}
endTransition();
}
} else {
var $previousView = $(settings.activeView);
var duration = settings.duration || 500;
var fadeOnly = !!settings.fadeOnly;
function startTransition() {
if (settings.cacheViews) {
if (settings.composingNewView) {
ko.virtualElements.prepend(parent, newChild);
}
} else {
ko.virtualElements.emptyNode(parent);
ko.virtualElements.prepend(parent, newChild);
}
var startValues = {
marginLeft: fadeOnly ? '0' : '20px',
marginRight: fadeOnly ? '0' : '-20px',
opacity: 0,
display: 'block'
};
var endValues = {
marginRight: 0,
marginLeft: 0,
opacity: 1
};
$(newChild).css(startValues);
var animateOptions = {
duration: duration,
easing : 'swing',
complete: endTransition,
done: scrollIfNeeded
};
$(newChild).animate(endValues, animateOptions);
}
if ($previousView.length) {
addScrollPositionFor(settings.activeView);
$previousView.fadeOut(fadeOutDuration, startTransition);
} else {
startTransition();
}
}
}).promise();
};
return scrollTransition;
});
A simpler approach could be to store the scroll position when the module deactivates and restore the scroll on viewAttached.
You could store the positions in some global app variable:
app.scrollPositions = app.scrollPositions || {};
app.scrollPositions[system.getModuleId(this)] = theCurrentScrollPosition;
When I add listener for itemcontextmenu for Grid/Tree I'm able to access view record, item, index, but how to get column?
What I want to create is a contextMenu but only if user click on items in first column.
Here is my listener function:
firstColumnContext: function(view,record,item,index,e,eOpts) {
console.log(view);
console.log(record.getName());//this works
console.log(index);
console.log('get column');//
},
My concept looks like this:
firstColumnContext: function(view,record,item,index,e,eOpts) {
e.stopEvent();
if(record.get('leaf') && 'first column')
{
//show context menu here
}
},
But as I wrote before I need to verify if rightclick was in first column.
See this code, setup your grids viewConfig like this:
viewConfig: {
listeners:{
beforecellcontextmenu: function(view, tableCell, columnIndex, record, tableRow, rowIndex){
//your menu code here
},
itemcontextmenu: function(view,record,item,index,e,eOpts){
e.stopEvent();
}
}
}
The column index is provided by the beforecellcontextmenu event but not the fired event does not provide the event itself, so you have to use a combination of both events, one to stop the default menu and the other to pop it up in the case you want it to show.
I had this problem too. Here is the hack I devised for Grids that works nicely. It compares the mouse position at click to the positions of each column and finds when they overlap.
grid.on('itemcontextmenu', function(view, record, item, index, e) {
var xPos = e.getXY()[0];
var cols = view.getGridColumns();
for(var c in cols) {
var leftEdge = cols[c].getPosition()[0];
var rightEdge = cols[c].getSize().width + leftEdge;
if(xPos>=leftEdge && xPos<=rightEdge) {
console.log(cols[c].dataIndex);
}
}
});