So, this is a very weird one to me.
in my Redux store, I have to following meal:
{id: 39,
name: "Poisson à la basque",
restaurant: {
id: 3,
MID: "123456",
name: "le pré catelan"
}
}
I have a Container parent component and a child component where the parent provides props to the child.
(for simplicity purposes I won't explain here with I can't access directly the redux store from the child component. The thing is: I can't.)
class Parent extends Components {
render() {
return (
<Child
mealId={this.props.meal.id}
MID={this.props.meal.restaurant.MID}
/>
)
}
}
class Child extends Components {
render() {
return (
<div>
{this.props.MID ? (
<p>There is a MID</p>
) : (
<p>there is no MID</p>
)
}
)
}
}
Here is what happens: Parent and Child are called within a Modal component from reactstrap. When the Modal (and hence the Parent and Childcomponents) mounts, everything works great.
But when I trigger the event that should hide the Modal component (e.g. click outside the modal or on the cross on the top right corner), is get the following error:
TypeError: Cannot read property 'MID' of undefinedand it highilights the line from Parent: MID={this.props.meal.restaurant.MID}.
Notice that it only happens when I try to use any value from the restaurant object that is inside the meal object. When using values directly from the meal object, I get no error.
How is this possible ?
Do not hesitate to comment if I forgot to add any relevant information to help me solve the issue.
edit: here is the reducer:
case TOGGLE_MEAL_MODAL: {
return {
...state,
mealModalOpen: !state.mealModalOpen,
meal: action.payload.meal
}
}
where action.payload.meal is the kind of object mentionned above if the modal is to be open, and {} if the modal is to be closed.
When the modal unmounts, it changes the meal object to {}in the redux store. Hence this.props.meal.restaurant.MID won't work; unless the empty object looks like so:
{meals:
{
restaurant: {
MID: ""
}
}
}
so that it remains a valid object.
Related
I am having a rather strange problem with vue (v2.6.14) in which I am creating a new array based on one receive as a prop. Here's the relevant code:
props: { employees: Array },
data() {
return {
sortedEmployees: [],
};
},
mounted() {
this.sortedEmployees = this.employees.slice(0);
},
Essencially what I want here is to create a new reference for the employees array so that I can sort it to display later without actually altering the original array. I am not worried about cloning the actual objects inside the array since I will not alter them.
The problem with this is that, when the app 'hot reloads'(due to some change in the code) it works as expected, the hook is called and the component data is set according to what is expected BUT if I actually refresh the page in the browser, even though the hook is called, the component data is not set and I end up with an empty array. I can solve this by setting up a watcher to the prop and then it would set the data there, but what I am interested here is understanding what's happening. If the hook is called when the page is refreshed why doesn't it set the data properly as it does when 'hot reloading'?
I have a minimal setup created with vue-cli, no fancy configurations whatsoever. Any clue what I might be missing?
I guess the employees are loaded async, right?
I don't know your exact application structure, but the problem is usually the following:
The mounted hook gets called, when the component mounts, of course. If the employees are loaded async in the parent component, the mount hook is called, before the async call is resolved. So it will copy an empty array at this time.
With a watcher you solve this problem, because the watcher fires as soon as the async call resolves (as it mutates the employees array).
Same happens to the hotreload. When the hotreload occurs, the mounted hook gets executed again - at this time the employees array is already prefilled with values and therefore the correct array is copied in the mount hook.
Update
If you want to avoid watchers, you could also wrap your component like this:
<your-component
v-if="employees.length > 0"
:employees="employees"
/>
Be aware, that the copied employees array IN your component is still not reactive. It just copies the array on the first time it has more than 1 value. A watcher really makes sense in this case.
If you use watchers, make sure to use the immediate: true option. This ensures, that the watcher is also called the first time on render (and also on hotreload).
Summary
If you really want to copy the array, use watchers (with the immediate: true flag).
If it's just about sorting, go for the computed property solution suggested by #Sebastian Scholl
It sounds like the component is Refreshing (reseting) with the prop change however it's not Re-mounting. This is what would cause the array to reset to it's default state ([]), whereas on hot-reload the actual page is reloading.
Try the following, and if it doesn't resolver the issue I would suggest going the route of using a Watcher.
<template>
<div>
sorted: {{ sortedEmployees }}
<br />
not sorted: {{ employees }}
</div>
</template>
<script>
export default {
props: {
employees: Array,
},
data() {
return {
sortedEmployees: Array.from(this.employees),
};
}
};
</script>
Another approach is to just use a Computed method so and add any filtering/sorting logic inside that method. It would be something like:
<template>
<div>
<input v-model="sortKey" />
sorted: {{ sortedEmployees }}
</div>
</template>
<script>
export default {
props: {
employees: Array,
},
data () {
return {
sortKey: ''
}
},
computed: {
sortedEmployees() {
return Array.from(this.employees).sort(this.sortingFunction);
},
},
methods: {
sortingFunction(a, b) {
// sorting function using this.sortKey
}
}
};
</script>
UPDATED ANSWER
I switched up the code in the example a little and believe to have gotten it to work as I you're describing.
App.js
First off, I made it so that the employees array is updated after 3 seconds.
<template>
<div id="app">
<Dashboard :employees="employees" />
</div>
</template>
<script>
import Dashboard from './components/Dashboard.vue';
export default {
name: 'App',
components: { Dashboard },
data () {
return {
employees: [
{
employeeId: '1',
firstName: 'Leite',
}
]
};
},
methods: {
updateEmployees () {
this.employees = this.employees.concat([
{
employeeId: '2',
firstName: 'Jacinto',
},
{
employeeId: '3',
firstName: 'Capelo',
}
]);
}
},
mounted () {
setTimeout(this.updateEmployees, 3000)
},
};
</script>
Dashboard.js
The updated() lifecycle hook runs whenever data changes are detected (props and data properties). This effectively detected the change in the prop passed by the parent App.js component and re-rendered the data - whereas the mounted hook only ran once per page load.
<template>
<div id="dashbord">
<div
v-for="(employee, index) in sortedEmployees"
:key="employee.employeeId"
>
{{ employee.firstName }}
</div>
</div>
</template>
<script>
export default {
name: 'Dashboard',
props: {
employees: Array
},
data() {
return {
sortedEmployees: Array.from(this.employees)
};
},
updated() {
this.sortedEmployees = Array.from(this.employees)
}
};
</script>
I have a Javascript complex data structure with 2 person fields - customer and payer (both are of type Person)
{
invoice: {
id: 123,
warehouseId: 456;
customer: {
id: 777,
name: "Coco"
}
payer: {
id: 778,
name: "Roro"
}
}
}
I am using child component for displaying Person object:
class ConnectedPersonFieldSet extends Component {
render () {
return
<div>
<div>{this.props.label}</div>
<div>{this.props.data.id}</div>
<div>{this.props.data.name}</div>
</div>
}
}
const PersonFieldSet = connect(mapStateToProps, mapDispatchToProps)(ConnectedPersonFieldSet);
export default PersonFieldSet;
And I have parent component that display full Invoice object and which has 2 child components for customer and payer respectively:
class ConnectedInvoice extends Component {
render () {
return
<div>
<div>{this.props.invoice.id}</div>
<div>{this.props.invoice.warehouseId}</div>
<PersonFieldSet label={"Customer" + /* this.props.customer.name */ } data={this.props.customer}></PersonFieldSet>
<PersonFieldSet label="Payer" data={this.props.payer}></PersonFieldSet>
</div>
}
}
const Invoice = connect(mapStateToProps, mapDispatchToProps)(ConnectedInvoice);
export default Invoice;
I have also complex logic that changes just invoice.customer.name. The updated customer name becomes visible in the Invoice component:
<div>{this.props.invoice.id}</div>
But, unfortunately, the
<PersonFieldSet label={"Customer" + /* this.props.customer.name */ } data={this.props.customer}></PersonFieldSet>
stays the same. If I uncomment /* this.props.customer.name */ then the updated customer.name becomes visible both in the label and in the name subcomponent of the PersonFieldSet.
So - my question is - why the child component, which receives the object, can not detect the change of the one attribute of this object and hence, does not update visual data upon the change of the one attribute of the object?
If the child component is able to feel somehow (e.g. via label={"..." + this.props.customer.name}) that the update of the attribute happened, then the child component displays the full update of all the attributes.
How to press the child component to detect that attributes can change the forwarded object?
I have read (e.g. React: why child component doesn't update when prop changes) that there is a trick with (more or less redundant) key attribute of the child element, but is this really my case?
My understanding is that React should support the hierarchical composition of both visual components and data components and do it without tricks or any other intrigues, but how to handle my situation? Should I really start to use hacks (key or others) in this situation that is pretty standard?
Added:
I am using Redux architecture for making updates - currently I am testing update of just one field - name:
const rootReducer = (state = initialState, action) => {
switch(action.type) {
case UPDATE_INVOICE_CUSTOMER: {
let person_id = action.payload.person_id;
let data = {
invoice: state.invoice
}
let newData = updateInvoiceByCustomer(data, person_id);
return {...state,
invoice: newData.invoice,
}
}
}
}
export function updateInvoiceByCustomer(data, person_id) {
let newData = {
invoice: data.invoice,
}
/* This will be replaced by the complex business logic, that retrieves
customer from the database using person_id and afterwards complex
calculations are done on the invoice, e.g. discounts and taxes
are assigned according to the rules relevant for the specific
customer. Possible all this code will have to be moved to the chain
of promises */
newData.invoice.customer.name='Test';
return newData;
}
Thanks #Yoshi for comments on my question and for persisting to check my Redux update logic. Indeed, when I have removed all the copying-update logic (which should be corrected to use cloning) and replaced it by:
return {
...state,
['invoice']: {...state['invoice'],
['customer']: {...state['invoice']['customer'],
['name']: 'REAL-TEST',
}
}
}
Then child component started to re-render and to show the actual value without any hacks or use of key-attributes. So, that was the cause of error.
I am new to vue and I am getting this error, I am not sure if I am passing the props right and executing it well in the other component. I will explain in details what I am trying to acheive.
I am hiding a component on click on this page and showing another element on click interchangeably here.
I have read a couple of solutions but I do not understand how I'm exactly suppose to fix it
<div v-if="hidden" class="orderSummary">
<div class="orderSummary__container">
<h2 class="orderSummary__header">Order Summary</h2>
<button #click="showForm()" class="total__button">Continue</button>
<PaymentForm v-if="!hidden" :hidden="hiddenMode" />
</div>
</div>
methods: {
showForm() {
if (this.subTotal > 1) {
this.hidden = false;
}
}
},
now in the Payment Form component I need to hide the component and make the other appear also, i want to do this by passing props.
This is my code
<div class="payForm">
<div #click="hideForm()" class="PayForm__icon">
<backIcon class="icon" />
<span class="PayForm__icon-text">Go back</span>
</div>
</div>
props: ["base_amount", "value_added_tax", "hiddenMode"],
methods: {
submit() {
const data = {
name: this.name,
};
},
hideForm() {
this.hiddenMode = true;
}
},
I'm getting the error below, what do I do
Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "hiddenMode"
Don't make hiddenMode a prop; set it to state through data:
//parent
props: ["base_amount", "value_added_tax"],
data() {
return {
hiddenMode: false
}
},
...
edit:
You should also move hideForm() to the parent component and instead bind to PaymentForm's onlick:
#click="$emit('clicked')"
then in the parent component bind hideForm to the clicked emit:
<PaymentForm v-if="!hidden" :hidden="hiddenMode" #clicked="hideForm"/>
Note: the $emit doesn't have to be called "clicked" you can name it anything
First, there is a logical problem here. If hidden is false then the first DIV and children including PaymentForm are not existing.
If hidden is true the PaymentForm not showing too because you have a <PaymentForm v-if="!hidden"
Second, your PaymentForm has a hiddenMode prop and you don't set it in the parent vue. You should have :hiddenMode="hidden" and not :hidden="hiddenMode"
For you hideForm function use $emit
this.$emit('update:hiddenMode', true);
Use the .sync modifier. This way the child component does not modify the property directly. So the parent would be
<PaymentForm v-if="!hidden" :hidden-mode.sync="hiddenMode" />
and the child would be
hideForm() {
this.$emit('update:hiddenMode', true);
}
Here's how my code is structured: parent component shuffles through child components via v-if directives, one of the child components is using a state to define its data. Everything works except when I switch between the child components. When I get back, no data can be shown because the state has become null.
Parent component:
<template>
<div>
<Welcome v-if="view==0" />
<Courses v-if="view==1" /> //the component that I'm working on
<Platforms v-if="view==2" />
</div>
</template>
Courses component:
<template>
<div>Content</div>
</template>
<script>
export default {
name: 'Courses',
computed: {
...mapState([
'courses'
])
},
data () {
return {
courseList: [],
len: Number,
}
},
created () {
console.log("state.courses:")
console.log(this.courses)
this.courseList = this.courses
this.len = this.courses.length
},
}
</script>
Let say the default value for "view" is 1, when I load the page, the "Courses" component will be shown (complete with the data). If I click a button to change the value of "view" to 0, the "Welcome" component is shown. However, when I tried to go back to the "Courses" component, the courses component is rendered but is missing all the data.
Upon inspection (via console logging), I found that when the "Courses" component was initially rendered, the state was mapped correctly and I could use it, but if I changed the "view" to another value to render another component and then changed it back to the original value, the "Courses" component still renders but the state became undefined or null.
EDIT: Clarification.
Set courseList to a component name and use <component>
and set some enum
eg.
<template>
<component :is="this.viewState" />
</template>
<script>
export default {
// your stuff
data() {
return {
viewState: 'Welcome',
}
},
methods: {
getComponent(stateNum) {
const States = { '1': 'Welcome', '2': 'Courses', '3': 'Platforms' }
Object.freeze(States)
return States[stateNum]
}
},
created() {
// do your stuff here
const view = someTask() // someTask because I don't get it from where you're getting data
this.viewState = this.getComponent(view)
}
}
</script>
I don't actually understood correctly but here I gave some idea for approaching your problem.
I am struggling to figure out how to implement data down, actions up in a glimmer component hierarchy (using Ember Octane, v3.15).
I have a parent component with a list of items. When the user clicks on a button within the Parent component, I want to populate an Editor component with the data from the relevant item; when the user clicks "Save" within the Editor component, populate the changes back to the parent. Here's what happens instead:
How can I make the text box be populated with "Hello", and have changes persisted back to the list above when I click "Save"?
Code
{{!-- app/components/parent.hbs --}}
<ul>
{{#each this.models as |model|}}
<li>{{model.text}} <button {{on 'click' (fn this.edit model)}}>Edit</button></li>
{{/each}}
</ul>
<Editor #currentModel={{this.currentModel}} #save={{this.save}} />
// app/components/parent.js
import Component from '#glimmer/component';
export default class ParentComponent extends Component {
#tracked models = [
{ id: 1, text: 'Hello'},
{ id: 2, text: 'World'}
]
#tracked currentModel = null;
#action
edit(model) {
this.currentModel = model;
}
#action
save(model) {
// persist data
this.models = models.map( (m) => m.id == model.id ? model : m )
}
}
{{!-- app/components/editor.hbs --}}
{{#if #currentModel}}
<small>Editing ID: {{this.id}}</small>
{{/if}}
<Input #value={{this.text}} />
<button {{on 'click' this.save}}>Save</button>
// app/components/editor.hbs
import Component from '#glimmer/component';
import { tracked } from "#glimmer/tracking";
import { action } from "#ember/object";
export default class EditorComponent extends Component {
#tracked text;
#tracked id;
constructor() {
super(...arguments)
if (this.args.currentModel) {
this.text = this.args.currentModel.text;
this.id = this.args.currentModel.id;
}
}
#action
save() {
// persist the updated model back to the parent
this.args.save({ id: this.id, text: this.text })
}
}
Rationale/Problem
I decided to implement Editor as a stateful component, because that seemed like the most idiomatic way to get form data out of the <Input /> component. I set the initial state using args. Since this.currentModel is #tracked in ParentComponent and I would expect re-assignment of that property to update the #currentModel argument passed to Editor.
Indeed that seems to be the case, since clicking "Edit" next to one of the items in ParentComponent makes <small>Editing ID: {{this.id}}</small> appear. However, neither the value of the <Input /> element nor the id are populated.
I understand that this.text and this.id are not being updated because the constructor of EditorComponent is not being re-run when currentModel changes in the parent... but I'm stuck on what to do instead.
What I've tried
As I was trying to figure this out, I came across this example (code), which has pretty much the same interaction between BlogAuthorComponent (hbs) and BlogAuthorEditComponent (hbs, js). Their solution, as applied to my problem, would be to write EditorComponent like this:
{{!-- app/components/editor.hbs --}}
{{#if this.isEditing}}
<small>Editing ID: {{#currentModel.id}}</small>
<Input #value={{#currentModel.text}} />
<button {{on 'click' this.save}}>Save</button>
{{/if}}
// app/components/editor.hbs
import Component from '#glimmer/component';
import { tracked } from "#glimmer/tracking";
import { action } from "#ember/object";
export default class EditorComponent extends Component {
get isEditing() {
return !!this.args.currentModel
}
#action
save() {
// persist the updated model back to the parent
this.args.save({ id: this.id, text: this.text })
}
}
It works! But I don't like this solution, for a few reasons:
Modifying a property of something passed to the child component as an arg seems... spooky... I'm honestly not sure why it works at all (since while ParentComponent#models is #tracked, I wouldn't expect properties of POJOs within that array to be followed...)
This updates the text in ParentComponent as you type which, while neat, isn't what I want---I want the changes to be persisted only when the user clicks "Save" (which in this case does nothing)
In my real app, when the user is not "editing" an existing item, I'd like the form to be an "Add Item" form, where clicking the "Save" button adds a new item. I'm not sure how to do this without duplicating the form and/or doing some hairly logic as to what goes in <Input #value...
I also came across this question, but it seems to refer to an old version of glimmer.
Thank you for reading this far---I would appreciate any advice!
To track changes to currentModel in your editor component and set a default value, use the get accessor:
get model() {
return this.args.currentModel || { text: '', id: null };
}
And in your template do:
{{#if this.model.id}}
<small>
Editing ID:
{{this.model.id}}
</small>
{{/if}}
<Input #value={{this.model.text}} />
<button type="button" {{on "click" this.save}}>
Save
</button>
Be aware though that this will mutate currentModel in your parent component, which I guess is not what you want. To circumvent this, create a new object from the properties of the model you're editing.
Solution:
// editor/component.js
export default class EditorComponent extends Component {
get model() {
return this.args.currentModel;
}
#action
save() {
this.args.save(this.model);
}
}
In your parent component, create a new object from the passed model. Also, remember to reset currentModel in the save action. Now you can just check whether id is null or not in your parent component's save action, and if it is, just implement your save logic:
// parent/component.js
#tracked currentModel = {};
#action
edit(model) {
// create a new object
this.currentModel = { ...model };
}
#action
save(model) {
if (model.id) {
this.models = this.models.map((m) => (m.id == model.id ? model : m));
} else {
// save logic
}
this.currentModel = {};
}