I'm trying to have a component that can find parent/child components of the same type.
These components will also have to be "repeatable" programmatically.
Such that:
<MyComponent id="1>
<div>
<MyComponent id="2">
<div>
<MyComponent id="3"></MyComponent>
</div>
<div>
<MyComponent id="4"></MyComponent>
</div>
</MyComponent>
</div>
</MyComponent>
Basically what I need, is a way to traverse the tree of MyComponents (traversal is logically controlled).
I can pass control/parameters to the parent component, or have the parent component pass control/parameters to children components in a predefined order (based on data).
I have two methods to do this, both involve preprocessors.
One is a preprocessor that generates a new Component for each MyComponent found, with some boilerplate. Something like:
var MyComponent_1 = React.createClass({
initialState: function(){ return {currentObject: 0} },
parentCallback: function(x){ /* traversal logic */ },
render: function(){
var nodes=[];
for(var i=0;i<RepeatParam;i++) nodes.push((<div><MyComponent_2 parent={parent}></MyComponent_2></div>));
return nodes;
}
});
var MyComponent_2 /** just like the above */
Another method was to add function closures, something like this:
var $parent=0, parent=0;
<div>
(function(){parent=$parent;$parent=1;
return (<MyComponent parent={parent}>
<div>
(function(){parent=$parent;$parent=2;
<MyComponent parent={parent}></MyComponent>
})()
</div></MyComponent>}))()</div>
Yet another method was to use global variables and inject them into the createClass.
All of these methods seem wrong, and as if I have a very big misunderstanding of how React should work. Is there a more elegant way to be able to traverse the tree of components, and what is the anti-pattern I am committing; how should I be doing this?
This can now be done using the "context" api
export class Hierarchy {
contextTypes: {
level: React.PropTypes.number
}
childContextTypes: {
level: React.PropTypes.number
}
getChildContext() {
return {
level: this.context.level + 1
};
}
render() {
return <div>{this.context.level}{this.children}</div>;
}
}
A combination of higher order components, contexts, and Flux makes it easier to implement.
Related
I want to assign some attributes and classes to the children VNode through data object. That just works. But during my Vue.js investigation, I have not seen such pattern in use, that's why I don't think it's good idea to modify children VNode's.
But that approach sometimes comes in handy – for example I want to assign to all the buttons in default slot the aria-label attribute.
See example below, using default stateful components:
Vue.component('child', {
template: '<div>My role is {{ $attrs.role }}</div>',
})
Vue.component('parent', {
render(h) {
const {
default: defaultSlot
} = this.$slots
if (defaultSlot) {
defaultSlot.forEach((child, index) => {
if (!child.data) child.data = {}
if (!child.data.attrs) child.data.attrs = {}
const {
data
} = child
data.attrs.role = 'button'
data.class = 'bar'
data.style = `color: #` + index + index + index
})
}
return h(
'div', {
class: 'parent',
},
defaultSlot,
)
},
})
new Vue({
el: '#app',
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<parent>
<child></child>
<child></child>
<child></child>
<child></child>
<child></child>
</parent>
</div>
And here is examples using stateless functional components:
Vue.component('child', {
functional: true,
render(h, {
children
}) {
return h('div', {
class: 'bar'
}, children)
},
})
Vue.component('parent', {
functional: true,
render(h, {
scopedSlots
}) {
const defaultScopedSlot = scopedSlots.default({
foo: 'bar'
})
if (defaultScopedSlot) {
defaultScopedSlot.forEach((child, index) => {
child.data = {
style: `color: #` + index + index + index
}
child.data.attrs = {
role: 'whatever'
}
})
}
return h(
'div', {
class: 'parent',
},
defaultScopedSlot,
)
},
})
new Vue({
el: '#app',
})
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<div id="app">
<parent>
<template v-slot:default="{ foo }">
<child>{{ foo }}</child>
<child>{{ foo }}</child>
<child>{{ foo }}</child>
</template>
</parent>
</div>
I am waiting for the following answers:
Yes, you can use it, there are no potential problems with this approach.
Yes, but these problem(s) can happen.
No, there are a lot of problem(s).
UPDATE:
That another good approach I have found it's to wrap child VNode into the another created VNode with appropriate data object, like this:
const wrappedChildren = children.map(child => {
return h("div", { class: "foo" }, [child]);
});
Using this approach I have no fear modifying children VNode's.
Thank you in advance.
There are potential problems with doing this. Used very sparingly it can be a useful technique and personally I would be happy to use it if no simple alternative were available. However, you're in undocumented territory and if something goes wrong you'll likely have to debug by stepping through Vue internals. It is not for the faint-hearted.
First, some of examples of something similar being used by others.
Patching key:
https://medium.com/dailyjs/patching-the-vue-js-virtual-dom-the-need-the-explanation-and-the-solution-ba18e4ae385b
An example where Vuetify patches a VNode from a mixin:
https://github.com/vuetifyjs/vuetify/blob/5329514763e7fab11994c4303aa601346e17104c/packages/vuetify/src/components/VImg/VImg.ts#L219
An example where Vuetify patches a VNode from a scoped slot: https://github.com/vuetifyjs/vuetify/blob/7f7391d76dc44f7f7d64f30ad7e0e429c85597c8/packages/vuetify/src/components/VItemGroup/VItem.ts#L58
I think only the third example is really comparable to the patching in this question. A key feature there is that it uses a scoped slot rather than a normal slot, so the VNodes are created within the same render function.
It gets more complicated with normal slots. The problem is that the VNodes for the slot are created in the parent's render function. If the child's render function runs multiple times it'll just keep getting passed the same VNodes for the slot. Modifying those VNodes won't necessarily do what you'd expect as the diffing algorithm just sees the same VNodes and doesn't perform any DOM updates.
Here's an example to illustrate:
const MyRenderComponent = {
data () {
return {
blueChildren: true
}
},
render (h) {
// Add a button before the slot children
const children = [h('button', {
on: {
click: () => {
this.blueChildren = !this.blueChildren
}
}
}, 'Blue children: ' + this.blueChildren)]
const slotContent = this.$slots.default
for (const child of slotContent) {
if (child.data && child.data.class) {
// Add/remove the CSS class 'blue'
child.data.class.blue = this.blueChildren
// Log it out to confirm this really is happening
console.log(child.data.class)
}
children.push(child)
}
return h('div', null, children)
}
}
new Vue({
el: '#app',
components: {
MyRenderComponent
},
data () {
return {
count: 0
}
}
})
.red {
border: 1px solid red;
margin: 10px;
padding: 5px;
}
.blue {
background: #009;
color: white;
}
<script src="https://unpkg.com/vue#2.6.10/dist/vue.js"></script>
<div id="app">
<my-render-component>
<div :class="{red: true}">This is a slot content</div>
</my-render-component>
<button #click="count++">
Update outer: {{ count }}
</button>
</div>
There are two buttons. The first button toggles a data property called blueChildren. It's used to decide whether or not to add a CSS class to the children. Changing the value of blueChildren will successfully trigger a re-render of the child component and the VNode does get updated, but the DOM is unchanged.
The other button forces the outer component to re-render. That regenerates the VNodes in the slot. These then get passed to the child and the DOM will get updated.
Vue is making some assumptions about what can and can't cause a VNode to change and optimising accordingly. In Vue 3 this is only going to get worse (by which I mean better) because there are a lot more of these optimisations coming along. There's a very interesting presentation Evan You gave about Vue 3 that covers the kinds of optimisations that are coming and they all fall into this category of Vue assuming that certain things can't change.
There are ways to fix this example. When the component is performing an update the VNode will contain a reference to the DOM node, so it can be updated directly. It's not great, but it can be done.
My own feeling is that you're only really safe if the patching you're doing is fixed, such that updates aren't a problem. Adding some attributes or CSS classes should work, so long as you don't want to change them later.
There is another class of problems to overcome. Tweaking VNodes can be really fiddly. The examples in the question allude to it. What if data is missing? What if attrs is missing?
In the scoped slots example in the question the child component has class="bar" on its <div>. That gets blown away in the parent. Perhaps that's intentional, perhaps not, but trying to merge together all the different objects is quite tricky. For example, class could be a string, object or array. The Vuetify example uses _b, which is an alias for Vue's internal bindObjectProps, to avoid having to cover all the different cases itself.
Along with the different formats are the different node types. Nodes don't necessarily represent components or elements. There are also text nodes and comments, where comment nodes are a consequence of v-if rather than actual comments in the template.
Handling all the different edge cases correctly is pretty difficult. Then again, it may be that none of these edge cases cause any real problems for the use cases you actually have in mind.
As a final note, all of the above only applies to modifying a VNode. Wrapping VNodes from a slot or inserting other children between them in a render function is perfectly normal.
I have the following component with a slot:
<template>
<div>
<h2>{{ someProp }}</h2>
<slot></slot>
</div>
</template>
For some reasons, I have to manually instantiate this component. This is how I am doing it:
const Constr = Vue.extend(MyComponent);
const instance = new Constr({
propsData: { someProp: 'My Heading' }
}).$mount(body);
The problem is: I am not able to create slot contents programmatically. So far, I can create simple string based slot:
const Constr = Vue.extend(MyComponent);
const instance = new Constr({
propsData: { someProp: 'My Heading' }
});
// Creating simple slot
instance.$slots.default = ['Hello'];
instance.$mount(body);
The question is - how can I create $slots programmatically and pass it to the instance I am creating using new?
Note: I am not using a full build of Vue.js (runtime only). So I don't have a Vue.js compiler available to compile the template on the fly.
I looked into TypeScript definition files of Vue.js and I found an undocumented function on Vue component instance: $createElement(). My guess is, it is the same function that is passed to render(createElement) function of the component. So, I am able to solve it as:
const Constr = Vue.extend(MyComponent);
const instance = new Constr({
propsData: { someProp: 'My Heading' }
});
// Creating simple slot
const node = instance.$createElement('div', ['Hello']);
instance.$slots.default = [node];
instance.$mount(body);
But this is clearly undocumented and hence questionable approach. I will not mark it answered if there is some better approach available.
I think I have finally stumbled on a way to programmatically create a slot element. From what I can tell, the approach does not seem to work for functional components. I am not sure why.
If you are implementing your own render method for a component, you can programmatically create slots that you pass to child elements using the createElement method (or whatever you have aliased it to in the render method), and passing a data hash that includes { slot: NAME_OF_YOUR_SLOT } followed by the array of children within that slot.
For example:
Vue.config.productionTip = false
Vue.config.devtools = false;
Vue.component('parent', {
render (createElement) {
return createElement('child', [
createElement('h1', { slot: 'parent-slot' }, 'Parent-provided Named Slot'),
createElement('h2', { slot: 'default' }, 'Parent-provided Default Slot')
])
}
})
Vue.component('child', {
template: '<div><slot name="parent-slot" /><slot /></div>'
})
new Vue({
el: '#app',
template: '<parent />'
})
<script src="https://cdn.jsdelivr.net/npm/vue#2.5.17/dist/vue.js"></script>
<div id='app'>
</div>
(This doesn't really answer How to create Vue.js slot programatically?. But it does solve your problem.)
This trick is less hackish compared to using $createElement().
Basically, create a new component that register MyComponent as a local component.
const Constr = Vue.extend({
template: `
<MyComponent someProp="My Heading">
<div>slot here !!!</div>
</MyComponent>
`,
components: {
MyComponent: MyComponent
}
});
const instance = new Constr().$mount('#app');
Demo: https://jsfiddle.net/jacobgoh101/shrn26p1/
I just came across an answer to this in vue forum:
slots
The principle is: There is nothing like createElement('slot'..)
Instead there is a render function which provides the slotted innerHtml as function:
$scopedSlots.default()
Usage:
render: function (createElement) {
const self = this;
return createElement("div", this.$scopedSlots.default());
}
If you want to provide a default in case there is no content given for the slots, you need to code a disctinction yourself and render something else.
(The link above holds a more detailed example)
The function returns an array, therefore it can not be used as a root for the render function. It need to be wrapped into single container node like div in the example above.
I am not sure how to seperate my data from my components in complex circumstances:
Imagine I am building a tabbed video game container that has an API like this:
const App = props =>
<VideoGameContainerGroup tabbed searchable>
<VideoGame id="cool-game-22" name="Cool Game, part 22" genre="RTS">
<CoolVideoGame /> {/* Implementation goes here */}
</VideoGame>
<VideoGame id="something-wild" name="Craziest game ever" genre="button">
<div>
<h1>Some inline defined game-like component</h1>
<button>click me</button>
</div>
</VideoGame>
{/* ... more games .. */}
</VideoGameContainerGroup>
It needs to not know about the game implementation, but wants to label the games somehow so it can index them, perform search and some other UI stuff:
class VideoGame extends component {
getDynamicBio() {
return `Hi, I am game ${this.props.name} fom genre ${this.props.genre}`;
}
render() {
<div className="some-VideoGameContainer-boilerplate">
{this.props.children /* implementation */}
</div>
}
}
class VideoGameContainerGroup extends Component {
render() {
return (
<div>
<div className="all-games">
{this.props.children}
</div>
{/* ... code, which through sub componentes resolve to: */}
<div className="SearchableGamesTabHeader">
{React.Children.map(this.props.children, (game, i) => {
{ /* but here I need access to complex class methods */}
<span>Game bio: {game.getDynamicBio()}</span>
{ /* this won't work */ }
})}
</div>
</div>
)
}
}
I have considered exporting a bag of helper functions, for instance:
const getDynamicBio = (component) =>
`Hi, I am game ${component.props.name} fom genre ${component.props.genre}`
But the methods are coupled to the VideoGame container and its data, and it seems weird to approach it this way.
I have considered doing something like:
<VideoGame videoGameData={ new VideoGameData({ id: 'cool-game-22', genre: 'RTS' }) }>
<GameImplementation />
</VideoGame>
And then accessing the data through VideoGame.props.videoGameData. But my insticts tell me this is really wrong.
VideoGame really is the component that contains all that meta data, from an OO approach I would store my methods related to that on that class. How would I solve this in a maintainable way in React?
We dont access child component methods, in parent, directly, in react. If you want that, do something like this.
class VideoGameContainerGroup extends Component {
render() {
return (
<div>
<div className="all-games">
{this.props.children}
</div>
{/* ... code, which through sub componentes resolve to: */}
<div className="SearchableGamesTabHeader">
{React.Children.map(this.props.children, (ChildEl, i) => {
<ChildEl complexMethodAsCb = {()=>//pass in particular work you need to do say, on click} neededPropsInComplexMethod={...}/>
<span>Game bio: {game.getDynamicBio()}</span>
</ChildEl/>
})}
</div>
</div>
)
}
}
So we trickle data and methods from top in react. We pass whatever known data and methods we have in parent, to children and children call those, after/before there specific methods.
Further, we also try to remove all data/methods out of components, in react. This is where a pattern called flux comes in. You may check redux to further read and simplify this.
Is there any way to implicitly pass data to all the child components of a parent? I'm looking for something similar to context in React. Here's a quick example of what I'm trying to accomplish:
<body>
<my-component :my-prop="foo"></my-component>
<my-component :my-prop="bar"></my-component>
</body>
-
import MyComponent from './my-component';
new Vue({
el: 'body',
components: {
MyComponent,
},
});
I'd like all of my-component's children to have access to myProp without having to pass it down every time. $root sounded like a good idea, but then I'd have to new up a vm for every component on my page, which doesn't feel to nice either.
From child component you can get parent component property like
this.$parent.myProp
https://vuejs.org/guide/components.html#Parent-Chain
Edit: Create function to find property in parents:
getProperty: function(vueComp, propName) {
var p = vueComp.$parent;
while (p) {
if(p.hasOwnProperty(propName)){
return p[propName];
}
p = p.$parent;
}
return null;
}
Use it like: var x = getProperty("myProp");
In Angular 2 you work a lot with this, which is fine but I've found that it also creates an issue when you want to pass down a function down the component hierarchy.
Take this for example:
export class ParentComponent {
myFunctionFromParent() {
this.isActive = true;
}
}
Then we pass this function down to a child:
<parent>
<child [onClick]="myFunctionFromParent"></child>
</parent>
And let's say child is a simple button:
<button (click)="onClick()"></button>
Now, when myFunctionFromParent runs, this should be the ParentComponent but it's not.
Instead it's the ChildComponent that will have it's this.isActive property changed.
This creates a lot of issues as you can't execute parent functions from a child component and expect the parent properties to change.
Passing down functions works as you would expect them to do in Angular 1, but now it seems broken.
Is this no longer the way to do things like this? What is the correct way to to this in Angular 2?
Instead of passing functions around use default Angular data binding with inputs and outputs:
class ParentComponent {
myFunctionFromParent() {
this.isActive = true;
}
}
class ChildComponent {
#Output() onClick = new EventEmitter();
}
<parent>
<child (onClick)="myFunctionFromParent()"></child>
</parent>
<button (click)="onClick.emit()"></button>
I would use this instead:
<parent>
<child (onClick)="myFunctionFromParent()"></child>
</parent>
and define an #Output in the child component:
#Component({
selector: 'child',
template: `
<button (click)="onClick()"></button>
`
})
export class ChildComponent {
#Output('onClick')
eventHandler:EventEmitter<any> = new EventEmitter();
onClick() {
this.eventHandler.emit();
}
}