Vue - Custom component parent - child communication not working - javascript

I'm currently learning vue.js and i'm struggling with the communication between parent and child components.
I'm trying to build two components (in separate files), a "accordion-container" and "accordion". The idea ist to then use them something like that on pages:
<accordion-container>
<accordion :title="'Accordion n1'">Insert HTML code here</accordion>
<accordion :title="'2nd Accordion'">Insert HTML code here</accordion>
</accordion-container>
Code for the container:
<template #closeAccordions="closeOtherAccordions">
<div class="accordion-container"><slot></slot></div>
</template>
<script>
export default {
props:['title'],
methods:{
closeOtherAccordions: function(){
console.log('Emit from child component received')
},
},
data: function() {
return {
}
}
};
</script>
Code for the accordions:
<template>
<div class="accordion" v-bind:class="{ open: isOpen }" :data-title="title">
<div class="title" #click="toggleAccordion">
<p>{{title}}</p>
</div>
<div class="content"><slot></slot></div>
</div>
</template>
<script>
export default {
props:['title'],
methods:{
toggleAccordion: function(){
this.isOpen = !this.isOpen
this.$emit('closeAccordions')
}
},
data: function() {
return {
isOpen: false
}
},
};
</script>
On the accordion i'm trying to emit "closeAccordions" (with the method toggleAccordion())
Then on the parent (accordion-container) i'd like to "listen" for that emit (with :closeAccordions="closeOtherAccordions"), and then execute a method on the parent.
But that method does not get called when i click the accordions.
Is my idea even possible? (Open to other ideas :) )

It won't work that way. The parent component cannot directly communicate to any components rendered within its slots via events, props, or by any other means that can only be achieved at the site where the slot contents are directly rendered (the container component doesn't control this).
When you are designing a component and you put a <slot> in the template, all you are doing is designating an insertion point within the template that users of the component can inject their own content.
You have 4 options:
(Advanced) Write the render function by hand and override the rendered slot vnodes to inject your own event listeners, props, etc.
Expose an API using scoped slots where you pass some data or methods to the slot which the user of the component would have to hook up in order for the component to operate correctly. Users of the component would have to remember to hook everything up correctly between the container and each accordion, so it's not ideal in this situation, but in general it is useful when you want to leave some of the functionality up to the user as to how the parent and children should operate.
Don't use events to communicate between the container and accordions, instead the accordions can call methods on the container directly via this.$parent.
Use provide/inject to allow the container to provide an API that each accordion can inject and use.
(3) is the recommended approach in this situation. The container and accordion components should be tightly coupled here. The accordion component can (and should) only be used directly within the container component, so it's OK if they communicate directly like that.
// Change this
this.$emit('closeAccordions')
// To this
this.$parent.closeOtherAccordions()
For more complicated components, (4) might be better.

Related

Change in HTML attribute of web component not reflected in Vue component

I'm facing the below problem.
I have a pure web component:
<my-web-comp options='["op1", "op2"]' active-option="op2"></my-web-comp>
This renders as two tabs with the second one selected by default. When you click on the other, the active-option HTML attribute changes to op1 and you can actually see that the property is changing in the DOM if you open the DevTools.
However, I cannot detect the change in the Vue component where I am using the web component. I have:
<template>
<div>
<my-web-comp :options="options" :active-option="activeOption"></my-web-comp>
</div>
</template>
<script>
export default {
name: 'MyVueComponent',
data() {
return {
options: '["op1", "op2"]',
activeOption: "op2"
}
},
computed: {
testVar() {
console.log("activeOption", this.activeOption) <--------- THIS LINE
},
}
}
</script>
The marked line only gets fired on the first load of the Vue component (printing "op2"). After that, testVar never gets modified again, doesn't mind if I click on the other tab and I don't see nothing in the console.
What can I be missing? I think it can be something related with Vue reactivity system, but can't wonder what.
This happens because your web-component mutates copy not a reference of this variable (copy created by your web component is also not reactive). There are two ways to change this:
You can modify your web component to use getters and setters to change value of this variable
You can use MutationObserver. To detect changes in your web-component. This approach will not require changes in this web-component
If you choose approach with MutationObserver then create this observer in vue mounted life-cycle-hook

Vue component parent from child recursive include

So I want to have many components nested to each other and included dynamically.
Lets assume simple case:
-container
-row
-container
-row
-widget
etc.
So how can I include container that will load row which will load previous component container in an elegant way (recursive I guess)
I want this functionality for more components than just container and row
I had the same problem myself right now:
it's usually webpack which cause this problem so you have two options:
Register your component Globally
On you child component, register the parent like this:
components: {
ComponentLoader: () => import('./ComponentLoader.vue')
}
you can read more here:
https://v2.vuejs.org/v2/guide/components-edge-cases.html#Circular-References-Between-Components
So to achieve my goal I have to build ComponentLoader and child components loaded from it.
ComponentLoader.vue
<template
v-for="(block, index) in data.children">
<component
v-if="component"
:is="component"
:key="`container-${block.id}-${index}`"
:data="block"/>
</template>
</template>
Which for instance will load Article component from its children:
ArticleComponent.vue
<template>
<SimpleArticle
:data="data"/>
<ComponentLoader
v-if="data.children"
:data="data"/>
</template>
So ArticleComponent will call ComponentLoader again if it has more children to load. This way works for me and is recursively going through the data tree.

Angular 1.5 to use transclude or not to use transclude

A question regarding transclude within an angular 1.5.8 component, and it's uses.
Here is an example of some code;
var app = angular.module('app', [])
function AccordionController () {
var self = this;
// add panel
self.addPanel = function(panel) {
// code to add panel
}
self.selectPanel = function() {
//code to select panel
}
}
// register the accordion component
app.component('accordion', {
template: '<!---accordion-template-->',
controller: AccordionController
}
function AccordionPanelController () {
// use parents methods here
var self = this;
// add panel
self.parent.addPanel(self);
// select panel
self.parent.selectPanel(self);
}
// register the accordion-panel component
app.component('accordionPanel', {
// require the parent component
// In this case parent is an instance of accordion component
require: {
'parent': '^accordion',
template: '<!---accrodion-panel-template-->',
controller: AccordionController
}
My question is would it be better to nest all the according panels within the parent using transclude or alternatively pass in a data array to the parent which this loops out the required number of panels based on the array passed inside using a binding.
Thanks
// added
Many thanks for your reply, an example I have of transclude possibly being necessary is in the following bit of code
<modal modal-id="editCompany" title="Edit Company"> <company-form company="$ctrl.company"></company-form> </modal>
Here we have a modal component which may have a variety of other components used within it, on the example above I am adding the company form, but this could we be an contact form. is there an alternative way?
I've worked with angular pretty extensively. Two enterprise tools managing and displaying large amounts of data, dozens of interactive widget modules, all that.
Never, once, have I had anything to do with transclude. At work we are explicitly told not to use it (link functions too). I thought this was a good thing, and the way Angular 2 turned out it seemed that thinking wasn't totally without reason.
I would go with the iteration to lay out the required number of items. At work this wouldn't be a choice because transclude wouldn't be an option.
The thing with using transclude in a component architecture is that it visually breaks the idea of single responsibility and messes with the architecture.
<html>
<navigation></navigation>
<accordion>
<accordion-panel></accordion-panel>
</accordion>
<footer></footer>
</html>
In this example you know your page has a navigation menu, an accordion and a footer. But at the index level (or root component) you don't want to know / see what the accordion contains.
So the accordion-panel component should only appear in its direct parent component.
As for your other question, through the use of require or attributes you pass an array of panels that you iterate using ng-repeat inside the accordion component.
app.component('accordion', {
template: `<accordion-panel ng-repeat="..."></accordion-panel>`,
controller: AccordionController
}

Having trouble building a simple audio player in React using HTML5

I've recently started learning React and I'm trying to build a simple audio player. I'm currently using this example as a reference but it's built in one file
https://github.com/CezarLuiz0/react-cl-audio-player
The one I'm trying to make is done in a "React" way where the UI has reusable components but I'm having trouble separating my code into meaningful and working components. For example, if I try to move some of the rendering code from the parent component (AudioPlayer) into (PlayButton), the audio methods that is created on the mounting of the parent component suddenly becomes inaccessible to the child components.
Here is my code repo.
https://github.com/vincentchin/reactmusicplayer
It works now but I'd like to improve it. Also it'd be great if someone can point out huge flaws in this since I'm sure I've broken some rules or standards to coding in React.
You can access parent component's methods from a child component by passing the method as a prop, and then invoking it inside the child component.
For example (in the child component's render method):
<button onClick={this.props.methodFromTheParent}>Click me</button>
You can also pass arguments to these methods:
<button onClick={this.props.methodFromTheParent.bind(null, 'Hello')}>Click me</button>
Remember to pass in null instead of this as the first argument when binding values to a method belonging to a parent component.
I skimmed through your repo as well. You could clean up the AudioPlayer component a lot by putting the different elements into their own components.
The render method could look something like this:
render() {
return (
<div>
<PlayButton onClick={this.togglePlay} playing={this.state.playing} />
{!this.state.hidePlayer ?
(<Player
playerState={this.state}
togglePlay={this.togglePlay}
setProgress={this.setProgress}
...
/>) : null}
</div>
);
}
And then inside the newly-created Player component:
render() {
var pState = this.props.playerState; // Just to make this more readable
return (
<div className="player">
<PlayButton onClick={this.props.togglePlay} playing={pState.playing} />
<Timeline
currentTimeDisplay={pState.currentTimeDisplay}
setProgress={this.props.setProgress}
progress={pState.progress}
...
/>
<VolumeContainer
onMouseLeave={this.props.noShow}
setVolume={this.setVolume}
toggleMute={this.toggleMute}
...
/>
</div>
);
}
You can break the layout into as many nested components as is needed and makes sense.
Remember to actually add the onClick attribute inside the child components as well (<button onClick={this.props.onClick}>Play</button>).

Re-rendering a ReactJS component hangs browser

I've been experimenting with creating a component based UI using ReactJS, versus my usual slapdash approach of a million global functions, variables and non-reusable markup. So far I really like React but I've hit a stumbling block.
Consider the following component layout
EventView
EventViewSidebar
EventViewList
EventViewListRow
EventViewDetail
In this layout, multiple occurrences of EventViewListRow are present for each unique key. Clicking an instance of EventViewListRow should update EventViewDetail with the details of that item.
This is the render function for the top level EventView component:
render: function () {
return (
<div className="event-view row-fluid">
<div className="event-view__sidebar col-md-4">
<EventViewSidebar projectId={this.state.projectId} />
</div>
<div className="event-view__content col-md-8" id="eventDetail">
</div>
</div>
);
}
And this is the EventViewDetail component
var EventViewDetail = React.createClass({
getInitialState: function () {
return { eventId: 0 };
},
render: function () {
if (this.state.eventId === 0) {
return (<h3>Nothing selected</h3>);
}
else {
return (
<div>
{this.state.eventId}
</div>
);
}
}
});
For the updating of EventViewDetail when a EventViewListRow is clicked, I have the following event handler defined in EventViewListRow
handleClick: function (event) {
event.preventDefault();
React.render(
React.createElement(EventViewDetail, { eventId: this.props.id }),
document.getElementById("eventDetail")
).setState({ eventId: this.props.id });
},
This all seems to be working fine (with the exception of the setState call above which I had to add otherwise clicking a different EventViewListRow didn't seem to have any effect - no doubt that's my first problem). The actual critical problem is that if I add default html to the eventDetail div defined in EventView then when I click the link in EventViewListRow, the following message is displayed in the console and the browser hangs.
Warning: React attempted to reuse markup in a container but the checksum was invalid. This generally means that you are using server rendering and the markup generated on the server was not what the client was expecting. React injected new markup to compensate which works but you have lost many of the benefits of server rendering. Instead, figure out why the markup being generated is different on the client or server:
(client) <h3 data-reactid=".0">Nothing selected
(server) <h3 data-reactid=".0.1.0">Select an even
Once the browser tab (Chrome 43) has hung, I have to terminate it using Task Manager.
Originally, I was calling an instance of the EventViewDetail directly, for example
<div className="event-view__content col-md-8" id="eventDetail">
<EventViewDetail />
</div>
but it also hangs if I just use vanilla HTML
<div className="event-view__content col-md-8" id="eventDetail">
<h3>Select an event to view</h3>
</div>
Clearly I'm doing something very wrong, but I'm somewhat unfamiliar with React so I don't know what that is. I read that I'm suppose to have state on the top level EventView component, but I don't have access to that and React doesn't seem to offer the ability to go back up the component chain. Unless you are supposed to pass the EventView instance as a property to each child component?
Oh, I should also add - I also tried removing the setState call from the EventViewListRow click handler in case that was the cause, but it had no effect.
Can anyone offer any advice on what it is I'm doing wrong. Should EventView have all the state for the child components, and if so, how do I reference the parent from a nested child component - do I have to pass the instance of EventView as a prop to every single child?
Sorry if these are idiot questions!
You should not call React.render in the handleClick function. Just call this.setState and React will automatically render again.

Categories

Resources