Can't use ref to select swiper methods - vue.js

I am trying to invoke .slideNext() & .slidePrev that come with swiper when the user presses the arrow keys.
I've managed to do this with querySelector like this:
const changeSlide = (event: KeyboardEvent) => {
const slides = document.querySelector('.swiper').swiper
if(event.key === 'ArrowLeft') {
slides.slidePrev();
} else if (event.key === 'ArrowRight') {
slides.slideNext();
}
}
However this method creates warnings and the usage of querySelector is not allowed in general for this project. So instead i wan't to use a ref to select the swiper but i've not yet succeeded at this.
This is what i tried:
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import { Navigation } from 'swiper';
import { Swiper, SwiperSlide } from 'swiper/vue';
import 'swiper/css';
import 'swiper/css/navigation';
// just some imports that are needed
const swiperRef = ref();
// swiper as ref
const changeSlide = (event: KeyboardEvent) => {
const slides = swiperRef.value.swiper
// instead of querySelector i use the ref.value
if(event.key === 'ArrowLeft') {
slides.slidePrev();
} else if (event.key === 'ArrowRight') {
slides.slideNext();
}
}
</script>
<template>
<swiper
ref="swiperRef"
:initial-slide="props.initialSlide"
:slides-per-view="1"
:space-between="0"
:modules="[Navigation]"
navigation
>
<swiper-slide> // left out the attributes since they are a mere distraction
<img/> // left out the attributes since they are a mere distraction
</swiper-slide>
</swiper>
</template>
The error i get from this code is:

From what I'm seeing in the source code, you can't access the swiper instance using ref on the swiper vue component because it's not exposed.
You have others way to do thought:
inside a child component of the <swiper> component, use the useSwiper() component.
on the parent component that instanciate the <swiper> component, use the #swiper event. It sends the swiper object once mounted:
<template>
<swiper #swiper="getRef" />
</template>
<script setup>
const swiper = ref(null)
function getRef (swiperInstance) {
swiper.value = swiperInstance
}
function next () {
swiper.value.slideNext() // should work
}
</script>

You don't have to use this method instead you can just import Keyboard like this:
import { Navigation } from 'swiper';
Add Keyboard to modules and attributes of <swiper> like this:
<swiper :modules="[Navigation, Keyboard]" keyboard />
And add the attribute to <swiper-slide> as well:
<swiper-slide keyboard />

Related

Vue.js - Element Plus - How to test el-dropdown component

I have a problem that I can't trigger el-dropdown menu. I've followed the testing approach done in element-plus repository but couldn't able to simulate mouseenter event and see whether dropdown menu is opened.
my code can be found below.
<template>
<el-dropdown>
<el-icon>
<MoreFilled/>
</el-icon>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>Send a message</el-dropdown-item>
<el-dropdown-item>Report</el-dropdown-item>
<el-dropdown-item>Block</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<script lang="ts" setup>
import { MoreFilled } from '#element-plus/icons-vue';
</script>
and my test code can be found here
import { mount } from "#vue/test-utils";
import { nextTick } from "vue";
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import EntryCardFooterDropdown from "../EntryCardFooterDropdown.vue";
import { ElTooltip } from "element-plus";
describe('EntryCardFooterDropdown', () => {
it('render', async () => {
const wrapper = mount(EntryCardFooterDropdown)
await nextTick()
const content = wrapper.findComponent(ElTooltip).vm as InstanceType<typeof ElTooltip>
vi.useFakeTimers();
const triggerElm = wrapper.find('.el-tooltip__trigger');
expect(content.open).toBe(false);
await triggerElm.trigger('mouseenter');
vi.runAllTimers();
expect(content.open).toBe(true);
})
})

Vue3 updating values between components

I have a basic SPA with two child components, a header and a side menu (left drawer).
I wish the user to be able to click a button on the header component to call a function in the side menu component.
I understand I can use props to access a variable between parent & child components however how can I update a value between two sibling components?
Header
<q-btn dense flat round icon="menu" #click="toggleLeftDrawer" />
Left Drawer
import { ref } from 'vue'
export default {
setup () {
const leftDrawerOpen = ref(false)
return {
leftDrawerOpen,
toggleLeftDrawer () {
leftDrawerOpen.value = !leftDrawerOpen.value
}
}
}
}
Use global stores. Create a file
/store.js (you can obviously use any name)
Inside this file store/write the following code:-
import { reactivity } from 'vue'
export const global = reactive({
yourVariable: 'initialValue'
})
You can then import this variable and interact with it from anywhere and the change will be global. See the code below:
In header component:-
<script setup>
import { global } from './store.js'
const clicked = ()=> {
global.yourVariable = 'changed'
}
</script>
<template>
<button #click="clicked">
</button>
</template>
In leftDrawer Component:-
<script setup>
import { global } from './store.js'
</script>
<template>
<div>
{{ global.yourVariable }}
<!--You'll see the change:)-->
</div>
</template>
Then add these two in your main vue:-
<script setup>
import headerComponent from '...'
import leftDrawerComponent from '....'
//....
</script>
<template>
<div>
<header-component />
<left-drawer-component />
</div>
</template>

Is there a 'simple' way to dynamically render views in vue?

Let's take a step back and look at the use case:
You're defining a modular interface, and any module that implements it must be able to 'render itself' into the application given a slot and a state.
How do you do it in vue?
Example solution
Let's have a look at the most basic implementation I can assemble:
(full example:
https://stackblitz.com/edit/vitejs-vite-8zclnp?file=src/App.vue)
We have a layout:
# Layout.vue
<template>
<div>
<hr />
<slot name="moduleView" />
<hr />
</div>
</template>
...and an app with a module:
# App.vue
<script lang="ts" setup>
import type { MyModuleState } from "./MyModule";
import Layout from "./Layout.vue";
import { ref } from "vue";
import { MyModule } from "./MyModule";
import ModView from "./ModView.vue";
const state = ref<MyModuleState>({ value: 0 });
const module = new MyModule();
const onClick = () => {
state.value = { value: state.value.value + 1 };
};
const renderModule = () => {
return module.view(state.value);
};
</script>
<template>
<div>currentValue: {{ state.value }}</div>
<div>update: <button #click="onClick">++</button></div>
<div>
<Layout>
<template v-slot:moduleView>
<mod-view :render="renderModule" :state="state" /> // <--- But this!
</template>
</Layout>
</div>
</template>
...but rendering into the slot requires a lot of jumping through obscure hoops:
# ModView.vue
<script lang="ts" setup>
import ModRender from "./ModRender";
import { ref, watch } from "vue";
import type { VNode } from "vue";
const props = defineProps<{
render: (state?: any) => VNode | Array<VNode>;
state?: any;
}>();
const nodes = ref(props.render(props.state));
watch( // <-- Obscure! The view won't update unless you explicitly watch props?
() => props.state,
(nextState) => {
nodes.value = props.render(nextState);
}
);
</script>
<template>
<mod-render :nodes="nodes" />
</template>
# ModRender.ts
import type { VNode } from "vue";
const ModRender = (props: { nodes: VNode | Array<VNode> }) => {
return props.nodes;
};
ModRender.props = {
nodes: {
required: true,
},
};
export default ModRender; // <--- Super obscure, why do you need a functional component for this?
Before we can define the actual module:
# MyModule.ts
import type { VNode } from "vue";
import { h } from "vue";
import ModuleView from "./MyModuleDisplay.vue";
interface AbstractModule<T> {
view: (state: T) => VNode;
}
export interface MyModuleState {
value: number;
}
export class MyModule implements AbstractModule<MyModuleState> {
view(state: MyModuleState): VNode {
return h(ModuleView, { state });
}
}
...and a component for it:
# MyModuleView.vue
<script setup lang="ts">
import type { MyModuleState } from "./MyModule";
const props = defineProps<{ state: MyModuleState }>();
</script>
<template>
<div>{{ state.value }}</div>
</template>
What.
This seems extremely obtuse and verbose.
Am I missing something?
In other component systems an implementation might look like:
export class MyModule implements AbstractModule<MyModuleState> {
view(state: MyModuleState): VNode {
return (<div>{state.value}</div>);
}
}
...
<div>
<Layout>{renderModule(state)}</Layout>
</div>
It seems surprising that so many wrappers and hoops have to be done in vue to do this, which makes me feel like I'm missing something.
Is there an easier way of doing this?
Vnode objects cannot be rendered in component templates as is and need to be wrapped in a component like ModRender. If they are used as universal way to exchange template data in the app, that's a problem. Vnodes still can be directly used in component render functions and functional components with JSX or h like <Layout>{renderModule(state)}</Layout>, this limits their usage.
AbstractModule convention may need to be reconsidered if it results in unnecessary code. Proceed from the fact that a "view" needs to be used with dynamic <component> at some point, and it will be as straightforward as possible.
There may be no necessity for "module" abstraction, but even if there is, module.view can return a component (functional or stateful) instead of vnodes. Or it can construct a component and make it available as a property, e.g.:
class MyModule {
constructor(state) {
this.viewComponent = (props) => h(ModuleView, { state, ...props })
}
}

Why does the reactivity of VUE 3 CompositionAPI not work?

Please tell me why reactivity between unrelated components does not work:
ModalsController.js:
import { ref } from 'vue';
export const useModal = (init = false)=>{
const isShowModal = ref(init);
const openModal = () => {
isShowModal.value = true;
};
const closeModal = () => {
isShowModal.value = false;
};
return {
isShowModal, openModal, closeModal
}
}
Header.vue:
<template>
<button #click="openModal">OpenModal</button>
{{isShowModal}}
<button #click="closeModal">CloseModal</button>
</template>
<script setup>
import {useModal} from "./ModalsController.js";
const { isShowModal,openModal,closeModal } = useModal();
</script>
Modal.vue:
<template>
<div v-if="isShowModal"> Modal window </div>
</template>
<script setup>
import {useModal} from "./ModalsController.js";
const {isShowModal} = useModal();
</script>
And everything works if I create a simple variable instead of a function like this:
ModalsController.js:
import { ref } from 'vue';
export const isShowModal = ref(false);
and accordingly, I change it in the header. But this is very inconvenient because there are way more functions (switching, etc.)
Thank you all in advance for your help. I put the code in the Playground for the test:
Not a working (func)
working (simple var)
The problem is useModal() creates a new ref() every time it's called. Each of your components calls useModal() to get the isShowModal ref, but each ref is a newly created one independent from each other.
To share the refs between components, move the ref creation outside of the useModal function definition:
import { ref } from 'vue';
const isShowModal = ref(false); 👈
export const useModal = (init = false) => {
// const isShowModal = ref(init); ❌ move this outside function
⋮
}
demo

Unable to implement Materialize-CSS initialization scripts with NextJS

I'm using Materialize CSS to style a web app I'm working on. Am building the app using NextJS with ReactJS. I have already styled the navbar using the classes defined in the Materialize CSS file. However, I'm unable to implement the responsive sidebar functionality using the prescribed initialization script as instructed on the Materialize website.
My Navbar component (./components/Navbar.js) looks like this:
import React, { Component, Fragment } from 'react';
import Link from 'next/link';
import $ from 'jquery';
class Navbar extends Component {
constructor(props) {
super(props)
this.props = props;
}
componentDidMount = () => {
document.addEventListener('DOMContentLoaded', function() {
var elems = document.querySelectorAll('.sidenav');
var instances = M.Sidenav.init(elems, options);
});
}
render() {
return (
<Fragment>
<nav>
<div className="nav-wrapper blue">
<Link prefetch href='/'>
Project Coco
</Link>
<i className="material-icons">menu</i>
<ul id="nav-mobile" className="right hide-on-med-and-down">
<li>
<Link prefetch href='/'>
<a>Home</a>
</Link>
</li>
<li>
<Link prefetch href='/about'>
<a>About</a>
</Link>
</li>
<li>
</li>
</ul>
</div>
</nav>
{/* Sidenav markup */}
<ul className="sidenav" id="slide-out">
<li>
<Link prefetch href='/'>
<a>Home</a>
</Link>
</li>
<li>
<Link prefetch href='/about'>
<a>About</a>
</Link>
</li>
</ul>
</Fragment>
);
}
}
export default Navbar;
As is obvious, this functionality (sidebar) needs JQuery. So I already have it added via yarn and am invoking it using import $ from 'jquery'. But upon running, it throws an error saying sidenav is not a function.
I even tried entirely doing away with JQuery and just going for the vanilla JS version of the initialization script in componentDidMount:
document.addEventListener('DOMContentLoaded', function() {
var elems = document.querySelectorAll('.sidenav');
var instances = M.Sidenav.init(elems, options);
});
But this time, although no error, the functionality still refuses to work and clicking on the hamburger menu doesn't trigger the sidenav, which it should.
The entire code repo is up on Github for reference and the prototype app is live at schandillia.com. Any solutions?
P.S.: It seems the problem is that the initialization code in ComponentDidMount gets executed before MaterializeCSS.js (being called as an external file), on which it depends. Any work around this could be a potential solution, although that's just an assumption.
You need to import materialize-css into your component. Sadly, if you try to do import 'materialize-css' you'll get a window is undefined error. This is because Next.js is universal, which means it executes code first server-side where window doesn't exist. The workaround I used for this problem is the following:
Install materialize-css if you haven't:
$ npm i materialize-css
Import materialize-css into your component only if window is defined:
if (typeof window !== 'undefined') {
window.$ = $;
window.jQuery = $;
require('materialize-css');
}
Then, in your componentDidMount method:
componentDidMount = () => {
$('.sidenav').sidenav();
}
So your component looks like this:
import React, { Component, Fragment } from 'react';
import Link from 'next/link';
import $ from 'jquery';
if (typeof window !== 'undefined') {
window.$ = $;
window.jQuery = $;
require('materialize-css');
}
class Navbar extends Component {
constructor(props) {
super(props)
this.props = props;
}
componentDidMount = () => {
$('.sidenav').sidenav();
}
render() {
return (
<Fragment>
<nav>
...
</nav>
{/* Sidenav markup */}
<ul className="sidenav" id="slide-out">
...
</ul>
</Fragment>
);
}
}
export default Navbar;
Sources: Next.js FAQ and this issue.
How about this one:-
_app.tsx
import 'materialize-css/dist/css/materialize.min.css';
import '../styles/globals.css';
function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />;
}
export default MyApp;
index.tsx
import React, { useEffect } from 'react';
const Index = () => {
useEffect(() => {
const M = require('materialize-css');
M.toast({ html: 'worlds' });
}, []);
return (
<div className='container'>
<h1>hello</h1>
</div>
);
};
export default Index;