Sync the states of multiple instances of same Ember Component - ember.js

Here's the situation:
I wrote a component facility-search which searches through some fixture data. I put multiple instances of {{facility-search}} on same template (Tab Pages). This component has some input boxes where we can write search keywords. I want to observe change in value of input box and update the same to another instance of component so that both of them will be in sync.
This is what I'm doing in components/facility-search.js
import Ember from 'ember';
import Em from 'ember';
var FacilitySearchComponent = Ember.Component.extend({
// Tag name appears in HTML
tagName: 'div',
// RandomNumber
searchBoxId: null,
// All Facilities
allFacilities: null,
// Search Values
textFacility: "",
textCountry: "",
textSpecies: "",
// Text Input Ids needed for <label for="id"/>
// Elements in multuple components should not have same id
textFacilityId: 'text-facility',
textCountryId: 'text-country',
textSpeciesId: 'text-species',
// Initialize Ids
randomNumber: function(){
this.set('searchBoxId',(Math.floor(Math.random() * 100) + 1));
this.set('textFacilityId', this.get('textFacilityId') + "-" + this.get('searchBoxId'));
this.set('textCountryId', this.get('textCountryId') + "-" + this.get('searchBoxId'));
this.set('textSpeciesId', this.get('textSpeciesId') + "-" + this.get('searchBoxId'));
}.on('init'),
// When component is inserted
didInsertElement: function() {
this.set('filteredFacilities', this.get('allFacilities'));
},
// Observe Search Values
watchForFilterChanges: function() {
this.filterResults();
}.observes('textFacility', 'textCountry', 'textSpecies'),
// Filter Data
filterResults: function() {
var facilities = // Some mechanism to filter data
self.sendAction('updateFacilities', facilities);
}.on('allFacilities'),
actions: {
clearSearch: function() {
this.set('textFacility', null);
this.set('textCountry', null);
this.set('textSpecies', null);
this.filterResults();
},
}
});
export default FacilitySearchComponent;
This is my templates/components/facility-search.hbs
<div class="card">
<div class="card-content directory-search">
<div class="card-title grey-text text-darken-3">
<h4>Search Facilities</h4>
<h4><small class="teal-text">{{filteredFacilities.length}} total</small></h4>
</div>
<form {{action "textSearch" this on="submit" data="lol"}}>
<div class="row">
<div class="input-field col s12">
<label for="{{textFacilityId}}">Search by facility</label>
{{input value=textFacility type="text" id=textFacilityId label="Facility Name"}}
</div>
<div class="input-field col s12">
<label for="{{textCountryId}}">Search by country</label>
{{input value=textCountry type="text" id=textCountryId label="Facility Country"}}
</div>
<div class="input-field col s12">
<label for="{{textSpeciesId}}">Search by species</label>
{{input value=textSpecies type="text" id=textSpeciesId label="Facility Species"}}
</div>
</div>
</form>
<a {{action 'clearSearch'}} class="waves-effect waves-light btn"><i class="material-icons right">clear_all</i>clear search</a>
</div>
</div>
This is my controllers/map.js
import Ember from 'ember';
import pagedArray from 'ember-cli-pagination/computed/paged-array';
export default Ember.Controller.extend({
// Facility to be shown in modal
selectedFacility: null,
// Facilities to be shown in search
filteredFacilities: [],
// Initialize filteredFacilities to model
initializeFacilities: function() {
this.set('filteredFacilities', this.get("model"));
}.observes('model'),
actions: {
showFacilityInModal: function(facility){
this.set('selectedFacility', facility);
},
updateFacilities: function(facilities){
this.set('filteredFacilities', facilities);
},
}
});
This is routes/map.js
import Ember from 'ember';
export default Ember.Route.extend({
model: function() {
return this.store.find('facility');
},
});
And this is how I'm using component in templates/map.hbs
{{facility-search allFacilities=model updateFacilities='updateFacilities'}}
I learned that if we put component multiple times; it will have complete new instances. So updating variables textFacility and others cannot be observed in another instance of same component. But I want to update those values in another instance as well. Any idea how we can sync the states of multiple instances of same component?

If I understand you question you want to share values between all component so if you change it in one, it changes in the other.
You can do this:
text : {
facility: "",
country: "",
species: "",
}
instead of
textFacility: "",
textCountry: "",
textSpecies: "",
Declaring it as an object means it will be shares across all component instances like a static variable.

I found a workaround !! Not sure if its a right way of doing it.
I put variables textCountry in controllers/map.js and passed it to component as follows:
{{facility-search textCountryComponentSpec=textCountry allFacilities=model updateFacilities='updateFacilities'}}
where textCountryComponentSpec holds that value in component. Then I observed changes in textCountryComponentSpec in component and update textCountry in controller. Since it is passed to components it reflects the changes.
If you know a better way please post.

Related

how emberjs get form values in a object?

templates/module.hbs
<form class="" method="post" {{ action "step1" on="submit"}}>
{{input type="email" value=email}}
{{input type="checkbox" checked=permission}}
{{input type="submit" value="next"}}
</form>
how can i reach email and checkbox value in a object (like model.email and model checkbox ) in Route
routes/module.js
export default Ember.Route.extend({
model() {
return this.store.createRecord('wizard');
},
actions: {
step1(){
alert(this.controller.get('model.email')); // returns undefined
// get form values like model.email model.checkbox
},
}
models/wizard.js
export default DS.Model.extend({
email: DS.attr('string'),
permission: DS.attr('boolean')
});
Update: [[ alerts returns undefined ]]
First you will have to create model. let's say you are working on model user
//routes/module.js
export default Ember.Route.extend({
model() {
return this.store.createRecord('wizard');
},
actions: {
step1(){
this.controller.get('model.email')// you will get email value here.
// get form values like model.email model.checkbox
}
}
then in template you have to use in the same format
//templates/module.hbs
<form class="" method="post" {{ action "step1" on="submit"}}>
{{input type="email" value=model.email}}
{{input type="checkbox" checked=permission}}
{{input type="submit" value="next"}}
</form>
In case you do not want to use a model for the simplest request, you can use jQuery's serialize method.
btnQueryClicked() {
const $form = $('.bar-query-query');
const params = $form.serializeArray();
// convert parameters to dictionary
const paramsDict = {};
params.forEach((param) => {
paramsDict[param['name']] = param['value'];
});
$.post('/query', paramsDict)
.done((data) => {
console.log(data);
});
},
Code above is what I use to make a simplest query request for data display purpose only. (It is not that elegent but you get the idea)
It is too heavy to me to create a model only for a simple request which does not need to be persistent anyway.

ember-data stores a string instead of a number

In my ember app I want to reuse a model attribute as soon as the form is submitted. But the store seems to keep it as string unless I reload the whole route. I am using this and the following components:
Ember : 1.12.0
Ember Data : 1.0.0-beta.18
jQuery : 1.11.3
/app/models/purchase.js
import DS from 'ember-data';
export default DS.Model.extend({
name: DS.attr('string'),
amount: DS.attr('number'),
createdAt: DS.attr('date', {
defaultValue: function() { return new Date(); }
}),
.. other callback and associations..
});
/app/controllers/ledger/purchases/new.js
import Ember from 'ember';
export default Ember.Route.extend({
model: function () {
return {
newPurchase: this.store.createRecord('purchase', {
name: null,
amount: null,
player: null
})
}
}
});
/app/templates/ledger/purchases/new.hbs
<div class="row">
<div class="col-xs-12">
<h4>New purchase</h4>
<form>
<div class="form-group">
<label for="name" class="sr-only control-label">name</label>
{{input id='name' type="text" value=newPurchase.name placeholder="What" class="form-control"}}
</div>
<div class="form-group">
<label for="amount" class="sr-only control-label">amount</label>
{{input id='amount' type='number' value=newPurchase.amount placeholder="How much" class="form-control"}}
</div>
<div class="form-group">
<button type="submit" class="btn btn-success" {{action "create"}}>create</button>
{{#link-to 'ledger.purchases' tagName="button" class="btn btn-link" }}cancel{{/link-to}}
</div>
</form>
</div>
</div>
/app/controllers/ledger/purchases/new.js
import Ember from 'ember';
export default Ember.Controller.extend({
newPurchase: Ember.computed.alias('model.newPurchase'),
actions: {
create: function() {
var np = this.get('newPurchase');
console.log(Ember.typeOf(np.get('amount')));
........
save np etc...
}
}
});
the console log call clearly shows that the type is a string. The ember inspector shows the same. However data are correctly saved to the backend because after reloading everything is fine. But I need the amount as a number as soon as it is submitted because I use it to make and show the sum of all purchases.
Okay, I think I know what's going on. Setting input type to number won't help here. Value is still recognized as string. Usually when you submit form, backend anyway returns this value formatted as a number and problem's gone. You can see this even when you mock your data with a number, without a backend.
My solution would be to use a computed property for input component. Model:
export default DS.Model.extend({
name: DS.attr('string'),
amount: DS.attr('number'),
createdAt: DS.attr('date', {
defaultValue: function() { return new Date(); }
}),
amountAsNum: Ember.computed('amount', {
get: function () {
return parseFloat(this.get('amount'));
},
set: function (key, value) {
var valueToSet = parseFloat(value);
this.set('amount', valueToSet);
return valueToSet;
}
}),
.. other callback and associations..
});
Template:
{{input id='amount' type='number' value=newPurchase.amountAsNum placeholder="How much" class="form-control"}}
Now, you can check typeof(amount) before save and it'll give you number. Demo on JS Bin.

how to trigger element features after render in ember-cli

I want to add tooltips onto a button in a component that can appear based on a set of results back from the server. (i.e. action buttons for delete, edit etc.)
I have created a “search” component that is rendering into the application and when a search button is clicked the server may return a number of rows into that same search component template.
so for example:
My-app/pods/factual-data/template.hbs
Contains:
…
{{#if results}}
<div class="row">
<div class="col-sm-3"><b>Factual ID</b></div>
<div class="col-sm-2"><b>Name</b></div>
<div class="col-sm-2"><b>Town</b></div>
<div class="col-sm-2"><b>Post Code</b></div>
<div class="col-sm-2"><b>Actions</b></div>
</div>
{{/if}}
{{#each result in results}}
<div class="row">
<div class="col-sm-3">{{result.factual_id}}</div>
<div class="col-sm-2">{{result.name}}</div>
<div class="col-sm-2">{{result.locality}}</div>
<div class="col-sm-2">{{result.postcode}}</div>
<div class="col-sm-2">
<button {{action "clearFromFactual" result.factual_id}} class="btn btn-danger btn-cons tip" type="button" data-toggle="tooltip" class="btn btn-white tip" type="button" data-original-title="Empty this Row<br> on Factual" ><i class="fa fa-check"></i></button>
</div>
</div>
{{/each}}
…
However I cannot get the tooltip code to function, due to an element insert detection/timing issue..
In the component
My-app/pods/factual-data/component.js
Contains:
...
didInsertElement : function(){
console.log("COMPONENT: didInsertElement");
Ember.run.scheduleOnce('afterRender', this, this.afterRenderEvent);
this.enableToolTips();
},enableToolTips: function() {
var $el = Ember.$('.tip');
console.log("TOOLTIP:", $el);
if($el.length > 0) {
$el.tooltip({
html:true,
delay: { show: 250, hide: 750 }
});
}
}
...
However it seems didInsertElement is only run when the component is first rendered, is there a different function that is called everytime something in the DOM is changed within a component?
I did try to use observes: i.e.
…
enableToolTips: function() {
var $el = Ember.$('.tip');
console.log("TOOLTIP:", $el);
if($el.length > 0) {
$el.tooltip({
html:true,
delay: { show: 250, hide: 750 }
});
}
}.observes('results')
…
Which does trigger when the results variable is changed however it is still triggering before the content is actually rendered. I am assuming this because is I manually run in the console Ember.$('.tip').tooltip() (after the button is displayed) then the tooltips work ok.
Any pointers on this issue?
Try
enableToolTips: function() {
Ember.run.scheduleOnce('afterRender', this, function() {
var $el = Ember.$('.tip');
console.log("TOOLTIP:", $el);
if($el.length > 0) {
$el.tooltip({
html:true,
delay: { show: 250, hide: 750 }
});
}
});
}.observes('results')
Checking Ember.Component API there are two hooks that can do that
willClearRender : When component html is about to change.
willInsertElement : When old html is cleared and new one is going to be placed.
But you need to have a look on scheduleOnce.
Its worth noting that didInsertElement runs every time. But when it runs view was not updated. To solve that you need to run your code inside a Run Loop like this
didInsertElement : function(){
var self = this;
Ember.run.scheduleOnce('afterRender', this, function(){
//run tool tip here
self.$().find(".tip").tooltip({
});
});
}

Multiple Choice Questionnaire Using Checkboxes and Computed Properties in Ember.js

Hi there SO community,
I'm new to development and have just recently begun learning Ember.js. As part of the Ember-Rails app that I'm building, I need to create a multi-step, multiple choice questionnaire. I've been stuck on a particular issue which I have not been able to find a solution for online.
You see, I have created a computed property, responseOptions, in my Questions1Controller. responseOptions is an array of multiple choice responses to the given question. Each question is a record in a questions table, and each multiple choice response is represented by a column on the questions table. I created the computed property responseOptions in order to display an array of checkboxes.
The problem that I am having is that I cannot pass the value of the selected array element, represented by a checked checkbox, to the Questions1Controller in order to create a new answer record. I am able to access the other properties of my questions records in the Questions1Controller, just not the computed property. I have included my Questions1Controller, along with the relevant template, below:
Questions1Controller:
RailsCharts.Questions1Controller = Ember.ObjectController.extend({
actions: {
createAnswer: function(){
var user_id = 5;
var question_id = this.get('hard_coded_id');
var selected_response = this.get('selected_answer');
var newAnswer = this.store.createRecord('answer', {
userId: user_id,
questionId: question_id,
answer: selected_response
});
newAnswer.save();
}
},
responseOptions: function () {
var option1Val = this.get('option_1');
var option2Val = this.get('option_2');
var option3Val = this.get('option_3');
var option4Val = this.get('option_4');
var option5Val = this.get('option_5');
var arryOfOptions = [option1Val, option2Val, option3Val, option4Val, option5Val];
var arryOfOptionsFiltered = [];
for (var i=0; i<arryOfOptions.length; i++){
arryOfOptions[i] !== null && arryOfOptionsFiltered.push(arryOfOptions[i]);
}
return arryOfOptionsFiltered;
}.property('arryOfOptionsFiltered.#each')
});
Questions/1 Template:
<h1>Question 1</h1>
<div class='form-group'>
<div class='wording'>
{{wording}}
</div>
<hr>
<div class='answer'>
{{#each responseOptions}}
<label class="checkbox inline">
{{input type='checkbox' checked=selected_answer}}{{this}}
</label>
</br>
{{/each}}
</div>
{{outlet}}
</div>
<button class="btn btn-default" {{action "createAnswer"}}>Submit</button>
Any help would be greatly appreciated.
You can't just have one "selected_answer" be the value of the checkbox. It needs to be some kind of array to get all of the values.
An example might be having a 'selected' property on the actual option object itself. Your model might look like this (although you can add selected at other points)
App.Questions1Route = Ember.Route.extend({
model: function(){
return Ember.Object.create({
option_1: Ember.Object.create({value: "x", selected: false}),
option_2: Ember.Object.create({value: "y", selected: false}),
option_3: Ember.Object.create({value: "z", selected: false}),
option_4: Ember.Object.create({value: "a", selected: false}),
option_5: Ember.Object.create({value: "b", selected: false})
});
}
});
Then your template would be:
<div class='answer'>
{{#each responseOptions}}
<label class="checkbox inline">
{{input type='checkbox' checked=this.selected}}{{this.value}}
</label>
</br>
{{/each}}
</div>
Let me know if that helps.
JS bin for you to play with:
http://emberjs.jsbin.com/masepexa/4/edit

getting back reference to a specific model using Ember's Array Controller

I'm new to Ember and am finding some of their concepts a bit opaque. I have a app that manages inventory for a company. There is a screen that lists the entirety of their inventory and allows them to edit each inventory item. The text fields are disabled by default and I want to have an 'edit item' button that will set disabled / true to disabled / false. I have created the following which renders out correctly:
Inv.InventoryitemsRoute = Ember.Route.extend({
model: function(params) {
return Ember.$.getJSON("/arc/v1/api/inventory_items/" + params.location_id);
}
<script type="text/x-handlebars" data-template-name="inventoryitems">
{{#each}}
<div class='row'>
<p>{{input type="text" value=header disabled="true"}}</p>
<p>{{input type="text" value=detail disabled="true"}}</p>
<button {{action "editInventoryItem" data-id=id}}>edit item</button>
<button {{action "saveInventoryItem" data-id=id}}>save item</button>
</div>
{{/each}}
</script>
So this renders in the UI fine but I am not sure how to access the specific model to change the text input from disabled/true to disabled/false. If I were just doing this as normal jQuery, I would add the id value of that specific model and place an id in the text input so that I could set the textfield. Based upon reading through docs, it seems like I would want a controller - would I want an ArrayController for this model instance or could Ember figure that out on its own?
I'm thinking I want to do something like the following but alerting the id give me undefined:
Inv.InventoryitemsController=Ember.ArrayController.extend({
isEditing: false,
actions: {
editInventoryItem: function(){
var model = this.get('model');
/*
^^^^
should this be a reference to that specific instance of a single model or the list of models provided by the InventoryitemsRoute
*/
alert('you want to edit this:' + model.id); // <-undefined
}
}
});
In the Ember docs, they use a playlist example (here: http://emberjs.com/guides/controllers/representing-multiple-models-with-arraycontroller/) like this:
App.SongsRoute = Ember.Route.extend({
setupController: function(controller, playlist) {
controller.set('model', playlist.get('songs'));
}
});
But this example is a bit confusing (for a couple of reasons) but in this particular case - how would I map their concept of playlist to me trying to edit a single inventory item?
<script type="text/x-handlebars" data-template-name="inventoryitems">
{{#each}}
<div class='row'>
<p>{{input type="text" value=header disabled="true"}}</p>
<p>{{input type="text" value=detail disabled="true"}}</p>
<button {{action "editInventoryItem" this}}>edit item</button>
<button {{action "saveInventoryItem" this}}>save item</button>
</div>
{{/each}}
</script>
and
actions: {
editInventoryItem: function(object){
alert('you want to edit this:' + object.id);
}
}
Is what you need. But let me explain in a bit more detail:
First of all, terminology: Your "model" is the entire object tied to your controller. When you call this.get('model') on an action within an array controller, you will receive the entire model, in this case an array of inventory items.
The {{#each}} handlebars tag iterates through a selected array (by default it uses your entire model as the selected array). While within the {{#each}} block helper, you can reference the specific object you are currently on by saying this. You could also name the iteration object instead of relying on a this declaration by typing {{#each thing in model}}, within which each object would be referenced as thing.
Lastly, your actions are capable of taking inputs. You can declare these inputs simply by giving the variable name after the action name. Above, I demonstrated this with {{action "saveInventoryItem" this}} which will pass this to the action saveInventoryItem. You also need to add an input parameter to that action in order for it to be accepted.
Ok, that's because as you said, you're just starting with Ember. I would probably do this:
<script type="text/x-handlebars" data-template-name="inventoryitems">
{{#each}}
<div class='row'>
<p>{{input type="text" value=header disabled=headerEnabled}}</p>
<p>{{input type="text" value=detail disabled=detailEnabled}}</p>
<button {{action "editInventoryItem"}}>edit item</button>
<button {{action "saveInventoryItem"}}>save item</button>
</div>
{{/each}}
</script>
with this, you need to define a headerEnabled property in the InventoryitemController(Note that it is singular, not the one that contains all the items), and the same for detailEnabled, and the actions, you can define them also either in the same controller or in the route:
App.InventoryitemController = Ember.ObjectController.extend({
headerEnabled: false,
detailEnabled: false,
actions: {
editInventoryItem: function() {
this.set('headerEnabled', true);
this.set('detailEnabled', true);
}
}
});
that's just an example how you can access the data, in case the same property will enable both text fields, then you only need one, instead of the two that I put . In case the 'each' loop doesn't pick up the right controller, just specify itemController.