Unable to implement Materialize-CSS initialization scripts with NextJS - materialize

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;

Related

Vue Testing Library with NaiveUI

I'm using Vue 3, NaiveUI, Vitest + Vue Testing Library and got to the issue with testing component toggle on button click and conditional rendering.
Component TSample:
<template>
<n-button role="test" #click="show = !show" text size="large" type="primary">
<div data-testid="visible" v-if="show">visible</div>
<div data-testid="hidden" v-else>hidden</div>
</n-button>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
import { NButton } from 'naive-ui'
export default defineComponent({
name: 'TSample',
components: {
NButton
},
setup() {
const show = ref(true)
return {
show
}
}
})
</script>
The test case I have:
import { render, waitFor } from '#testing-library/vue'
import TSample from './TSample.vue'
import userEvent from '#testing-library/user-event'
describe('Tests TSample component', () => {
it('toggles between visible and hidden text inside the button', async () => {
const user = userEvent.setup()
const { getByText, queryByText, getByRole } = render(TSample)
expect(getByRole('test')).toBeInTheDocument()
expect(getByText(/visible/i)).toBeInTheDocument()
expect(queryByText('hidden')).not.toBeInTheDocument()
await user.click(getByRole('test'))
await waitFor(() => expect(queryByText(/hidden/i)).toBeInTheDocument()) <-- fails
})
})
The error:
<transition-stub />
<span
class="n-button__content"
>
<div
data-testid="visible"
>
visible
</div>
</span>
</button>
</div>
</body>
</html>...Error: expect(received).toBeInTheDocument()
received value must be an HTMLElement or an SVGElement.
Moreover in Testing Preview I get:
<button
class="n-button n-button--primary-type n-button--large-type mx-4"
tabindex="0"
type="button"
disabled="false"
role="test"
>
<transition-stub>
</transition-stub><span class="n-button__content">
<div data-testid="visible">visible</div>
</span>
</button>
a button, which makes it more confusing to me... Same situation happened when I replaced waitFor with nextTick from Vue, the component didn't do a toggle at all on click.
What works but isn't acceptable
When I changed the n-button to just button, the test passed and divs are toggled, but that's not the goal of this component. The component isn't supposed to be changed.
What I have tried:
I tried different approaches with reaching the div that contains hidden text. Either it was like above - queryByText/getByText or getByTestId, but test fails at the same point.
Also followed with similar approach shown at Testing Library - Disappearance Sample
but doesn't work in my case above.
What actually is going on and how can I test the change on click with such third-party components?
If more info is needed/something is missing, also let me know, I'll update the question.
Any suggestions, explanations - much appreciated.

Can't use ref to select swiper methods

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 />

Testing with vitest and testing-library is not working: it is due to using the SFC Script Setup?

I'm new to Vue and especially with the composition functions. I'm trying to test a component that uses the script setup; however, it seems that it is not working.
The component is this one:
<template>
<el-card class="box-card" body-style="padding: 38px; text-align: center;" v-loading="loading">
<h3>Login</h3>
<hr class="container--separator">
<el-form ref="formRef"
:model="form"
>
<el-form-item label="Username">
<el-input v-model="form.username" placeholder="Username"/>
</el-form-item>
<el-form-item label="Password">
<el-input type="password" v-model="form.password" placeholder="Password" />
</el-form-item>
<el-button color="#2274A5" v-on:click="submitForm()">Login</el-button>
</el-form>
</el-card>
</template>
<script lang="ts" setup>
import {reactive, ref} from 'vue'
import { useRouter } from 'vue-router'
import type {FormInstance} from 'element-plus'
import {useMainStore} from "../../stores/index"
import notification from "#/utils/notification"
import type User from "#/types/User"
const formRef = ref<FormInstance>()
const form: User = reactive({
username: "",
password: "",
})
const router = useRouter()
const loading = ref(false)
const submitForm = (async() => {
const store = useMainStore()
if (form.username === "") {
return notification("The username is empty, please fill the field")
}
if (form.password === "") {
return notification("The password is empty, please fill the field")
}
loading.value = true;
await store.fetchUser(form.username, form.password);
loading.value = false;
router.push({ name: "home" })
})
</script>
<style lang="sass" scoped>
#import "./LoginCard.scss"
</style>
When I try to test it:
import { test } from 'vitest'
import {render, fireEvent} from '#testing-library/vue'
import { useRouter } from 'vue-router'
import LoginCard from '../LoginCard/LoginCard.vue'
test('login works', async () => {
render(LoginCard)
})
I had more lines but just testing to render the component gives me this error.
TypeError: Cannot read properties of undefined (reading 'deep')
❯ Module.withDirectives node_modules/#vue/runtime-core/dist/runtime-core.cjs.js:3720:17
❯ Proxy._sfc_render src/components/LoginCard/LoginCard.vue:53:32
51| loading.value = false;
52|
53| router.push({ name: "home" });
I tried to comment parts of the component to see if it was an issue with a specific line (the router for example), but the problem seems to continue.
I tried to search about it but I don't know what I'm doing wrong, it is related to the component itself? Should I change how I've done the component?
I had the same issue, and was finally able to figure it out. Maybe this will help you.
The problem was I had to register global plugins used by my component when calling the render function.
I was trying to test a component that used a directive registered by a global plugin. In my case, it was maska, and I used the directive in a input that was rendered somewhere deeply nested inside my component, like so:
<!-- a global directive my component used -->
<input v-maska="myMask" .../>
#vue/test-utils didn't recognize it automatically, which caused the issue. To solve it, I had to pass the used plugin in a configuration parameter of the render() function:
import Maska from 'maska';
render(MyComponent, {
global: {
plugins: [Maska]
}
})
Then, the issue was gone. You can find more info about render()
configuration here:
https://test-utils.vuejs.org/api/#global

Open file dialogbox in vue composition API

I am trying to open the Select file dialog box when clicking on the button, It is possible using this.$refs.fileInput.click() in VUE, but this is not working in composition API.
Here is the code for reference: https://codepen.io/imjatin/pen/zYvGpBq
Script
const { ref, computed, watch, onMounted, context } = vueCompositionApi;
Vue.config.productionTip = false;
Vue.use(vueCompositionApi.default);
new Vue({
setup(context) {
const fileInput = ref(null);
const trigger = () => {
fileInput.click()
};
// lifecycle
onMounted(() => {
});
// expose bindings on render context
return {
trigger,fileInput
};
}
}).$mount('#app');
Template
<div id="app">
<div>
<div #click="trigger" class="trigger">Click me</div>
<input type="file" ref="fileInput"/>
</div>
</div>
Thank you.
Have you tried to access it using context.refs.fileInput.click();?
Don't forget that it's setup(props, context) and not setup(context).
Try my edit here: https://codepen.io/ziszo/pen/oNxbvWW
Good luck! :)
I'm working in Vue 3 CLI and have tried several different recommendations and found the following to be the most reliable.
<template>
<input class="btnFileLoad" type="file" ref="oFileHandler" #change="LoadMethod($event)" />
<button class="btn" #click="fileLoad">Load</button>
</template>
<script>
import {ref} from "vue";
export default{
setup(){
const hiddenFileElement = ref({})
return {hiddenFileElement }
}
methods:{
fileLoad() {
this.hiddenFileElement = this.$refs.oFileHandler;
this.hiddenFileElement.click();
},
}
}
</script>
<style>
.btn{ background-color:blue; color:white; }
.btnFileLoad{ display:none }
</style>
I also discovered in Chrome that if the call from the button element to the hidden file handler takes to long, an error message that reads "File chooser dialog can only be shown with a user activation." is displayed in the source view. By defining the hiddenFileElement in setup the problem went away.

React Router 4 can not load new content on same component with <Link>

I can't seem to trigger any other react component life cycle method other than render() when I click on a link that leads to a page that loads exactly the same component, even though the url is different. So here's my code
//index.js - the entry point
import React from 'react';
import { render } from 'react-dom';
import { BrowserRouter, Route } from 'react-router-dom';
import Config from './Settings/Config';
import App from './Components/App';
const c = new Config();
render(
<BrowserRouter basename={c.routerBaseName}>
<App />
</BrowserRouter>
, document.getElementById('root'));
Here's my App JS
// Components/App.js
import React, {Component} from 'react';
import {Route} from 'react-router-dom';
import BlogEntry from './BlogEntry';
export default class App extends Component {
render() {
console.log('app');
return (
<div>
<Route exact path="/blog/:name" component={BlogEntry} />
</div>
)
}
}
And here is my BlogEntry.js
// Components/BlogEntry.js
import React from 'react';
import { Link } from 'react-router-dom';
export default class BlogEntry extends React.Component {
async componentDidMount() {
const [r1] = await Promise.all([
fetch(`http://api.myservice.com/${this.props.match.params.name}`)
]);
this.setState({content:await r1.json()});
console.log('fetch');
}
render() {
console.log('render');
if(!this.state) return <div></div>;
if(!this.state.content) return <div></div>;
const content = this.state.content;
return (
<div id="blog-entry" className="container">
<h1>{content.title}</h1>
<div dangerouslySetInnerHTML={{__html:content.content}}></div>
<div className="related-other">
<h2>Related Content</h2>
<ul>
<li><Link to="/blog/new-york-wins-the-contest">New York Wins the Contest!</Link></li>
<li><Link to="/blog/toronto-with-some-tasty-burgers">Toronto with Some Tasty Burgers</Link></li>
</ul>
</div>
</div>
);
}
}
So what happens is that when I click on the link for Toronto with Some Tasty Burgers or New York Wins the Contest! I see the url in my web browser address bar update accordingly. But my componentDidMount does not fire. And hence no new content is fetched or loaded.
React also won't let me put an onPress event handler to the <Link> object. And even if I did, managing the history state when browser clicks back button would be a nightmare if I were to create my own onpress event handler to load pages.
So my question is, how do I make it so that clicking on one of the links actually causes the component to fetch new data and redraw and also be part of the browser back button history?
I added this to my BlogEntry.js and everything works now:
componentWillReceiveProps(nextProps) {
this.props = nextProps;
}
I don't think your proposed solution, via componentWillReceiveProps (deprecated) is good enough. It's a hack.
Why don't you keep the route id in the state (as in /blog/:id).
Then something like this:
componentDidUpdate() {
const { match: { params: { id: postId } = {} } } = this.props;
if(this.state.postId !== postId) {
// fetch content
}
}