I am using EmberJS CLI and I am trying to wrap my head around components. However I don't quite understand what the best way is to communicate between them. I am trying to make a simple graphics editor with a side menu and a canvas. I have a "graphics-editor" component which contains the "side-menu" and "editor-canvas" components as children. If I choose an option such as "insert text" on the "side-menu" component, how do I tell the "editor-canvas" to add some text?
I have heard of actions up, data down. So should I pass the side menu model to the canvas to observe (I may also have a menu bar which can tell the canvas to do stuff later on too, so I would also have to observe that somehow)? So whenever an action on the side menu occurs, its model is updated and the canvas can observe this and act on changes to it?
Or is there a clean way to bubble the side menu action up to the parent editor component and then catch it and call an action on the canvas component?
Or should I be using views and controllers (even though EmberJS seem to be steering people away from them) instead of components. Then the controller of graphics-editor can catch an action and pass it onto editor-canvas using controllerFor?
Or should I be using Ember.Evented?
In my main application template I have:
{{graphics-editor model=model}}
My graphics editor (graphics-editor.hbs) component looks like:
<div class="ui-layout-north nopadding">
{{file-menu menuItems=model.fileMenu.menuItems}}
</div>
<div class="ui-layout-west">
{{side-menu addLabel='addLabel'}}
</div>
<div class="ui-layout-center nopadding emptyBackground">
{{editor-canvas canvasModel=model.canvas}}
</div>
<div class="ui-layout-south nopadding">
{{editor-footer}}
</div>
Keep the components. In this case of sibling components messaging each other quite a lot (side menu and canvas), I think it warrants using Ember.Evented.
You could have a event bus service (Ember.Service.extend(Ember.Evented)) injected in your components for a pub/sub mechanism. For instance in your side menu component:
eventBus: Ember.inject.service(),
actions: {
insertText: function(text) {
this.get('eventBus').trigger('insertText', text);
}
...
}
to which the canvas component would be subscribed.
I recently wrote about this subject: http://emberigniter.com/parent-to-children-component-communication/
Related
I'm often in doubt when I want a new behaviour of a component.
Let's make a simple example, I have <app-title> component:
<div>
<h1>{{title}}</h1>
</div>
Some time after, inside another page I need to put a button alongside the title. The problem is, should I create a new title component or should I parametrize the existing one?
I can edit <app-title> to look like this:
export class AppTitleComponent implements OnInit {
#Input() showButton: boolean;
title = 'App title';
constructor() {}
ngOnInit() {}
}
<div>
<h1>{{title}}</h1>
<button *ngIf="showButton">{{buttonTitle}}</button>
</div>
This is a simple example and may be obvious but using Angular I always have this problem, think about complex components: the #Input() would become many using this method, but creating a new component would increase files and complexity.
From this example you could say to create two components, one for title and another for button but that's only because this is a very simple case. Think about changing a component from "compact" mode to "expanded" and viceversa. On the one hand you may need to have the large component and on the other hand have it smaller in size and showing less information
Is there some guideline about this?
Thanks
I think it's important thinking about behavior within the context of your component. Is the button core to behavior of the title component? Does it make sense to not only display the button, but also handle its events within the context of the title component? If the answer is no, then at some granular level I'd split the components.
Here are some other things you can consider:
Anticipating that your title component may need some content wrapped with the title, you can use transclusion:
<div>
<h1>{{title}}</h1>
<ng-content></ng-content>
</div>
Then, in the parent, you'd do something like this:
<div>
<app-title-component title='title'>
<button>Some Button Text</button>
</app-title-component>
</div>
You could write a wrapper component that packages the title and the button together... ie:
<div>
<app-title-component></app-title-component>
<button>Some Button Text</button>
</div>
You could, as suggested, parameterize the configuration. I recommend thinking about if the behavior that you are parameterizing is part of the core behavior of the component. For example, if you want to paramaterize whether or not a legend shows on a chart, that makes sense because the legend is a core feature of the chart. But I probably wouldn't parameterize whether or not the chart should be followed by a raw data sheet. Instead I would create a new component for that and render them in sequence, because the data sheet is not part of the core behavior of the chart, even though sometimes I'd want to put them next to each other.
At the end of the day, you have to think about the decision in terms of your app, your app's future usability, and developer ease (e.g.- will it make sense to a future developer that this button is packaged with title).
I hope you find this helpful.
I am stuck trying pass data from Child A ($emit) component to Parent and from Parent to Child B (props).
Using nuxt.js I have:
layouts/default.vue
This default template will load a lot of components.
Those components will be used or not based on variable from child, the variable will set the v-if directive.
The children are the pages like:
pages/blog/index.vue
pages/about/index.vue
...
The goal is the Child set on Parent what components would be used, the flag can change anytime, the user can choose what will be rendered on admin area.
I have tried use local computed methods on child component, and vuex, no luck with both.
The idea on layouts/default.vue.
<template>
<div>
<TopBar v-if=showTopBar></TopBar>
<Nav v-if=showNav></Nav>
etc...
<nuxt />
</div>
</template>
<script>
import TopBar from "../components/TopBar";
import Nav from "../components/Nav";
etc...
export default {
data() {
return {
showTopBar: false,
showNav: false
etc...
};
},
}
</script>
On child already have use the $emit but no luck.
Child on this situation are pages, and the layout of those pages will be defined by variable from a fetch on the API, user can change the layout anytime.
The goal is have someting like double way between Child Components, example:
Calling route /blog will call pages/blog/index.vue
This would send to layout/default.vue using $emit what components would be rendered (choosed from user in admin area and fetched from API) and the component ID. (example: {topBar: true, topBarID: 2})
On layouts/default.vue after get the $emit from pages/blog/index.vue I would have for example TopBar false, and then not render it, or have received true with an ID, this Id will be send to TopBar as prop for render the customized TopBar made by user on Admin area.
Would be possible someone show an example how to get the pass those data for this specific cenario please?
(Does not matter if using local variables from the Child component or vuex, just looking for an example how to get the contents of variable from Child instead an plain object or undefinied object).
PS.: If there an better approach to deal with dynamic layouts, I am accepting suggestions too.
PS2.: I know I would use specific template per page, like layout/blog and layout/contact, etc... but since the idea is make an CMS, this would not fit on this scenario, I mean, from the admin area user should be able to create pages enabling or disabling components through an page Wizard (the idea is getting something like Wix, every component customization from user will be stored in the database using an Id, and on layouts user choose the previous components mounting the page, in the end all call will be made using the ids of those), and not need to add specific layouts programing, because this the Idea of set all possible components and layouts in layout/default.vue sounds at this moment an better approach, but if is not, I would love see other ways to get same goal.
The correct way to do it would be:
<child-component-1 :showNav.sync="showNav">
And within the child component you would update that by doing:
this.$emit('update:showNav', value)
The parent would define this property:
data() {
return {
showNav: default_value
}
}
You would have to pass that variable to every child component. Every child component would have to define it as a property.
Perhaps a better way to do it would be to instead create a simple store within nuxt and use that to house the settings.
Currently building a web page in Vue, and have hit a bit of an issue parsing and then rendering the <slot>'s child components.
I need to be able to take the slot, parse the components into an array, and then render those components for the end-user.
What I've Tried
I've tried many variations of things, most starting with this: this.$slots.default
This is the last version I tried
let slotComponents = [];
this.$slots.default.forEach(vNode => {
slotComponents.push(vNode);
});
But I've also tried selecting the elements within the vNode and using things like $childeren to select the components. No luck so far.
Potential Issues
The cause could be any number of things, but here is what I thought was going on (in order)
I'm not getting the components into the array properly
I'm not rendering them properly or missed something about how they render
Vue isn't supposed to do this?
Edit - Context
Seems like it would be easier if I gave you the full context of my specific problem.
Goal
To create a dynamic tab component. Should look like this.
// Example of component use
<tab-container>
<tab>
<!-- Tab Content -->
</tab>
<tab>
<!-- Tab Content -->
</tab>
<tab>
<!-- Tab Content -->
</tab>
<trash>
<!-- This one won't show up -->
</trash>
</tab-container>
In order to parse through this content, I needed to get the slot data out.
// Inside the <tabs-container> component
computed: {
tabs: function() {
let tabs = []
this.$slots.default.forEach(vNode => {
tabs.push(vNode);
});
return tabs;
}
}
// Inside the <tabs-container> template
<div>
{{tabs[currentTab]}}
</div>
You shouldn't be using template and computed properties if you want to programmatically render out <tab> inside <tab-container>. {{}} in templates are designed to perform basic operations of JS. The most appropriate way will be to use render function.
Render functions - Vue docs
Here is a working example that takes in few tabs components and shows only active tab component: https://jsfiddle.net/ajitid/eywraw8t/403667/
I am reworking an old app of mine and I am having issues with dom manipulation and basic selections within a vue instance.
Essentially I have information in a database that I load in via ajax.
Each record in the db has 2 sections. The header tab(title, time, date etc) and the body of the record(notes, ideas, etc)
When loaded, the header shows normally to the user but if they want to see what that note contains, they have to click on the header for the bottom to appear.
consider the following html:
<vuejs for loop>
<div v-bind:id='item._id' class="tabW" v-on:click="blueTabClick" >
<div class="blueTabMainColor">
<!-- header stuff here -->
</div>
<div class="notesOpenedW">
<!-- interior informaton here, HIDDEN BY CSS -->
</div>
</div>
<vuejs for loop ender>
This HTML is essentially inside a Vue for/loop directive, and generates however many "tabs(tabW)" as needed based on how much info I have in the DB
All I want the user to do is to be able to click whichever tab(tabW) they want information on, and for the notes show underneath(notesOpenedW).
I stripped my entire app and js and tried to keep it as simple a test as possible and even with the below, I still can't get anything.
here is my JS(JQ):
$(document).ready(function(evt){
$(".blueTabMainColor").click(function(){
$(this).next(".notesOpenedW").fadeToggle();
});
});
With this basic code, when I put it inside a Vue instance, via:
methods: {
blueTabClick: function (evt) {
evt.preventDefault();
$(".blueTabMainColor").click(function(){
//alert("you clicked me");
$(this).next(".notesOpenedW").fadeToggle();
});
}
}
It doesn't work, but if I take it out of the Vue instance, it works just fine.
how can I get this to work? or am I going about it the wrong way?
Vue will not cohabit happily with JQuery. You're $(this) will not work because you're not even in the document at that point, you're in pure js, virtual DOM, another universe. Then, if it did, the event listener you call may not exist. You will need to fundamentally transition this code to Vue if you want it to work, I fear.
You can achieve this by setting a ref on "notesOpenedW".
https://v2.vuejs.org/v2/api/#ref
I would strongly recommend to wrap this behaviour in a dedicated component
That would have the following content :
<div class="tabW" v-on:click="blueTabClick" >
<div class="blueTabMainColor">
<!-- header stuff here -->
</div>
<div class="notesOpenedW" ref="notesToggleDiv">
<!-- interior informaton here, HIDDEN BY CSS -->
</div>
</div>
And the method :
methods: {
blueTabClick: function () {
$(this.$refs.notesToggleDiv).fadeToggle();
}
}
Be aware that when using Vue, manipulating directly the dom is usually a bad idea.
As i showed you, it is possible to use jQuery with Vue if you absolutely need it (or cannot afford to rework more deeply your application).
Edit : Just found this article that i think would help you a lot :
https://www.smashingmagazine.com/2018/02/jquery-vue-javascript/?utm_campaign=Revue%20newsletter&utm_medium=Newsletter&utm_source=Vue.js%20Developers
I am create an app to drag a component(angular2 component, same below) from a list and drop it into another component. In this process I used interactjs to drag and drop component. But when I drop it into another component, It only load the static html in the component template which dragged.
For example I have this component to be added into another
import {Component, View} from 'angular2/angular2'
#Component{
selector:"sample"
}
#View{
template:`<p>static content</p><p>{{contentToBind}}</p>`
}
class Sample{
contentToBind:string= "I am the binding content."
}
when it added, it should looks like below
static content
I am the binding content.
But actually it just display static content like
static content
You can check this example in this plunker.
And then I bind a click HostListener to this Sample component, when I click the component has been already added into another component, "I am binding content" will appear(other event triggered is same).
I think due to interact it has jumped out of angular2's lifecycle.
Any similar method as $apply() in angularJs 1.x?
Interact is not a part of angular 2, maybe your code in Interact is out of angular 2 scope as you said.
You can use NgZone in your Interactjs file in plunker, I edit your code and it's worked now. You should send your event inside of NgZone.
something like this.
myParent.ngZone.run(
() => {
console.log("onDrop...");
ce.drop("Hello");
}
);
See it on plunker
Plunker