Warning: <Element /> is using incorrect casing. Use PascalCase for React components, or lowercase for HTML elements - javascript

I'm trying to automatically create React Elements from strings corresponding to the react-icons library. But I am getting the following errors in the console:
Warning: <RiHeartPulseFill /> is using incorrect casing. Use PascalCase for React components, or lowercase for HTML elements.
Warning: The tag <RiHeartPulseFill> is unrecognized in this browser. If you meant to render a React component, start its name with an uppercase letter.
Currently I have a data file that consists of a name and iconName (see below)
const categoriesData = [
{
name: 'Vitals',
iconName: 'RiHeartPulseFill',
},
{
name: 'Body',
iconName: 'RiBodyScanFill',
},
{
name: 'Sleep',
iconName: 'RiHotelBedFill',
},
{
name: 'Metabolism',
iconName: 'RiLungsFill',
},
{
name: 'Stress',
iconName: 'RiMentalHealthFill',
},
{
name: 'Strength & Training',
iconName: 'RiRunFill',
},
{
name: 'Lifestyle',
iconName: 'RiCellphoneFill',
},
]
export default categoriesData
I want to dynamically render React elements with the exact name as the iconName in the above datafile as React-icons require specific elements with those names.
Then I try to create a list of navigation links (using the React Router <Link> syntax and adding a React-icon + Name. See the code below:
const menuCategories = categoriesData.map((category) => {
const IconElement = category.iconName
return (
<Link
to={`/data/${category.name.toLowerCase()}`}
key={category.name}
className="flex flex-row items-center gap-2"
>
<IconElement />
{category.name}
</Link>
)
})
The issue I run into is the following error: Warning: <RiHeartPulseFill /> is using incorrect casing. Use PascalCase for React components, or lowercase for HTML elements.
I does not seems to be incorrect as it actually IS PascalCase. However when I check dev tools I see the following: <riheartpulsefill></riheartpulsefill>
I have no Idea why this happens. Any solutions?
Extra: Does anyone know how I can also import those icon names based on the initial data files. I'm thinking about creating an icon selection tool, so only the selected icons should be imported from the react-icons lib.

If you want to dynamically render these icon components then you'll typically need to import and specify them in the config instead of strings corresponding to their names.
Example:
import {
RiHeartPulseFill,
RiBodyScanFill,
RiHotelBedFill,
RiLungsFill,
RiMentalHealthFill,
RiRunFill,
RiCellphoneFill,
} from "react-icons/ri";
const categoriesData = [
{
name: 'Vitals',
iconName: RiHeartPulseFill,
},
{
name: 'Body',
iconName: RiBodyScanFill,
},
{
name: 'Sleep',
iconName: RiHotelBedFill,
},
{
name: 'Metabolism',
iconName: RiLungsFill,
},
{
name: 'Stress',
iconName: RiMentalHealthFill,
},
{
name: 'Strength & Training',
iconName: RiRunFill,
},
{
name: 'Lifestyle',
iconName: RiCellphoneFill,
},
];
export default categoriesData;
const menuCategories = categoriesData.map((category) => {
const IconElement = category.iconName;
return (
<Link
to={`/data/${category.name.toLowerCase()}`}
key={category.name}
className="flex flex-row items-center gap-2"
>
<IconElement />
{category.name}
</Link>
);
});
An alternative is to create and export a lookup object for the icon components.
import {
RiHeartPulseFill,
RiBodyScanFill,
RiHotelBedFill,
RiLungsFill,
RiMentalHealthFill,
RiRunFill,
RiCellphoneFill,
} from "react-icons/ri";
export const iconMap = {
RiHeartPulseFill,
RiBodyScanFill,
RiHotelBedFill,
RiLungsFill,
RiMentalHealthFill,
RiRunFill,
RiCellphoneFill,
};
const categoriesData = [
{
name: 'Vitals',
iconName: 'RiHeartPulseFill',
},
{
name: 'Body',
iconName: 'RiBodyScanFill',
},
{
name: 'Sleep',
iconName: 'RiHotelBedFill',
},
{
name: 'Metabolism',
iconName: 'RiLungsFill',
},
{
name: 'Stress',
iconName: 'RiMentalHealthFill',
},
{
name: 'Strength & Training',
iconName: 'RiRunFill',
},
{
name: 'Lifestyle',
iconName: 'RiCellphoneFill',
},
];
export default categoriesData;
const menuCategories = categoriesData.map((category) => {
const IconElement = iconMap[category.iconName];
return (
<Link
to={`/data/${category.name.toLowerCase()}`}
key={category.name}
className="flex flex-row items-center gap-2"
>
<IconElement />
{category.name}
</Link>
);
});
To allow for any react-icons/ri icon then in the UI component import all of react-icons/ri and conditionally render the icon component if it exists.
import { Link } from 'react-router-dom';
import * as ReactRiIcons from "react-icons/ri"; // <-- all RI icons
import * as ReactRxIcons from "react-icons/rx"; // <-- all RX icons
const ReactIcons = { // <-- all merged icons set
...ReactRiIcons,
...ReactRxIcons
};
...
const menuCategories = categoriesData.map((category) => {
const IconElement = ReactIcons[category.iconName];
return (
<Link
to={`/data/${category.name.toLowerCase()}`}
key={category.name}
className="flex flex-row items-center gap-2"
>
{IconElement && <IconElement />} // <-- handle possible undefined icon
{category.name}
</Link>
);
});
...

Use React.createElement. Take a look here to see how: Create react component dynamically
Heres my recursive example:
const demoData = [
{
tagName: 'MyButtonComponent',
children: [
{
tagName: 'MyChildComponent'
}
]
},
{
tagName: 'MyOtherComponent'
},
]
function recursivelyRenderChildren(elements) {
if(elements.length) {
return elements.map((element, index) => {
return React.createElement(elementData.tagName, {
key: element.fieldType+'-'+index,
children: recursivelyRenderChildren(element.children)
});
})
}
}
const arrayOfElements = recursivelyRenderChildren(demoData)

Related

Working with state on recursive components

I'm writing a component that renders itself inside recursively and is data-driven
Attaching my sandbox snippet, as it will be easier to see there.
This is my data:
var builderStructureData = [
{
id: 123,
value: 3,
children: []
},
{
id: 345,
value: 5,
children: [
{
id: 4123,
value: 34,
children: [
{
id: 342342,
value: 33,
children: []
}
]
},
{
id: 340235,
value: 3431,
children: [
{
id: 342231342,
value: 3415,
children: []
}
]
}
]
}
];
and it renders like this:
This is my App.js:
import { useState } from "react";
import "./App.css";
import Group from "./components/group";
import builderStructureData from "./components/builderStructureData";
function App() {
const [builderStructure, setBuilderStructure] = useState(
builderStructureData
);
return (
<div className="App">
{builderStructure.map((x) => {
return <Group key={x.id} children={x.children} value={x.value} />;
})}
</div>
);
}
export default App;
And this is my recursive component:
import React from "react";
export default function Group(props) {
let childrenArray = [];
if (props.children) {
props.children.map((x) => childrenArray.push(x));
}
return (
<div className="group" draggable>
<p>this is value: </p>
<input value={props.value} readOnly={true}></input>
<button>Add Group</button>
{childrenArray.map((x) => {
return <Group key={x.id} children={x.children} value={x.value} />;
})}
</div>
);
}
I can render the components based on the data, and it seems to be handling recursion fine. I need to store the state on the App.js page and be able to change it from within child components. For example, if I update the "value" field of the component with ID = 342342, I want it to update that corresponding object in the state no matter how deeply nested it is, but not sure how to do that as it is not as simple as just passing a prop.
Am I taking the right approach with my code snippet? How can I do the state update?
I would advise the state normalization approach - here is an example for redux state - https://redux.js.org/usage/structuring-reducers/normalizing-state-shape - but you can use this approach with your state. So - your state will look like this:
state = {
items: {
[123]: {
id: 123,
value: 3,
childrenIds: []
},
[345]: {
id: 345,
value: 5,
childrenIds: [4123, 340235]
},
[4123]: {
id: 4123,
value: 34,
parentId: 345,
childrenIds: [342342]
},
[342342]: {
id: 342342,
value: 33,
parentId: 4123,
childrenIds: []
},
[340235]: {
id: 340235,
value: 3431,
parentId: 345,
childrenIds: [342231342]
},
[342231342]: {
id: 342231342,
value: 3415,
parentId: 340235
childrenIds: []
}
}
}
Here the field "childrenIds" is an optional denormalization for ease of use, if you want - you can do without this field. With this approach, there will be no problem updating the state.
You are thinking this in a wrong way, it should be very easy to do what you want.
The most imported thing is to make a little small changes in Group
Please have a look
import React from "react";
export default function Group(props) {
const [item, setItem] = React.useState(props.item);
let childrenArray = [];
if (item.children) {
item.children.map((x) => childrenArray.push(x));
}
const updateValue = ()=> {
// this will update the value of the current object
// no matter how deep its recrusive is and the update will also happen in APP.js
// now you should also use datacontext in app.js togather with state if you want to
// trigger somethings in app.js
item.value =props.item.value= 15254525;
setState({...item}) // update the state now
}
return (
<div className="group" draggable>
<p>this is value: </p>
<input value={item.value} readOnly={true}></input>
<button>Add Group</button>
{childrenArray.map((x) => {
return <Group item={x} />;
})}
</div>
);
}
The code above should make you understand how easy it is to think about this as an object instead of keys.
Hop this should make it easy for you to understand

Getting content of currently active Text component wrapped inside popover of antd

I am using antd components for my react app. I have a Text component wrapped inside of Popover component. Now in my case this Popover is applied to one particular column of table, i.e. every row-element in that column has a Popover component rendered for it upon mouse hovering.
title: "Name",
dataIndex: "name",
key: "name-key",
sortType: "string",
sortDirections: ["descend", "ascend"],
sorter: (a, b) => a.name.length - b.name.length,
render: (text, record) => (
<Popover>
<Text onMouseOver={handleOnMouseOverCommitId}> {name} </Text>
</Popover>
)
I want to get hold of the row-element's value, the one contained by the above Text component whenever I hover over it. In this case the value denoted by {name} above.
I tried getting it with e.target.value via onMouseOver event, but it returned undefined.
I think I get the reason behind it, because the event.target returns an html node of type <span>.
With a normal div element e.target.value has worked in the past for me. But doing the same thing with a predefined component like antd's Text seems a bit trickier.
Just to elaborate, the Popover has two buttons and based on which button user clicks, I need to render some other components, something like an overlay component.
But in order to do that I would also need to get hold of the text value which originally triggered the Popover.
Below is the code(most of the things removed for preciseness).
record.name is what I ultimately need to capture.
<Popover
content={
<>
<Space>
<Button onClick={showSomeOverlayPaneForName}>
{"View Details for record.name"}
</Button>
<Button href={"https://abc.xyz.com/" + record.role}>
{"View Role Details"}
</Button>
</Space>
</>
}
trigger={"hover"}
>
<Text style={{"color": blue.primary}} copyable={true} onMouseOver={handleOnMouseOverName}>{record.name}</Text>
</Popover>
The handleOnMouseOverName function(which doesn't work anyway) :
const handleOnMouseOverName = (e) => {
//console.log("e.target.value :--- ", e.target.value);
setCurrentActiveName(e.target.value)
}
And once my currentActiveName variable is set(via useState), I use that value inside my function showSomeOverlayPaneForName
const showSomeOverlayPaneForName = (e) => {
axios
.get(
`some-url`,
{
params: {name: currentActiveName}
}
)
.then((response) => {
setData(response.data);
}).catch(reason => {
//xyz
});
}
You need to pass on the record of the enclosing render function to the handleOnMouseOverName function.
Check the following example
import React from 'react';
import 'antd/dist/antd.css';
import './index.css';
import { Space, Table, Button, Popover } from 'antd';
const App = () => {
const data = [
{
key: '1',
name: 'John Brown',
address: 'New York No. 1 Lake Park',
role: 'admin',
},
{
key: '2',
name: 'Jim Green',
address: 'London No. 1 Lake Park',
role: 'user',
},
{
key: '3',
name: 'Joe Black',
address: 'Sidney No. 1 Lake Park',
role: 'manager',
},
];
const columns = [
{
title: 'Name',
dataIndex: 'name',
key: 'name',
render: (name, record) => {
const content = (
<>
<Space>
<Button
onClick={() => {
viewDetail(record);
}}
>
{'View Details for ' + record.name}
</Button>
<Button href={'https://abc.xyz.com/' + record.role}>
{'View Role Details'}
</Button>
</Space>
</>
);
return (
<>
<Popover content={content} title="Details">
<div
onMouseOver={() => {
handleOnMouseOverName(record);
}}
>
{name}
</div>
</Popover>
</>
);
},
},
{
title: 'Address',
dataIndex: 'address',
key: 'address',
},
];
const handleOnMouseOverName = (record) => {
console.log(record);
};
const viewDetail = (record) => {
console.log(record);
};
return <Table columns={columns} dataSource={data} />;
};
export default App;
Output:
I hope this helps.
From antd docs: https://ant.design/components/popover/#header
Apparently you're supposed to render the <Popover/> with a content={content}-prop
For example
const content = <div>Content to render under title</div>
const App = () => {
const value = "Text to hover";
return (
<Popover content={content} title="Title">
<Text>{value}</Text>
</Popover>
)
}

React JS: How to add multiple placeholder object inside components

Sorry this question might be duplicated, but none of the existing answers helped me
I'm a beginner in React and js
I want to add multiple objects inside the component
Like:
src={url}
name={text}
subTitle={subtext}
my index.js
const tableColumns = [
{
title: 'Title/Artist',
dataIndex: 'name',
key: 'name',
render: (text) => (
<div className="d-flex align-items-center">
<AvatarStatus
shape="square"
src="https://i.scdn.co/image/ab67616d00001e02bd26ede1ae69327010d49946"
name={text}
subTitle="Dua Lipa"
/>
</div>
),
},
];
return (
<>
<Table
className="no-border-last"
columns={tableColumns}
dataSource={recentReleasesData}
rowKey='id'
pagination={false}
/>
</>
my data.js
export const RecentReleasesData = [
{
id: '#5332',
artwork: 'https://i.scdn.co/image/ab67616d00001e02bd26ede1ae69327010d49946',
name: 'Future Nostalgia',
artist: 'Dua Lipa',
label: 'Warner Records',
barcode: '19029500',
releasedate: '2021-02-11',
tracks: '11',
promolink: 'Smart Link',
status: 'Approved',
},
{
id: '#6438',
artwork: 'https://i.scdn.co/image/ab67616d00001e02caf82abb2338880577e472be',
name: 'Love',
artist: 'Someonw',
label: 'UMG Records',
barcode: '50029500',
releasedate: '2017-08-21',
tracks: '2',
promolink: 'Smart Link',
status: 'Rejected',
},
];
My comp AvatarStatus.js
import React from 'react';
import PropTypes from 'prop-types'
import { Avatar } from 'antd';
const renderAvatar = props => {
return <Avatar {...props} className={`ant-avatar-${props.type}`}>{props.text}
</Avatar>;
}
export const AvatarStatus = props => {
const { name, suffix, subTitle, id, type, src, icon, size, shape, gap, text,
onNameClick } = props
return (
<div className="avatar-status d-flex align-items-center">
{renderAvatar({icon, src, type, size, shape, gap, text })}
<div className="ml-2">
<div>
{
onNameClick ?
<div
onClick={() => onNameClick({name, subTitle, src, id})}
className="avatar-status-name clickable">{name}
</div>
:
<div className="avatar-status-name"><a href="javascript:void(0)">
{name}</a>
</div>
}
<span>{suffix}</span>
</div>
<div className="text-muted avatar-status-subtitle">{subTitle}</div>
</div>
</div>
)
}
AvatarStatus.propTypes = {
name: PropTypes.string,
src: PropTypes.string,
type: PropTypes.string,
onNameClick: PropTypes.func
}
export default AvatarStatus;
https://reactjs.org/docs/components-and-props.html
components are like JavaScript functions. They accept arbitrary inputs (called “props”) and return React elements describing what should appear on the screen.
This function is a valid React component because it accepts a single “props” (which stands for properties) object argument with data and returns a React element. We call such components “function components” because they are literally JavaScript functions.
codepen example
I found the solution
index.js
render: (_, record) => (
<Flex>
<AvatarStatus
shape="square"
size={50}
src={record.artwork}
name={record.title}
subTitle={record.artist}/>
</Flex>
),

uuid for v-for key with vue-uuid?

I'm trying to use a random string (UUID v4) with vue-uuid for current items and items added to the list in the future (this is a to-do list type app) but I'm not sure what to correct syntax is.
I installed it and added it to my project in main.js:
import UUID from 'vue-uuid';
Vue.use(UUID);
However, I don't know how to use it in my Vue component. This is what I tried:
Template:
<transition-group
name="list"
enter-active-class="animated bounceInUp"
leave-active-class="animated bounceOutDown"
>
<li v-for="item in skills" :key="uuid">{{ item.skill }}</li>
</transition-group>
Script:
import { uuid } from 'vue-uuid';
export default {
name: 'Skills',
data() {
return {
uuid: uuid.v4(),
skill: '',
skills: [{ skill: 'Vue.js' }, { skill: 'React' }]
};
},
};
For :key="uuid", I get an error saying Expected 'v-bind:key' directive to use the variables which are defined by the 'v-for' directive (vue/valid-v-for). I also tried changing it to :key="item.uuid" which makes that error go away, but then the list doesn't appear.
project repo (based on this Udemy Vue crash course)
Try this:
<template>
<div id="app">
<p :key="item.uuid" v-for="item in skills">{{ item.skill }}</p>
</div>
</template>
<script>
import { uuid } from "vue-uuid";
export default {
name: "App",
data() {
return {
skills: [
{ uuid: uuid.v4(), skill: "Vue.js" },
{ uuid: uuid.v4(), skill: "React" }
]
};
}
};
</script>
This is a working demo: https://codesandbox.io/s/nifty-sutherland-b0k9q
UPDATED
to be dynamic
There are two moments that you could add the uuid to each element in the skills array:
1 When adding a new skill:
addSkill() {
this.$validator.validateAll().then(result => {
if (result) {
this.skills.push({ uuid: uuid.v4(), skill: this.skill });
this.skill = "";
}
});
}
2 When rendering them, in this case, you might use a computed property like so:
import { uuid } from 'vue-uuid';
export default {
name: 'Skills',
data () {
return {
skill: '',
skills: [{ skill: 'Vue.js' }, { skill: 'React' }]
};
},
computed: {
computedSkills () {
return this.skills.map(skill => {...skill, uuid: uuid.v4() })
}
}
};
And then using the computedSkills computed property for rendering rather than the skills property. Something like:
<li v-for="item in computedSkills" :key="item.uuid">{{ item.skill }}</li>

creating abstract components that can manage external data

Currently I use Vuetify for base components and would like to create reusable extensions. For example a list containing checkboxes, a datatable column with some functionality etc.
For this question I will take the list containing checkboxes example. I created the following component called CheckboxGroup.vue
<template>
<v-container>
<v-checkbox
v-for="(item, index) in items"
:key="index"
v-model="item.state"
:label="item.title"
></v-checkbox>
</v-container>
</template>
<script>
export default {
props: {
items: Array,
required: true
}
};
</script>
This component takes an array of objects as a property and creates a checkbox for each entry.
Important parts are v-model="item.state" and :label="item.title". Most of the time the state attribute will have a different name, same for the title attribute.
For testing purposes I created a view file called Home.vue holding an array of documents.
<template>
<v-container>
<CheckboxGroup :items="documents"/>
<v-btn #click="saveSettings">Save</v-btn>
</v-container>
</template>
<script>
import CheckboxGroup from "../components/CheckboxGroup";
export default {
components: {
CheckboxGroup
},
data: function() {
return {
documents: [
{
id: 1,
name: "Doc 1",
deleted: false
},
{
id: 2,
name: "Doc 2",
deleted: false
},
{
id: 3,
name: "Doc 3",
deleted: true
}
]
};
},
methods: {
saveSettings: function() {
console.log(this.documents);
}
}
};
</script>
This time title is called name and state is called deleted. Obviously CheckboxGroup is not able to manage the documents because the attribute names are wrong.
How would you solve this problem? Would you create a computed property and rename these attributes? Would be a bad idea I think...
And by the way, is using v-model a good idea? A different solution would be to listen to the changed event of a checkbox and emit an event with the item index. Then you would have to listen for the change in the parent component.
I don't think there is a way to create something like
<CheckboxGroup :items="documents" titleAttribute="name" stateAttribute="deleted"/>
because it would be bad design anyway. I hope that this is a very trivial problem and every Vue developer has been confronted with it, since the primary goal should always be to develop abstract components that can be reused multiple times.
Please keep in mind that this checkbox problem is just an example. A solution for this problem would also solve same or similar problems :)
If I understood what you wanted, it`s not so trivial. Using props is a good idea. You dont need to manage the documents attribute names, just set the attribute name to your component.
Note
Renaming the attributes or using proxies is more resource-intensive like this solution, because you need to run loop to rename the attribute names or apply aliases to data array objects.
Example
CheckboxGroup.vue
<template>
<v-container fluid>
<v-checkbox
v-for="(item, index) in items"
:key="index"
v-model="item[itemModel]"
:label="item[itemValue]"
></v-checkbox>
<hr>
{{items}}
</v-container>
</template>
<script>
export default {
name: "CheckboxGroup",
props: {
items: {
type: Array,
required:true
},
itemValue:{
type:String,
default: 'title',
// validate props if you need
//validator: function (value) {
// return ['title', 'name'].indexOf(value) !== -1
// }
// or make required
},
itemModel:{
type:String,
default: 'state',
// validate props if you need
//validator: function (value) {
// validate props if you need
// return ['state', 'deleted'].indexOf(value) !== -1
// }
// or make required
}
}
};
</script>
Home.vue
<template>
<div id="app">
<checkbox-group :items="documents"
item-value="name"
item-model="deleted"
>
</checkbox-group>
</div>
</template>
<script>
import CheckboxGroup from "./CheckboxGroup.vue";
export default {
name: "App",
components: {
// HelloWorld,
CheckboxGroup
},
data: function() {
return {
documents: [
{
id: 1,
name: "Doc 1",
deleted: false
},
{
id: 2,
name: "Doc 2",
deleted: false
},
{
id: 3,
name: "Doc 3",
deleted: true
}
]
}
}
};
</script>
Based on your example I`v tried to show how to create component to managing object attributes in child component. If you need more information, please let me know.
Some good answers here that definitely solve your issue - you are essentially wanting to pass data down to a child (which isn't bad design - you were on the right track!)..
I am kind of shocked that slots or scoped-slots haven't been mentioned yet... so I figured I would chime in..
Scoped-slots allow you to take advantage of data you are passing to a child - but within the parent. The child essentially "reflects" data back to the parent, which allows you to style the child component/slot however you wish, from the parent.
This is different than just passing data via a prop attribute, because you would have to rely on styling within the child - you couldn't change the styles on a 'per-use' basis. The styles you set in the child would be "hard coded"..
In this example I am riding on top of the already provided label slot that Vuetify provides - just passing my own custom scoped-slot to it.. How to find documentation on v-checkbox slots
I made some minor changes to help spice some things up, and to show how you have greater control over styles this way (and you can use any object prop for the label you want .name, .whatever, .label, etc..)
Lastly, it is important to note that Vuetify already provides a "grouped checkbox" component - v-radio-group - I know it's called "radio-group" but it supports checkboxes...
Edit: fixed the state "issue"...
Scoped Slots With Render Function - Original Answer Moved To Bottom
Thanks to #Estradiaz for collaborating on this with me!
Vue.component('checkboxgroup', {
props: {
items: { type: Array, required: true }
},
render (h) {
return h('v-container', this.items.map((item) => {
return this.$scopedSlots.checkbox({ item });
}));
},
})
new Vue({
el: "#app",
data: {
documents: [{
id: 1,
name: "Doc 1 - delete",
deleted: false,
icon: "anchor",
},
{
id: 12,
title: "Doc 1 - state",
state: false,
icon: "anchor",
},
{
id: 2,
name: "Doc 2 - delete",
deleted: false,
icon: "mouse"
},
{
id: 3,
name: "Doc 3 - delete",
deleted: true,
icon: "watch"
}
]
},
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.min.js"></script>
<script src="https://unpkg.com/vuetify/dist/vuetify.min.js"></script>
<link href='https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Material+Icons' rel="stylesheet" type="text/css">
<link href="https://unpkg.com/vuetify/dist/vuetify.min.css" rel="stylesheet" type="text/css"></link>
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link href="https://use.fontawesome.com/releases/v5.0.8/css/all.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/MaterialDesign-Webfont/2.1.99/css/materialdesignicons.min.css" rel="stylesheet" />
<div id="app">
<v-app>
<v-container>
<CheckboxGroup :items="documents">
<template #checkbox={item}>
<v-checkbox
v-model="item[(item.name && 'deleted') || (item.title && 'state') ]" color="red">
<template #label>
<v-icon>mdi-{{item.icon}}</v-icon>
{{ item.name || item.title }}
{{ item }}
</template>
</v-checkbox>
</template>
</CheckboxGroup>
</v-container>
</v-app>
</div>
My attempt of a json to component parser
usefull names are welcome
so basically you can target element tagnames as slot #[slotname] or put slot names and target entries to overwrite the default component.
omiting tag property in the component will append children to the parent vnode
Consider:
[
{
ElementTag: 'Liste',
id: 1,
tag: 'p',
items: [
{
ElementTag: 'input',
id: 11,
type: 'checkbox',
title: "Sub Doc 1 - state",
state: true,
slotName: "slotvariant"
},
{
ElementTag: 'input',
id: 12,
type: 'date',
title: "Sub Doc 2 - Date",
date: "",
}
]
},
{
ElementTag: 'input',
id: 2,
type: 'checkbox',
title: "Doc 2 - deleted",
deleted: true,
slotName: 'deleted'
}
]
Example :
Vue.component('Liste', {
props:["tag", "items"],
render(h){
console.log(this.items)
let tag = this.tag || (this.$parent.$vnode && this.$parent.$vnode.tag)
if(tag === undefined) throw Error(`tag property ${tag} is invalid. Scope within valid vnode tag or pass valid component/ html tag as property`)
return h(tag, this.items.map(item => {
const {ElementTag, slotName, ...attrs} = item;
return (
this.$scopedSlots[slotName || ElementTag]
&& this.$scopedSlots[slotName || ElementTag]({item})
)
|| h(ElementTag, {
attrs: attrs,
scopedSlots: this.$scopedSlots
})
}))
}
})
new Vue({
data(){
return {
items: [
{
ElementTag: 'Liste',
id: 1,
tag: 'p',
items: [
{
ElementTag: 'input',
id: 11,
type: 'checkbox',
text: "Sub Doc 1 - state",
state: true,
slotName: "slotvariant"
},
{
ElementTag: 'input',
id: 12,
type: 'date',
title: "Sub Doc 2 - Date",
date: "",
}
]
},
{
ElementTag: 'input',
id: 2,
type: 'checkbox',
title: "Doc 2 - deleted",
deleted: true,
slotName: 'deleted'
}
]}
}
}).$mount('#app')
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.min.js"></script>
<div id="app">
<Liste tag="p" :items="items">
<template #input="{item}">
<label :for="item.id">
{{ item.title }}
</label>
<input :type="item.type" :id="item.id" v-model="item.date"/>
</template>
<template #slotvariant="{item}">
slotvariant - {{item.text}}<br>
</template>
<template #deleted="{item}">
<label :for="item.id">
{{ item.title }}
</label>
<input :type="item.type" :id="item.id" v-model="item.deleted"/>
</template>
</Liste>
</div>
Typescript:
import {Vue, Component, Prop} from 'vue-property-decorator'
export type AbstractElement = {
[key: string]: any // passed as $attrs | useable for assigned $props
ElementTag: string
slotName?: string
}
#Component<List>({
render(h){
let tag = this.tag
|| (this.$parent.$vnode && this.$parent.$vnode.tag)
|| (this.$parent.$el && this.$parent.$el.tagName)
if(tag === undefined) throw Error(`tag prperty: ${tag} is invalid. Scope within valid vnode tag or pass valid component/ html tag as property`)
return h(tag, this.items.map(item => {
const {ElementTag, slotName, ...attrs} = item;
console.log("slotName", slotName)
return (this.$scopedSlots[slotName || ElementTag]
&& this.$scopedSlots[slotName || ElementTag]({item}))
|| h(ElementTag, {
attrs: attrs,
slot: slotName || ElementTag,
scopedSlots: this.$scopedSlots
})
}))
}
})
export default class List extends Vue{
#Prop(String) readonly tag?: string
#Prop(Array) readonly items!: Array<AbstractElement>
}
will raise this here
You can use a Proxy to map the document property names during access.
Note
In my original answer, I used Proxy handlers for get and set, which is sufficient for plain javascript objects, but fails when used with Vue data properties because of the observer wrappers that Vue applies.
By also trapping has in the Proxy, this can be overcome. I left the original answer below for anyone interested in this problem.
Here is a demo of how to use Proxy to 'alias' Vue reactive properties to different names
without affecting the original data structure
without having to copy the data
console.clear()
Vue.config.productionTip = false
Vue.config.devtools = false
Vue.component('checkboxgroup', {
template: '#checkboxGroup',
props: { items: Array, required: true },
});
const aliasProps = (obj, aliasMap) => {
const handler = {
has(target, key) {
if (key in aliasMap) {
return true; // prevent Vue adding aliased props
}
return key in target;
},
get(target, prop, receiver) {
const propToGet = aliasMap[prop] || prop;
return Reflect.get(target, propToGet);
},
set(target, prop, value, receiver) {
const propToSet = aliasMap[prop] || prop;
return Reflect.set(target, propToSet, value)
}
};
return new Proxy(obj, handler);
}
new Vue({
el: '#app',
data: {
documents: [
{ id: 1, name: "Doc 1", deleted: false },
{ id: 2, name: "Doc 2", deleted: false },
{ id: 3, name: "Doc 3", deleted: true },
]
},
computed: {
checkBoxItems() {
const aliases = {
title: 'name',
state: 'deleted'
}
return this.documents.map(doc => aliasProps(doc, aliases));
}
},
methods: {
saveSettings: function() {
console.log(this.documents);
}
},
});
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<script src="https://unpkg.com/vuetify/dist/vuetify.min.js"></script>
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Material+Icons" rel="stylesheet"/>
<link href="https://unpkg.com/vuetify/dist/vuetify.min.css" rel="stylesheet"/>
<div id="app">
<v-app id="theapp">
<v-container>
<checkboxgroup :items="checkBoxItems"></checkboxgroup>
<v-btn color="info"
#click="saveSettings">Save</v-btn>
</v-container>
</v-app>
</div>
<template id="checkboxGroup">
<v-container style="display: flex">
<v-checkbox
v-for="(item, index) in items"
:key="index"
v-model="item.state"
:label="item.title"
></v-checkbox>
</v-container>
</template>
Original answer
You can use a Proxy to map the document property names during access.
<template>
...
<CheckboxGroup :items="checkBoxItems"/>
...
</template>
<script>
export default {
...
computed: {
checkBoxItems() {
const handler = {
get: function(target, prop) {
return prop === 'title' ? target.name :
prop === 'state' ? target.deleted :
target[prop];
},
set(obj, prop, value) {
const propToSet =
prop === 'title' ? 'name' :
prop === 'state' ? 'deleted' :
prop;
obj[propToSet] = value;
}
};
return documents.map(doc => new Proxy(doc, handler))
},
},
...
}
</script>
Demo
const documents = [
{ id: 1, name: "Doc 1", deleted: false },
{ id: 2, name: "Doc 2", deleted: false },
{ id: 3, name: "Doc 3", deleted: true },
]
const handler = {
get: function(target, prop) {
return prop === 'title' ? target.name :
prop === 'state' ? target.deleted :
target[prop];
},
set(obj, prop, value) {
const propToSet =
prop === 'title' ? 'name' :
prop === 'state' ? 'deleted' :
prop;
obj[propToSet] = value;
}
};
const checkItems = documents.map(doc => new Proxy(doc, handler))
console.log('Accessing new property names via checkItems')
checkItems.forEach(ci => console.log(ci.id, ci.title, ci.state))
console.log('After update, values of documents')
checkItems.forEach(ci => ci.state = !ci.state )
documents.forEach(doc => console.log(doc.id, doc.name, doc.deleted))

Categories

Resources