I'm trying to create react components dynamically based on my JSON structure using a recursive function. Here is my implementation.
components.js
import React from "react";
import Text from "./components/Text";
import Image from "./components/Image";
const Components = {
text: Text,
image: Image
};
export default (block) => {
if (typeof Components[block.type] !== "undefined") {
return React.createElement(Components[block.type], {
key: block.id,
block: block
});
}
return React.createElement(
() => <div>The component {block.type} has not been created yet.</div>,
{ key: block.id }
);
};
dummy data
const data = {
userId: 123123,
pages: [
{
id: 1,
type: "div",
data: { text: "hello" },
content: [
{
id: 123,
type: "text",
data: { text: "hello" }
},
{
id: 456,
type: "image",
data: { source: "url", link: "url" }
}
]
}
]
};
There is a third argument in the React.createElement which is to render the children. Extended your Components component like below to do the recursive rendering:
const ComponentsMap = {
div: Div,
text: Text,
image: Image
};
const Components = (block) => {
if (typeof ComponentsMap[block.type] !== "undefined") {
return React.createElement(
ComponentsMap[block.type],
{
key: block.id,
block: block
},
block.content ? block.content.map((subComp) => Components(subComp)) : null
);
}
return React.createElement(
() => <div>The component {block.type} has not been created yet.</div>,
{ key: block.id }
);
};
export default Components;
Working Demo:
Related
I'm trying to create custom plugin that creates simple anchor. I used code from documentation (https://ckeditor.com/docs/ckeditor5/latest/framework/guides/tutorials/implementing-a-block-widget.html) and now I'am trying to add ID attribude from prompt.
It is simple empty element with class (for ck-editor css usage) and id for reference.
Everything is working fine, but I`m not able to add the ID attribude obtained from prompt -> no id attribute is added to the div element.
Any idea?
export default class Anchor extends Plugin {
static get requires() {
return [ AnchorEditing, AnchorUI ]
}
}
Anchor UI class
class AnchorUI extends Plugin {
init() {
const editor = this.editor
editor.ui.componentFactory.add( 'Anchor', locale => {
const command = editor.commands.get( 'insertAnchor' )
const buttonView = new ButtonView( locale )
buttonView.set( {
label: "Vložit kotvu",
tooltip: true,
icon
} )
buttonView.bind( 'isOn', 'isEnabled' ).to( command, 'value', 'isEnabled' )
this.listenTo( buttonView, 'execute', () => {
const id = prompt("Zadejte ID")
if(id){
editor.execute( 'insertAnchor', {id})
}
} )
return buttonView
} )
}
}
AnchorEditing class
class AnchorEditing extends Plugin {
static get requires() {
return [ Widget ]
}
init() {
this._defineSchema()
this._defineConverters()
this.editor.commands.add( 'insertAnchor', new InsertAnchorCommand( this.editor ))
}
_defineSchema() {
const schema = this.editor.model.schema
schema.register( 'Anchor', {
isObject: true,
allowWhere: '$block'
})
}
_defineConverters() {
const conversion = this.editor.conversion
conversion.for( 'upcast' ).elementToElement( {
model: 'Anchor',
view: {
name: 'div',
attributes: {
id: "anchor",
'data-class': "anchor"
}
}
} )
conversion.for( 'dataDowncast' ).elementToElement( {
model: 'Anchor',
view: {
name: 'div',
attributes: {
id: "anchor",
'data-class': "anchor"
}
}
} )
conversion.for( 'editingDowncast' ).elementToElement( {
model: 'Anchor',
view: ( modelElement, { writer: viewWriter } ) => {
const section = viewWriter.createContainerElement( 'div', { 'data-class': 'anchor' } )
return toWidget( section, viewWriter, { label: 'anchor widget' } )
}
} )
conversion.attributeToAttribute({
model: {
name: "div",
key: 'id'
},
view: {
name: "div",
key: "id"
}
})
}
}
InsertAnchorCommand class
class InsertAnchorCommand extends Command {
execute({id}) {
this.editor.model.change( writer => {
this.editor.model.insertContent( createAnchor( writer, id ) )
} )
}
refresh() {
const model = this.editor.model
const selection = model.document.selection
const allowedIn = model.schema.findAllowedParent( selection.getFirstPosition(), 'Anchor' )
this.isEnabled = allowedIn !== null
}
}
createAnchor function
function createAnchor( writer, id ) {
const Anchor = writer.createElement( 'Anchor' )
writer.setAttribute('id', id, Anchor)
console.log(Anchor)
writer.append( {}, Anchor )
return Anchor
}
expected result: 'div data-class="anchor" id="from-prompt"> '
actual result: 'div data-class="anchor" id="anchor"> '
Im making a react page with fluent ui and i wanted to open a certain component (documentpane.tsx) when i click a certain button from my command bar.
this is my commandbar code:
const theme = getTheme();
// Styles for both command bar and overflow/menu items
const itemStyles: Partial<IContextualMenuItemStyles> = {
label: { fontSize: 18 },
icon: { color: uPrinceTheme.palette.themePrimary },
iconHovered: { color: uPrinceTheme.palette.themeSecondary },
};
// For passing the styles through to the context menus
const menuStyles: Partial<IContextualMenuStyles> = {
subComponentStyles: { menuItem: itemStyles, callout: {} },
};
const getCommandBarButtonStyles = memoizeFunction(
(originalStyles: IButtonStyles | undefined): Partial<IContextualMenuItemStyles> => {
if (!originalStyles) {
return itemStyles;
}
return concatStyleSets(originalStyles, itemStyles);
},
);
// Custom renderer for main command bar items
const CustomButton: React.FunctionComponent<IButtonProps> = props => {
const buttonOnMouseClick = () => alert(`${props.text} clicked`);
// eslint-disable-next-line react/jsx-no-bind
return <CommandBarButton {...props} onClick={buttonOnMouseClick} styles={getCommandBarButtonStyles(props.styles)} />;
};
// Custom renderer for menu items (these must have a separate custom renderer because it's unlikely
// that the same component could be rendered properly as both a command bar item and menu item).
// It's also okay to custom render only the command bar items without changing the menu items.
const CustomMenuItem: React.FunctionComponent<IContextualMenuItemProps> = props => {
// Due to ContextualMenu implementation quirks, passing styles or onClick here doesn't work.
// The onClick handler must be on the ICommandBarItemProps item instead (_overflowItems in this example).
return <ContextualMenuItem {...props} />;
};
const overflowProps: IButtonProps = {
ariaLabel: 'More commands',
menuProps: {
contextualMenuItemAs: CustomMenuItem,
// Styles are passed through to menu items here
styles: menuStyles,
items: [], // CommandBar will determine items rendered in overflow
isBeakVisible: true,
beakWidth: 20,
gapSpace: 10,
directionalHint: DirectionalHint.topCenter,
},
};
export const CommandBarButtonAsExample: React.FunctionComponent = () => {
return (
<CommandBar
overflowButtonProps={overflowProps}
// Custom render all buttons
buttonAs={CustomButton}
items={_items}
ariaLabel="Use left and right arrow keys to navigate between commands"
/>
);
};
const _items: ICommandBarItemProps[] = [
{
key: 'newItem',
text: 'Create',
iconProps: { iconName: 'Add' },
},
{
key: 'upload',
text: 'Read',
iconProps: { iconName: 'Read' },
href: 'https://developer.microsoft.com/en-us/fluentui',
},
{ key: 'share', text: 'Update', iconProps: { iconName: 'Share' } },
{ key: 'download', text: 'Delete', iconProps: { iconName: 'Delete' } },
];
export default CommandBarButtonAsExample;
and this is my index.tsx now:
import { StrictMode } from "react";
import ReactDOM from "react-dom";
import { initializeIcons } from "#fluentui/react/lib/Icons";
import App from "./App";
const rootElement = document.getElementById("root");
initializeIcons();
ReactDOM.render(
<StrictMode>
<App />
</StrictMode>,
rootElement
);
i will also add a git repo with my code in it so you guys can see it better.
https://github.com/robbe-delsoir/app2
thank you very much for the help!
Robbe
Add an onClick method in the commandbar item which sets the show component state for documentpane.tsx.
const _items: ICommandBarItemProps[] = [
{
key: 'newItem',
text: 'Create',
iconProps: { iconName: 'Add' },
},
{
key: 'upload',
text: 'Read',
iconProps: { iconName: 'Read' },
onClick:{ () => { setShowDocumentPane(true) } }
href: 'https://developer.microsoft.com/en-us/fluentui',
},
{ key: 'share', text: 'Update', iconProps: { iconName: 'Share' } },
{ key: 'download', text: 'Delete', iconProps: { iconName: 'Delete' } },
];
Let's see we have the simple component ToggleButton:
const ButtonComponent = Vue.component('ButtonComponent', {
props: {
value: Boolean
},
methods: {
handleClick() {
this.$emit('toggle');
}
},
template: `
<button
:class="value ? 'on' : 'off'"
#click="handleClick"
>
Toggle
</button>`
});
And the story for that component:
import ToggleButton from './ToggleButton.vue';
export default {
title: 'ToggleButton',
component: ToggleButton,
argTypes: {
onToggle: {
action: 'toggle' // <-- instead of logging "toggle" I'd like to mutate `args.value` here
}
}
};
export const Default = (_args, { argTypes }) => ({
components: { ToggleButton },
props: Object.keys(argTypes),
template: `
<ToggleButton
:value="value"
:toggle="onToggle"
/>
`
});
Default.args = {
value: false
}
What I want to achieve is to handle toggle action inside the story and change value that I've used in Default.args object to change the button style by changing the class name from .off to .on.
I had the same exact issue, and kept looking for days, till I stumbled upon this github post:
https://github.com/storybookjs/storybook/issues/12006
Currently in my React (am sure vue approach will be similar), I do following:
import React from 'react';
import CheckboxGroupElement from '../CheckboxGroup';
import { STORYBOOK_CATEGORIES } from 'elements/storybook.categories';
import { useArgs } from '#storybook/client-api';
export default {
component: CheckboxGroupElement,
title: 'Components/CheckboxGroup',
argTypes: {
onChange: {
control: 'func',
table: {
category: STORYBOOK_CATEGORIES.EVENTS,
},
},
},
parameters: { actions: { argTypesRegex: '^on.*' } },
};
const Template = (args) => {
const [_, updateArgs] = useArgs();
const handle = (e, f) => {
// inside this function I am updating arguments, but you can call it anywhere according to your demand, the key solution here is using `useArgs()`
// As you see I am updating list of options with new state here
console.log(e, f);
updateArgs({ ...args, options: e });
};
return <CheckboxGroupElement {...args} onChange={handle} />;
};
export const CheckboxGroup = Template.bind({});
CheckboxGroup.storyName = 'CheckboxGroup';
CheckboxGroup.args = {
//Here you define default args for your story (initial ones)
controller: { label: 'Group controller' },
options: [
{ label: 'option 1', checked: true },
{ label: 'option 2', checked: false },
{ label: 'option 3', checked: false },
],
mode: 'nested',
};
I want to use a mixin to find a referenced Node and then append some HTML to it rendered using Vue, so I can pass data into it.
const Tutorial = guide => ({
mounted() {
this.guide = guide;
this.html = Vue.compile(`<p>Test</p>`).render;
guide['add-location'].forEach(step => {
this.$refs[step.ref].appendChild(this.html);
})
},
data: function() {
return {
guide: null,
html: null
}
}
});
export default Tutorial;
This is what I have at the moment, it gets the ref correctly, just can't append the HTML as I don't think i'm using Vue.compile correctly.
Failed to execute 'appendChild' on 'Node': parameter 1 is not of type 'Node'
In my opinion, It's better if we can avoid mutate DOM directly. What about replace ref with v-html?
const tutorial = guide => ({
mounted() {
guide['add-location'].forEach(step => {
this[step.ref] += this.html;
})
},
data: function() {
return {
...guide['add-location'].reduce((result, step) => {
result[step.ref] = ''
return result
}, {}),
html: `<p>Test</p>`
}
}
});
const Foo = {
template: `
<div>
<div v-html='foo'></div>
<div v-html='bar'></div>
</div>
`,
mixins: [tutorial({
'add-location': [
{ ref: 'foo' },
{ ref: 'bar' }
]
})]
}
Another idea is using wrapper component to wrap target or if your target is a component then you create a wrapper as mixin too.
Using with html property:
<wrapper ref='foo'>
<div>Foo</div>
</wrapper>
const Wrapper = {
props: ['html'],
render(h) {
return h('div', [this.$slots.default, h('div', {
domProps: {
innerHTML: this.html
}
})])
}
}
...
this.$refs.foo.html = '<h1>Hello Foo</h1>'
Example
Or using with custom appendChild method:
const Wrapper = {
data: () => ({
children: []
}),
methods: {
appendChild(child) {
this.children.push(child)
}
},
render(h) {
return h('div', [
this.$slots.default,
...this.children.map(child => h('div', {
domProps: {
innerHTML: child
}
}))
])
}
}
...
this.$refs.foo.appendChild('<h1>Hello Foo</h1>')
this.$refs.foo.appendChild('<h1>Hello Bar</h1>')
Example
Or using with Vue.compile in case that html is not plain html:
const Wrapper = {
data: () => ({
template: '',
context: {}
}),
methods: {
setChild(template, context) {
this.template = template
this.context = context
}
},
render(h) {
let res = Vue.compile(this.template)
return h('div', [
this.$slots.default,
h({
data: () => this.context,
render: res.render,
staticRenderFns: res.staticRenderFns
})
])
}
}
...
this.$refs.foo.setChild('<h1>Hello {{ name }}</h1>', {
name: 'Foo'
})
Example
So currently i have a react component. I have declared an array which contains a series of objects for some sample data. As the initial state for 'currentStep' is 0, I expect the <div>Screen 1</div> to render, however, all i get is a blank screen.
Any ideas?
import React, { Component } from 'react';
/**
* sample data to pass through
*/
const contents =
[
{ title: 'First Screen', step: 0, children: <div>Screen 1</div> },
{ title: 'Second Screen', step: 1, children: <div>Screen 2</div> },
{ title: 'Third Screen', step: 2, children: <div>Screen 3</div> },
];
class Wizard extends Component {
state = {
currentStep: 0,
}
Content = () => {
const { currentStep } = this.state;
contents.map((content) => {
if (content.step === currentStep) { return content.children; }
return null;
});
}
render() {
return (
<div>{this.Content()}</div>
);
}
}
export default Wizard;
You need to return your map in Content. Right now you are returning nothing. For ex:
Content = () => {
const { currentStep } = this.state;
return contents.map((content) => {
if (content.step === currentStep) { return content.children; }
return null;
});
}
Your Content function isn't actually returning anything, but your map function is. If you return contents.map(...), then you should get what you are expecting.