replace deprecated Ember.ObjectController used in View in ember.js - ember.js

I am using Ember.js to create a one page map editing software.
In my app, I use a model to represent the layer's state of the map and to associate it with an actual openlayers' layer.
There is a summary of my work:
in my entry point map.hbs, I call the mapLayers view:
{{view "mapLayers"}}
Here is the mapLayers view definition:
export default Ember.View.extend({
templateName: "mapLayers",
classNames: ["map-layers"]
});
The mapLayers template :
<ul>
{{#each layer in tileLayers itemController="mapLayer"}}
<li {{bind-attr id=layer.identifier}}>
<a>
<label class="hint--top" {{bind-attr data-hint=layer.title}}>
{{str-sub layer.title 20}}
</label>
{{input class="range" type="range" name="range" min="0" max="100" value=layer.opacity}}
</a>
</li>
{{/each}}
</ul>
And the mapLayer controller:
export default Ember.ObjectController.extend({
opacity: function(key, value){
var model = this.get('model');
if (value === undefined) {
// property being used as a getter
console.log("get layer opacity: " + model.get('opacity'));
return model.get('opacity') * 100;
} else {
// property being used as a setter
model.set('opacity', value / 100);
model.get('layer').setOpacity(value / 100);
model.save();
return value;
}
}.property('model.opacity')
});
As you see, I am using the proxy ObjectController to modify on the fly the values set and get in the view.
I'm trying to understand how to remove the ObjectController without success.
I tried to change to Ember.Controller but how can I proxy my model properties then??
I read this without help:
OBJECTCONTROLLER
Experienced Ember users have enjoyed the use of proxying behavior in
the Ember.ObjectController class since 1.0. However, this behavior
will be removed in Ember 2.0 as the framework migrates to routable
components.
New users hit three roadbumps when learning about the object
controller pattern.
Given a certain model, which of the three controller options should I
be using? Which controller is generated by the framework if I do not
specify one? When using an object controller, why should the this
context not be passed to actions if it has the properties of my model?
For these reasons, the Road to Ember 2.0 RFC listed object controllers
as a concept to be removed from the framework.
To migrate from an explicitly defined object controller, first convert
the class definition to inherit from Ember.Controller. For example:
import Ember from "ember";
// Change: export default Ember.ObjectController.extend({ // To:
export default Ember.Controller.extend({
// ...
Next update any use of {{modelPropertyName}} in templates with
{{model.modelPropertyName}}. You should also review any computed
property dependent keys, observer keys, and get and set statements on
the route and controller.

Instead of proxying you just have to fully qualify you are getting the property off of the model, instead of the controller, which is what is in scope in your template and controller.
Template
<ul>
{{#each layer in tileLayers itemController="mapLayer"}}
<li id={{layer.model.identifier}}>
<a>
<label class="hint--top" data-hint={{layer.model.title}}>
{{str-sub layer.model.title 20}}
</label>
{{input class="range" type="range" name="range" min="0" max="100" value=layer.opacity}}
</a>
</li>
{{/each}}
</ul>
Controller
export default Ember.Controller.extend({
opacity: function(key, value){
var model = this.get('model');
if (value === undefined) {
// property being used as a getter
console.log("get layer opacity: " + model.get('opacity'));
return model.get('opacity') * 100;
} else {
// property being used as a setter
model.set('opacity', value / 100);
model.get('layer').setOpacity(value / 100);
model.save();
return value;
}
}.property('model.opacity')
});

Related

Ember cli pagination - unable to receive model?

Working with Ember 3.19 and trying to use ember-cli-pagination. I have all my posts from JSONplaceholder in the data-store under model type 'post'. I was able to view all the posts from the data-store without pagination but have been unsuccessful in implementing ember-cli-pagination. Console shows currentPage and totalPages as undefined. The Articles element shows in the ember inspector but blank in chrome. The PageNumbers element appears but it is rendered as <<< ... NaN >>>
Controller - articles.js
import Controller from "#ember/controller";
import { tracked } from "#glimmer/tracking";
import { alias, oneWay } from "#ember/object/computed";
import pagedArray from "ember-cli-pagination/computed/paged-array";
import { inject as service } from '#ember/service'
export default class ArticlesController extends Controller {
// setup our query params
queryParams: ["page", "perPage"];
// set default values, can cause problems if left out
// if value matches default, it won't display in the URL
#tracked page = 1;
#tracked perPage = 10;
// can be called anything, I've called it pagedContent
// remember to iterate over pagedContent in your template
#pagedArray('model', {
page: alias("parent.page"),
perPage: alias("parent.perPage"),
})
pagedContent;
// binding the property on the paged array
// to a property on the controller
#oneWay("pagedContent.totalPages") totalPages;
}
Handlebar - articles.hbs
<h2>Posts</h2>
<div>
<ul>
{{#each #pagedContent as |post|}}
<li>User: {{post.user}} Title: {{post.title}} - {{post.body}}</li>
{{/each}}
</ul>
</div>
<PageNumbers #content={{#pagedContent}} />
Model - post.js
import Model, { attr } from '#ember-data/model';
export default class ArticleModel extends Model {
#attr title;
#attr body;
#attr userId;
}
The issue is in the articles.hbs file:
Since the pagedContent is defined in the corresponding controller and not any kind of argument, the property has to be used with this and not with #. Hence change this template code should work.
<h2>Posts</h2>
<div>
<ul>
{{#each this.pagedContent as |post|}}
<li>User: {{post.user}} Title: {{post.title}} - {{post.body}}</li>
{{/each}}
</ul>
</div>
<PageNumbers #content={{this.pagedContent}} />
Also, there is a typo in the controller file. Since this is a class component, the qps has to defined like:
queryParams = ["page", "perPage"];

Ember component call an action in a route or controller

I have a component the main purpose of which is to display a row of items.
Every row has a delete button to make it possible to delete a row. How is possible to pass an action from a template to the component which will trigger an action in a router ?
Here is the template using the component:
#templates/holiday-hours.hbs
{{#each model as |holidayHour|}}
{{holiday-hour holiday=holidayHour shouldDisplayDeleteIcon=true}}
{{/each}}
Here is the component template:
# templates/components/holiday-hour.hbs
...
div class="col-sm-1">
{{#if shouldDisplayDeleteIcon}}
<button type="button" class="btn btn-danger btn-sm mt-1" {{action 'deleteHoliday' holiday}}>
<span class="oi oi-trash"></span>
</button>
{{/if}}
</div>
I'm using the same component to display a row and to create a new item (holiday-hour).
I'm using ember 3.1.2
Thank you
You have to send the actions up from the component to the route. The main way to do this is by adding actions to your component that "send" the action to the parent. Once the action is sent you have to tell the component what action on the route to trigger by passing in the action as a parameter. Below is an example of how to do this.
Component js
# components/holiday-hour.js
...
actions: {
deleteHoliday(){
this.sendAction('deleteHoliday');
}
}
Template for route
#templates/holiday-hours.hbs
...
{{#each model as |holidayHour|}}
{{holiday-hour holiday=holidayHour shouldDisplayDeleteIcon=true deleteHoliday='deleteHoliday'}}
{{/each}}
Route js
#routes/holiday-hours.js
...
actions: {
deleteHoliday(){
//code to delete holiday
}
}
I will try to give a general answer because your question is not giving enough/all info regarding the route actions etc. Long answer short, using closure functions. Assuming this is your route js file routes/holiday-hours.js
import Route from '#ember/routing/route';
export default Route.extend({
model(){ /*... some code */ },
setupController(controller){
this._super(controller);
controller.set('actions', {
passToComponent: function(param) { //.... function logic }
})
}
});
Note: in the above snippet, I'm using setupController to create actions. Alternatively, you can put the actions inside a controller file otherwise actions directly inside the route will throw an error.
So I want the action passToComponent to be called from the component. This is what you do to make it accessible inside the component.
{{#each model as |holidayHour|}} {{holiday-hour holiday=holidayHour shouldDisplayDeleteIcon=true callAction=(action 'passToComponent')} {{/each}}
Now we have passed the action to the component and here's how to call it from the component. Note: I have added a param just to show that it can take a param when called within the component.
import Component from '#ember/component';
export default Component.extend({
actions: {
deleteHoliday: ()=> {
this.get('callAction')() /*Pass in any params in the brackets*/
}
}
});
You will also see demonstrations using sendAction which is rather old and acts more of an event bus that is not very efficient. Read more from this article

Filtering an array in ember

Ok so I'm fairly new to programing, I know how to run a filter on a JSON Array but I cant seem to figure it out when I'm pulling the data from firebase and viewing it in an Ember app.
this is my route.js code:
import Ember from 'ember';
export default Ember.Route.extend({
model: function() {
return this.store.findAll('user');
}
});
This is my template.hbs code the href="#!" is the generic from materialize for the dropdown buttons:
<div class="list-wrapper col-xs-10 col-xs-offset-1">
<div class="col-xs-12 button-div">
{{#md-btn-dropdown text='Filter' class="col-xs-2" belowOrigin=true}}
<li>Female</li>
<li>Male</li>
<li>All</li>
{{/md-btn-dropdown}}
</div>
{{#each model as |info|}}
<div class="col-xs-3 user-card">
<div class="card-info">
<ul>
<li>Last Name- {{info.lastName}}</li>
<li>First Name- {{info.firstName}}</li>
<li>Gender- {{info.gender}}</li>
<li>{{info.email}} </li>
</ul>
</div>
</div>
{{/each}}
</div>
{{outlet}}
This is my controller.js code which I no is all wrong:
import Ember from 'ember';
export default Ember.Controller.extend({
customFilter: function(gender) {
return function(el) {
var r = el.user;
return r.gender === gender;
};
}
});
and this is my model:
import DS from 'ember-data';
import Ember from 'ember';
export default DS.Model.extend({
lastName: DS.attr('string'),
firstName: DS.attr('string'),
gender: DS.attr('string'),
email: DS.attr('string')
});
I've searched high and low and I'm sure I'm just missing something basic and stupid. What I want is for the dropdown menu to be able to filter and display only female, male or all. Again I'm new to this stuff so I apologize if this is a pretty basic thing. Thank You
What your missing is an action that updates your controller when an item in the dropdown is actually selected.
Some helpful reading:
Triggering changes with actions
Computed Properties
Here's how to put actions in your dropdown component
{{#md-btn-dropdown text='Filter' class="col-xs-2" belowOrigin=true}}
<li><a {{action "filterUpdated" "female"}}>Female</a></li>
<li><a {{action "filterUpdated" "male"}}>Male</a></li>
<li><a {{action "filterUpdated"}}>All</a></li>
{{/md-btn-dropdown}}
In your controller you then need to handle this action like so:
import Ember from 'ember';
export default Ember.Controller.extend({
// the people property is an alias of the model object
// which essentially makes people a synonym for model
// read more http://emberjs.com/api/classes/Ember.computed.html#method_alias
people: Ember.computed.alias('model'),
// holds the currently selected gender, e.g., "female". A null value indicates there is no filter.
currentFilter: null,
/*
filteredPeople is a computed array containing people models.
The array is recomputed every time the model changes or the currentFilter changes,
see the .property() bit at the end.
read more: http://emberjs.com/api/classes/Ember.computed.html#method_filter
*/
filteredPeople: Ember.computed.filter('people', function(person/*, index, array*/) {
// this function is passed each item in the model array, i.e., the person argument
// there's no need to use the index nor array arguments, so I've commented them out
if(this.get('currentFilter') === null) {
// when the filter is null, we don't filter any items from the array
return true;
} else {
// otherwise only return true if the gender matches the current filter
return person.gender === this.get('currentFilter');
}
}).property('people', 'currentFilter'),
actions: {
filterUpdated: function (value) {
if (Ember.isEmpty(value)) {
// an empty value means we should clear the filter
this.set('currentFilter', null);
}
else {
this.set('currentFilter', value);
}
}
}
});
Finally, edit your template to change
{{#each model as |info|}}
to
{{#each filteredPeople as |info|}}
Also at a meta level, don't apologize for asking questions! Everyone is new at something at somepoint, and often asking is the best way to learn. That's what stackoverflow is all about :)
Something like this would work:
gender: 'All',
filteredModel: Ember.computed.filter('model', function(person) {
return person.gender === this.get('gender');
}).property('gender'),
this assumes that it starts on all, and then when the dropdown changes the value of gender, then the filteredModel will get updated. You can then in your hbs file change the result to:
{{#each filteredModel as |info|}}

Emberjs: Accessing parent route model which is a Promise (master-detail)

I've got master-detail page layout as on image. I access this page through #/masters/:master_id app url.
Routes a defined as follows:
App.Router.map(function() {
this.resource('masters', { path: '/masters' }, function() {
this.route('detail', { path: '/:master_id' });
});
});
App.MastersRoute = Ember.Route.extend({
model: function() {
return App.DataStore.getData('/api/masters'); //returns Promise!
},
setupController: function(controller, model) {
controller.set("content", model);
}
});
App.MastersDetailRoute = Ember.Route.extend({
model: function(params) {
return this.modelFor("masters").find(function(item) {
return item.get("id") == params.master_id;
});
}
});
Templates:
<script type="text/x-handlebars-template" data-template-name="masters">
<div id="masters-grid">
{{#each master in model}}
<div {{action "show" master}}>
{{master.name}}
</div>
{{/each}}
</div>
<div id="detail">
{{outlet}}
</div>
</script>
<script type="text/x-handlebars-template" data-template-name="masters/detail">
{{model.name}} <br />
{{model.age}} <br />
{{model.address}} <br />
</script>
When clicking through masters in the grid I want to show their details in Detail outlet and I do not want to reload all masters from API when changing the master selection.
I have a problem with MastersDetailRoute's model, because this.modelFor("masters") returns undefined. I think, it is caused by returning Promise in model hook. Any idea or workaround how to access one item from Masters model or controller in "child route" model hook?
I see a few things here.
when defining routes that have the same url as the route name theres no need to specify the path
the detail route should also be a resource as it is a route backed by a model
In the Masters route returning a promise is correct and supported natively by ember. The route wont be resolved until the promise is.
setup controller isn't required
its usually best to do the required api call to fetch the individual record in the detail route. This will only be used when loading the page for the first time (if f5 ing or coming from a bookmark)
in your masters template you can use id instead of typing data-template-name or better still look into use ember-cli/brocolli or grunt to precompile your templates
to prevent ember refetching your model when selecting a row use the handlebars helper link-to
{{#link-to 'masterDetail' master}}
{{master.name}}
{{/link-to}}
just to clarify, using link-to in this way passes the object specified in the second parameter as the model to the specified route (first parameter). In your case master will now be set as the model to the master detail route.
in masters detail theres no need to type "model" the default context (i.e. the value of "this") in your template is the controller, then if the property is not found on the controller it looks for it in the model.
Hope this helps

Calling a controller method whenever the value of a {{input}} Ember Handlebars helper changes

Ember 1.0.0 RC3 provides a Handlebars helper named {{input}}.
Instead of merely updating a String property in the underlying controller, I would like to additionally call a method on the underlying controller whenever the input has changed.
Is there a way to call a controller method after changes to an {{input}} textfield?
Use the .observes("propertyChangedByInputHelper") on the controller method that shall react on changes.
<--Handlebars template -->
<script type="text/x-handlebars" id="autocomplete">
{{input type="text" value=searchText placeholder="Search users..."}}
<table>
{{#each searchResults}}
<tr><td>
{{#linkTo "user" this}}
{{firstName}} {{lastName}}
{{/linkTo}}
</td></tr>
{{/each}}
</table>
</script>
.
//inside Ember application / app.js
App.AutocompleteController = Ember.ObjectController.extend({
searchText: null, // mutated by "autocomplete" Handlebars template
searchResults: Ember.A(), //initialize to empty array
searchForUsers: function() {
this.set("searchResults", Ember.A() ); // clear possibly non-empty array
var searchResultsBuilder = Ember.A();
//...
//... making modifications to searchResultsBuilder
//...
this.get("searchResults").addObjects(searchResultsBuilder);
}.observes("searchText")
});
N.B.: When searchText changes, a .property("searchText") wouldn't be enough to trigger the searchForUsers method: .property(..) makes the method act only lazily on demand, while .observes(..) makes the method act eagerly.