Add a class to a nabar modal in a component Vue.js - javascript

I wanted to know how can I add a class to a modal in a navbar components? My navbar is in App.vue and I wanted to create a message that would add the class "is-active" to a modal in my navbar when I click on it. But I can't find the way to do that..
Thank you

Usually when you have a parent -> child relationship you can use events. In this case since you have two components that are not linked (directly) then you have two alternatives.
Using store (it is usually used in cases where your application is of a considerate size)
You can use vuex to have a central place where you will have your global state. A simple example would be:
store/main.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
isModalOpen: false
},
getters: {
isModalOpen => (state) => state.isModalOpen,
},
mutations: {
setIsModalOpen (state, isOpen) {
state.isModalOpen = isOpen;
}
}
})
then you can access the store in your component as such:
<template>
<navbar :class="[isNavBarOpen ? "is-active" : ""]" />
</template>
export default {
computed: {
isNavBarOpen () {
this.$store.getters['isModalOpen']
}
}
}
Event bus (it is usually used in cases where you have a small app and do not need a global state manager)
Read more about EventBus here.
You can create a simple EventBus
services/eventBus.js
import Vue from 'vue';
const export EventBus = new Vue();
then on your component when the modal is open you can do:
// # -> is an alias to your root folder. Most projects scafolded by Vue CLI has this by default
import {EventBus} from "#/services/eventBus"
export default {
methods: {
openStore: () => {
// your logic to open modal
EventBus.$emit('modal-open');
}
}
}
then on your App.vue you just listen to this event
App.vue
<template>
<navbar :class="[isModalOpen ? "is-active" : ""]" />
</template>
// # -> is an alias to your root folder. Most projects scafolded by Vue CLI has this by default
import {EventBus} from "#/services/eventBus"
export default {
data() {
return {
isModalOpen: false,
}
},
created() {
EventBus.$on('modal-open', this.onModalOpen);
},
methods: {
onModalOpen() {
this.isModalOpen = true;
}
}
}
The one you will pick depends on our application structure and if you think it is complex enough to use a central state management (vuex).
There might contain some errors in the code but the main idea is there.

Related

Dynamically create a component in Vue JS

I need to create a component in Vue JS dynamically on click and then route to that component. I am using Vue 3. Everything needs to happen in one click.
My code looks something like this
methods:{
routerClick(value){
console.log("number is "+value)
this.$router.push({path:'New', name:'New', component: ()=>Vue.component('New')})
}
},
I do not need to move a component that is already created. I want to create a component inside this method and then route to the component using this router. Please, any suggestions will be highly appreciated.
Below is a simplistic solution that works (I'm not an expert in Vue 3).
The main point is to use addRoute before pushing to it, because you cannot specify the route component when pushing to a route.
Here is the codesandbox with the working solution.
<template>
<router-link to="/">Home</router-link>
<button #click="createComponent">Create Component</button>
<router-view></router-view>
</template>
<script>
import { getCurrentInstance } from "vue";
import { useRouter } from "vue-router";
export default {
name: "App",
setup() {
const app = getCurrentInstance().appContext.app;
const router = useRouter();
const createComponent = () => {
// Check if the component has been alreadey registered
if (!app.component("NewComponent")) {
app.component("NewComponent", {
name: "NewComponent",
template: `<div>This is a new component</div>`
});
}
const newComponent = app.component("NewComponent");
// Adding a new route to the new component
router.addRoute({ path: "/new", component: newComponent });
router.push("/new");
};
return {
createComponent,
};
},
};
</script>

Storybook not showing styles

I have a dialog component which is using the Primereact dialog internally. When I make a storybook for the same, the custom css for button is being imported as it is imported inside dialog.jsx. But the default css of Primereact dialog is not loading and reflecting in the storybook. Although it is being loaded in my React app.
dialogComp.jsx
import { Dialog } from "primereact/dialog";
const DialogComp = (props) => {
return (
<Dialog
className="dialog-modal"
header={props.header}
visible={true}
>
{props.children}
</Dialog>
);
};
export default DialogModal;
dialog.storybook.js
import React from "react";
import DialogModal from "./dialogComp";
import { addDecorator, addParameters } from "#storybook/react";
import { Store, withState } from "#sambego/storybook-state";
import { store } from "./../../utils/storyStore";
const DialogModalComp = (props) => {
return [
<div>
<DialogModal
header="Dialog Modal"
displayModal={true}
>
Modal content
</DialogModal>
</div>,
];
};
addDecorator(withState());
addParameters({
state: {
store,
},
});
export default {
title: "dialog",
};
export const DialogModalComponent = () => DialogModalComp;
storybook---main.js
module.exports = {
"stories": [
"../src/**/*.stories.mdx",
"../src/**/*.stories.#(js|jsx|ts|tsx)"
],
"addons": [
"#storybook/addon-links",
"#storybook/addon-essentials",
"#storybook/preset-create-react-app"
]
}
Am I missing something in the configuration?
You'll need to import any styles you use in App.js globally in Storybook, by importing them in .storybook/preview.js (create the file if it doesn't already exist).
Every component in React is self contained - your DialogModal component won't get styled because in Storybook it is not being rendered within your App component (where you're importing your styles).
To simulate your app when using Storybook, you import the css in a preview.js file.
Docs:
To control the way stories are rendered and add global decorators and
parameters, create a .storybook/preview.js file. This is loaded in the
Canvas tab, the “preview” iframe that renders your components in
isolation. Use preview.js for global code (such as CSS imports or
JavaScript mocks) that applies to all stories.
TL;DR
import your styles in .storybook/preview.js
import "../src/index.css";
export const parameters = {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
};
If you use storybook and emotion, and if you implement Global styles or Theming, you may add a decorator into the .storybook/preview.js like this:
I'm using Create React App, therefore I'm using jsxImportSource
/** #jsxImportSource #emotion/react */
import { Global } from '#emotion/react'
import { GlobalStyles } from '../src/styles'
const withGlobalProvider = (Story) => (
<>
<Global styles={GlobalStyles} />
<Story />
</>
)
export const decorators = [withGlobalProvider]
You may find more information on: https://storybook.js.org/docs/react/essentials/toolbars-and-globals#global-types-and-the-toolbar-annotation

How to throw data to main App.vue from views? [duplicate]

I have two components:
App.vue
Sidekick.vue
In my App.vue component, I have a property that I would like to access from Sidekick.vue
App.vue
<template>
<div id="app">
<p>{{ myData }}</p>
<div class="sidebar">
<router-view/> // our sidekick component is shown here
</div>
</div>
</template>
<script>
export default {
name: 'App',
data () {
return {
myData: 'is just this string'
}
}
}
</script>
Sidekick.vue
<template>
<div class="sidekick">
{{ myData }}
</div>
</template>
<script>
export default {
name: 'Sidekick'
}
</script>
I would like access to myData (which is declared in App.vue) from Sidekick.vue
I have tried importing App.vue from within Sidekick.vue by doing something like:
Sidekick.vue (incorrect attempt)
<script>
import App from '#/App'
export default {
name: 'Sidekick',
data () {
return {
myData: App.myData
}
}
}
</script>
I have read about props - but have only seen references to child / parent components. In my case, Sidekick.vue is shown in a div inside App.vue (not sure if this makes it a "child"). Do I need to give access of myData to <router-view/> somehow?
UPDATE: (to show relationship between App.vue and Sidekick.vue
index.js (router file)
import Vue from 'vue'
import Router from 'vue-router'
import Sidekick from '#/components/Sidekick',
import FakeComponent from '#/components/FakeComponent'
Vue.use(Router)
const router = new Router({
routes: [
{
path: '/',
redirect: '/fakecomponent'
},
{
path: '/sidekick',
name: 'Sidekick',
component: Sidekick
},
{
path: '/fakecomponent',
name: 'FakeComponent',
component: FakeComponent
}
]
})
export default router
Sidekick.vue gets rendered when we hit /sidekick
Just keep in mind, the rule of thumb is using props to pass data in a one-way flow
props down, events up.
https://v2.vuejs.org/v2/guide/components.html#Composing-Components
Quick solution:
Global event bus to post messages between your <App/> and <Sidekick/> components.
https://v2.vuejs.org/v2/guide/components.html#Non-Parent-Child-Communication
Long term solution:
Use a state management library like vuex to better encapsulates data in one place (a global store) and subscribe it from your components tree using import { mapState, mapMutations } from 'vuex'
When you have parent-child communication, the best and recommended
option is to use props and events. Read more in Vue docs
When want to have shared state between many components the best and
recommended way is to use Vuex.
If you want to use simple data sharing you can use Vue observable.
Simple example: Say that you have a game and you want the errors to be accessible by many components. (components can access it and manipulate it).
errors.js
import Vue from "vue";
export const errors = Vue.observable({ count: 0 });
Component1.vue
import { errors } from 'path-of-errors.js'
export default {
computed: {
errors () {
get () { return errors.count },
set (val) { errors.count = val }
}
}
}
In Component1 the errors.count is reactive. So if as a template you have:
<template>
<div>
Errors: {{ errors }}
<button #click="errors++">Increase</button>
</div>
</template>
While you click the Increase button, you will see the errors increasing.
As you might expect, when you import the errors.js in another component, then both components can participate on manipulating the errors.count.
Note: Even though you might use the Vue.observable API for simple data sharing you should be aware that this is a very powerful API. For example read Using Vue Observables as a State Store
App.vue:
<router-view pass_data='myData'/>
Sidekick.vue:
export default {
name: "Sidekick",
props: ["pass_data"],
created() {
alert("pass_data: "+this.pass_data)
}
}
If App.js(Parent) and Sidekick(Child)
App.js
in Template
In script
import Sidekick from './Sidekick.vue:
Sidekick.vue
props: ['myData']
now you can access myData anywhere in sidekick.
In template myData and
in scripts this.myData

How to design a store in Vuex to handle clicks in nested, custom components?

I'm trying to design a store to manage the events of my Vuex application. This far, I have the following.
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
const state = { dataRows: [], activeDataRow: {} };
const mutations = {
UPDATE_DATA(state, data) { state.dataRows = data; state.activeDataRow = {}; },
};
export default new Vuex.Store({ state, mutations });
I'm going to have a number of list items that are supposed to change the value of the data in the store when clicked. The design of the root component App and the menu bar Navigation is as follows (there will be a bunch of actions in the end so I've collected them in the file actions.js).
<template>
<div id="app">
<navigation></navigation>
</div>
</template>
<script>
import navigation from "./navigation.vue"
export default { components: { navigation } }
</script>
<template>
<div id="nav-bar">
<ul>
<li onclick="console.log('Clickaroo... ');">Plain JS</li>
<li #click="updateData">Action Vuex</li>
</ul>
</div>
</template>
<script>
import { updateData } from "../vuex_app/actions";
export default {
vuex: {
getters: { activeDataRow: state => state.activeDataRow },
actions: { updateData }
}
}
</script>
Clicking on the first list item shows the output in the console. However, when clicking on the second one, there's nothing happening, so I'm pretty sure that the event isn't dispatched at all. I also see following error when the page's being rendered:
Property or method "updateData" is not defined on the instance but referenced during render. Make sure to declare reactive data properties in the data option.
I'm very new to Vuex so I'm only speculating. Do I need to put in reference to the updateData action in the store, alongside with state and mutations? How do I do that? What/where's the "data option" that the error message talks about? Isn't it my components state and it's properties?
Why the error
You are getting the error, because when you have <li #click="updateData"> in the template, it looks for a method updateData in the vue component which it does not find, so it throws the error. To resolve this, you need to add corresponding methods in the vue component like following:
<script>
import { updateData } from "../vuex_app/actions";
export default {
vuex: {
getters: { activeDataRow: state => state.activeDataRow },
actions: { updateData }
},
methods:{
updateData: () => this.$store.dispatch("updateData")
}
}
</script>
What this.$store.dispatch("updateData") is doing is calling your vuex actions as documented here.
What/where's the "data option"
You don't have any data properties defined, data properties for a vue component can be used, if you want to use that only in that component. If you have data which needs to be accessed across multiple components, you can use vuex state as I believe you are doing.
Following is the way to have data properties for a vue component:
<script>
import { updateData } from "../vuex_app/actions";
export default {
date: {
return {
data1 : 'data 1',
data2 : {
nesteddata: 'data 2'
}
}
}
vuex: {
getters: { activeDataRow: state => state.activeDataRow },
actions: { updateData }
},
methods:{
updateData: () => this.$store.dispatch("updateData")
}
}
</script>
You can use these data properties in the views, have computed properies based on it, or create watchers on it and many more.

How to show loading UI when calling getComponent in react-router?

I'm really new to React and I can't figure out how to render a "loading..." screen when a route is being loaded with getComponent. The getComponent call works fine and displays the component, but there's no indication on the UI that anything is happening between the request and the response. That's what I'm trying to figure out.
import Main from './pages/Main.jsx';
import Test from './pages/Test.jsx';
import Home from './pages/Home.jsx';
var Routes = {
path: "/",
component: Main,
indexRoute: {
component: Home
},
childRoutes: [
{
path: "test",
component: Test
},
{
path: "about",
getComponent: function(path, cb) {
require.ensure([], (require) => {
cb(null, require("./pages/about/About.jsx"));
});
}
}
]
};
export default Routes;
After trying to unsuccessfully force a "loading" component to display using onEnter or within the getComponent function, I thought maybe I should try using Redux to set a loading state to true/false and getting my main view component to display a loading screen:
import React from 'react';
import {connect} from 'react-redux';
import NavBar from '../components/Navigation/NavBar.jsx';
import Footer from '../components/Footer.jsx';
import Loading from './Loading.jsx';
import navItems from '../config/navItems.jsx';
import setLoading from '../actions/Loading.jsx';
var Main = React.createClass({
renderPage: function() {
if (this.props.loading) {
return (
<Loading/>
);
} else {
return this.props.children;
}
},
render: function() {
return (
<div>
<header id="main-header">
<NavBar navigation={navItems}/>
</header>
<section id="main-section">
{this.renderPage()}
</section>
<Footer id="main-footer" />
</div>
);
}
});
function mapStateToProps(state) {
return {
loading: state.loading
}
}
export default connect(mapStateToProps)(Main);
This seems to work if I manually set the loading state using an action, which is what I was looking to do. But (and I feel this is going to be a real noob question) I can't figure out how to access the store/dispatcher from within the router.
I'm not sure if I'm using the wrong search terms or whatever, but I'm completely out of ideas and every react-router/redux tutorial seems to skip over what I feel like has to be a common problem.
Can anyone point me in the right direction (and also let me know if what I'm doing is best practice?)?
EDIT: I'll try and clarify this a bit more. In the first code block, you can see that if I click a <Link to="/about"> element then the getComponent function will fire, which will lazy-load the About.jsx component. The problem I am having is I can't figure out how to show some sort of loading indicator/spinner that would appear immediately after clicking the link and then have it get replaced once the component loads.
MORE EDITING: I've tried creating a wrapper component for loading async routes and it seems to work, however it feels really hacky and I'm sure it isn't the right way to go about doing this. Routes code now looks like this:
import Main from './pages/Main.jsx';
import Test from './pages/Test.jsx';
import Home from './pages/Home.jsx';
import AsyncRoute from './pages/AsyncRoute.jsx';
var Routes = {
path: "/",
component: Main,
indexRoute: {
component: Home
},
childRoutes: [
{
path: "test",
component: Test
},
{
path: "about",
component: AsyncRoute("about")
}
]
};
export default Routes;
The AsyncRoute.jsx page looks like this:
import React from 'react';
function getRoute(route, component) {
switch(route) {
// add each route in here
case "about":
require.ensure([], (require) => {
component.Page = require("./about/About.jsx");
component.setState({loading: false});
});
break;
}
}
var AsyncRoute = function(route) {
return React.createClass({
getInitialState: function() {
return {
loading: true
}
},
componentWillMount: function() {
getRoute(route, this);
},
render: function() {
if (this.state.loading) {
return (
<div>Loading...</div>
);
} else {
return (
<this.Page/>
);
}
}
});
};
export default AsyncRoute;
If anyone has a better idea, please let me know.
I think I have this figured out. It may or may not be the correct way to go about things, but it seems to work. Also I don't know why I didn't think of this earlier.
First up, move my createStore code to its own file (store.jsx) so I can import it into the main entry point as well as into my Routes.jsx file:
import {createStore} from 'redux';
import rootReducer from '../reducers/Root.jsx';
var store = createStore(rootReducer);
export default store;
Root.jsx looks like this (it's an ugly mess, but I'm just trying to get something that works on a basic level and then I'll clean it up):
import {combineReducers} from 'redux';
import user from './User.jsx';
import test from './Test.jsx';
var loading = function(state = false, action) {
switch (action.type) {
case "load":
return true;
case "stop":
return false;
default:
return state;
}
};
export default combineReducers({
user,
test,
loading
});
I've made a basic component that shows Loading/Loaded depending on the Redux store's value of "loading":
import React from 'react';
import {connect} from 'react-redux';
var Loading = React.createClass({
render: function() {
if (this.props.loading) {
return (
<h1>Loading</h1>
);
} else {
return (
<h1>Loaded</h1>
);
}
}
});
export default connect(state => state)(Loading);
And now my Routes.jsx file looks like this (note I've imported the Redux store):
import Main from './pages/Main.jsx';
import Test from './pages/Test.jsx';
import Home from './pages/Home.jsx';
import store from './config/store.jsx';
var Routes = {
path: "/",
component: Main,
indexRoute: {
component: Home
},
childRoutes: [
{
path: "test",
component: Test
},
{
path: "about",
getComponent: function(path, cb) {
store.dispatch({type: "load"})
require.ensure([], (require) => {
store.dispatch({type: "stop"});
cb(null, require("./pages/about/About.jsx"));
});
}
}
]
};
export default Routes;
This seems to work. As soon as a <Link/> is clicked to go to the /about route, an action is dispatched to set the "loading" state to true in the main store. That causes the <Loading/> component to update itself (I envision it would eventually render a spinner in the corner of the window or something like that). That weird require.ensure([]) function is run to get webpack to do its fancy code splitting, and once the component is loaded then another action is dispatched to set the loading state to false, and the component is rendered.
I'm still really new to React and while this seems to work, I'm not sure if it's the right way to do it. If anyone has a better way, please chime in!
Following the same approach as #David M I implemented a loading reducer and a function to wrap the dispatches.
Excluding the store creation and manage, they are basically as follows:
loadingReducer:
// ------------------------------------
// Constants
// ------------------------------------
export const LOADING = 'LOADING'
// ------------------------------------
// Actions
// ------------------------------------
const loadQueue = []
export const loading = loading => {
if (loading) {
loadQueue.push(true)
} else {
loadQueue.pop()
}
return {
type: LOADING,
payload: loadQueue.length > 0
}
}
export const actions = {
loading
}
// ------------------------------------
// Action Handlers
// ------------------------------------
const ACTION_HANDLERS = {
[LOADING]: (state, action) => (action.payload)
}
// ------------------------------------
// Reducer
// ------------------------------------
const initialState = false
export default function reducer (state = initialState, action) {
const handler = ACTION_HANDLERS[action.type]
return handler ? handler(state, action) : state
}
Notice how loadingQueue keeps the loading message active while there are remaining modules to fetch, for nested routes.
withLoader function:
import { loading } from 'loadingReducer'
const withLoader = (fn, store) => {
return (nextState, cb) => {
store.dispatch(loading(true))
fn(nextState, (err, cmp) => {
store.dispatch(loading(false))
cb(err, cmp)
})
}
}
export default withLoader
Now when defining new routes we can dispatch the loading action implicitly using withLoader:
someRoute:
import withLoader from 'withLoader'
import store from 'store'
const route = {
path: 'mypath',
getComponent: withLoader((nextState, cb) => {
require.ensure([], require => {
cb(null, require('something').default)
}, 'NamedBundle')
}, store)
}
export default route
OK, let's see if I can shed some light on this here:
I can't figure out how to access the store/dispatcher from within the router
There is no need to do that AFAIK. You can specify all routes, listing the components that should answer each route (like you did above), and then connect each of the components to the redux store. For connecting, your mapStateToProps function can be written in a much simpler fashion, like this:
export default connect(state => state)(Main);
Regarding the loading state: I think it is a step in the wrong direction to have a slow-loading component and to display a waiting indicator while it is loading. I would rather have a fast-loading component that loads all of its data asynchronously from the backend, and while the data is not yet available, the component renders a waiting indicator. Once the data is available, it can be displayed. That is basically what you sketched in your second edit.
It would be even better if you could drive this off of your actual data, i.e. no data present -> show the loading screen / data present -> show the real screen. This way, you avoid issues in case your loading flag gets out of sync. (More technically speaking: Avoid redundancy.)
So, instead of making the wrapper generic, I would rather create a standalone component for the loading screen and display that whenever each individual component feels the need for it. (These needs are different, so it seems to be difficult to handle this in a generic way.) Something like this:
var Page = function(route) {
return React.createClass({
getInitialState: function() {
// kick off async loading here
},
render: function() {
if (!this.props.myRequiredData) {
return (
<Loading />
);
} else {
return (
// display this.props.myRequiredData
);
}
}
});
};
dynamic load async routers are using require.ensure, which use jsonp to download scripts from network.
because of slow networking, sometime, UI blocks, the screen is still showing the previews react component.
#Nicole , the really slow is not the data loading inside component, but is the component self, because of jsonp

Categories

Resources