How do I render components with a switch in Svelte? - javascript

I would like to conditionally render components with a switch case statement in Svelte like so:
// index.svelte
<script>
import TextContent from './components/text-content.svelte'
import { someData } from './api/some-data.js'
const ContentSwitch = (data) => {
switch (data._type) {
case 'block':
return data.children.map((child) => ContentSwitch(child));
case 'span':
return (
<TextContent>
<span slot="text-content">{data.text}</span>
</TextContent>
);
}
for (let index = 0; index < data.length; index++) {
return data.map((item) => ContentSwitch(item));
}
};
</script>
<div>
{#each someData as data}
{ContentSwitch(data)}
{/each}
</div>
TextContent component:
// components/text-content.svelte
<slot name="text-content">
<span />
</slot>
It seems that this approach does not work in Svelte as I'm getting an Unexpected Token error.
Is rendering components with a switch possible in Svelte?

What you are writing there resembles more JSX which is for React. In Svelte you do not write HTML in your JavaScript but instead keep those separate.
What you would do is make a lookup table and use svelte:component to render the correct component:
<script>
const BlockTypes = {
"span": TextContent
}
</script>
{#each children as child}
{#if child.type === 'block'}
<svelte:self {...child} />
{:else}
<svelte:component this={BlockTypes[child.type]} {...child} />
{/if}
{/each}
The svelte:self is under the assumptions that this is in itself also an element of type block, for reasons you cannot import a component into itself so you need this special case here. Having this gives you nested blocks out of the box.
In this example you pass all the properties of the child on to the rendered component so you would have to rewrite your components slightly, you could also use slots but that would be a severe mess with named slots.

I think returning the html syntax in the switch 'span' inside the (java)script tag can't work like this.
Actually it's not a switch between different components but rendering differently nested 'data.text' fields all inside a TextContent component?
What's the structure of someData? Assuming it looks something like this
let someData = [
{
_type: 'block',
children: [{
_type: 'span',
text: 'textValue#1'
}]
},
{
_type: 'span',
text: 'textValue#2'
}
]
A recursive function could be used to get all nested text fields
function getSpanTexts(dataArr) {
return dataArr.flatMap(data => {
switch (data._type) {
case 'block':
return getSpanTexts(data.children)
case 'span':
return data.text
}
})
}
$: textArr = getSpanTexts(someData) // ['textValue#1', 'textValue#2']
The text fields then can be iterated with an each loop inside the html, each rendered inside a TextContent component
<div>
{#each textArr as text}
<TextContent>
<span slot="text-content">{text}</span>
</TextContent>
{/each}
</div>
See this working REPL of the code snippets

Related

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

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>

array of html elements or strings typescript [duplicate]

Using React with TypeScript, there are several ways to define the type of children, like setting it to JSX.Element or React.ReactChild or extending PropsWithChildren. But doing so, is it possible to further limit which particular element that React child can be?
function ListItem() {
return (
<li>A list item<li>
);
}
//--------------------
interface ListProps {
children: React.ReactChild | React.ReactChild[]
}
function List(props: ListProps) {
return (
<ul>
{props.children} // what if I only want to allow elements of type ListItem here?
</ul>
);
}
Given the above scenario, can List be set up in such a way that it only allows children of type ListItem? Something akin to the following (invalid) code:
interface ListProps {
children: React.ReactChild<ListItem> | React.ReactChild<ListItem>[]
}
You can't constrain react children like this.
Any react functional component is just a function that has a specific props type and returns JSX.Element. This means that if you render the component before you pass it a child, then react has no idea what generated that JSX at all, and just passes it along.
And problem is that you render the component with the <MyComponent> syntax. So after that point, it's just a generic tree of JSX nodes.
This sounds a little like an XY problem, however. Typically if you need this, there's a better way to design your api.
Instead, you could make and items prop on List which takes an array of objects that will get passed as props to ListItem inside the List component.
For example:
function ListItem({ children }: { children: React.ReactNode }) {
return (
<li>{children}</li>
);
}
function List(props: { items: string[] }) {
return (
<ul>
{props.items.map((item) => <ListItem>{item}</ListItem> )}
</ul>
);
}
const good = <List items={['a', 'b', 'c']} />
In this example, you're just typing props, and List knows how to generate its own children.
Playground
Here's a barebones example that I am using for a "wizard" with multiple steps. It uses a primary component WizardSteps (plural) and a sub-component WizardStep (singular), which has a "label" property that is rendered in the main WizardSteps component. The key in making this work correctly is the Children.map(...) call, which ensures that React treats "children" as an array, and also allows Typescript and your IDE to work correctly.
const WizardSteps: FunctionComponent<WizardStepsProps> & WizardSubComponents = ({children}) => {
const steps = Children.map(children, child => child); /* Treat as array with requisite type */
return (
<div className="WizardSteps">
<header>
<!-- Note the use of step.props.label, which is properly typecast -->
{steps.map(step => <div className="WizardSteps__step">{step.props.label}</div>)}
</header>
<main>
<!-- Here you can render the body of each WizardStep child component -->
{steps.map(step => <div className="WizardSteps__body">{step}</div>)}
</main>
</div>
);
}
const Step: FunctionComponent<WizardStepProp> = ({label, onClick}) => {
return <span className="WizardSteps__label">
{label}
</span>
}
WizardSteps.Step = Step;
type WizardSubComponents = {
Step: FunctionComponent<WizardStepProp>
}
type WizardStepsProps = {
children: ReactElement<WizardStepProp> | Array<ReactElement<WizardStepProp>>
};
type WizardStepProp = {
label: string
onClick?: string
children?: ReactNode
}
Absolutely. You just need to use React.ReactElement for the proper generics.
interface ListItemProps {
text: string
}
interface ListProps {
children: React.ReactElement<ListItemProps> | React.ReactElement<ListItemProps>[];
}
Edit - I've created an example CodeSandbox for you:
https://codesandbox.io/s/hardcore-cannon-16kjo?file=/src/App.tsx

Using JSX in this.state but it gets rendered as plain text

I am dynamically rendering a list of elements. Depending on key-value pairs in those elements, I need to insert other elements in front of them.
I'd like to use <sup></sup> tags on those elements, but they are showing as plain text instead of superscript.
How can I use JSX in the state which is an array of strings and not have it come out as plain text?
The line in question is this one: allOptions.splice(index, 0, this.props.levelNames[j]);
The prop would be : [...., '1<sup>st</sup> Level',...]
But when rendered it just comes out as plain text.
import React, { Component } from 'react';
import './App.css';
export class Chosen extends Component {
render() {
let allOptions = this.props.chosenSpells.map((val, i) => this.props.selectMaker(val, i, 'chosen'));
let index;
let headings=this.props.levelNames;
for (let j=this.props.highestSpellLevel; j>0;j--) {
index = this.props.chosenSpells.findIndex(function findLevel (element) {return element.level==j});
console.log(index);
console.log(headings[j]);
if (index>=0){
allOptions.splice(index, 0, this.props.levelNames[j]);
}
}
return (
<div>
<b><span className="choose">Chosen (click to remove):</span></b><br/>
<div className="my-custom-select">
{allOptions}
</div>
</div>
);
}
}
export class Parent extends Component {
constructor(props) {
super(props);
this.state = {
levelNames: ['Cantrips', '1<sup>st</sup> Level', '2nd Level', '3rd Level']
};
In order to display HTML tags in React JSX, you need to pass the HTML string to dangerouslySetInnerHTML props. Not inside a JSX tag. Please check this official documentation about how to do it: https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
So, instead doing this:
<div className="my-custom-select">
{allOptions}
</div>
You should doing this way:
<div className="my-custom-select" dangerouslySetInnerHTML={{__html: allOptions}}/>
It is due to security concern. Quoted from React documentation:
dangerouslySetInnerHTML is React’s replacement for using innerHTML in the browser DOM. In general, setting HTML from code is risky because it’s easy to inadvertently expose your users to a cross-site scripting (XSS) attack. So, you can set HTML directly from React, but you have to type out dangerouslySetInnerHTML and pass an object with a __html key, to remind yourself that it’s dangerous.
If you insist to put the HTML string inside JSX tag instead passing to props, alternatively you can use an additional library from this NPM package: https://www.npmjs.com/package/react-html-parser . So, it will be something looks like this:
import React from 'react';
import ReactHtmlParser, { processNodes, convertNodeToElement, htmlparser2 } from 'react-html-parser';
class HtmlComponent extends React.Component {
render() {
........
return <div className="my-custom-select">{ ReactHtmlParser(allOptions) }</div>;
}
}

How to change VueJS's <slot> content before component creation

I have a VueJS component,
comp.vue:
<template>
<div>
<slot></slot>
</div>
</template>
<script>
export default {
data () {
return {
}
},
}
</script>
And I call this Vue component just like any other component:
...
<comp>as a title</comp>
<comp>as a paragraph</comp>
...
I would like to change comp.vue's slot before it is rendered so that if the slot contains the word "title" then the slot will be enclosed into an <h1>, resulting in
<h1>as a title</h1>
And if the slot contains "paragraph" then the slot will be enclosed in <p>, resulting in
<p>as a paragraph</p>
How do I change the component slot content before it is rendered?
This is easier to achieve if you use a string prop instead of a slot, but then using the component in a template can become messy if the content is long.
If you write the render function by hand then you have more control over how the component should be rendered:
export default {
render(h) {
const slot = this.$slots.default[0]
return /title/i.test(slot.text)
? h('h1', [slot])
: /paragraph/i.test(slot.text)
? h('p', [slot])
: slot
}
}
The above render function only works provided that the default slot has only one text child (I don't know what your requirements are outside of what was presented in the question).
You can use $slots(https://v2.vuejs.org/v2/api/#vm-slots):
export default {
methods: {
changeSlotStructure() {
let slot = this.$slots.default;
slot.map((x, i) => {
if(x.text.includes('title')) {
this.$slots.default[i].tag = "h1"
} else if(x.text.includes('paragraph')) {
this.$slots.default[i].tag = "p"
}
})
}
},
created() {
this.changeSlotStructure()
}
}

Get multiple components instead of one. They are rendered from the list. vue.js

I have a list of instruments that should render a c-input with autosuggest window when the user types something. Also, I need an option for c-input to add or remove autosuggest component.
/* instrument component */
<template>
<c-input ref="input"
:values="inputValue"
:placeholder="placeholder"
#input="onInput"
#change="onChangeInput"
#reset="reset" />
<autosuggest
v-if="showSuggests"
:inputValue="inputValue"
:suggests="suggests"
#onSelectRic="selectRicFromList"
></autosuggest>
</div>
</template>
<script>
export default {
name: 'instrument',
data: () => ({
suggests: [],
inputValue: '',
}),
computed: {
showSuggests() {
return this.isNeedAutosuggest && this.showList;
},
showList() {
return this.$store.state.autosuggest.show;
},
isloading() {
return this.$store.state.instruments.showLoading;
},
defaultValue() {
if (this.instrument.name) {
return this.instrument.name;
}
return '';
},
},
[...]
};
</script>
This is a parent component:
<template>
<div>
<instrument v-for="(instrument, index) in instruments"
:key="instrument.name"
:instrument="instrument"
:placeholder="$t('change_instrument')"
:isNeedAutosuggest="true" /> <!--that flag should manage an autosuggest option-->
<instrument v-if="instruments.length < maxInstruments"
ref="newInstrument"
:isNeedAutosuggest="true" <!-- here too -->
:placeholder="$t('instrument-panel.ADD_INSTRUMENT')" />
</div>
</template>
The main issues are I have so many autosuggests in DOM as I have instruments. In other words, there is should be 1 autosuggest component when the option is true. Moving autosuggest to the parent level is not good because of flexibility and a lot of logically connected with c-input.
Have you any ideas to do it?
[UPDATE]
Here is how I've solve this;
I created an another component that wraps input and autosuggest components. If I need need an input with autosuggest I will use this one, either I will use a simple input.
/* wrapper.vue - inserted into the Instrument.vue*/
<template>
<span>
<fc-input ref="input"
:values="value"
:placeholder="placeholder"
:isloading="isloading"
#input="onInput"
#changeInput="$emit('change', $event)"
#resetInput="onResetInput" />
<fc-autosuggest
v-if="isSuggestsExist"
:suggests="suggests"
/>
</span>
</template>
You can do it if you create a function inside each instrument component, which will call the parent component and search the first component instrument to find autosuggest. Function will be like that:
name: 'instrument',
...
computed: {
autosuggestComponent () {
// this is a pseudo code
const parentChildrenComponents = this.$parent.children();
const firstChild = parentChildrenComponents[0];
const autosuggestEl = firstChild.$el.getElementsByTagName('autosuggest')[0];
// return Vue component
return autosuggestEl.__vue__;
}
},
methods: {
useAutosuggestComponent () {
this.autosuggestComponent.inputValue = this.inputValue;
this.autosuggestComponent.suggests = [{...}];
}
}
This solution is not so beautiful, but it allows to keep the logic inside the instrument component.
But my advice is create some parent component which will contain instrument components and I suggest to work with autosuggest through the parent. You can create autosuggest component in the parent and pass it to the children instruments. And if instrument doesn't receive a link to a autosuggest (in props), than it will create autosuggest inside itself. It will allow to use instrument for different conditions.
Let me know if I need to explain my idea carefully.

Categories