Imagine I have a <progress-bar> UI component and I want to create an <app-progress-bar> component that is the same as the <progress-bar> but with some style tweaks.
AppProgressBar.vue:
<template>
<progress-bar class="app-progress-bar"></progress-bar>
</template>
This will break because <progress-bar> requires a percent prop. So I can pass it in:
<progress-bar class="app-progress-bar" percent="percent"></progress-bar>
This seems fine, but is brittle when <progress-bar> has many props. Can I simply pass through all props? Theoretical syntax:
<progress-bar class="app-progress-bar" {...props}></progress-bar>
Which is similar to React/JSX.
Like already answered, extending the component would be a nice way to create your components in this scenario. Similar to class inheritance in other languages.
However, you can also pass an object as prop. If you want to keep things clean, and don't want to extend your components, you could pass a prop like this:
//object with many values that you need to pass. for ex.
myObjectWithSuff: { percent: 10%, someOtherStuff: value, ... }
<progress-bar class="app-progress-bar" myProp="myObjectWithStuff"></progress-bar>
Inside the progress-bar component, declare the prop myProp. Then you can access any properties on that object. For example: this.myProp.percent.
This is just quick and simple, but depending on your architecture, extending components may be the way to go.
After researching this, I don't think it's possible. Instead, you can extend a component and put an extraClass key on data or computed:
import ProgressBar from './ProgressBar'
export default {
extends: ProgressBar,
data() {
return { extraClass: 'app-progress-bar' }
}
}
ProgressBar.vue:
<template>
<div class="progress-bar" :class="extraClass">...</div>
</template>
This isn't as clean or flexible as React, but it works.
Abstract
This can be done by binding $attrs to the child. So in your case it would be:
<template>
<progress-bar v-bind="$attrs" class="app-progress-bar"></progress-bar>
</template>
Link to Docs
vm.$attrs: Contains parent-scope attribute bindings (except for class and style)...and can be passed down to an inner component
Source: Vue $attrs documentation
Example
Let's say that a component called <inner> has a boolean prop called dark and a string prop called color.
Now let's define an outer component called <outer> which wraps <inner> in its definition.
<template>
<inner></inner>
</template>
.
.
.
name: 'outer'
The issue here is if we use the <outer> component, we can't transfer the dark and color props through to the <inner> component which knows what to do with them. For example <inner dark color="blue"></inner> works, but <outer dark color="blue"></outer> doesn't.
If you change the definition of <outer> to include the $attrs binding, it will transfer all the props for you (this doesn't include style and class). Here is the new definition:
<template>
<inner v-bind="$attrs"></inner>
</template>
.
.
.
name: 'outer'
Now you can use <outer dark color="blue"></outer>.
Additional note
You can also use the inheritAttrs prop to prevent this behaviour by default. You can take a look at the official Vue docs here and I also found this JSFiddle which gives an example of inheritAttrs being used.
Related
I have been working on a Vue 2 project for a while, and upon upgrading our linting requirements I discovered that we had prop mutation errors in many of our child components. In our project, we pass a singleton object as a prop to many components and were originally updating the object directly from the child components. Vue seems to suggest using the v-bind.sync feature for updating props from child components (or using the equivalent v-bind and v-on). This, however, doesn't solve the issue of prop modification from nested components in an array.
Take this (pseudo)code for example that uses prop mutation:
Note: Assume const sharedObject: { arrayElements: Array<{ isSelected: boolean }> } = ...
Page.vue
<template>
...
<Component1 :input1="sharedObject" />
...
</template>
Component1.vue
<template>
...
<template v-for="elem in sharedObject.arrayElements">
<Component2 :input2="elem" />
</template>
...
</template>
Component2.vue
<template>
...
<q-btn #click="input2.isSelected = !input2.isSelected"></q-btn>
...
</template>
What is the proper way of updating a property like input2.isSelected from nested components in Vue 2? All of the approaches I've thought of are flawed.
Flawed Approaches
I believe that we would like to bubble up that input2.isSelected has been modified in Component2 to Page.vue, however, this seems to either lead to messy code or an uneasy feeling that we are just suppressing linting errors in a roundabout way.
To demonstrate the "messy code" approach, first note that Page.vue does not know the index of of elem in sharedObject.arrayElements. Therefore, we would need to emit an object to Page.vue from Component1 which contains the state of input2.isSelected as well of the index of elem in sharedObject.arrayElements. This gets messy quickly. What about the example where we have:
Component1.vue
<template>
...
<template v-for="elem in sharedObject.arrayElements">
<template v-for="elem2 in elem.arrayElements">
<Component2 :input2="elem2" />
</template>
</template>
...
</template>
in this case, then we could need to pass up 2 indices! It doesn't seem like a sustainable solution to me.
The alternative that I thought of is a callback function (passed as a prop through the component hierarchy) that takes as input the element we want to update and an object that contains the properties we want to update (using Object.assign).
This makes me very uneasy since I don't know the real reason why we can't update a pass-by-reference prop from a child component. To me, it seems like it's just a roundabout way of updating the passed-in from Component2 without the linter noticing. If there is some magic modification that happens to props when they're passed to child components, then surely passing in the object that I received in Component2 to the callback function and modifying it in the parent component would basically just be updating the prop in the child component, but more complicated.
What is the proper way of approaching this problem in Vue 2?
Very good question and analysis of the current state of this long-standing issue in Vue ecosystem.
Yes, modifying "value type" props from the child is a problem as it creates runtime issues (parent overwriting the changes when re-rendered) and thus Vue generates a runtime error when this happens...
Modifying a property of object passed as prop is OK from the "code works fine" POV. Unfortunately there are some influential people in the community with opinion (and many who blindly follow them) that this is an anti-pattern. I do not agree with that and raised my arguments many times (for example here). You described the reasons very well - it just creates unnecessary complexity/boilerplate code...
So what you are dealing with is really just a linting rule (vue/no-mutating-props). There is an ongoing issue/discussion that proposes the configuration option that should allow to ease the strictness of the rule with many good arguments but it gets very little attention from the maintainers (feel free to raise your voice there too)
For now what you can do is:
Disable the rule (far from perfect but luckily thanks to a Vue runtime error you can catch the real incorrect cases during development just fine)
Accept the reality and use workarounds
Workaround - use global state (store like Vuex or Pinia)
Note: Pinia is preferred as next version of Vuex will have same API
General idea is to place the sharedObject in the store and use props only to navigate the child components to the right object - in your case the Component2 will receive an index by prop and retrieve the right element from the store using it.
Stores are great for sharing the global state but using is just to overcome the linting rule is bad. Also as a result, components are coupled to the store hence both reusability suffers and testing is harder
Workaround - events
Yes it is possible to create mess and lot of boilerplate code using only events (especially if you nest components more than 2 levels) but there are ways to make things cleaner.
For example in your case Component2 does not need to know the index as you can handle the event like this
// Component1
<template>
...
<template v-for="elem in sharedObject.arrayElements">
<template v-for="(elem2, index) in elem.arrayElements">
<Component2 :input2="elem2" #update="updateElement($event, index)" />
</template>
</template>
...
</template>
In your case, the Component2 handles only change of single boolean property so $event can be simple boolean. If there are more than one property to be changed inside Component2, $event can be an object and you can use object spread syntax to "simplify" the Component2 (using one event instead of multiple - one for each property)
// Component2
<template>
...
<input v-model="someText" type="text">
<q-btn #click="updateInput('isSelected', !input2.isSelected)"></q-btn>
...
</template>
<script>
export default {
props: ['input2'],
computed: {
someText: {
get() { return this.input2.someText },
set(newVal) { updateInput('someText', newVal) }
}
},
methods: {
updateInput(propName, newValue) {
const updated = { ...this.input2 } // make a copy of input2 object
updated[propName] = newValue // change selected property
this.$emit('update', updated) // send updated object to parent
}
}
}
</script>
Well...I prefer just to disable the rule and set some clear naming conventions to indicate that the component is responsible for changing it's input...
Note that there are other workarounds like using this.$parent, inject\provide or event bus but those are really bad
In official react docs, It says the difference between 'other libraries's slot' and 'react's props.children' is as follows :
This approach may remind you of “slots” in other libraries but there are no limitations on what you can pass as props in React.
https://reactjs.org/docs/composition-vs-inheritance.html
And it sounds like there are some things can't pass through a 'vue slot'. Is there any thing can't be passed by vue slot's props?
In vue you have props as well. The difference and what may have been confusing you is that:
(vue) props === (react) props
(vue) slots === (react) props.children
You can pass data through props in both frameworks/libraries, but what you place inside <YourComponent>[content]</YourComponent will in vue terms be a slot and in react terms be accessible through props.children.
Let's say we have a popup/modal component which sole purpose is to act as a frame for the actual popup/modal content:
// parent component
<Modal>
<p>Watch out! Do you want to continue</p>
<button>Yes</button>
<button>No</button>
</Modal>
Then you would have the modalcomponent itself
// react
<div>
// this will output whatever you put inside the
// <Modal> tags in you parent component
{props.childen}
</div>
// vue
<div>
// the <slot> tag works the same way
// as React's {props.children}
<slot></slot>
</div>
Read more about vue slots here and vue props here.
When you're more familiar with the concepts you can read about vue's named slots here
Newbie here, I am studying the documentation of react and in React Context API, I couldn't understand something, I won't understand the rest of the subject if I don't understand it. Can anyone help me what does it mean through using an example?
The Toolbar component must take an extra "theme" prop
and pass it to the ThemedButton. This can become painful
if every single button in the app needs to know the theme
because it would have to be passed through all components.
class App extends React.Component {
render() {
return <Toolbar theme="dark" />;
}
}
function Toolbar(props) {
// The Toolbar component must take an extra "theme" prop
// and pass it to the ThemedButton. This can become painful
// if every single button in the app needs to know the theme
// because it would have to be passed through all components.
return (
<div>
<ThemedButton theme={props.theme} />
</div>
);
}
class ThemedButton extends React.Component {
render() {
return <Button theme={this.props.theme} />;
}
}
The Toolbar component must take an extra "theme" prop
this can be like <Toolbar theme="dark">
and pass it to the ThemedButton
how Toolbar component pass this prop to ThemedButton? and kindly clarify the rest of the comment as well.
Thank you for any help? You are kind
In your Toolbar component, it takes a parameter props, props is whatever properties have been passed to it when calling it, as in <Toolbar param1="someString" param2={someVariable}>, in this case the props value in Toolbar will be an object with the data you passed as key=value like for example: {param1: "someString", param2: content_of_someVariable}
And if you don't actually use those props (properties)/parameters in Toolbar, but rather in a subcomponent, then you have to pass them again to another level, like in <ThemedButton theme={props.theme} />, then ThemedButton itself finally passes the value to the component that actually makes use of, which is in your case: <Button theme={this.props.theme} />;.
So you had to pass the theme across multiple components, which don't use it or care at all about it, just to get it through to the final Button component.
(answer ends here, below is my effort to explain context API in an easy way)
To avoid that annoying level to level to another..., you can use the context API. Because it is really incontinent to pass a value across 3-4+ levels/components every time you want to use it in the last one in the chain.
Think about the context like a variable defined and exported on a root level and holds some data (like the user login status for example, or the theme infomation), and whenever you require that data, you import it and use it directly. You use the Provider property of the context you define (MyContext.Provider) to assign the data to it, and you use the Consumer property (MyContext.Consumer) to consume/access that data you assigned in the provider.
The beauty of the context consumer, is that whenever the data is updated in the provider, the consumer immediately gets the new data and triggers a re-render with the new data.
I hope I explained it in a simple and clear way. Write a comment with any questions or unclear parts and I can try my best to improve the answer.
Best of luck!
Props are properties that help define the way your JSX appears on the page.
When you use a component that you have created, you can pass it props like below:
<MyComponent myProp={myPropValue} />
You can continue to pass props down through the component hierarchy as well. So say you have a component tree like below:
MyComponent
--MySubComponent
----MySubSubComponent
You can pass props from MyComponent to MySubSubComponent like so:
<MyComponent myProps={MyProps} />
<MySubComponent mySubProps={props.myProps} /> //Props are the value you gave in the parent component
<MySubSubComponent mySubSubProps={props.mySubProps} />
Whatever title you give the props when declaring the component in JSX is the title you will call to get the value of the prop like props.myProps
I have a component that contains a child component which have some property that we can bind to. like:
<template>
<div>
<Child :bar="foo"></Child>
</div>
</template>
<script>
export default {
name: 'Parent',
props: ['foo'],
}
</script>
Now I want to bind the property 'bar' of the Child component conditionally. For example, I want to bind the property 'bar' of the child component only when the 'foo' property from the parent component is not undefined, how can I do that?
Update for the guys that advise me to use v-if directive:
Using v-if directive ISN'T an ideal solution, I think. When the conditions get complicated and the number of conditional binding properties get larger (like 5), the cases will grow exponentially and lead to a VERY LONG if-elseif-else chain. It's ugly, so NO.
I'm wondering if there is a REACT-LIKE way to solve this problem - just working with vdom and do binding pragmatically. but I don't know if I can do this in the render function, because I don't discover any API function that can do the same thing as v-bind directive on a child component.
You can use v-if:
<div v-if="foo">
<Child :bar="foo"></Child>
</div>
<div v-else>
<Child></Child>
</div>
However, I would recommend you not to render conditionally. It's because you can just check the bar props is not undefined.
Update:
I have found v-bind with object is useful in your case:
<Child v-bind="{bar: foo || '' }"></Child>
Also, you may try like this - ES6 feature: (But I'm not sure if this works)
<Child v-bind="...(foo && {bar: foo})"></Child>
In anticipation of Routable Components coming soon, I'm attempting to use Components wherever possible in my Ember 2.0 application. I'm running into a confusing issue where I cannot access the parent component's properties from the template when provided in block form. It may be very well that this isn't possible, but wanted to be sure. Here's an example:
Template:
// templates/approvals.hbs
{{#x-secure/schedule/approvals}}
{{#x-secure/layout/full sectionTitle=sectionTitle sectionDescription=sectionDescription}}
...
{{/x-secure/layout/full}}
{{/x-secure/schedule/approvals}}
Component Template:
// templates/components/schedule/approvals.hbs
{{yield}}
Component:
// components/schedule/approvals.js
import Ember from 'ember';
export default Ember.Component.extend({
/////////////////////////////////////
// PROPERTIES
/////////////////////////////////////
sectionTitle: 'Scheduling: Approvals',
sectionDescription: 'Lots of magical , fanstastic stuff.'
});
The issue I'm having is that I'm unable to access sectionTitle and sectionDescription from the parent component (approvals) and pass it into the layout/full component. However, if I remove code from the block of the component and move it to the templates/components/schedule/approvals.hbs template, it works as expected. Is it just not possible to access a parent component's properties from the block form of a component?
Thanks!
It is not possible indeed. The component's properties are available in the component's template, but not in the template that instantiates the component.
If you need the component to make things available, it should yield them explicitly:
// templates/components/schedule/approvals.hbs
{{yield sectionTitle sectionDescription}}
And using the component:
// templates/approvals.hbs
{{#x-secure/schedule/approvals as |title description|}}
{{#x-secure/layout/full sectionTitle=title sectionDescription=description}}
...
{{/x-secure/layout/full}}
{{/x-secure/schedule/approvals}}
Notice the as |x y ...| notation to assign names to yielded values.
Anything may be yielded this way, including this (be careful with that though, that breaks encapsulation) and actions.