Reactivity in props in Vue3 Composition API? - javascript

I'm watching a couple of props on a child component (basicSalaryMin and basicSalaryMax). When the value changes I'm then trying to update a reactive the value on the parent component (data.companyModels which is also passed to the child component as a prop inside allReactiveData).
Child component:
<template>
<div>
{{allReactiveData.companyModels}} // all data is rendered!
</div>
</template>
<script>
import { toRefs, watch, ref, reactive } from "vue";
export default {
name: 'SimPrivate',
props: {
reactiveData: {
required: true,
type: Object
},
},
setup (props, { emit }) {
const allReactiveData = ref(props.reactiveData);
const basicsalaryMin = ref(props.reactiveData.basicsalaryMin);
const basicsalaryMax = ref(props.reactiveData.basicsalaryMax);
const changeCompanyProfit = ref(props.changeCompanyProfit)
watch([basicsalaryMin, basicsalaryMax], ([newBSMin, newBSMax], [prevBSMin, prevBSMax]) =>
{
let wagesArray =[]
wagesArray.push(newBSMin, newBSMax);
adjustAllWorkersSalaries(wagesArray);
allReactiveData.companyModels.forEach(function(company)
//console is saying Uncaught (in promise) TypeError:
Cannot read property 'forEach' of undefined!!
and Can't do anything to the object from this point forward
I then need to add new sub-properties depending on
how many __ranks__ the property companyModel has...
but I'll get to that later
{
for (const [key, value] of Object.entries(company)) {
if (key === 'ranks') {
// if 1 rank add sub-object to companyModel.wages with var basicsalaryMin value called "wages: {1: basicsalaryMin}"
// if 2 ranks add sub-object: "wages:{1: basicsalaryMin, 2: basicsalaryMax }
// if 3 ranks add sub object: "wages...
// as in the model bellow but allowing for more levels
}
}
})
})
return {
allReactiveData,
basicsalaryMin,
basicsalaryMax,
}
}'
Parent component:
<template>
<div>
<input #change="handleMaxSalaries(basicsalaryMax)" id="maxsalaryInput" v-model.number='basicsalaryMax'>
<SimPrivate :reactiveData='reactiveData' #adjustAllWorkersSalaries='adjustAllWorkersSalaries'/>
</div>
</template>
<script>
</script>
import { toRefs, watch, ref, reactive } from "vue";
import SimPrivate from '../views/SimPrivate.vue'
export default {
name: "Simulator",
components: {
Slider,
SimPrivate
},
props: {},
setup( props, {emit}) {
let data = reactive({
avrgProfit: 0,
basicsalaryMin: 3000,
basicsalaryMax: 5000,
TotalUBICreatedPerMonth: 0,
companyModels: [
{ id: 'Big', workers: 250, ranks: 5, companyAvrgProfit: 0, totalWages: Number, wages: {1: '3000', 2: '3500', 3:'4000', 4: '4500', 5: '5000' }},
{ id: 'Medium', workers: 75, ranks: 3, companyAvrgProfit: 0, totalWages: Number, wages: {1: '3000', 2: '4000', 3:'5000' }},
{ id: 'Small', workers: 10, ranks: 2, companyAvrgProfit: 0, totalWages: Number, wages: {1: '3000', 2: '5000' }},
{ id: 'Individual', workers: 1, ranks: 1, companyAvrgProfit: 0, totalWages: Number, wages: {1: '3000'}}}
],
)}
let reactiveData = toRefs(data)
return {
allReactiveData,
basicsalaryMin,
basicsalaryMax,
}
)}
The goal is to then check the value of ranks (which will vary between 1 and 100) and create as many equidistant wage values as needed to match the rank number.
Any thoughts?

If you want to access or modify a ref from your script you need to do
yourref.value.
e.g.
yourref.value = 'Hello'
console.log(yourref.value)
// outputs : 'Hello'
So in your case allReactiveData.value
See docs

Related

Uncaught TypeError: Cannot read properties of undefined (reading 'undefined')

I'm trying to change the state created with the useState hook when clicked. But I do not understand this mistake.
Uncaught TypeError: Cannot read properties of undefined (reading 'undefined'). I do not understand why this happens after setState (state.activeQuestion + 1)
import React, {useState} from 'react'
import classes from './Quiz.module.css'
import ActiveQuiz from '../../components/ActiveQuiz/ActiveQuiz'
export default function Quiz () {
const [state, setState] = useState(
{ activeQuestion: 0,
quiz: [
{
question: 'Якого коліру небо',
rightAnswerId: 2,
id: 1,
answers: [
{text: 'чорний', id: 1},
{text: 'синій', id: 2},
{text: 'червоний', id: 3},
{text: 'зелений', id: 4}
]
},
{
question: 'Якому році 2 світова',
rightAnswerId: 3,
id: 2,
answers: [
{text: '1954', id: 1},
{text: '1948', id: 2},
{text: '1949', id: 3},
{text: '1918', id: 4}
]
}
],}
)
const onAnswerClickHandler = answerId => {
console.log('Answer Id: ', answerId);
setState(state.activeQuestion + 1)
}
return(
<div className={classes.Quiz}>
<div className={classes.QuizWraper}>
<h1> Дайте відповідь на всі Питання </h1>
<ActiveQuiz
answers={state.quiz[state.activeQuestion].answers}
question={state.quiz[state.activeQuestion].question}
onAnswerClick={onAnswerClickHandler}
quizLength={state.quiz.length}
answerNumber={state.activeQuestion + 1}
/>
</div>
</div>
)
}
enter image description here
in your code, with onAnswerClickHandler you are changing the shape of your state. Your state is an object and holds different values.
const onAnswerClickHandler = answerId => {
console.log('Answer Id: ', answerId);
setState(state.activeQuestion + 1)
}
in here you are changing your state to a number:
state = {object stuff here}//
//after you call the function
state = 0 + 1 // all the other stuff is gone
as #cybercoder commented, you should use spread operator:
setState(prev=>({...state, activeQuestion : prev.activeQuestion + 1})) –
with spread operator, you copy all the data that your object contains and you only update the necessary value that you want to update

How to map enum to select dropdown in Storybook?

I have a simple JS "enum" like this
const MyEnum = {
Aaa: 1,
Bbb: 84,
};
And I have a simple story:
import MyEnum from 'models/my-enum';
import HotSpot from 'hot-spot/hot-spot.vue';
import hotSpotProp from './hot-spot.stories.defaults';
export default {
title: 'components/catalog/images/HotSpot',
args: {
hotspotProp: hotSpotProp,
currentWidth: 360,
selectedCallouts: [],
calloutMode: true,
originalWidth: 2100,
title: 'Example tooltip',
},
argTypes: {
oemId: {
options: Object.keys(MyEnum), // an array of serializable values
mapping: MyEnum, // maps serializable option values to complex arg values
control: {
type: 'select', // type 'select' is automatically inferred when 'options' is defined
// labels: MyEnum,
},
},
},
};
const Template = (args, { argTypes }) => ({
components: { HotSpot },
template: `<HotSpot v-bind="$props" />`,
props: Object.keys(argTypes),
});
export const Default = Template.bind({});
Example from docs is not working.
I have a select dropdown working, but it returns a String instead of a Number from mapping.
I get an error in my storybook in the console:
[Vue warn]: Invalid prop: type check failed for prop "oemId". Expected Number with value NaN, got String with value "Aaa".
How to map enum to select dropdown in Storybook?
That storybook doc example is absolute horror.
Here's an example that will instantly show you what to do.
myValueList: {
options: [0, 1, 2], // iterator
mapping: [12, 13, 14], // values
control: {
type: 'select',
labels: ['twelve', 'thirteen', 'fourteen'],
},
}
Enums end up as Objects, so:
enum Nums {
Zero,
One,
Two,
Three,
}
Seems to become an Object that looks like:
{
0: "Zero",
1: "One",
2: "Two",
3: "Three",
One: 1,
Three: 3,
Two: 2,
Zero: 0,
}
Since all object keys are strings or symbols in JavaScript, the only way I've been able to guarantee I only get the string values from an Enum is to use Object.values and filter strings:
oemId: {
options: Object.values(MyEnum).filter(x => typeof x === "string"),
mapping: MyEnum,
control: {
type: 'select',
},
},
Or, filter out the keys and retain an Object - this way Storybook can still default the value without issues:
options: Object.entries(MyEnum)
.filter(([, value]) => typeof value !== "string")
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {})
You are looking for Object.values not the .keys.
const MyEnum = {
Aaa: 1,
Bbb: 84,
};
Object.values(MyEnum); // -> [ 1, 84 ]
Object.keys(MyEnum); // -> [ "Aaa", "Bbb" ]
"Easy" (with this little helper):
function enumOptions(someEnum) {
return {
options: Object.keys(someEnum)
.filter((key) => !isNaN(parseInt(key)))
.map((key) => parseInt(key)),
mapping: someEnum,
control: {
type: 'select',
labels: Object.values(someEnum).filter(
(value) => typeof value === 'string'
),
},
}
}
This can the be used in the OP's example code as follows:
export default {
title: 'components/catalog/images/HotSpot',
args: {
oemId: MyEnum.MyValue // default value for all stories can be used,
// here "MyValue" will be preselected in dropdown
// (or individual `story.args` oemId value from MyEnum)
},
argTypes: {
oemId: enumOptions(MyEnum)
},
};
It is indeed surprising that this is not an out-of-the-box feature in storybook, requiring such a rather contrived workaround.
Thanks to #Anthony's and #Lee Chase's answers pointing in the right direction.
The easiest way without any helper for me was:
export default {
title: 'One/SingleBarItem',
component: SingleBarItem,
// 👇 Creates drowdown to select Phase enum values
argTypes: {
phase: {
options: Object.values(NodeExecutionPhase),
mapping: Object.values(NodeExecutionPhase),
control: {
type: 'select',
labels: Object.keys(NodeExecutionPhase),
},
},
},
} as ComponentMeta<typeof SingleBarItem>;
Where NodeExecutionPhase defined as:
enum Phase {
UNDEFINED = 0,
QUEUED = 1,
}
I have given up on mapping for now and use a computed value, it pollutes the template a bit but a utility function or two can make it look a little tidier.
argTypes: {
myValueList: {
options: [0, 1, 2], // iterator
control: {
type: 'select',
labels: ['twelve', 'thirteen', 'fourteen'],
},
}
}
// .
// .
// .
const mappingMyValueList = [12, 13, 14];
// .
// .
// .
computed() {
computedMyValueList() {
return () => mappingMyValueList[this.$props.myValueList];
}
}
// .
// .
// .
<div>{{computedMyValueList}}</div>

Rendering React/Redux app multiple times on a single page?

I've been working on a React/Redux application for building a quote. A gross simplification of my state would look something like this:
{
account: { name: 'john doe' },
lineItems:[
{ product: {id: 123, ...}, price: 10, units: 5 },
{ product: {id: 124, ...}, price: 10, units: 5 },
],
modifiers: { couponCode: 'asdf', vip: true }
}
and my reducers would be sliced something like this:
const appReducer = combineReducers<GlobalState>({
account: accountReducer,
lineItems: lineItemReducer,
modifiers: modifersReducer,
});
I've just recently gotten a requirements where I would essentially need to be able to render the entire app multiple times on a single page (basically show 1 or more quotes for different accounts on a single page). So a single state would now need to look something like this:
{
quotes: {
"0": {
account: { name: 'john doe' },
lineItems:[
{ product: {id: 123, ...}, price: 10, units: 5 },
{ product: {id: 124, ...}, price: 10, units: 5 },
],
modifiers: { couponCode: 'asdf', vip: true }
},
"1": {
account: { name: 'billy jean' },
lineItems:[
{ product: {id: 123, ...}, price: 10, units: 5 },
],
modifiers: { couponCode: '', vip: false }
},
}
}
But obviously this new state shape doesn't really work with how I've sliced my reducers. Also, seems like I'd have to refactor all my actions so that I know which quote they should be operating on? For example, if I had an action like this:
{
type: 'UPDATE_PRICE'
payload: { productId: 123, newPrice: 15 }
}
Seems like the product 123 on both quotes would be updated.
Maybe there is instead some way I can just render the entire app on the page without having to refactor my entire state? I'm not sure what my best approach would be that wouldn't requirement me to rewrite large portions of the app.
This should give you the idea. It's basically using one reducer inside another one. As simple as using a function within another function body. You can run it on runkit.com as well.
const { createStore, combineReducers } from 'redux';
const UPDATE_ACCOUNT = 'app/updat-account';
const ADD_QUOTE = 'quote/add-quote';
const appActions = {
updateAcount: (q_id, a) => ({ type: UPDATE_ACCOUNT, payload: { q_id, name: a }}),
};
const quoteActions = {
addQuote: q_id => ({ type: ADD_QUOTE, payload: q_id }),
};
const accountReducer = (app = {}, action) => {
const { type, payload } = action;
switch (type) {
case UPDATE_ACCOUNT:
return { ...app, name: payload.name }
default:
return app;
}
};
const appReducer = combineReducers({
account: accountReducer,
lineItems: (app ={}, action) => app, // just a placeholder
modifiers: (app ={}, action) => app, // just a placeholder
});
const quoteReducer = (state = {}, action) => {
const { type, payload } = action;
switch (type) {
case ADD_QUOTE:
return { ...state, [payload]: {} };
case UPDATE_ACCOUNT: {
const app = state[payload.q_id];
return app
? { ...state, [payload.q_id]: appReducer(state[payload.q_id], action) }
: state;
}
default:
return state;
}
}
const store = createStore(quoteReducer);
store.dispatch(quoteActions.addQuote(3));
store.dispatch(quoteActions.addQuote(2));
store.dispatch(appActions.updateAcount(3, 'apple'));
store.dispatch(appActions.updateAcount(4, 'orange')); // non-existent quote
store.getState():
/**
{
"2": {},
"3": {
"account": {
"name": "apple"
},
"lineItems": {},
"modifiers": {}
}
}
*/
Just wanted to add my specific answer here..
Basically I added a new root reducer as norbertpy suggested. However, I also had to add a parameter quoteId to each action to specify which quote the action originated from and should operate on. This was the most time consuming part of the refactor as now each component that dispatches actions must have access to the quote key.
Reducer
const quoteReducer = combineReducers({
account: accountReducer,
lineItems: lineItemReducer,
modifiers: modifersReducer,
});
const rootReducer = (state = {quotes: []}, action) => {
const newQuoteState = quoteReducer(state.quotes[action.quoteId], action);
const newQuotes = {...state.quotes};
newQuotes[action.quoteId] = newQuoteState;
return {...state, ...{quotes: newQuotes}};
};
Action
{
type: 'UPDATE_PRICE'
quoteId: '0',
payload: { productId: 123, newPrice: 15 }
}

Simply return a value from another component

Wondering if you guys can help. I am trying to create a generic component which when called, will return a value.
The code currently stands as follows:
import React, {Component} from 'react'
class Clients extends Component {
render () {
var userEnum = {
SMALL: 1,
MEDIUM: 2,
LARGE: 3,
properties: {
1: {name: "Admin", value: 1},
2: {name: "Manager", value: 2},
3: {name: "Standard", value: 3}
}
};
const clientName = (value) => {
return userEnum.properties[value].name
}
return null
}
}
export default Clients
and in another component, I try calling the clientName function (done an import too).
import ClientHelper from '../../helpers/clients'
...
const test = ClientHelper.clientName(2)
console.log(test)
I should expect a return value of 'Manager' but I get
TypeError: WEBPACK_IMPORTED_MODULE_9__helpers_clients.a.clientName
is not a function
You are declaring the function clientName inside the render method of the class Clients. This function is only accessible inside it's scope, the render method.
To access the function like you would, by calling the class Clients static method clientName, you should write it like this:
import React, { Component } from 'react'
class Clients extends Component {
static userEnum = {
SMALL: 1,
MEDIUM: 2,
LARGE: 3,
properties: {
1: { name: "Admin", value: 1 },
2: { name: "Manager", value: 2 },
3: { name: "Standard", value: 3 }
}
};
static clientName(value) {
return Clients.userEnum.properties[value].name;
}
render() {
return null;
}
}
export default Clients
If you do not intend to render anything with this class, you do not need react, and can simply create a utility/static class like below:
export default class Clients {
static userEnum = {
SMALL: 1,
MEDIUM: 2,
LARGE: 3,
properties: {
1: { name: "Admin", value: 1 },
2: { name: "Manager", value: 2 },
3: { name: "Standard", value: 3 }
}
};
static clientName(value) {
return Clients.userEnum.properties[value].name;
}
}
the function clientName is not a property of your class, but a local function inside the render function and therefore not accessible from the outside.
To solve this, you have to make clientName as well as your userEnum properties of the Clients object, for example in the constructor:
import React, {Component} from 'react'
class Clients extends Component {
constructor(props){
super(props);
this.userEnum = {
SMALL: 1,
MEDIUM: 2,
LARGE: 3,
properties: {
1: {name: "Admin", value: 1},
2: {name: "Manager", value: 2},
3: {name: "Standard", value: 3}
}
};
}
function clientName (value) {
return this.userEnum.properties[value].name
}
function render () {
return null
}
}
export default Clients

Multiple select Vue.js and computed property

I'm using Vue.js 2.0 and the Element UI library.
I want to use a multiple select to attribute some roles to my users.
The list of all roles available is received and assigned to availableRoles. Since it is an array of object and the v-model accepts only an array with value, I need to extract the id of the roles trough the computed property computedRoles.
The current roles of my user are received and assigned to userRoles: [{'id':1, 'name':'Admin'}, {'id':3, 'name':'User'}].
computedRoles is then equals to [1,3]
The preselection of the select is fine but I can't change anything (add or remove option from the select)
What is wrong and how to fix it?
http://jsfiddle.net/3ra1jscx/3/
<div id="app">
<template>
<el-select v-model="computedRoles" multiple placeholder="Select">
<el-option v-for="item in availableRoles" :label="item.name" :value="item.id">
</el-option>
</el-select>
</template>
</div>
var Main = {
data() {
return {
availableRoles: [{
id: 1,
name: 'Admin'
}, {
id: 2,
name: 'Power User'
}, {
id: 3,
name: 'User'
}],
userRoles: [{'id':1, 'name':'Admin'}, {'id':3, 'name':'User'}]
}
},
computed : {
computedRoles () {
return this.userRoles.map(role => role.id)
}
}
}
I agree mostly with #wostex answer, but he doesn't give you the userRoles property back. Essentially you should swap computedRoles and userRoles. userRoles becomes a computed property and computedRoles is a data property. In my update, I changed the name of computedRoles to selectedRoles.
var Main = {
data() {
return {
availableRoles: [{
id: 1,
name: 'Admin'
}, {
id: 2,
name: 'Power User'
}, {
id: 3,
name: 'User'
}],
selectedRoles:[1,2]
}
},
computed : {
userRoles(){
return this.availableRoles.reduce((selected, role) => {
if (this.selectedRoles.includes(role.id))
selected.push(role);
return selected;
}, [])
}
}
}
var Ctor = Vue.extend(Main)
new Ctor().$mount('#app')
And here is the fiddle.
Check the solution: jsfiddle
The caveat here is that computed properties are getters mainly. You can define setter for computed property, but my approach is more vue-like in my opinion.
In short, instead of v-model on computed set v-model for data property.
Full code:
<script src="//unpkg.com/vue/dist/vue.js"></script>
<script src="//unpkg.com/element-ui/lib/index.js"></script>
<div id="app">
<template>
<el-select v-model="ids" multiple placeholder="Select" #change="logit()">
<el-option v-for="item in availableRoles" :label="item.name" :value="item.id">
</el-option>
</el-select>
</template>
</div>
var Main = {
data() {
return {
availableRoles: [{
id: 1,
name: 'Admin'
}, {
id: 2,
name: 'Power User'
}, {
id: 3,
name: 'User'
}],
userRoles: [{'id':1, 'name':'Admin'}, {'id':3, 'name':'User'}],
ids: []
}
},
mounted() {
this.ids = this.userRoles.map(role => role.id);
},
methods: {
logit: function() {
console.log(this.ids);
}
}
}
var Ctor = Vue.extend(Main)
new Ctor().$mount('#app')

Categories

Resources