I am currently developing in a react project and I want to show the current page title in my header. To currently do this, I am using the props.location.pathname and a switch-case to get the desired output.
This looks something like below.
var currentPathname = this.props.location.pathname;
switch (currentPathname) {
case '/':
return 'Home';
}
The amount of paths are adding up and I do not want to make one big switch-case. So I was wondering, if there is any better way to set a (custom) title depending on a path. I expected something to be available in the react-router but I have not found such a thing yet.
Any insights are welcome.
Use a map instead of using switch statement.
const {pathname} = this.props.location;
const pathToTitleMap = {
"/": "Home",
"/something": "Amazing page title"
}
document.title = pathToTitleMap[pathname]
I'd say that you don't really have a different option. You'll need some place that maps the path to the title (If your titles cant be retrieved from the path name itself). If you want to make it look a bit shorter, you could use the ternary operator like this:
const path = this.props.location.pathname;
const title = path == '/' ? 'Home' : path == '/contact' ? 'customContactTitle' ...
You could make a hook out of it, like 'getTitle' and then just import it in your pages.
I think your solution looks clean enough. As long as you separate the logic for getting the path and its title into a single file (hook) it should be maintainable since you normally don't add static paths too often.
If you're using Next.js, you could store the paths and their corresponding titles in a database or CMS and then fetch them once during build time (with Next's getStaticProps) if that would help with maintainability.
Have you thought about making the paths equal to or related the title so you would only need to display a custom title once on the index '/' page and otherwise retrieve it from the path?
You can have a map of paths and title
const pathToTitleDictionary = {
"/": "Home",
"/about": "About",
...
}
function PageTitle({path}) {
useEffect(() => {
document.title = pathToTitleDictionary[path];
},[path]);
}
Use <PageTitle path={this.props.location.pathname} /> to update the title
You can do it by maintaining a JSON like
var titleForRoutes=[
{
route:"/", //route
title:"Home", //title to show on header
component:HomePage //React Component
},
{
route:"/Login",
title:"Login",
component:LoginPage
}
]
then in routes declare routed by iterating on that Json and on the other hand you can show the title using the same JSON.
Thank you.
I use a custom element to ensure my paths and titles are tidy and next to each other. While a little longer than some of the other answers, it keeps everything inside properties. Could likely be improved by using function component for Page.
<CustomRoute
path="/path/is/here"
component={ComponentHere}
title="Title goes here"
/>
/* * * * * * * * * * * * * * */
export const CustomRoute = ({ component = null, title = null, ...props }) => {
return (
<Route
{...props}
render={
props2 => <Page {...props2} component={component} title={title} />
}
key={props.path}
/>
);
};
class Page extends Component {
componentDidMount() {
document.title =
(this.props.title ? this.props.title + ' - ' : '') + 'Website Name';
}
render() {
const { component, ...props } = this.props;
const PageComponent = component;
return <PageComponent {...props} />;
}
}
Related
I have a React site with aside and main content. I want to use search-ui for searching on the site.
The search bar should be on the aside, and when the user searches for something, results should be displayed on the main content. Aside and main content are two separated react components.
In my aside, I'm configuring search-ui SearchBox like this
<SearchBox
autocompleteResults={{
titleField: "title",
urlField: "url"
}}
autocompleteSuggestions={true}
onSubmit={searchTerm => {
navigate("/elastic-search?q=" + searchTerm);
}}
onSelectAutocomplete={(selection, {}, defaultOnSelectAutocomplete) => {
if (selection.suggestion) {
navigate("/elastic-search?q=" + selection.suggestion);
} else {
defaultOnSelectAutocomplete(selection);
}
}}
/>
So when the user searches something the app will redirect to a separate page named elastic-search and I'm passing the searchTerm in the URL through navigate method.
On MainContent I have results like this:
<Results titleField='title' urlField='url'/>
Now the question is how can I fetch searchTerm and display the results on main content. The structure of the app is like this:
<App>
<SearchProvider config={config}>
<Aside /> ---- Here I have <SearchBox>
<MainContent /> ---- Here I have <Results>
</SearchProvider>
</App>
When I search the app redirects to /elastic-search with searchTerm in URL, but the results are not displaying. If I refresh the page they are displayed. How can I notify Results or re-render the page, so I can show the searched results.
Your Results seems to be missing some parameters and should look something like this:
<>
<Results
titleField="title"
urlField=""
view={SearchView}
resultView={SearchResultView}
/>
</>
And your SearchView (Used to override the default view for this Component.) and SearchResultView (Used to override individual Result views.) components, should look something like this:
const SearchView = ({ children }) => {
return <div>{children}</div>
};
const SearchResultView = ({ result: searchResult }) => {
return <div>{searchResult.content}</div>
}
Additional suggestion
This is a working example in the Next.js app with import { useRouter } from "next/router"; that needs to be replaced with your routing solution. In the SearchBox component:
export const SearchBoxComponent = () => {
const router = useRouter();
return (
<>
<SearchBox
searchAsYouType={true}
autocompleteResults={{
titleField: "title",
urlField: "",
shouldTrackClickThrough: true,
clickThroughTags: ["test"],
}}
autocompleteSuggestions={true}
onSubmit={(searchTerm) => {
const urlEncodedQuery = encodeURI(searchTerm).replace(/%20/g, "+");
router.push(`/search?q=${urlEncodedQuery}`);
}}
...
</>
)
}
I want to have my background video and navigation bar spread across all pages I make but I also want them to be behind all of the other components rendered. I understand that I need to wrap the root element inside of gatsby-browser.js but the problem I have been faced with is that the background covers all of the elements. Is there any way to fix this?
Current gatsby-browser.js - Currently covers all components with background.
import React from 'react';
import BackGround from "../../src/components/bg"
export const wrapPageElement = ({ element }) => {
return <BackGround>{element}</BackGround>;
};
"use strict";
var _interopRequireDefault = require("#babel/runtime/helpers/interopRequireDefault");
var _interopRequireWildcard2 = _interopRequireDefault(require("#babel/runtime/helpers/interopRequireWildcard"));
/* global __PATH_PREFIX__ */
// Taken from https://github.com/netlify/netlify-identity-widget
var routes = /(confirmation|invite|recovery|email_change)_token=([^&]+)/;
var errorRoute = /error=access_denied&error_description=403/;
var accessTokenRoute = /access_token=/;
export const onInitialClientRender = function (_, _ref) {
var _ref$enableIdentityWi = _ref.enableIdentityWidget,
enableIdentityWidget = _ref$enableIdentityWi === void 0 ? true : _ref$enableIdentityWi,
_ref$publicPath = _ref.publicPath,
publicPath = _ref$publicPath === void 0 ? "admin" : _ref$publicPath;
var hash = (document.location.hash || "").replace(/^#\/?/, "");
if (enableIdentityWidget && (routes.test(hash) || errorRoute.test(hash) || accessTokenRoute.test(hash))) {
Promise.resolve().then(function () {
return (0, _interopRequireWildcard2.default)(require("netlify-identity-widget"));
}).then(function (_ref2) {
var netlifyIdentityWidget = _ref2.default;
netlifyIdentityWidget.on("init", function (user) {
if (!user) {
netlifyIdentityWidget.on("login", function () {
document.location.href = __PATH_PREFIX__ + "/" + publicPath + "/";
});
}
});
netlifyIdentityWidget.init();
});
}
};
If any additional information is needed please let me know.
I don't think that wrapPageElement nor wrapRootElement APIs fits your requirements since they will wrap your entire page in a provided component, and that's what you are trying to avoid. Of course, they will prevent your component to be unmounted, however, this will be automatically handled by #reach/routing with the following approach.
What you are trying to achieve it's a simple shared component across all pages that extends from your <Layout>:
const Layout = ({ children }) => {
return <section>
<BackGround>
<YourNavigationComponent />
</BackGround>
<main>{children}</main>
</section>;
};
Something like this will wrap your <YourNavigationComponent /> with your background component across all site, avoiding the wrapping of the whole elements.
I am trying to create a menu component that reads the contents of the pages folder at build time. However I haven't had any success. Here is what I have tried:
import path from "path";
import * as ChangeCase from "change-case";
export default class Nav extends React.Component {
render() {
return (
<nav>
{this.props.pages.map((page) => (
<a href={page.link}>{page.name}</a>
))}
</nav>
);
}
async getStaticProps() {
let files = fs.readdirSync("../pages");
files = files.filter((file) => {
if (file == "_app.js") return false;
const stat = fs.lstatSync(file);
return stat.isFile();
});
const pages = files.map((file) => {
if (file == "index.js") {
const name = "home";
const link = "/";
} else {
const link = path.parse(file).name;
const name = ChangeCase.camelCase(link);
}
console.log(link, name);
return {
name: name,
link: link,
};
});
return {
props: {
pages: pages,
},
};
}
}
This does not work, the component does not receive the pages prop. I have tried switching to a functional component, returning a promise from getStaticProps(), switching to getServerSideProps(), and including the directory reading code into the render method.
The first two don't work because getStaticProps() and getServerSideProps() never get called unless the component is a page, and including the code in the render method fails because fs is not defined or importable since the code might run on the front end which wouldn't have fs access.
I've also tried adding the code to a getStaticProps() function inside _app.js, with the hopes of pushing the pages to the component via context, but it seems getStaticProps() doesn't get called there either.
I could run the code in the getStaticProps function of the pages that include the menu, but I would have to repeat that for every page. Even if I extract the logic into a module that gets called from the getStaticProps, so something like:
// ...
export async function getStaticProps() {
return {
props: {
pages: MenuMaker.getPages(),
// ...
}
}
}
and then pass the pages to the navigation component inside the page via the Layout component:
export default function Page(props) {
return (
<Layout pages={props.pages}></Layout>
)
}
then that's still a lot of boilerplate to add to each page on the site.
Surely there is a better way... It can't be that there is no way to add static data to the global state at build time, can it? How do I generate a dynamic menu at build time?
I managed to get this working by exporting a function from next.config.js and setting an environment variable that contains the menu structure. I abstracted the menu loading code into it's own file. After seeing the result, I understand better why I was not able to find an example of anyone doing something similar:
The menu is not ordered the way I would like. I could sort it alphabetically, or by the modification date but realistically it almost always needs to be manually sorted in relation to the subject of the pages. I could use an integer, either tacked on to the filename or somewhere in the file (perhaps in a comment line). But in retrospect I think that just hard coding the links in a component is probably the best way after all since it offers much more flexibility and probably isn't going to be much more work even in the very long run.
That being said I am sharing my solution as it is a way to initialize an app wide static state. It's not ideal, you will have to restart the dev server if you wish to recalculate the variables here, which is why I'm still interested in other possible solutions, but it does work. So here it is:
next.config.js
const menu = require("./libraries/menu.js");
module.exports = (phase, { defaultConfig }) => {
return {
// ...
env: {
// ...
menu: menu.get('pages'),
// ...
},
// ...
};
};
libraries/menu.js
const fs = require("fs");
const path = require("path");
const ccase = require("change-case");
module.exports = {
get: (pagePath) => {
if (pagePath.slice(-1) != "/") pagePath += "/";
let files = fs.readdirSync(pagePath);
files = files.filter((file) => {
if (file == "_app.js") return false;
const stat = fs.lstatSync(pagePath + file);
return stat.isFile();
});
return files.map((file) => {
if (file == "index.js") {
return {
name: "Home";
link: "/";
};
} else {
link = path.parse(file).name;
return {
link: link;
name: ccase.capitalCase(link);
};
}
});
},
};
Then the actual menu is generated from the environment variable in a component that can be included in the layout:
components/nav.js
import Link from "next/link";
export default class Nav extends React.Component {
render() {
return (
<nav>
{process.env.menu.map((item) => (
<Link key={item.link} href={item.link}>
<a href={item.link}>
{item.name}
</a>
</Link>
))}
</nav>
);
}
}
You can try this:
const fg = require('fast-glob');
const pages = await fg(['pages/**/*.js'], { dot: true });
I'd like part of the record to be included in the label for a BooleanField (and BooleanInput). I'm trying to use WithProps to accomplish this.
If I use
<BooleanField source="FileSystem" label="FileSystem" />
This seems to work just fine. If, instead I try to wrap it
const makeLabel = (props)=>{
let label = `Filesystem for ${props.record.id}`;
return {label};
}
const withLabel = withProps(makeLabel);
const BooleanFieldWithLabel = compose(withLabel)((props)=>{
console.log("props after compose",props);
return <BooleanField {...props}/>
});
And then use <BooleanFieldWithLabel source="FileSystem" /> It doesn't render any label. I've tried a few different ways and nothing seems to work even though I can see in the console.log that the correct label is in props. What am I doing wrong here?
I have the same question, I cannot display the label base on field's value on "Show" page.
From react-admin source code, it seems only I set "addLabel" prop on the direct child of "SimpleShowLayout" or "TabbedShowLayout", then I can see label on my custom field.
But it is not configurable, I want to show/hide label base on field's value. Do I need to implement my own custom "SimpleShowLayout" or "TabbedShowLayout"? Or is there any better approaches?
Update my post.
I just figure out the solution by implementing an HOC like below. I am wondering is there any better approaches to implement the same feature?
import React from "react";
import get from "lodash/get";
import { TextField, DateField, Labeled } from "react-admin";
const NullableField = WrappedComponent => props => {
const { record, source } = props;
const value = get(record, source);
return value ? (
<Labeled {...props}>
<WrappedComponent {...props} />
</Labeled>
) : null;
};
const NullableTextField = NullableField(TextField);
const NullableDateField = NullableField(DateField);
export { NullableTextField, NullableDateField };
I am trying to conditionally render tag name based on prop value.
Example
const SimpleTagName = `Ball${size === 'large' ? 'Large' : 'Small'}`;
return (<SimpleTagName />
but the problem is that I get rendered 'balllarge' tag with all lower case letters. What I am doing wrong ?
Try with this method:
import React from 'react';
import { PhotoStory, VideoStory } from './stories';
const components = {
photo: PhotoStory,
video: VideoStory
};
function Story(props) {
// Correct! JSX type can be a capitalized variable.
const SpecificStory = components[props.storyType];
return <SpecificStory story={props.story} />;
}
Official doc ref to handle this pattern: https://reactjs.org/docs/jsx-in-depth.html#choosing-the-type-at-runtime
JSX gets converted to a React.createElement() call, so what you're doing effectively turns into:
React.createElement('balllarge')
Which is not what you want. You need to pass it an component instead of a string, but you can still determine that dynamically, like so:
import { BallLarge, BallSmall } from './Balls' // or whatever
const Component = ({ size }) => {
const BallComponent = size === 'large' ? BallLarge : BallSmall
return <BallComponent />
}
(If you have more than two options, you may need a different way to handle the mapping between your props and variable types, but the principle remains the same: assign a component to a variable, and then use when rendering.)