Is it possible to update a property on all instances of a component?
If I have 10 instances of the component below on a page, I would like to set the currentTrack property to false on all of them. Is this possible? Can it be done from inside one of the components?
import Ember from 'ember';
export default Ember.Component.extend({
currentTrack: true,
});
I'm using Ember 2.12
You can use Ember.Evented for this use case.
Here, there is a simple twiddle for it.
template.hbs
{{your-component-name currentTrack=currentTrack}}
{{your-component-name currentTrack=currentTrack}}
{{your-component-name currentTrack=currentTrack}}
// btn for disabling
<a href="#" class="btn" onclick={{action 'makeAllcurrentTracksFalse'}}>To false</a>
controller.js
currentTrack: true,
actions: {
makeAllcurrentTracksFalse() {this.set('currentTrack', false)}
}
or in your-component-name.js - you can use the same action as above and it will be applied to all components
How about you create entries for what ever thing your're trying to achieve.
const SongEntry = Ember.Object.extend({
});
To create an entry you would call (probably add a song to playlist?)
songs: [],
addNewSongToList: function(songName) {
const newEntry = MyMusicEntry.create({
isCurrent: false,
title: songName
});
this.get('songs').pushObject(newEntry);
},
activateNewSong: function(newSongToActivate) {
this.get('songs').forEach(s => s.set('isCurrent', false);
newSongToActivate.set('isCurrent', true)
}
Template would look like this
{{each songs as |song|}}
{{my-song-component songEntry=song changeSong=(action "activateNewSong")}}
{{/each}}
//my-song-component.js
<div class="song-layout" {{action "changeSong" song}}>
Related
could it be that Inertia.js page components are blocking the reactivity of vue?
I have a Page component, in this component is a normal single file component.
I have a function that adds items to the ItemsManager.items object.
When I'm running this function the single component below doesnt adds this items in the v-for.
But when I'm reload the Page Component it works and the previously added items appear.
Here the single file component:
<template>
<div>
<div v-for="item in items" :key="item.$key">
test
</div>
</div>
</template>
<script>
import { ItemsManager } from "./utils.js";
export default {
name: "test-component",
data: () => ({
items: ItemsManager.items
}),
};
</script>
utils.js:
export const ItemsManager = {
items: [],
add(item) {
item.$key = this.items.length;
this.items.unshift(item);
},
};
function that adds the items (in page component):
addItem(title, options) {
ItemsManager.add({
name: title,
options: options
});
},
Thanks in advance!
Since you're using Vue2, you need to know that there are some caveats when adding/deleting things to Objects/Arrays. You don't show any code relevant to your actual way of adding stuff to your object, but I can still recommend that you'd check this page to understand and fix your issue.
https://v2.vuejs.org/v2/guide/reactivity.html#Change-Detection-Caveats
I am new to Ember and I am currently working on 3.8 version of Ember. I just want an array to be initialize in component file and then to use the array in template file. Thanks in advance.
#mistahenry suggesting initialize array in the init function like
init() {
this._super(...arguments);
this.set('foo', [{
id: 0,
name: "baz"
}, {
id: 1,
name: "bazzz"
}]);
}
otherwise eslint throwing error with latest Ember 3.8.
on js side you can initialize like
import { computed } from '#ember/object';
export default Component.extend({
foo: computed(function() {
return [{id:0,name:"baz"},{id:1,name:"bazzz"}]
}),
...
});
on template side just call your variable
{{#each foo as |item|}}
{{item.name}}
{{/each}}
The best way to initialize an array on an Ember Object (which a component is a subclass of) is to use the Ember A helper. This is a function that returns a new Mutable Array (which provides a lot of helpful methods on top of a regular [].
New Syntax:
import Component from '#ember/component';
import { A } from '#ember/array';
export default class MyComponent extends Component {
myArray = A()
}
Older Syntax
export default Component.extend({
myArray: A()
})
Why is this even an issue? Because when you define a component, you're defining a factory for that component. When it comes time to use it, ember returns a new instance of that component class. Arrays in javascript are mutable and are stored by reference, so when you update that array, then create a new component instance, the new value of the array is also updated.
See this article for more information: https://dockyard.com/blog/2015/09/18/ember-best-practices-avoid-leaking-state-into-factories
You can create a new Ember.NativeArray by using an init hook in you component/controller.
import Component from '#ember/component';
import { A } from '#ember/array';
export default Component.extend({
tagName: 'ul',
classNames: ['pagination'],
init() {
this._super(...arguments);
if (!this.get('content')) {
this.set('content', A());
this.set('otherContent', A([1,2,3]));
}
}
});
I'm building a small vue application where among other things it is possible to delete an entry of a music collection. So at this point I have a list of music albums and next to the entry I have a "delete" button. When I do the following:
<li v-for="cd in cds">
<span>{{cd.artist}} - {{cd.album}}</span> <button v-on:click="deleteAlbum(cd.ID)">Delete</button>
</li>
and then in my methods do:
deleteAlbum(id){
this.$http.delete('/api/cds/delete/'+id)
.then(function(response){
this.fetchAll()
// });
},
this works fine so far, but to make it more nice, I want the delete functionality to appear in a modal/popup, so I made the following changes:
<li v-for="cd in cds">
<div class="cd-wrap">
<span>{{cd.artist}} - {{cd.album}}</span>
<button #click="showDeleteModal({id: cd.ID, artist: cd.artist, album: cd.album})" class="btn">Delete</button>
</div>
<delete-modal v-if="showDelete" #close="showDelete = false" #showDeleteModal="cd.ID = $event"></delete-modal>
</li>
so, as seen above I created a <delete-modal>-component. When I click on the delete button I want to pass the data from the entry to <delete-modal> component with the help of an eventbus. For that, inside my methods I did this:
showDeleteModal(item) {
this.showDelete = true
eventBus.$emit('showDeleteModal', {item: item})
}
Then, in the <delete-modal>, inside the created()-lifecycle I did this:
created(){
eventBus.$on('showDeleteModal', (item) => {
console.log('bus data: ', item)
})
}
this gives me plenty of empty opened popups/modals!!??
Can someone tell me what I'm doing wrong here?
** EDIT **
After a good suggestion I dumped the eventBus method and pass the data as props to the <delete-modal> so now it looks like this:
<delete-modal :id="cd.ID" :artist="cd.artist" :album="cd.album"></delete-modal>
and the delete-modal component:
export default {
props: ['id', 'artist', 'album'],
data() {
return {
isOpen: false
}
},
created(){
this.isOpen = true
}
}
Only issue I have now, is that it tries to open a modal for each entry, how can I detect the correct ID/entry?
I am going to show you how to do it with props since it is a parent-child relation.I will show you a simple way of doing it.You need to modify or add some code of course in order to work in your app.
Parent component
<template>
<div>
<li v-for="cd in cds" :key="cd.ID">
<div class="cd-wrap">
<span>{{cd.artist}} - {{cd.album}}</span>
<button
#click="showDeleteModal({id: cd.ID, artist: cd.artist, album: cd.album})"
class="btn"
>
Delete
</button>
</div>
<delete-modal v-if="showDelete" :modal.sync="showDelte" :passedObject="objectToPass"></delete-modal>
</li>
</div>
</template>
<script>
import Child from 'Child'
export default {
components: {
'delete-modal': Child
},
data() {
return {
showDelete: false,
objectToPass: null,
//here put your other properties
}
},
methods: {
showDeleteModal(item) {
this.showDelete = true
this.objectToPass = item
}
}
}
</script>
Child Component
<template>
/* Here put your logic component */
</template>
<script>
export default {
props: {
modal:{
default:false
},
passedObject: {
type: Object
}
},
methods: {
closeModal() { //the method to close the modal
this.$emit('update:modal')
}
}
//here put your other vue.js code
}
</script>
When you use the .sync modifier to pass a prop in child component then,there (in child cmp) you have to emit an event like:
this.$emit('update:modal')
And with that the modal will close and open.Also using props we have passed to child component the object that contains the id and other stuff.
If you want to learn more about props, click here
This official guide describes how you can bind a boolean property to disabled attribute of a HTML element. Yet it talks about a controller.
I have a button, that when clicked transitions the route (sorry it has to be a button and cannot be a link-to):
/templates/trails.hbs
<button type="button" class="btn btn-primary" disabled={{isEditing}}
onclick={{route-action 'addNew'}}>Add New</button>
(route-action is a helper that allows me to use closure actions in routes)
/routes/trails.js
import Ember from 'ember';
export default Ember.Route.extend({
actions: {
addNew() {
this.transitionTo('trails.new');
}
}
});
So, after the button is clicked, the route is changed to 'trails.new'
/routes/trails/new.js
import Ember from 'ember';
export default Ember.Route.extend({
isEditing: true,
});
This property appears to be ignored and is not bound as I had expected it would be. I also tried adding a controller:
/controllers/trails/new.js
import Ember from 'ember';
export default Ember.Controller.extend({
isEditing: true,
});
So how does the official guide suggest something that seems to not work? What piece of ember magic am I missing here?
Your template is templates/trails.hbs but you set isEditing in a subroute controller controllers/trails/new.js
You need to have controllers/trails.js and deinfe isEditing in it.
So in routes/trails.js implement this :
actions: {
willTransition: function(transition) {
if(transtions.targetName === 'trails.new'){
this.controller.set('isEditing', true);
}
else{
this.controller.set('isEditing', false);
}
}
}
After some digging around I discovered that what I was trying to do is not the right way to go about this at all. I would have to add a controller/trails.js and put the property 'isEditing' in that.
So I refactored this into a component: add-new-button. This is a far more 'ember' way.
First, I need an initializer (thanks to this question):
app/initializers/router.js
export function initialize(application) {
application.inject('route', 'router', 'router:main');
application.inject('component', 'router', 'router:main');
}
export default {
name: 'router',
initialize
};
(this injects the router into the component, so I can watch it for changes and also 'grab' the currentRoute)
My code refactored into the component:
app/components/add-new-button.js
import Ember from 'ember';
export default Ember.Component.extend({
isEditing: function() {
let currentRoute = this.get('router.currentRouteName');
return ~currentRoute.indexOf('new');
}.property('router.currentRouteName')
});
templates/components/add-new-button.hbs
<button type="button" class="btn btn-primary" disabled={{isEditing}}
onclick={{route-action 'addNew'}}>Add New</button>
templates/trails.hbs
{{add-new-button}}
The beauty of this is now I can use this button on my other top level templates to trigger route changes to the new route for each resource (and disable the button on arrival at the new route).
NOTE
return ~currentRoute.indexOf('new');
is doing a substring check on the route, if it finds 'new' returns true, otherwise returns false. See this.
In ES6 it can be replaced with (so I have!):
return currentRoute.includes('new);
I have two simple ember components; a list component and a list-item component. Data gets passed as an array to the list where it runs an each loop and makes a list-item component for each item in the array.
I'd like to, within the list-item component, take the data being passed to it from its parent list component and overwrite it. Eventually I would like to do more than just overwrite it and use it as a parameter in a function to return a new, parsed, value.
For the sake of example, lets say that this is a list of tweets.
Here is my code.
ROUTER.JS
import Ember from 'ember';
import config from './config/environment';
var Router = Ember.Router.extend({
location: config.locationType
});
Router.map(function() {
this.route('tweets');
});
export default Router;
TEMPLATES/TWEETS.HBS
{{tweet-list tweets=model}}
ROUTES/TWEETS.JS
import Ember from 'ember';
export default Ember.Route.extend({
model(){
return[{
tweetText: "this is an example #request tweet text1 #user"
},{
tweetText: "tweet of the #text2 how #cool"
}, {
tweetText: "tweet toot took text3"
}];
}
});
COMPONENTS/TWEET-LIST/COMPONENT.JS
import Ember from 'ember';
export default Ember.Component.extend({
});
COMPONENTS/TWEET-LIST/TEMPLATE.HBS
<ul>
{{#each tweets as |tweet|}}
<li>{{tweet-item tweet=tweet}}</li>
{{/each}}
</ul>
COMPONENTS/TWEET-ITEM/COMPONENT.JS
import Ember from 'ember';
export default Ember.Component.extend({
// model(){
// return "over written value here"
// }
});
COMPONENTS/TWEET-ITEM/TEMPLATE.HBS
{{tweet.tweetText}} - <!-- {{overwritten value here}} -->
I believe I have to do the overwriting in the COMPONENTS/TWEET-ITEM/COMPONENT.JS file ? How do I go about overwriting or, even better, returning a new value based off of the data passed down from the parent component?
Use different component properties for given and overwritten tweets. For example:
// components/tweet-item/component.js
import Ember from 'ember';
export default Ember.Component.extend({
// given tweet
tweet: null,
// overwritten tweet
parsedTweet: Ember.computed('tweet', function() {
return {
tweetText: this.get('tweet').tweetText + ' #overwritten'
};
}),
// you may also modify given tweet here
// but the better approach to send action up
// in favor of data-down-action-up principe
actions: {
publish: function(tweet) {
this.sendAction('publish', tweet);
}
}
});
// components/tweet-item/template.hbs
tweetText: {{parsedTweet.tweetText}}
<button {{action 'publish' tweet}}> Publish </button>
// components/tweet-list/component.js
actions: {
publish: function(tweet) {
// your logic
}
}
// components/tweet-list/template.hbs
<ul>
{{#each tweets as |tweet|}}
<li>{{tweet-item tweet=tweet publish='publish'}}</li>
{{/each}}
</ul>