How to return a component/component-tag dynamically in vue/nuxt? - javascript

I am trying to convert a next.js app(https://medium.com/strapi/how-to-create-pages-on-the-fly-with-dynamic-zone-8eebe64a2e1) to a nuxt.js app. In this app I can fetch data from an API and the next.js app uses the APIs data to generate new pages with its corresponding content. Works well in Next.js.
The data/content from the API consists of Seo data for the page, some static values and very important of blocks. These blocks have an attribute called __component where the components name is saved and also have the components data like images, text, etc. So I only have to deal with next.js when adding new components.
In next.js I used the catch-all-route ./pages/[[...slug]].js to catch any slug an user may enter. Then the API is fired with the slug from the context.query and I get back the pages data if it exists. Now the APIs json data only needs to be passed to the blockmanager component.
const Universals = ({blocks}) => {
return <div><BlockManager blocks={blocks}></BlockManager></div>;
};
Here the blockmanager gets the json list of blocks, from which to parse the components.
import Hero from '../../blocks/Hero';
import Pricing from '../../blocks/Pricing';
const getBlockComponent = ({__component, ...rest}, index) => {
let Block;
switch (__component) {
case 'blocks.hero':
Block = Hero;
break;
case "blocks.prices":
Block = Pricing;
break;
}
return Block ? <Block key={`index-${index}`} {...rest}/> : null;
};
const BlockManager = ({ blocks }) => {
return <div> {blocks.map(getBlockComponent)} </div>;
};
BlockManager.defaultProps = {
blocks: [],
};
export default BlockManager;
How can I replicate this line now in nuxt js?
return Block ? <Block key={`index-${index}`} {...rest}/> : null;
How to return a component/component-tag dynamically in vue/nuxt ?
Is there maybe another solution to automatically insert the wanted component?
Maybe someones knows ho to convert the blockmanagers logic to vue/nuxt logic entirely.

I think you're looking for the is attribute. You can read about it here.
Your template would look like:
<component
:is="__component"
key={`index-${index}`}
/>

Ok I think I got it. No strange stuff actually. I thought about it too complicated. Wanted all dynamically created but no need as I saw later ...
<template v-if="blocks">
<div id="example-1">
<div v-for="({__component, ...rest}=block, i) in blocks" :key="i">
<Hero :data="rest" v-if="__component === 'blocks.hero'"/>
<Pricing :data="rest" v-if="__component === 'blocks.pricing'"/>
</div>
</div>
</template>
<script>
import Hero from '../../blocks/Hero/Hero.vue';
import Pricing from '../../blocks/Pricing/Pricing.vue';
export default {
components: {
Hero, Pricing
},
props: {
blocks: Array
}
}
</script>

Related

Vue3 v-for Maximum recursive updates exceeded in component <Carousel>

New to Vue and JS. I have a vue page myLists which takes an array of lists (containing media IDs for a title) which I used to make axios API calls and build a carousel (using vue3-carousel package) in the child with the return data sent as a prop. I'm currently dealing with a "Maximum recursive updates exceeded in v-for component " warning that I believe has to do with how I make my API calls. Here is the relevant code below:
Parent "myLists", has multiple lists (each list has movies) and fetches data from api using axios:
<template>
<div v-if="isReady">
<List v-for="list of theList" :key="list.id" :aList="list"></List>
</div>
</template>
export default {
setup() {
const theList = ref([]);
for (var myList of myLists) {
const aList = ref([]);
for (var ids of myList) {
var apiLink = partLink + ids;
axios.get(apiLink).then((response) => {
aList.value.push({
title: response.data.original_title || response.data.name,
overview: response.data.overview,
url: "https://image.tmdb.org/t/p/w500" + response.data.poster_path,
year: response.data.first_air_date || response.data.release_date,
type: ids[1],
id: response.data.id,
});
});
}
theList.value.push(aList.value);
}
return { theList };
},
computed: {
isReady() {
//make sure all lists are loaded from the api before rendering
return this.theList.length == myLists.length;
},
},
Child component "List" (not including script tag as I don't think it's too relevant) takes the fetched data as a prop and builds a carousel with it:
<template>
<Carousel :itemsToShow="4.5" :wrapAround="true" :breakpoints="breakpoints">
<Slide v-for="slide of aList" :key="slide">
<img
#click="showDetails"
class="carousel__item"
:src="slide.url"
alt="link not working"
:id="slide"
/>
</Slide>
<template #addons>
<Navigation />
<Pagination />
</template>
</Carousel>
</template>
Don't know what's causing the error exactly. I have a feeling it could be how I do all my API calls, or maybe it's something else obvious. Anyone have a clue?
The fix was actually absurdly simple, I was on the right track. The component was trying to render before my data was ready, so simply adding a
"v-if="list.length!==0"
directly on my Carousel component (within my List component), as opposed to the parent, fixed my issue.
I assumed that props are automatically ready when passed to a child if I used a v-if in my parent, turns out I was wrong, and that there's a delay.
Found the solution on the github issues page:
https://github.com/ismail9k/vue3-carousel/issues/154

Vue JS: API call request on app.vue in single page application

in my project i'm suffering from delay in API requests and that's because i have huge amount of data in API, so i added a cache but it still appears white page when page creates, so i was thinking of adding API call on app.vue so request will be faster... is there a way to do it?
now i'm making API request on each page in my project
code below:
//building.vue where buildings api used
<template>
<b-row class="icon-examples">
<b-col lg="3" md="6" v-for="(building, index) in buildings"
:key="index" >
<button type="button" class="btn-icon-clipboard" #click="GoToBuilding(building._id)"
>
<div>
<i class="ni ni-building"></i>
<router-link
class="question-routerToDetail"
:to="`/tables/${building._id}`"
> <span > B info - </span>
<span>{{building.building_number}}</span></router-link>
</div>
</button>
</b-col>
</b-row>
</template>
<script>
import BuildingsService from "../../../services/ApiService"
export default {
data() {
return {
};
},
components: {
props:
['buildings'],
BaseHeader,
// buildings:[]
},
}
}
</script>
app.vue:
<template>
<router-view :number="count" #send="getNewCount" #reset="onReset" :buildings="buildings">
<sidebar :number="count" #reset="onReset"/>
</router-view>
</template>
<script>
export default {
components: {
sidebar,
},
data() {
return {
buildings: []
};
},
created(){
BuildingsService.getBuildings().then((response) => {
this.buildings = response.data.response;
console.log(this.buildings , "skk")
});
}
}
</script>
can i add saved API request array in app.vue and use it on other pages? and is it gonna improve my API call?
thanks in advance
Edit: updated question depending on answer below.. but getting empty data with no console errors
Yes, you can call the API a single time in the component that contains router-view and pass down the results to rendered components through props. The rendered components must declare the props that will be passed down to them, but only if they will be using it.
<template>
<router-view :number="count"
#send="getNewCount" #reset="onReset"
:buildings="buildings">
<sidebar :number="count" #reset="onReset"/>
</router-view>
</template>
<script>
import BuildingsService from "../../../services/ApiService"
export default {
components: {
sidebar,
},
data() {
return {
buildings: []
};
},
created(){
BuildingsService.getBuildings().then((response) => {
this.buildings = response.data.response;
});
}
}
</script>
But if the API call returns a huge amount of data or is really slow, then I really suggest that you check into optimizing the backend queries of your API, paginating results, and loading them progressively with lazy load on scroll or something.
Also it would be good if you can keep something in your state to check if the data is being loaded or not, so that you can show a loader or something rather than a white page until you get the results.
One possible solution is to adapt getBuildings(), such that it will call the backend only once and store the result in a property of BuildingsService, e.g. cachedBuildings.
If cachedBuildings is empty, getBuildings() calls the backend and stores the result in cachedBuildings.
If cachedBuildings is not empty, getBuildings() returns cachedBuildings.
The advantage of this solution is, that you only adapt your code in one place and the caching is transparent for every component.

GraphQL Query to Array List using React?

I am currently working on a gatsby website and am mostly using react components doing so. The way our jobs posting page currently works is that it fetches a list of jobs from an array that stores the information for each position and maps them out with proper stylization.
positions.js (section)
const Positions = () => (
<StyledPositions>
<GlobalContainer>
{PositionList.map((position, index) => (
<StyledPosition key={index}>
<StyledPositionName>
<Link to={position.link} activeClassName="active">
{position.name}
</Link>
</StyledPositionName>
</StyledPosition>
))}
</GlobalContainer>
</StyledPositions>
);
export default Positions;
PositionList is the array and it looks like this.
positionslist.js
const Positionlist = [
{
name: "Senior Software Engineer- Infrastructure",
link: "/careers/open_positions/sr_cloud_eng",
},
{
name: "System Software Engineer",
link: "/careers/open_positions/system_soft_eng",
},
{
name: "Software Engineer (Database Development)",
link: "/careers/open_positions/soft_eng_database",
},
];
export default Positionlist;
What I'm trying to do is to populate the job posting site from a GraphQL query from an external job posting management site. I am able to fetch the information fine, but I'd like to somehow turn the information into an array like positionslist.js so that positions.js can simply map the information the same way. The query looks like this
query MyQuery {
allLever {
edges {
node {
text
categories {
commitment
location
team
}
applyUrl
}
}
}
}
All GraphQL queries store their result inside props.data as you can see in Gatsby's tutorial. At this point, you only need to enter to the nested structure like:
import * as React from 'react'
import { graphql } from 'gatsby'
const Positions = ({data}) => {
let PositionList = data.edges;
return <StyledPositions>
<GlobalContainer>
{PositionList.map(({node: position}) => (
<StyledPosition key={position.name}>
<StyledPositionName>
<Link to={position.link} activeClassName="active">
{position.name}
</Link>
</StyledPositionName>
</StyledPosition>
))}
</GlobalContainer>
</StyledPositions>
}
export const query = graphql`
query MyQuery {
allLever {
edges {
node {
name: text
categories {
commitment
location
team
}
link: applyUrl
}
}
}
}
`
export default Positions
Note: it's better to avoid using index as key so you can use another field
Keep in mind that you barely need to change your previous structure (because of some destructuring + aliasing) but, what's important is that page queries, only work in pages (hence the name) so I'm assuming that Positions is a separate page. Otherwise, you will need to pass down the props from the page component.
Because of the aliasing, applyUrl will be aliased as link and text to name, so your previous structure will work exactly in the same way.

Switch the language in React without using external libraries

What ways to change language in React can you suggest without using external libraries? My way is to use the ternary operator {language === 'en'? 'title': 'titre'}. If language is en, displaytitle if not, display titre. What other way can you recommend. For example, that the translations should be placed in a separate json file.
Code here: https://stackblitz.com/edit/react-eu9myn
class App extends Component {
constructor() {
super();
this.state = {
language: 'en'
};
}
changeLanguage = () => {
this.setState({
language: 'fr'
})
}
render() {
const {language} = this.state;
return (
<div>
<p>
{language === 'en' ? 'title' : 'titre'}
</p>
<button onClick={this.changeLanguage}>change language</button>
</div>
);
}
}
Internationalization (i18n) is a hard problem with a few existing, standard solutions designed by expert translators and linguists to account for the breadth of language quirks across the world. You shouldn't generally try to come up with your own solution, even when you are fluent in all target languages.
That doesn't mean you need a library (you could implement one of those standards yourself) but writing the i18n logic inline will not scale and probably won't work well.
The easiest case of i18n is if you're translating strings that do not depend on context and are complete sentences with no interpolation. You could get away with a very basic approach there, like using a big dictionary of translations and just looking up each string in it. It would look sort of like your ternary but at least it would scale for many languages, and it would be reasonable to do that with no library:
l10n = {
'title': {en: 'title', fr: 'titre'}
}
<p>
{l10n['title'][lang]}
</p>
However, if there is going to be string interpolation in your website/application/whatever, please consider a library that implements, say, ICU.
Now, let me show you why it would be a bad idea. Suppose you have the string "you can see (n) rocks" where you want to replace (n) with an actual number, and you want the sentence to be grammatically correct so you need to compute number agreement, right ? so, "0 rocks", "1 rock", "2+ rocks"… looks like English plural is just adding an "s" (not true, but let's assume for now), you could implement that with ternaries. I see you used French in your example so, how about that ? "0 cailloux", "1 caillou", "2+ cailloux". Right, there are multiple plural forms in French. How do you write your code to account for that ? And what if you need a German translation ? maybe the translator will decide that the object should go first in the sentence, rather than last. How does your code handle word order based on language ?
All these problems should be delegated to the translator who encodes them into an ICU string, which is then evaluated by some code given a context to get a correct translation. Whether you use a library or implement it yourself, what you want in the end is some function — let's call it localize(string, context) that is pretty much independent from React and that you use in your components like this:
import localize from './somewhere'
<p>
{localize('title')}
</p>
If you really want to, you can pass the locale as an argument and have it stored in React's state somehow. This library decided it wasn't necessary because real users rarely switch language and it's OK to reload the whole application when that happens.
I just implemented a simple language component for work that uses a Localisation context/provider and a dictionary (e.g JSON). I'll go through the steps, and there's a workable codesandbox example at the end. This is a very basic approach, but it works well for us at the moment.
The example has:
1) A simple "dictionary" that contains the tokens you want to translate in each language defined by a short code
{ EN: { welcome: 'Welcome' }, FR: { welcome: 'Bienvenue' }, IT: { welcome: 'Benvenuto' } };
2) An initial state and reducer that you can update when the language changes
export const initialState = {
defaultLanguage: 'EN',
selectedLanguage: 'IT'
}
export function reducer(state, action) {
const { type, payload } = action;
switch (type) {
case 'LANGUAGE_UPDATE': {
return { ...state, selectedLanguage: payload };
}
default: return state;
}
}
3) A Localisation Context/Provider. You can wrap your code in the provider and every child component can get access to the state through the context. We import the dictionary and state/reducer, create the new context and then set up the provider into which we pass the state and dictionary.
import dictionary from './dictionary';
import { initialState, reducer } from './localisationReducer';
export const LocalisationContext = React.createContext();
export function LocalisationProvider({ children }) {
const localisationStore = useReducer(reducer, initialState);
return (
<LocalisationContext.Provider value={{ localisationStore, dictionary }}>
{children}
</LocalisationContext.Provider>
);
}
4) An example app. You can see the LocalisationProvider wrapping the other elements, but also a dropdown, and a component called Translate. I'll describe those next.
<LocalisationProvider>
<Dropdown />
<div className="App">
<h1>
<Translate token="welcome" />
</h1>
<h2>Start editing to see some magic happen!</h2>
</div>
</LocalisationProvider>
5) The dropdown accesses the Localisation context and builds a dropdown with the languages. The key part is the handleSelected function which uses the dispatch from the localisation store to change the state (update the language):
import { LocalisationContext } from './localisation';
const langs = [
{ shortCode: 'EN', label: 'English' },
{ shortCode: 'FR', label: 'Français' },
{ shortCode: 'IT', label: 'Italiano' }
];
export function Dropdown() {
const {
localisationStore: [ state, dispatch ]
} = useContext(LocalisationContext);
const { selectedLanguage } = state;
const handleSelected = (e) => {
const { target: { value } } = e;
dispatch({ type: 'LANGUAGE_UPDATE', payload: value });
}
function getOptions(langs, selectedLanguage) {
return langs.map(({ shortCode, label }) => {
return <option value={shortCode}>{label}</option>
});
}
return (
<select onChange={handleSelected}>
{getOptions(langs, selectedLanguage)}
</select>
);
}
6) The Translate component which also accesses the state and dictionary through the context, and performs the translation based on the selected language.
import { LocalisationContext } from './localisation';
export default function Translator({ token }) {
const {
localisationStore: [state], dictionary
} = useContext(LocalisationContext);
const {
selectedLanguage, defaultLanguage
} = state;
const translatedToken = dictionary[selectedLanguage][token] || dictionary[defaultLanguage][token];
return (
<Fragment>
{translatedToken}
</Fragment>
);
}
Here's the codesandbox example for you to explore. Just select a new language from the dropdown to see the main "welcome" text change.

I can't get the html output from draft-js?

I've been playing around with draft-js by Facebook, but I can't actually figure out how to get the html output of the editor. The console.log in the following example outputs some _map properties, but they don't seem to contain my actual content?
class ContentContainer extends React.Component {
constructor(props) {
super(props);
this.state = {
value: '',
editorState: EditorState.createEmpty()
};
this.onChange = (editorState) => this.setState({editorState});
this.createContent = this.createContent.bind(this);
}
createContent() {
console.log(this.state.editorState.getCurrentContent());
}
render() {
const {editorState} = this.state;
const { content } = this.props;
return (
<Template>
<br /><br /><br />
<ContentList content={content} />
<div className="content__editor">
<Editor editorState={editorState} onChange={this.onChange} ref="content"/>
</div>
<FormButton text="Create" onClick={this.createContent.bind(this)} />
</Template>
);
}
}
There is a handy library I used, draft-js-export-html. Import the library and you should be able to see HTML once you invoke the function, stateToHTML:
console.log(stateToHTML(this.state.editorState.getCurrentContent()));
I'm pretty new to React so hopefully this works for you. I looked under the hood of contentState and there is a fair bit going on there that makes using a library to parse out the entities that much more enticing.
The author, sstur, answers a tangentially-related question where I learned about his libraries.
Ewan. I am also playing with Draft.js and came across the same problem. Actually, Victor has provided a great solution.
Here are two libraries that I found. The one mentioned by Victor has more stars on GitHub.
https://github.com/sstur/draft-js-export-html
https://github.com/rkpasia/draft-js-exporter
I just want to add that there is a way to print out the content (in JSON format) without using an external library. It is documented under the Data Conversion session.
Here is how I print out user input using the "convertToRaw" function
console.log(convertToRaw(yourEditorContentState.getCurrentContent()));
Make sure you imported the convertToRaw function from Draft.js by writing:
import { convertFromRaw, convertToRaw } from 'draft-js';
Here is a great blog written by rajaraodv named How Draft.js Represents Rich Text Data. It explained data conversion in detail.
There is readonly attribute to generate just HTML:
<Editor editorState={editorState} readOnly/>
If not willing to add another library to your code, #farincz's approach can work well.
<Editor editorState={this.state.editorState} readOnly/>
The editor state can be directly stored in your storage layer and when you are rendering it to the DOM it is easily available and can help in editing.
By clicking on the text you can make it editable, or bind that click with an edit button. You cannot directly bind click to 'Editor' component, but you can have it on the wrapper containing the 'Editor'.
<div className="editor" onClick={this.editContent.bind(this)}>
<Editor
editorState={this.state.editorState}
onChange={this.onChange}
handleKeyCommand={this.handleKeyCommand}
readOnly={this.state.edit}
/>
</div>
Just add 'edit' to your state as true, making sure that readOnly is true (you can make the name 'edit' of the state more obvious, if it is confusing).
this.state = {
editorState: EditorState.createEmpty(),
edit: true
};
Finally change the value of 'edit' to false on click
editContent() {
this.setState({
edit: false
})
}
To expand on the libraries shared above, here's another good one : draftjs-to-html
It converts raw editor state (JSON object) into plain HTML.
import draftToHtml from 'draftjs-to-html';
import {convertToRaw} from "draft-js";
const rawContentState = convertToRaw(editorState.getCurrentContent());
const markup = draftToHtml(rawContentState);
The most suitable package to convert to HTML and from HTML is draft-convert
However, you should be aware to use the Advanced usage to be able to convert links and to customize the convert process:
const htmlResult = convertToHTML({
entityToHTML: (entity, originalText) => {
if (entity.type === 'LINK') {
return <a href={entity.data.url}>{originalText}</a>;
}
return originalText;
}
})(editorState.getCurrentContent());
const contentState = convertFromHTML({
htmlToEntity: (nodeName, node, createEntity): any | void => {
if (nodeName === 'a') {
return createEntity(
'LINK',
'MUTABLE',
{url: node.href}
)
}
}
})(htmlInput);

Categories