I am creating list of items looped through .map function. I want each of these items be rendered in a single page with some other details.
import React from 'react'
import {faArrowRight, faMusic, faPlay, faPlayCircle, faTachometerAlt} from "#fortawesome/free-solid-svg-icons";
import {FontAwesomeIcon} from "#fortawesome/react-fontawesome";
import music from '../mocks/music.json'
import { Link } from 'gatsby'
import Music from '../pages/music'
const newData = music.map( (data) => {
return (
<div className="row no-gutters justify-content-between align-items-center">
<div className="col-auto">
<button className="btn-gradient btn-circle">
<FontAwesomeIcon icon={faPlayCircle} />
</button>
</div>
<div className="col">
<div className="music-list-content">
<span className="artist">{data.author}</span>
<Link to={`/music/${data.id}`}>{data.title}</Link>
<span className="play">
<FontAwesomeIcon icon={faPlay} /> {data.duration}
</span>
</div>
</div>
<div className="col-auto">
<span className="badge-dark badge">{data.genre}</span>
</div>
</div>
)
})
const membersToRender = music.filter(member => member.id)
const numRows = membersToRender.length
const Musics = () => {
return (
<div>
<div className="title">
<h5>New Music</h5>
<span>{numRows} new songs</span>
</div>
<div>
<div className="music-list card-wrapper">
{newData}
</div>
</div>
<div className="footer-wrapper">
<div>
<FontAwesomeIcon icon={faMusic} />
<span>Song Library</span>
</div>
<FontAwesomeIcon icon={faArrowRight} />
</div>
</div>
)
}
export default Musics
I created a link which whenever I click, it takes me to another page (page not found) with id appended and .js extension.
Please, how do go about it? I want a click on the title and have it displayed on a full page.
Your logic seems good, however, you are missing the most important part, the page creation, since you are not creating the pages, all of your links are broken.
In Gatsby, you have two different ways of creating pages:
Using gatsby-node.js to create pages dynamically: when dealing with a huge amount of data, like your JSON, it's easier to let Gatsby deal with this responsibility of creating pages for Gatsby. Since you are sourcing from a JSON, you need everything set to create dynamic pages.
const path = require("path")
// Implement the Gatsby API “createPages”. This is called once the
// data layer is bootstrapped to let plugins create pages from data.
exports.createPages = async ({ graphql, actions, reporter }) => {
const { createPage } = actions
const musics= require("./data/mocks/musics.json")
const musicTemplate = path.resolve(`src/templates/music-template.js`)
musics.forEach(music) => {
createPage({
path: `/music/${music.slug}`
component: musicTemplate,
context: {
title: music.title,
description: music.description,
// and so on for the rest of the fields
},
})
})
}
Note: I'm assuming that your JSON is properly defined and formatted, having all the fields I queried.
Your musicTemplate must be a template (inside /templates folder).
Notice that you are passing some fields through Gatsby's context, this means that those fields will be available through props.pageContext in your template. So, there, create a template like:
import React from "react"
import Layout from "../components/layout"
export default function MusicTemplate({pageContext}) {
return (
<Layout>
<div>Hello musician {pageContext.title}</div>
</Layout>
)
}
So, as I said, with this approach you are creating dynamic pages based on your JSON file, and they will be available inside localhost:8000/music/{music.slug}, and all your reference and links that point there, will be valid.
I would also recommend using static query/useStaticQuery to retrieve data from your JSON in that loop. If you create a static query from that data (in a separate component) you will be able to fetch it on-demand across your project, so you will be reusing an interesting part of logic. It's better to use it rather than requesting a JSON directly.
You can follow this guide from the great Jason Lengstorf which is mostly what you need.
Adding .js files in your /pages folder: Gatsby infers the internal structure of your /pages folder and will create pages accordingly to that structure. For instance, if you have a structure like: /pages/musicians/name1.js Gatsby will create a page like localhost:8000/musicians/name1.
As it has been said, the first approach fits your requirements and it's preferred for this use-cases, since the second one will be less scalable and maintainable.
You should do some routing with React-Router (https://reactrouter.com/web/example/basic).
So the link have to point to a Route in a Switch, as is in the example of the link.
Related
I have a menu where every menu item is a button and I want to load a specific reactjs component into a specific div without reloading the whole page.
This is the current code, clearly is bad but I don't know where to start fixing it...
...
<Button onClick={this.loadTarget}>
{menuItem.name}
</Button>
...
loadTarget(event) {
document.getElementById("datapanel").innerHTML="abc<TranslationsList />";
}
When I click a menu Item I want to load my div with the value "abc<TranslationsList />". "abc" is displayed but the custom component "TranslationsList" is not and I guess this is normal as the TranslationsList tag is not a HTML tag. But how could I load my component?
I could use links instead of buttons but in this case the question is how could I update the div content with a specific link?
It's hard if you've programmed plain JS before, but you have to forget the "good old JS pattern" in React. I also had a hard time getting used to not using standard JS elements (target, innerHTML, etc.) to solve such a problem.
So the solution in React is to use the framework and your page reload problem will be solved immediately. useState for the state of the component and handlers for the click. My main code looks like this. You can find a working application at Codesandbox.
export default function App() {
const [showComponent, setShowComponent] = useState(false);
const handleButtonClick = (e) => {
setShowComponent(!showComponent);
};
return (
<div className="App">
<h1>
Load specific DIV with a react component without reloading the whole
page
</h1>
<a href="https://stackoverflow.com/questions/74654088/load-specific-div-with-a-react-component-without-reloading-the-whole-page">
Link to Stackoverflow
</a>
<div style={{ marginTop: "20px" }}>
<button onClick={handleButtonClick}>Magic</button>
</div>
{showComponent ? (
<div style={{ marginTop: "20px" }}>
This is the place of your component!
</div>
) : (
""
)}
</div>
);
}
In the first place I wpuld not use vanilla JS syntax on a react app if it is not necessary. i.e: document.getElementById("datapanel").innerHTML="abc<TranslationsList />".
If you are using React you should be managing the State in the component of the DIV, giving the order to make an element appear once the button is clicked.
A simple example can be this:
CodeSandbox
import { useState } from "react";
export default function App() {
const [divState, setDivState] = useState(null);
const divElement = () => <div>I am the element that should appear</div>;
const handleDiv = () => {
setDivState(divElement);
};
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<button onClick={handleDiv}>Show DIV</button>
<div>{divState}</div>
</div>
);
}
I agree with the answers given above. Since you are already using React, you should take advantage of its features/functionalities. No need to reinvent the wheel.
However, if you are still interested in how to make your current implementation work. You may use renderToString(), which can be imported from ReactDOMServer. Please refer to the following code snippet as an example.
import { renderToString } from 'react-dom/server'
const TranslationsList = () => {
return <div>TranslationsList Content</div>
}
export default function App() {
const loadTarget = () => {
document.getElementById("datapanel").innerHTML=`abc${renderToString(<TranslationsList />)}`;
}
return (
<div>
<button onClick={loadTarget}>Insert Component</button>
<div id="datapanel">Data Panel Holder</div>
</div>
);
}
I have a component in React which displays (or doesn't at the moment) an image within an src tag.
The file path & file name of the image is passed via props after being selected by the user, so I can't do an import at the top of the file. Apparently, I should be able to do something like src={require(file)} but this causes webpack to throw a hissy fit and give the following error: Error: cannot find module "."
As an e.g., a typical filepath/filename I pass to the component is: '../../../images/black.jpg'
This is a stripped-down version of the code in question:
import React, { Component } from "react";
class DisplayMedia extends Component {
render() {
return (
<div className="imgPreview">
<img src={this.props.file} alt="piccy" />
</div>
);
}
}
export default DisplayMedia;
Depending on your set up...
If the images are dynamic (during production, images will be added, edited, or deleted):
I'd recommend a microservice that only handles images. I go in depth on how to approach such a service: Image returned from REST API always displays broken
If the images are static (during production, the images are bundled within the "bundle.js" file):
- I'd recommend importing all of the images within the component, creating an array of the imported images, and then utilizing the array index and React state to cycle through them. For example:
import React, { Component } from "react";
import Image1 from "../images/Image1.png";
import Image2 from "../images/Image2.png";
import Image3 from "../images/Image3.png";
const images = [Image1, Image2, Image3];
export default class ShowImage extends Component {
state = { index: 0 };
handleChange = ({ target: { value } }) => {
this.setState({ index: value });
};
render = () => (
<div className="container">
<h1>Utilizing Array Indexes</h1>
<select
style={{ marginBottom: 20 }}
value={this.state.index}
onChange={this.handleChange}
>
{images.map((val,idx) => (
<option key={idx} value={idx}>Image {idx + 1}</option>
))}
</select>
<img src={images[this.state.index]} />
</div>
);
}
While I can't create an exact codesandbox of the above, this working example should give you the basic idea: https://codesandbox.io/s/ovoo077685
You don't need to add require in src. When relative path is used it will go the images availale in your server but when url is given image will be loaded. You can find more info here
When using src as /images/black.jpg it will convert to localhost:3000/images/black.jpg
Right now, I'm trying to make a website that shows recent news posts which is supplied my NodeJS API.
I've tried the following:
HTML
<div id="news" class="media" v-for="item in posts">
<div>
<h4 class="media-heading">{{item.title}}</h4>
<p>{{item.msg}}</p>
</div>
</div>
JavaScript
const news = new Vue({
el: '#news',
data: {
posts: [
{title: 'My First News post', msg: 'This is your fist news!'},
{title: 'Cakes are great food', msg: 'Yummy Yummy Yummy'},
{title: 'How to learnVueJS', msg: 'Start Learning!'},
]
}
})
Apparently, the above didn't work because Vue can't render multiple root elements.
I've looked up the VueJS's official manual and couldn't come up with a solution.
After googling a while, I've understood that it was impossible to render multiple root element, however, I yet to have been able to come up with a solution.
The simplest way I've found of adding multiple root elements is to add a single <div> wrapper element and make it disappear with some CSS magic for the purposes of rendering.
For this we can use the "display: contents" CSS property. The effect is that it makes the container disappear, making the child elements children of the element the next level up in the DOM.
Therefore, in your Vue component template you can have something like this:
<template>
<div style="display: contents"> <!-- my wrapper div is rendered invisible -->
<tr>...</tr>
<tr>...</tr>
<tr>...</tr>
</div>
</template>
I can now use my component without the browser messing up formatting because the wrapping <div> root element will be ignored by the browser for display purposes:
<table>
<my-component></my-component> <!-- the wrapping div will be ignored -->
</table>
Note however, that although this should work in most browsers, you may want to check here to make sure it can handle your target browser.
You can have multiple root elements (or components) using render functions
A simple example is having a component which renders multiple <li> elements:
<template>
<li>Item</li>
<li>Item2</li>
... etc
</template>
However the above will throw an error. To solve this error the above template can be converted to:
export default {
functional: true,
render(createElement) {
return [
createElement('li', 'Item'),
createElement('li', 'Item2'),
]
}
}
But again as you probably noticed this can get very tedious if for example you want to display 50 li items. So, eventually, to dynamically display elements you can do:
export default {
functional: true,
props: ['listItems'], //this is an array of `<li>` names (e.g. ['Item', 'Item2'])
render(createElement, { props }) {
return props.listItems.map(name => {
return createElement('li', name)
})
}
}
INFO in those examples i have used the property functional: true but it is not required of course to use "render functions". Please consider learning more about functional componentshere
Define a custom directive:
Vue.directive('fragments', {
inserted: function(el) {
const children = Array.from(el.children)
const parent = el.parentElement
children.forEach((item) => { parent.appendChild(item) })
parent.removeChild(el)
}
});
then you can use it in root element of a component
<div v-fragments>
<tr v-for="post in posts">...</tr>
</div>
The root element will not be rendered in DOM, which is especially effective when rendering table.
Vue requires that there be a single root node. However, try changing your html to this:
<div id="news" >
<div class="media" v-for="item in posts">
<h4 class="media-heading">{{item.title}}</h4>
<p>{{item.msg}}</p>
</div>
</div>
This change allows for a single root node id="news" and yet still allows for rendering the lists of recent posts.
In Vue 3, this is supported as you were trying:
In 3.x, components now can have multiple root nodes! However, this does require developers to explicitly define where attributes should be distributed.
<!-- Layout.vue -->
<template>
<header>...</header>
<main v-bind="$attrs">...</main>
<footer>...</footer>
</template>
Multiple root elements are not supported by Vue (which caused by your v-for directive, beacause it may render more than 1 elements). And is also very simple to solve, just wrap your HTML into another Element will do.
For example:
<div id="app">
<!-- your HTML code -->
</div>
and the js:
var app = new Vue({
el: '#app', // it must be a single root!
// ...
})
I want to render the pure HTML coming from some external source into react component. I saw few solutions where people are talking about some conversion tools (HTML to JSX) but I want to handle everything in my component so while mounting it will get the HTML response and that needs to render.
You can use dangerouslySetInnerHTML for this:
function createMarkup() { return {__html: 'First · Second'}; };
<div dangerouslySetInnerHTML={createMarkup()} />
But as the method name suggests: you should be very sure of what you are doing there and the security implications it has.
This shouldn't be difficult to do . Assign your HTML to a div and then render it using {variable name} JSX allows you to do this and with ES6 integration you can also use class instead of className.
var Hello = React.createClass({
render: function() {
var htmlDiv = <div>How are you</div>
return <div>Hello {this.props.name}
{htmlDiv}
</div>;
}
});
ReactDOM.render(
<Hello name="World" />,
document.getElementById('container')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.8/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.8/react-dom.min.js"></script>
<div id="container">
<!-- This element's contents will be replaced with your component. -->
</div>
I want to implement a search box that changes what it searches based on whichever controller is being used. If you are on the "posts" view it will search the posts api, if you are on the videos view, it searches the videos api. It seems the search box would need its own controller maybe. I'm pretty sure I need to inject a search service into all the model controllers but I'm not exactly sure how to change the url it searches or tie the input to the different controller scopes.
So any ideas how to have a global search box that changes where it searches based on whichever controller is making use of it and tying its state back into a changing view?
To make a resource call dynamic api i would first create two $resources that map to your two endpoints, posts and videos. Then put an ng-change event on your global search that calls a function in your base controller.
This function firsts need to figure out what api to search. Then make the appropriate api call. The important part is in the callback and i think this is what you are looking for.
In the callback you could $broadcast the resp data from your api query. Each of your controllers will be listening for an event with an $on function. The listeners will then populate the correct scope variable with the callback data.
Pseudo below.
Same html layout with ng-change
<html>
<body ng-controller="AppController">
<form>
<label>Search</label>
<input ng-model="global.search" ng-change="apiSearch()" type="text" class="form-control" />
</form>
<div ui-view="posts">
<div ng-controller="PostController">
<p ng-repeat="post in posts | filter: global.search">{{ post.name }}</p>
</div>
</div>
<div ui-view="videos">
<div ng-controller="VideoController">
<p ng-repeat="video in videos | filter: global.search">{{ video.name }}</p>
</div>
</div>
</body>
</html>
AppController
.controller('AppController', function ($scope, PostService, VideoService) {
$scope.apiSearch = function() {
// Determine what service to use. Could look at the current url. Could set value on scope
// every time a controller is hit to know what your current controller is. If you need
// help with this part let me know.
var service = VideoService, eventName = 'video';
if ($rootScope.currentController == 'PostController') {
service = PostService;
eventName = 'post';
}
// Make call to service, service is either PostService or VideoService, based on your logic above.
// This is pseudo, i dont know what your call needs to look like.
service.query({query: $scope.global.search}, function(resp) {
// this is the callback you need to $broadcast the data to the child controllers
$scope.$broadcast(eventName, resp);
});
}
})
Each of your child controllers that display the results.
.controller('PostController', function($scope) {
// anytime an event is broadcasted with "post" as the key, $scope.posts will be populated with the
// callback response from your search api.
$scope.$on('post', function(event, data) {
$scope.posts = data;
});
})
.controller('VideoController', function($scope) {
$scope.$on('video', function(event, data) {
$scope.videos = data;
});
})
Client side filtering.
If you are not looking for anything to crazy that can be achieved in a super simple way for global search. I didnt even know if this would work so i just did a quick test and it does. Obviously this could be solved in a much more detailed and controlled way using services and injecting them where they are needed. But since i don't know excatly what you are looking for i will provide this solution, if you like it, great accept it. If you don't i could probably help you with service injection solution
Quick solution is to have an app wide contoller with $rootScope ng-model. Lets call it global.search.
$rootScope.global = {
search: ''
};
For the app wide search input.
<form>
<label>Search</label>
<input ng-model="global.search" type="text" class="form-control" />
</form>
In separate partials you just need to filter data based on the global.search ng-model. Two examples
<p ng-repeat="post in posts | filter: global.search">{{ post.name }}</p>
Second template with different scope
<p ng-repeat="video in videos | filter: global.search">{{ video.name }}</p>
Note how they both implement | filter: global.search. Whenever global.search changes, any filters in the current view will be changed. So posts will be filtered on the posts view, and videos on the videos view. While still using the same global.search ng-model.
I tested this, it does work. If you need more detail explaining the setup and child controller hierarchy let me know. Here is a quick look at a full template
<html>
<body ng-controller="AppController">
<form>
<label>Search</label>
<input ng-model="global.search" type="text" class="form-control" />
</form>
<div ui-view="posts">
<div ng-controller="PostController">
<p ng-repeat="post in posts | filter: global.search">{{ post.name }}</p>
</div>
</div>
<div ui-view="videos">
<div ng-controller="VideoController">
<p ng-repeat="video in videos | filter: global.search">{{ video.name }}</p>
</div>
</div>
</body>
</html>