I have been developing a multi-level dropdown menu component for the intranet I am building. Deciding to go with React on this I thought that I multi-level menu would be simple. Apparently not :)
What I have developed so far works quite well with one exception: When clicking on a NavLink the open menu items do not collapse.
All the CSS classes I have added are appearance only with no positioning statements. I have built this using a JSON source file and Reactstrap.
Here is the code for my component.
class MenuBar extends Component {
constructor(props) {
this.NavListItem = this.NavListItem.bind(this);
}
NavListItem = (item, level) => {
if (item.children.length > 0) {
return (
<UncontrolledDropdown direction={level!==0?"right":"down"} nav inNavbar className="menu menu-UncontrolledDropdown" key={"UCD_" + item.pageId}>
<DropdownToggle nav caret
className="menu menu-dropdown-toggle item title"
key={"DDToggle_" + item.pageId}
id={"DDToggle_" + item.pageId}
>
{item.title}
</DropdownToggle>
<DropdownMenu className="menu menu-dropdown-container">
<Nav>
{item.children.map((listItem) => this.NavListItem(listItem, level + 1))}
</Nav>
</DropdownMenu>
</UncontrolledDropdown>
)
}
else {
return (
<NavItem className="menu menu-item-container" key={"DDNavItem_" + item.pageId}>
<NavLink onClick={() => { this.props.updateCurrentPage(item) }} className="menu menu-link" key={"DDNavLink_" + item.pageId}>{item.title}</NavLink>
</NavItem>
)
}
}
render() {
const setIsOpen = (value) => {
this.setState({ isOpen: value })
}
const toggle = () => setIsOpen(!this.state.isOpen);
return (
<div className="row">
<div className="col-2 p-3 menu">
<div onClick={() => this.props.updateCurrentPage(null)}
className="align-top met-logo">
</div>
</div>
<div className="col col-md-6 offset-1 menu ">
<Navbar color="light" light expand="md" className="menu menu-bar">
<NavbarToggler onClick={toggle} className="menu menu-toggler" />
<Nav className="mr-auto" navbar>
{this.props.siteMap.siteMapData.map((link) => this.NavListItem(link, 0))}
</Nav>
</Navbar>
</div>
</div>
);
}
}
The only problem I have now is the open items do not close when an option is clicked. I would like preferably to make this stateless and put it in as a functional component ultimately but for now I am pulling Sitemap from Redux.
Thanks.
I could not find a solution but did come up with a functional workaround that takes very little resource.
In my MainComponent of the SPA I am hosting both the component as well as the current page component. What I did, when I realized that clicking off of the menu closed it, was to add a little snippet into the componentDidUpdate() function:
componentDidUpdate(){
//work around for menu not closing.
document.getElementById("rootDiv").click();
}
This effectively causes the menu to close once the new page has begun loading. Problem solved.
Related
for some reason my exit and new animation is not working. I would like new animation to start every time user click on different menu link. I have also tried with " animate='visible'" , and I have tried also to put directly over motion, and it still not doing exit or starting new animation. I am using .map and framer motion together. Can someone please check it out.
This is the code
Thanks
const [forMapping, setForMapping] = useState(wines)
function menuHandler(index, alt) {
setIsActive(index)
if (alt === 'wine') {
setForMapping(wines)
} else if (alt === 'rakia') {
setForMapping(rakia)
}
}
const variants = {
visible: i => ({
y: 0,
transition: {
duration: .7
}
}),
hidden: {
y: '40%'
}
}
<AnimatePresence>
{forMapping.map((item, index) => {
const {
name,
description,
alt,
imageSrc,
price,
glass_price,
iconSrc,
alc
} = item;
return (
<motion.div
exit={{y: '100'}}
viewport={{once: true}}
custom={index}
whileInView='visible'
initial='hidden'
variants={variants}
key={index}
className='item'>
<div className="image">
<Image
width={200}
height={400}
objectFit='cover'
src={imageSrc}
alt={alt}/>
</div>
<div className="info">
<div className="info-header">
<header>
{name}
</header>
<p className="price">
{price.toFixed(2)} EUR
</p>
</div>
<p className="description">
{description}
</p>
<div className="bottom">
<p>
{alc} %VOL
</p>
<div className='image-price'>
<Image
width={18}
height={20}
objectFit='cover'
src={iconSrc}
alt='wine glass'/>
<p className="black">
{glass_price.toFixed(2)} EUR
</p>
</div>
</div>
</div>
</motion.div>
)
})}
</AnimatePresence>
You should not use the loop index as the key in your motion.div. Since the indices (and thus the keys) will always be the same, it doesn't let <AnimatePresence> track when elements have been added or removed in order to animate them.
Instead use a value like a unique id property for each element. This way React (and AnimatePresence) will know when it's rendering a different element (vs the same element with different data). This is what triggers the exit and enter animations.
Here's a more thorough explanation:
react key props and why you shouldn’t be using index
I have the following DOM Configuration
<div class="sticky-nav-wrapper">
<app-header (notificationClick)="notificationData=$event; sidenav.toggle()"></app-header>
</div>
<mat-sidenav-container>
<mat-sidenav-content>
<router-outlet></router-outlet>
</mat-sidenav-content>
<mat-sidenav #sidenav mode="over">
<app-notifications [notificationData]="notificationData" [opened]="sidenav.opened" (notificationItemClick)="sidenav.close()"></app-notifications>
</mat-sidenav>
</mat-sidenav-container>
In my app-notification component which is inside mat-sidenav, I want to listen to the scroll. Following is my template for app-notification
<div class="class-notification-container" cdkScrollable>
<div *ngIf="notifications && notifications.length > 0">
<div *ngFor="let item of notifications">
<div class="class-notification" [ngClass]="{'class-notification-new': item.localStatus === 'new','class-notification-old': item.localStatus === 'seen'}" (click)="onNotificationClick(item)">
<app-avatar [imageId]="item?.fromUser?.id"></app-avatar>
</div>
<mat-divider></mat-divider>
</div>
</div>
</div>
In my app-notification.ts:
#ViewChild(CdkScrollable) notificationContainer: CdkScrollable;
and
ngAfterViewInit(): void {
this.notificationContainer.elementScrolled()
.subscribe((scrollPosition) => {
this.ngZone.run(() => {
if (!this.endOfNotifications) {
this.pageNumber = this.pageNumber + 1;
this.getNotifications();
}
});
}, (error) => {});
}
However, this subscription is never invoked, no matter how much I scroll inside the mat-side-nav. The docs says that the CdkScrollable directive emits an observable on host elemnet scroll.
Returns observable that emits when a scroll event is fired on the host
element.
The other alternative is to listen to the scrollDispatcher. The issue however is scrolldispatcher is sending events on window scroll and not specifically on side-nav scroll.
I seem to be doing everything OK as per the doc. Can someone please suggest where the things are going wrong. I specifically want to listen to the scroll of my component within the side-nav i.e. app-notification component. Also I do not want to listen to the window scroll or to say the mat-side-nav-content scroll.
It may be you're listening on too high of a parent.
Have you tried listening on the div with the *ngFor ?
<div >
<div *ngIf="notifications && notifications.length > 0">
<div class="class-notification-container" cdkScrollable *ngFor="let item of notifications">
<div class="class-notification" [ngClass]="{'class-notification-new': item.localStatus === 'new','class-notification-old': item.localStatus === 'seen'}" (click)="onNotificationClick(item)">
<app-avatar [imageId]="item?.fromUser?.id"></app-avatar>
</div>
<mat-divider></mat-divider>
</div>
</div>
</div>
I'm trying to implement a hover effect on an icon link for a component in a site built using Nuxt.js with Bootstrap 4. I've attempted using the #mouseover/#mouseenter and #mouseleave events to switch the src attribute from one icon image to another, but it doesn't cause a change unless the icon link is clicked. Does this have something to do with focus? Is there a better way to get the effect I want?
The component is below.
<template>
<b-row class="main-focus px-3 pt-3">
<b-col md="12" class="mb-4">
<h1 class="clr-t mb-4 px-2 pb-1 clr-brdr-btm">resume</h1>
<p class="drk-t pl-2">{{description[0].text}}</p>
<b-link
#mouseover="icon = 'assets/images/icons/resume-icon-clicked.svg'"
#mouseleave="icon = 'assets/images/icons/resume-icon.svg'"
:href="resume.url"
target="_blank"
>
<b-img
class="icon bg-lt"
v-bind="iconProps"
rounded
:src="icon"/>
</b-link>
</b-col>
</b-row>
</template>
<script>
export default {
props: {
description: Array,
resume: Object
},
data () {
return {
icon: 'assets/images/icons/resume-icon.svg',
iconProps: { width: 100 }
}
}
}
</script>
The above behaviour is because b-link supports only #click event as stated in docs.
You can achieve the functionality by wrapping b-link in a div as below.
<div #mouseover="icon = 'assets/images/icons/resume-icon-clicked.svg'"
#mouseleave="icon = 'assets/images/icons/resume-icon.svg'">
<b-link
:href="resume.url"
target="_blank"
>
<b-img
class="icon bg-lt"
v-bind="iconProps"
rounded
:src="icon"/>
</b-link>
</div>
I am using latest version of Microsoft ChatBot, when i send or receive messages,
the display does not scroll down to the latest one
the entry bar is starting at the top of the window(not from the bottom of the window, attached image) not sure if this is the intended behaviour.
react component
class Layout extends Component {
render() {
return(
<Aux>
<main className="Container">
<ReactWebChat
botAvatarInitials= 'BOT'
userAvatarInitials= 'USER'
directLine={secret}
styleSet={styleSet}
/>
</main>
</Aux>
)
}}
You need to wrap your ReactWebChat component in a div and set the height and width of the div and its children in the css to get the conversation to scroll to the bottom and have the entry bar start at the bottom. See below for how your code should look.
React Webchat Component
class Layout extends Component {
render() {
return(
<Aux>
<main className="Container">
<div id="webchat">
<ReactWebChat
botAvatarInitials= 'BOT'
userAvatarInitials= 'USER'
directLine={secret}
styleSet={styleSet}
/>
</div>
</main>
</Aux>
)
}}
CSS
#webchat,
#webchat>* {
height: 750px;
width: 400px;
}
Screenshot
Hope this helps!
In my current meteor.js project, user can create a project and add data nodes to it. I'm using D3 to display the nodes in force graph. When they click a particular node from the graph, the corresponding text in the side panel must be highlighted. For this, I need to track with node is selected. But, I don't want to store a "selected" field on the database.
I'm using this data transform to add selected field right now -
/lib/routes.js
Router.route('/project/:code', {
name: 'projectPage',
data: function() {
return {
project: Projects.findOne({code : this.params.code}),
nodes: Nodes.find({project: this.params.code}, {transform: function (doc) {
doc.selected = false;
return doc;
}})
}
}
});
The template is /client/templates/projectPage.html
<template name="projectPage">
<div class="project-page page">
<h3>{{project.title}}</h3>
<p>{{project.summary}}</p>
<div class="work-area">
<div class="map-space">
{{> nodeDisplay nodes=nodes}}
</div>
<div class="type-space">
{{> typeDisplay nodes=nodes}}
</div>
</div>
</div>
</template>
<template name="nodeDisplay">
<div id="svgdiv"></div>
</template>
<template name="typeDisplay">
{{#each nodesData}}
<p>{{text}}</p>
<br/>
{{/each}}
</template>
The click event is handled /client/js/projects.js
Template.nodeDisplay.events({
'click .node':function(event, template){
/*remove previous selection*/
d3.selectAll('.selected circle').attr("r",32);
d3.selectAll('.selected').each(
function(d){
d.fixed = false;
d3.select(this)
.classed('selected', false);
}
);
/*add new selections*/
d3.select(event.currentTarget)
.classed("selected", true)
d3.selectAll('.selected circle').attr("r",40);
var selected_id = $(event.currentTarget).data("id");
Nodes.update(selected_id.toString(), {$set: {selected: true}});
}
});
However, this updates the database to include the "selected" field.
Is there a better way to do this and keep reactivity?
The meteor way is to use session variables and helper functions.
So instead of
Nodes.update(selected_id.toString(), {$set: {selected: true}});
use
Session.set("selected_node", this._id);
and an accompnying helper in Template.typeDisplay.helpers
isNodeSelected: function() {
if(Session.get("selected_node") === this._id) {
return "selected"
}
}
in the template displaying each node (this code assumes that you want to select the corresponding text in the typeDisplay by applying the classname 'selected'):
<template name="typeDisplay">
{{#each nodesData}}
<p class="{{isNodeSelected}}">{{text}}</p>
<br/>
{{/each}}
</template>