I'm using the DropdownList component from react-widget. In my code, there are a couple of dropdowns that get their values from an Ajax call. Some of them, like a list of languages, are too big and it's very slow to get the list from Ajax and render it (takes 4 to 5 seconds!). I would like to provide to the dropdwon a small list of languages and an 'Extend' or 'Load Full List' option; if clicking on Extend the dropdown would be refreshed with the full list of languages.
Here is my solution: the code of the parent component:
const languages = ajaxCalls.getLanguages();
const langs = {"languages": [["eng", "English"], ["swe", "Swedish"], ["deu", "German"], ["...", "Load Full List"]]};
const common_langs = langs.languages.map(([id, name]) => ({id, name}));
<SelectBig data={common_langs} extend={languages} onSelect={x=>this.setValue(schema, path, x)} value={this.getValue(path)} />;
And here is the code for SelectBig component:
import React from 'react/lib/ReactWithAddons';
import { DropdownList } from 'react-widgets';
const PT = React.PropTypes;
export const SelectBig = React.createClass({
propTypes: {
data: PT.array,
value: PT.string,
onSelect: PT.func.isRequired,
},
maxResults: 50,
shouldComponentUpdate(nextProps, nextState) {
console.log("nextProps = " , nextProps, " , nextState = ", nextState);
const len = x => (x && x.length !== undefined) ? x.length : 0;
// fast check, not exact, but should work for our use case
return nextProps.value !== this.props.value
|| len(nextProps.data) !== len(this.props.data);
},
getInitialState(){
return {
lastSearch: '',
results: 0,
dataList: [],
};
},
select(val) {
if(val.id === "..."){
this.setState({dataList: this.props.extend})
}
this.props.onSelect(val.id);
},
filter(item, search) { .... },
renderField(item) { .... },
render() {
const busy = !this.props.data;
let data;
if(!this.props.extend){
data = this.props.data || [];
} else {
data = this.state.dataList;
}
return (
<DropdownList
data={data}
valueField='id'
textField={this.renderField}
value={this.props.value}
onChange={this.select}
filter={this.filter}
caseSensitive={false}
minLength={2}
busy={busy} />
);
}
});
But it doesn't look good: When the user chooses 'Load Full List', the dropdown list will be closed and user need to click again to see the updated list. Does anyone have a better solution or a suggestion to improve my code?
The picture shows how it looks like right now!
https://www.npmjs.com/package/react-select-2
Better go with this link, it will work
Related
PROBLEM
Unavailable variant combinations are different from sold-out because customers don't understand it's the selects which make certain combinations 'not possible'.
Shopify's way of handling this is to display 'Unavailable' in the buy button. But customers think this means sold-out when in reality, they've chosen the wrong combination of variants...
The previous JS workarounds to remove unavailable or 'not possible' variants don't work in Shopify 2.0's new default/flagship theme, Dawn because the JS is different.
As far as I can tell, Dawn's variant JS was recently moved from /asstes/variants.js to line 497 in /assets/global.js.
SKILL
My CSS is decent but my JS is lame, I'm a designer sorry.
QUESTIONS
Based on user interaction with the first variant, how do you hide unavailable variants (not sold-out) in Shopify 2.0 Dawn?
How do you make one variant option set a checkbox instead of a radio button or radio?
What's the best way to add custom text as the first option in selects? e.g. 'Choose a size...' or 'Choose a color...' etc. Is it best to hard-code or use JS for this as well?
RESOURCES/EXAMPLES
Here's a pull request which grabs sold-out from the new Dawn JS but I don't understand how to adapt it for 'Unavailable' sorry (which is a different exception from sold-out): https://github.com/Shopify/dawn/pull/105
Here's an example of how to hide unavailable variants in the older Debut theme which doesn’t seem to work in the newer Dawn JS: https://www.youtube.com/watch?v=vspWDu_POYA
Here's a link to the JS gist referenced in that video: https://gist.github.com/jonathanmoore/c0e0e503aa732bf1c05b7a7be4230c61
And finally, here's the new code from Dawn at line 497 in /assets/global.js
class VariantSelects extends HTMLElement {
constructor() {
super();
this.addEventListener('change', this.onVariantChange);
}
onVariantChange() {
this.updateOptions();
this.updateMasterId();
this.toggleAddButton(true, '', false);
this.updatePickupAvailability();
if (!this.currentVariant) {
this.toggleAddButton(true, '', true);
this.setUnavailable();
} else {
this.updateMedia();
this.updateURL();
this.updateVariantInput();
this.renderProductInfo();
}
}
updateOptions() {
this.options = Array.from(this.querySelectorAll('select'), (select) => select.value);
}
updateMasterId() {
this.currentVariant = this.getVariantData().find((variant) => {
return !variant.options.map((option, index) => {
return this.options[index] === option;
}).includes(false);
});
}
updateMedia() {
if (!this.currentVariant || !this.currentVariant?.featured_media) return;
const newMedia = document.querySelector(
`[data-media-id="${this.dataset.section}-${this.currentVariant.featured_media.id}"]`
);
if (!newMedia) return;
const parent = newMedia.parentElement;
parent.prepend(newMedia);
window.setTimeout(() => { parent.scroll(0, 0) });
}
updateURL() {
if (!this.currentVariant) return;
window.history.replaceState({ }, '', `${this.dataset.url}?variant=${this.currentVariant.id}`);
}
updateVariantInput() {
const productForms = document.querySelectorAll(`#product-form-${this.dataset.section}, #product-form-installment`);
productForms.forEach((productForm) => {
const input = productForm.querySelector('input[name="id"]');
input.value = this.currentVariant.id;
input.dispatchEvent(new Event('change', { bubbles: true }));
});
}
updatePickupAvailability() {
const pickUpAvailability = document.querySelector('pickup-availability');
if (!pickUpAvailability) return;
if (this.currentVariant?.available) {
pickUpAvailability.fetchAvailability(this.currentVariant.id);
} else {
pickUpAvailability.removeAttribute('available');
pickUpAvailability.innerHTML = '';
}
}
renderProductInfo() {
fetch(`${this.dataset.url}?variant=${this.currentVariant.id}§ion_id=${this.dataset.section}`)
.then((response) => response.text())
.then((responseText) => {
const id = `price-${this.dataset.section}`;
const html = new DOMParser().parseFromString(responseText, 'text/html')
const destination = document.getElementById(id);
const source = html.getElementById(id);
if (source && destination) destination.innerHTML = source.innerHTML;
document.getElementById(`price-${this.dataset.section}`)?.classList.remove('visibility-hidden');
this.toggleAddButton(!this.currentVariant.available, window.variantStrings.soldOut);
});
}
toggleAddButton(disable = true, text, modifyClass = true) {
const addButton = document.getElementById(`product-form-${this.dataset.section}`)?.querySelector('[name="add"]');
if (!addButton) return;
if (disable) {
addButton.setAttribute('disabled', true);
if (text) addButton.textContent = text;
} else {
addButton.removeAttribute('disabled');
addButton.textContent = window.variantStrings.addToCart;
}
if (!modifyClass) return;
}
setUnavailable() {
const addButton = document.getElementById(`product-form-${this.dataset.section}`)?.querySelector('[name="add"]');
if (!addButton) return;
addButton.textContent = window.variantStrings.unavailable;
document.getElementById(`price-${this.dataset.section}`)?.classList.add('visibility-hidden');
}
getVariantData() {
this.variantData = this.variantData || JSON.parse(this.querySelector('[type="application/json"]').textContent);
return this.variantData;
}
}
customElements.define('variant-selects', VariantSelects);
class VariantRadios extends VariantSelects {
constructor() {
super();
}
updateOptions() {
const fieldsets = Array.from(this.querySelectorAll('fieldset'));
this.options = fieldsets.map((fieldset) => {
return Array.from(fieldset.querySelectorAll('input')).find((radio) => radio.checked).value;
});
}
}
customElements.define('variant-radios', VariantRadios);
Any help or pointers in the right direction would be much appreciated. Cheers
I have list that should have 10 elements, if the list contains 11 elements i need to show scroll, the container is fixed size.
Everything is ok but how i can check that scroll is exist?
cy.get('[data-testid=list-box]')
You could get the count of elements and if the length of the list is less than or equal to 10 do some action, else check for the visibility of scrollbar. Please try below test and let me know
it('Check the length of the list', () => {
cy.get('[data-testid=list-box]')
.then(list => {
const listCount = Cypress.$(list).length;
if(listCount <= 10){
// do some action if the list count is less than 10..
}else{
cy.get('#scrollbar_Id').should('be.visible');
}
});
})
I'm doing this by measuring the height and scrollHeight with jQuery.
it("should force scroll within a large body", () => {
cy.get(".lorem-ipsum-header").click();
cy.get(".section-body")
.should("have.length", 1)
.eq(0)
.should("contain.text", "Lorem ipsum")
.then(($body) => {
cy.wrap($body).invoke("outerHeight").should("eq", 583);
cy.wrap($body).invoke("prop", "scrollHeight").should("eq", 1892);
});
});
Here I'm getting the last element. Then used cypress scrollIntoView() to scroll to that last element. And using should() to see it's visible or not.
cy.get('Selector')
.last()
.within(($element) => {
if (!$element.is(':visible')) {
cy.wrap($element).scrollIntoView();
}})
.should('be.visible');
Basically, we get the element from the jquery wrapper and then test if it is scrollable by comparing scrollWidth and actualWidth
export enum ScrollType {
scrollable='scrollable',
nonScrollable='non-scrollable',
}
export const isXScrollable = (element: HTMLElement) => {
return element.scrollWidth > element.clientWidth
};
export const isYScrollable = (element: HTMLElement) => {
return element.scrollHeight > element.clientHeight;
}
export const isScrollable = (element: HTMLElement) => {
return isXScrollable(element) || isYScrollable(element)
}
describe('Test',() => {
it('check that max of only 4 lines to be shown in the text area, and then it should add the scroll', () => {
mount(<Textarea defaultValue="Line 1" id="textbox-abc2" ></Textarea>);
const textbox = cy.get('#textbox-abc2');
textbox.type('{enter}Line 2{enter}Line 3{enter}Line 4');
textbox.then(a => {
const scrollable = isYScrollable(a[0]) ? ScrollType.scrollable: ScrollType.nonScrollable
expect(scrollable).to.eq(ScrollType.nonScrollable);
})
textbox.type('{enter}Line 5');
textbox.then(a => {
const scrollable = isYScrollable(a[0]) ? ScrollType.scrollable: ScrollType.nonScrollable
expect(scrollable).to.eq(ScrollType.scrollable);
})
})
})
I have a problem getting react-beautiful-dnd to work without flickering. I have followed the example in the egghead course. Here is my code sample.
Item List Container
onDragEnd = (result) => {
if (this.droppedOutsideList(result) || this.droppedOnSamePosition(result)) {
return;
}
this.props.itemStore.reorderItem(result);
}
droppedOnSamePosition = ({ destination, source }) => destination.droppableId
=== source.droppableId && destination.index === source.index;
droppedOutsideList = result => !result.destination;
render() {
return (
<DragDropContext onDragEnd={this.onDragEnd}>
<div>
{this.props.categories.map((category, index) => (
<ListCategory
key={index}
category={category}
droppableId={category._id}
/>
))}
</div>
</DragDropContext>
);
}
Item Category
const ListCategory = ({
category, droppableId,
}) => (
<Droppable droppableId={String(droppableId)}>
{provided => (
<div
{...provided.droppableProps}
ref={provided.innerRef}
>
<ListTitle
title={category.name}
/>
<ListItems category={category} show={category.items && showIndexes} />
{provided.placeholder}
</div>
)}
</Droppable>
);
List items
<Fragment>
{category.items.map((item, index) => (
<ListItem
key={index}
item={item}
index={index}
/>
))}
</Fragment>
Items
render() {
const {
item, index, categoryIndex, itemStore,
} = this.props;
return (
<Draggable key={index} draggableId={item._id} index={index}>
{(provided, snapshot) => (
<div
role="presentation"
className={cx({
'list-item-container': true,
'selected-list-item': this.isSelectedListItem(item._id),
})}
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={getItemStyle(snapshot.isDragging, provided.draggableProps.style)}
onClick={this.handleItemClick}
>
<div className={cx('select-title')}>
<p className={cx('list-item-name')}>{item.title}</p>
</div>
{capitalize(item.importance)}
</div>
</div>
)}
</Draggable>
);
}
Method to reorder Items (I'm using Mobx-State_Tree)
reorderItem: flow(function* reorderItem(result) {
const { source, destination } = result;
const categorySnapshot = getSnapshot(self.itemCategories);
const sourceCatIndex = self.itemCategories
.findIndex(category => category._id === source.droppableId);
const destinationCatIndex = self.itemCategories
.findIndex(category => category._id === destination.droppableId);
const sourceCatItems = Array.from(categorySnapshot[sourceCatIndex].items);
const [draggedItem] = sourceCatItems.splice(source.index, 1);
if (sourceCatIndex === destinationCatIndex) {
sourceCatItems.splice(destination.index, 0, draggedItem);
const prioritizedItems = setItemPriorities(sourceCatItems);
applySnapshot(self.itemCategories[sourceCatIndex].items, prioritizedItems);
try {
yield itemService.bulkEditPriorities(prioritizedItems);
} catch (error) {
console.error(`Problem editing priorities: ${error}`);
}
} else {
const destinationCatItems = Array.from(categorySnapshot[destinationCatIndex].items);
destinationCatItems.splice(destination.index, 0, draggedItem);
const prioritizedSourceItems = setItemPriorities(sourceCatItems);
applySnapshot(self.itemCategories[sourceCatIndex].items, prioritizedSourceItems);
const prioritizedDestItems = setItemPriorities(destinationCatItems);
applySnapshot(self.itemCategories[destinationCatIndex].items, prioritizedDestItems);
try {
const sourceCatId = categorySnapshot[sourceCatIndex]._id;
const originalItemId = categorySnapshot[sourceCatIndex].items[source.index]._id;
yield itemService.moveItemToNewCategory(originalItemId, sourceCatId, destinationCatIndex);
} catch (error) {
console.error(`Problem editing priorities: ${error}`);
}
}
}),
Sample data
const itemData = [
{
_id: 'category-1',
title: 'Backlog',
items: [
{ _id: 'item-1', title: 'Here and back again' },
},
{
_id: 'category-2',
title: 'In progress',
items: []
},
{
_id: 'category-3',
title: 'Done',
items: []
}
}
}
Summary
When and item is dragged and dropped, I check to see if the item is dropped in the outside the dnd context or in the same position it was dragged from. If true, i do nothing.
If the item is dropped within the context, i check to see if it was dropped in the same category. if true, i remove the item from its current position, put it in the target position, update my state, and make an API call.
If it was dropped in a different category, i remove the item from the source category, add to the new category, update the state and make an API call.
Am I missing something?
I am using both mst and the react-beautiful-dnd library
I will just paste my onDragEnd action method
onDragEnd(result: DropResult) {
const { source, destination } = result;
// dropped outside the list
if (!destination) {
return;
}
if (source.droppableId === destination.droppableId) {
(self as any).reorder(source.index, destination.index);
}
},
reorder(source: number, destination: number) {
const tempLayout = [...self.layout];
const toMove = tempLayout.splice(source, 1);
const item = toMove.pop();
tempLayout.splice(destination + lockedCount, 0, item);
self.layout = cast(tempLayout);
},
I think in order to avoid the flicker you need to avoid using applySnapshot
You can replace this logic
const sourceCatItems = Array.from(categorySnapshot[sourceCatIndex].items);
const [draggedItem] = sourceCatItems.splice(source.index, 1);
sourceCatItems.splice(destination.index, 0, draggedItem);
const prioritizedItems = setItemPriorities(sourceCatItems);
applySnapshot(self.itemCategories[sourceCatIndex].items, prioritizedItems);
just splice the items tree
const [draggedItem] = categorySnapshot[sourceCatIndex].items.splice(destination.index, 0, draggedItem)
this way you don't need to applySnapshot on the source items after
I believe this issue is caused by multiple dispatches happening at the same time.
There're couple of things going on at the same time. The big category of stuff is going on is the events related to onDragStart, onDragEnd and onDrop. Because that's where an indicator has to show to the user they are dragging and which item they are dragging from and to.
So especially you need to put a timeout to onDragStart.
const invoke = (fn: any) => { setTimeout(fn, 0) }
Because Chrome and other browser will cancel the action if you don't do that. However that is also the key to prevent flickery.
const DndItem = memo(({ children, index, onItemDrop }: DndItemProps) => {
const [dragging, setDragging] = useState(false)
const [dropping, setDropping] = useState(false)
const dragRef = useRef(null)
const lastEnteredEl = useRef(null)
const onDragStart = useCallback((e: DragEvent) => {
const el: HTMLElement = dragRef.current
if (!el || (
document.elementFromPoint(e.clientX, e.clientY) !== el
)) {
e.preventDefault()
return
}
e.dataTransfer.setData("index", `${index}`)
invoke(() => { setDragging(true) })
}, [setDragging])
const onDragEnd = useCallback(() => {
invoke(() => { setDragging(false) })
}, [setDragging])
const onDrop = useCallback((e: any) => {
invoke(() => { setDropping(false) })
const from = parseInt(e.dataTransfer.getData("index"))
onItemDrop && onItemDrop(from, index)
}, [setDropping, onItemDrop])
const onDragEnter = useCallback((e: DragEvent) => {
lastEnteredEl.current = e.target
e.preventDefault()
e.stopPropagation()
setDropping(true)
}, [setDropping])
const onDragLeave = useCallback((e: DragEvent) => {
if (lastEnteredEl.current !== e.target) {
return
}
e.preventDefault()
e.stopPropagation()
setDropping(false)
}, [setDropping])
return (
<DndItemStyle
draggable="true"
onDragStart={onDragStart}
onDragEnd={onDragEnd}
onDrop={onDrop}
onDragOver={onDragOver}
onDragEnter={onDragEnter}
onDragLeave={onDragLeave}
dragging={dragging}
dropping={dropping}
>
{(index < 100) && (
cloneElement(children as ReactElement<any>, { dragRef })
)}
</DndItemStyle>
)
})
I have to apply two more timeout invoke in the above DndItem, the reason for that is during the drop, there're two many events are competing with each other, to name a few
onDragEnd, to sugar code the indicator
onDrop, to re-order
I need to make sure re-order happens very quickly. Because otherwise you get double render, one with the previous data, and one with the next data. And that's why the flickery is about.
In short, React + Dnd needs to apply setTimeout so that the order of the paint can be adjusted to get the best result.
I am trying to save PivotGrid state for future load. I have two problems
1: the expand property of row items is not changed at run time. Test here https://dojo.telerik.com/#Mzand/obIkEdAY : When the user expands an item at runtime the expand property of the returned row by dataSource.rows() is the same as what it was at initialization.
2: I can't find a way to save/load the selected feilds (slices) using the "Fields to Include" menu along side with rows,columns and measures.
I'm working in React but you should be able to do something similar.
This is a bug, to work around it you can listen to expandMember and collapseMember events to manually track the axis and path of expanded rows/columns. see code below.
If you mean the Configurator, just set its dataSource to that of the pivot grid after you create it. See below in createGrid().
Bonus, see the end of createGrid to auto expand the items in the Configurator.
createGrid = () => {
$(`#pivot-grid`).kendoPivotGrid({
dataSource: {
data: data,
schema: schema,
columns: this.state.columns,
rows: this.state.rows,
measures: this.state.measures,
filter: this.state.filter
},
expandMember: this.onExpand,
collapseMember: this.onCollapse
});
let grid = $(`#pivot-grid`).data('kendoPivotGrid');
if (grid) {
if (this.state.expands) {
this.state.expands.forEach(expand => {
if (expand.axis === 'rows') {
grid.dataSource.expandRow(expand.path);
} else {
grid.dataSource.expandColumn(expand.path);
}
});
}
$(`#pivot-config`).kendoPivotConfigurator({
dataSource: grid.dataSource,
filterable: true
});
}
// Expand the items in the configurator fields.
let tree = $('.k-treeview').data('kendoTreeView');
if (tree) {
tree.expand('.k-item');
}
};
onExpand = e => {
this.setState({
expands: [...this.state.expands, {
axis: e.axis,
path: e.path
}]
});
};
onCollapse = e => {
this.setState({
expands: this.state.expands.filter(ex => ex.axis !== e.axis && JSON.stringify(ex.path) !== JSON.stringify(e.path))
});
};
Here is my try on this
what i like is, that it actually updates the data source as you would expect
function onExpandOrCollapseMember(e, expand) {
var pivot = e.sender;
var axisToUpdate = '_' + e.axis;
var field = _.find(pivot.dataSource[axisToUpdate], f => JSON.stringify(f.name) === JSON.stringify(e.path));
field.expand = expand;
}
And on the pivot options i pass in
expandMember: e => onExpandOrCollapseMember(e,true),
collapseMember: e => onExpandOrCollapseMember(e, false),
I tried reactive form valueChanges but valueChanges method doesn't return input field name which has changed.
I thought code like this. but I think this is not smart way. Because I have to compare every each input field. so I need more smart way.
// get init view data from local storage
this.localstorageService.getPlaceDetail().subscribe(data => {
this.initPlaceDetail = data;
// watch changed value
this.editPlaceForm.valueChanges.subscribe(chengedVal => {
if (chengedVal['ja_name'] !== this.initPlaceDetail.languages.ja.name.text) {
this.changedJA = true;
}
if (chengedVal['ja_romaji'] !== this.initPlaceDetail.languages.ja.name.romaji) {
this.changedJA = true;
}
// ...... I have to check all input fields??
});
});
I'm adding form controls from an array and something like this worked for me. Just reference the item you need instead of expecting the valueChanges observable to pass it to you.
myFields.forEach(item => {
const field = new FormControl("");
field.setValue(item.value);
field.valueChanges.subscribe(v => {
console.log(item.name + " is now " + v);
});
});
This is my way to get changed control in form.
I shared for whom concerned.
Method to get list control changed values
private getChangedProperties(form): any[] {
let changedProperties = [];
Object.keys(form.controls).forEach((name) => {
let currentControl = form.controls[name];
if (currentControl.dirty)
changedProperties.push({ name: name, value: currentControl.value });
});
return changedProperties;
}
If you only want to get latest changed control you can use
var changedProps = this.getChangedProperties(this.ngForm.form);
var latestChanged = changedProps.reduce((acc, item) => {
if (this.changedProps.find(c => c.name == item.name && c.value == item.value) == undefined) {
acc.push(item);
}
return acc;
}, []);
Instead of listening to whole form changes you can listen to value changes event for each form control as shown in below code:
this.myForm.get('ja_name').valueChanges.subscribe(val => {
this.formattedMessage = `My name is ${val}.`;
});