In my Carousel, there is an option to show more than 1 item at a time. It is activated by selecting the number of items you wish to show (max 4). The code refers to this as perView.
Items to display example
This carousel is glide.js, so everything is modular. I have 2 things of note getting activated, the Controls (next/prev button) and Breakpoints (think media queries).
The goal is to turn off the controls in mobile only if perView equals anything other than NaN (not a number)... When it is inactive, it returns NaN... so I guess the comment would be:
if perView does not equal NaN, then controls = false
and because i need this in a mobile breakpoint, I would put the code in the breakpoint bracket.
import AEM from 'base/js/aem';
import Glide, { Controls, Breakpoints, Autoplay, Keyboard, Swipe } from '#glidejs/glide/dist/glide.modular.esm';
class Carousel extends AEM.Component {
init() {
this.initCarousel();
}
initCarousel() {
const el = this.element;
const props = this.props;
const pauseButton = this.element.querySelector('.pause-button');
let perView = parseInt(props.cmpPerView, 10);
let cmpDelay = parseInt(props.cmpDelay, 10);
let autoPlay = true;
if (this.props.cmpAutoplay === 'false' ||
this.props.carouselAutoplay === 'false' ||
!Number.isNaN(perView) && perView < 1 ||
window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
autoPlay = false;
}
this.modules = {
Controls: Controls,
Breakpoints: Breakpoints,
Autoplay: Autoplay,
Keyboard: Keyboard,
Swipe: Swipe
};
this.options = {
type: props.cmpType,
perView: Number.isNaN(perView) ? 1 : perView,
autoplay: autoPlay ? cmpDelay : false,
peek: 0,
keyboard: true,
animationDuration: 400,
rewind: true,
breakpoints: {
768: {
perView: Number.isNaN(perView) || perView === 1 ? 1 : 3,
},
578: {
peek: perView > 1 ? 125 : 0,
perView: 1,
controls: perView > 1,
},
375: {
peek: perView > 1 ? 85 : 0,
perView: 1,
}
}
};
const glide = new Glide(el, this.options);
if (pauseButton && autoPlay) {
const pauseClasses = pauseButton.classList;
glide.on('play', () => {
pauseClasses.add('fa-pause');
pauseClasses.remove('fa-play');
});
glide.on('pause', () => {
pauseClasses.add('fa-play');
pauseClasses.remove('fa-pause');
});
pauseButton.addEventListener('click', () => {
if (pauseClasses.contains('fa-play')) {
glide.play();
} else {
glide.pause();
}
});
} else if (pauseButton) {
pauseButton.remove();
}
if (window.Granite && window.Granite.author && window.Granite.author.MessageChannel) {
/*
* Editor message handling:
* - subscribe to "cmp.panelcontainer" message requests sent by the editor frame
* - check that the message data panel container type is correct and that the id (path) matches this specific
* - Carousel component. If so, route the "navigate" operation to enact a navigation of the Carousel based
* - on index data
*/
this.element.querySelectorAll('.glide__slide').forEach(e => {
if (!e.classList.contains('glide__slide--active')) {
e.classList.add('visually-hidden');
}
});
window.CQ = window.CQ || {};
window.CQ.CoreComponents = window.CQ.CoreComponents || {};
window.CQ.CoreComponents.MESSAGE_CHANNEL = window.CQ.CoreComponents.MESSAGE_CHANNEL ||
new window.Granite.author.MessageChannel('cqauthor', window);
window.CQ.CoreComponents.MESSAGE_CHANNEL.subscribeRequestMessage('cmp.panelcontainer', message => {
if (message.data && message.data.type === 'cmp-carousel' &&
message.data.id === el.dataset.cmpPanelcontainerId) {
if (message.data.operation === 'navigate') {
let index = message.data.index;
if (index < 0) {
return;
}
this.element.querySelectorAll('.glide__slide').forEach((slide, i) => {
if (i === index) {
slide.classList.add('glide__slide--active');
slide.classList.remove('visually-hidden');
} else {
slide.classList.remove('glide__slide--active');
slide.classList.add('visually-hidden');
}
});
}
}
});
} else {
glide.mount(this.modules);
}
}
}
export { Carousel };
So I have
controls: perView > 1
in the 578 breakpoint... but it doesn't work.
Using the Embla Carousel, React version, I'm trying to turn/on off the carousel based on breakpoint. So for mobile, it's on and for tablet up it's off. Here's what I tried that seems to work initially, but won't reinitialize. I'm guessing it's because destory was called so it can reInit and tried init, but no luck there either.
const emblaOptions = {};
const [viewportRef, emblaApi] = useEmblaCarousel(emblaOptions);
const handleEmblaInit = () => {
if (window.innerWidth > 768) {
emblaApi.destroy();
} else {
emblaApi.reInit(emblaOptions);
}
};
useEffect(() => {
if (emblaApi) {
handleEmblaInit();
window.addEventListener("resize", handleEmblaInit);
}
}, [emblaApi]);
Pass null instead of the emblaRef when you want it to be inactive, like demonstrated here:
https://github.com/davidcetinkaya/embla-carousel/issues/99#issuecomment-688730519
From version 7 and up you can use the active option together with the breakpoints option to achieve this. Example from the Embla Carousel docs:
const options = {
active: true,
breakpoints: {
'(min-width: 768px)': { active: false },
},
}
Usage with React:
const [emblaRef, emblaApi] = useEmblaCarousel({
active: true,
breakpoints: {
'(min-width: 768px)': { active: false },
},
});
backAction = () => {
if (this.props.navigation.isFocused() && !this.state.otpScreen) {
this.setState({ showLoginScreen: true });
return true;
} else if(this.state.showLoginScreen) {
this.props.navigation.dispatch(CommonActions.reset({
index: 0,
routes: [
{ name: 'Login' },
],
}))
return false;
}
};
the code above is for action that should be done by the hardware back press, at first it should show the one screen and on the second press it should exit the app, both otp screen and login screen are on the same component, I'm just hiding based on the condition ...
at the first time it is working as per the condition but for the second time it's not.
componentDidMount() {
this.focusListener = this.props.navigation.addListener('focus', () => {
this.setState({
mobile: '',
showLoginScreen: true,
});
});
this.getDataFromStorage();
AsyncStorage.setItem('countryCode', '+91');
BackHandler.addEventListener(
"hardwareBackPress",
this.backAction
);
}
componentWillUnmount() {
clearInterval(this.interval)
BackHandler.addEventListener('hardwareBackPress', () => { return false })
}
can anyone pls help me how to do this,thanks in Advance
I guess your problem is that you can't access the 'current' value of a state inside a listener.
More Details here
Try to use a Reference instead of a state
I'm trying to implement an infinite scroll in my vue chrome extension. I have this code but the problem is that when the page bottom is reached, the ajax call to fetch new data is fired more than once. How i can fix?
mounted() {
this.$store.commit('preloader', false)
window.onscroll = () => {
if( window.scrollY + window.innerHeight >= document.body.scrollHeight ){
console.log('fired')
this.nextPageLoaded = false
this.nextPage()
}
}
},
methods: {
nextPage() {
console.log('Loading next page')
axios.get('https://localhost:8080/api/media')
.then( (response) => {
console.log(response.data)
this.$store.commit('profileMedia', response.data.media)
this.nextPageLoaded = true
})
}
}
I've tried by setting a variable nextPageLoad to true after data are loaded and on false when the scroll event reach the bottom but not work as expected. Any solution will be appreciaetd
Perhaps it's a typo but I don't see that you are actually using the nextPageLoaded property as a flag in the if statement.
It should be something like
mounted() {
this.$store.commit('preloader', false)
window.onscroll = () => {
if ( (window.scrollY + window.innerHeight >= document.body.scrollHeight)
&& !this.nextPageLoaded) {
console.log('fired')
this.nextPageLoaded = true
this.nextPage()
}
}
},
methods: {
nextPage() {
console.log('Loading next page')
axios.get('https://localhost:8080/api/media')
.then( (response) => {
console.log(response.data)
this.$store.commit('profileMedia', response.data.media)
this.nextPageLoaded = false
})
}
}
Also have in mind that I switched the values of the nextPageLoaded property assignments because I think it's more intuitive this way (assigns to true immediately after triggering the nextPage method, assigns to false after ending the AJAX call).
I am hoping you can help. I am fairly new to Unit Testing. I have a Karma + Jasmine set up which is running a PhantomJS browser. This is all good.
What I am struggling with is I have a link on the page, when this link is clicked it injects some HTML. I want to test that the HTML has been injected.
Now at this point, I have the test working but only sometimes, from what I can figure out if my JS runs fast enough the HTML gets injected before the expect() is run. If not the test fails.
How can I make my Jasmine test wait for all JS to finish executing before the expect() is run?
The test in question is it("link can be clicked to open a modal", function() {
modal.spec.js
const modalTemplate = require('./modal.hbs');
import 'regenerator-runtime/runtime';
import 'core-js/features/array/from';
import 'core-js/features/array/for-each';
import 'core-js/features/object/assign';
import 'core-js/features/promise';
import Modal from './modal';
describe("A modal", function() {
beforeAll(function() {
const data = {"modal": {"modalLink": {"class": "", "modalId": "modal_1", "text": "Open modal"}, "modalSettings": {"id": "", "modifierClass": "", "titleId": "", "titleText": "Modal Title", "closeButton": true, "mobileDraggable": true}}};
const modal = modalTemplate(data);
document.body.insertAdjacentHTML( 'beforeend', modal );
});
it("link exists on the page", function() {
const modalLink = document.body.querySelector('[data-module="modal"]');
expect(modalLink).not.toBeNull();
});
it("is initialised", function() {
spyOn(Modal, 'init').and.callThrough();
Modal.init();
expect(Modal.init).toHaveBeenCalled();
});
it("link can be clicked to open a modal", function() {
const modalLink = document.body.querySelector('[data-module="modal"]');
modalLink.click();
const modal = document.body.querySelector('.modal');
expect(modal).not.toBeNull();
});
afterAll(function() {
console.log(document.body);
// TODO: Remove HTML
});
});
EDIT - More Info
To further elaborate on this, The link Jasmine 2.0 how to wait real time before running an expectation put in the comments has helped me understand a bit better, I think. So what we are saying it we want to spyOn the function and wait for it to be called and then initiate a callback which then resolves the test.
Great.
My next issue is, if you look at the structure of my ModalViewModel class below, I need to be able to spyOn insertModal() to be able to do this, but the only function that is accessible in init(). What would I do to be able to move forward with this method?
import feature from 'feature-js';
import { addClass, removeClass, hasClass } from '../../01-principles/utils/classModifiers';
import makeDraggableItem from '../../01-principles/utils/makeDraggableItem';
import '../../01-principles/utils/polyfil.nodeList.forEach'; // lt IE 12
const defaultOptions = {
id: '',
modifierClass: '',
titleId: '',
titleText: 'Modal Title',
closeButton: true,
mobileDraggable: true,
};
export default class ModalViewModel {
constructor(module, settings = defaultOptions) {
this.options = Object.assign({}, defaultOptions, settings);
this.hookModalLink(module);
}
hookModalLink(module) {
module.addEventListener('click', (e) => {
e.preventDefault();
this.populateModalOptions(e);
this.createModal(this.options);
this.insertModal();
if (this.options.closeButton) {
this.hookCloseButton();
}
if (this.options.mobileDraggable && feature.touch) {
this.hookDraggableArea();
}
addClass(document.body, 'modal--active');
}, this);
}
populateModalOptions(e) {
this.options.id = e.target.getAttribute('data-modal');
this.options.titleId = `${this.options.id}_title`;
}
createModal(options) {
// Note: As of ARIA 1.1 it is no longer correct to use aria-hidden when aria-modal is used
this.modalTemplate = `<section id="${options.id}" class="modal ${options.modifierClass}" role="dialog" aria-modal="true" aria-labelledby="${options.titleId}" draggable="true">
${options.closeButton ? '<a href="#" class="modal__close icon--cross" aria-label="Close" ></a>' : ''}
${options.mobileDraggable ? '<a href="#" class="modal__mobile-draggable" ></a>' : ''}
<div class="modal__content">
<div class="row">
<div class="columns small-12">
<h2 class="modal__title" id="${options.titleId}">${options.titleText}</h2>
</div>
</div>
</div>
</section>`;
this.modal = document.createElement('div');
addClass(this.modal, 'modal__container');
this.modal.innerHTML = this.modalTemplate;
}
insertModal() {
document.body.appendChild(this.modal);
}
hookCloseButton() {
this.closeButton = this.modal.querySelector('.modal__close');
this.closeButton.addEventListener('click', (e) => {
e.preventDefault();
this.removeModal();
removeClass(document.body, 'modal--active');
});
}
hookDraggableArea() {
this.draggableSettings = {
canMoveLeft: false,
canMoveRight: false,
moveableElement: this.modal.firstChild,
};
makeDraggableItem(this.modal, this.draggableSettings, (touchDetail) => {
this.handleTouch(touchDetail);
}, this);
}
handleTouch(touchDetail) {
this.touchDetail = touchDetail;
const offset = this.touchDetail.moveableElement.offsetTop;
if (this.touchDetail.type === 'tap') {
if (hasClass(this.touchDetail.eventObject.target, 'modal__mobile-draggable')) {
if (offset === this.touchDetail.originY) {
this.touchDetail.moveableElement.style.top = '0px';
} else {
this.touchDetail.moveableElement.style.top = `${this.touchDetail.originY}px`;
}
} else if (offset > this.touchDetail.originY) {
this.touchDetail.moveableElement.style.top = `${this.touchDetail.originY}px`;
} else {
this.touchDetail.eventObject.target.click();
}
} else if (this.touchDetail.type === 'flick' || (this.touchDetail.type === 'drag' && this.touchDetail.distY > 200)) {
if (this.touchDetail.direction === 'up') {
if (offset < this.touchDetail.originY) {
this.touchDetail.moveableElement.style.top = '0px';
} else if (offset > this.touchDetail.originY) {
this.touchDetail.moveableElement.style.top = `${this.touchDetail.originY}px`;
}
} else if (this.touchDetail.direction === 'down') {
if (offset < this.touchDetail.originY) {
this.touchDetail.moveableElement.style.top = `${this.touchDetail.originY}px`;
} else if (offset > this.touchDetail.originY) {
this.touchDetail.moveableElement.style.top = '95%';
}
}
} else {
this.touchDetail.moveableElement.style.top = `${this.touchDetail.moveableElementStartY}px`;
}
}
removeModal() {
document.body.removeChild(this.modal);
}
static init() {
const instances = document.querySelectorAll('[data-module="modal"]');
instances.forEach((module) => {
const settings = JSON.parse(module.getAttribute('data-modal-settings')) || {};
new ModalViewModel(module, settings);
});
}
}
UPDATE
After working through it has been discovered that .click() events are asynchronous which is why I am gettnig the race issue. Documentation & Stack Overflow issues thoughtout the web recommend using createEvent() and dispatchEvent() as PhantomJs does not understand new MouseEvent().
Here is my code which is now trying to do this.
modal.spec.js
// All my imports and other stuff
// ...
function click(element){
var event = document.createEvent('MouseEvent');
event.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
element.dispatchEvent(event);
}
describe("A modal", function() {
// Some other tests
// Some other tests
it("link can be clicked to open a modal", function() {
const modalLink = document.body.querySelector('[data-module="modal"]');
click(modalLink);
const modal = document.body.querySelector('.modal');
expect(modal).not.toBeNull();
});
// After all code
// ...
});
Unfortunately this is producting the same results. 1 step closer but not quite there.
After a touch of research, it looks as though your use of the click event is triggering an asynchronous event loop essentially saying "Hey set this thing to be clicked and then fire all the handlers"
Your current code can't see that and has no real way of waiting for it. I do believe you should be able to build and dispatch a mouse click event using the info here.
https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/dispatchEvent
I think that should allow you to build a click event and dispatch it onto your element. The difference is that dispatchEvent is synchronous - it should block your test until the click handlers have completed. That should allow you to do your assertion without failures or race conditions.
I have finally found a solution.
There are 2 parts to this, the first part came from #CodyKnapp. His insight into a click() function running asynchronously helped to solve the first part of the issue.
Here is the code for this part.
modal.spec.js
// All my imports and other stuff
// ...
function click(element){
var event = document.createEvent('MouseEvent');
event.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
element.dispatchEvent(event);
}
describe("A modal", function() {
// Some other tests
// Some other tests
it("link can be clicked to open a modal", function() {
const modalLink = document.body.querySelector('[data-module="modal"]');
click(modalLink);
const modal = document.body.querySelector('.modal');
expect(modal).not.toBeNull();
});
// After all code
// ...
});
This allowed for the code to run synchronously.
The second part was a poor understanding on my part of how to write Jasmine tests. In my original tests I was running Modal.init() inside of it("is initialised", function() { when actually I want to be running this inside of beforeAll(). This fixed the issue I had where my tests would not always be successful.
Here is my final code:
modal.spec.js
const modalTemplate = require('./modal.hbs');
import '#babel/polyfill';
import Modal from './modal';
function click(element){
var event = document.createEvent('MouseEvent');
event.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
element.dispatchEvent(event);
}
describe("A modal", function() {
beforeAll(function() {
const data = {"modal": {"modalLink": {"class": "", "modalId": "modal_1", "text": "Open modal"}, "modalSettings": {"id": "", "modifierClass": "", "titleId": "", "titleText": "Modal Title", "closeButton": true, "mobileDraggable": true}}};
const modal = modalTemplate(data);
document.body.insertAdjacentHTML( 'beforeend', modal );
spyOn(Modal, 'init').and.callThrough();
Modal.init();
});
it("link exists on the page", function() {
const modalLink = document.body.querySelector('[data-module="modal"]');
expect(modalLink).not.toBeNull();
});
it("is initialised", function() {
expect(Modal.init).toHaveBeenCalled();
});
it("link can be clicked to open a modal", function() {
const modalLink = document.body.querySelector('[data-module="modal"]');
click(modalLink);
const modal = document.body.querySelector('.modal');
expect(modal).not.toBeNull();
});
afterAll(function() {
console.log(document.body);
// TODO: Remove HTML
});
});