Ember: filter model by text field - javascript

I want to filter a model by a text field, the filter should be applied as the user types. I am coming from angular (not a pro but I managed to create such a filter there), so I expected this to be easy. Heh.
list.hbs:
{{input type='text' placeholder='Filter' size='50' valueBinding='searchKeyword'}}
<ol>
{{#each model.articles as |article|}}
<li>{{{article.title}}</li>
{{/each}}
</ol>
I know that this question is asked a lot and I did a lot of research before I decided that the amout of searching is inappropriate for such a problem and that I do not understand some core ideas of ember. It seems like that with the transition from 1.x to 2.x most examples, questions and guides are invalid. My question has been asked multiple times already:
EmberJS filter array of items as the user types
text field filtering a list using ember + ember data
Emberjs - Connecting an {{ input }} filter bar with my list of Objects. As I type, the list filters
but all of the answers use controllers. The docs say that "controllers will be replaced with components". So... how to filter a model the new way?
--
UPDATE
I used Remi's example to create this component:
export default Ember.Component.extend({
filteredArticles: Ember.computed('articles', 'filter', function() {
var keyword = this.get('filter');
var filtered = this.get('articles');
if (keyword) {
keyword = keyword.toLowerCase().trim();
filtered = this.get('articles').filter((item) => item.get('title').toLowerCase().includes(keyword));
}
return filtered;
})
});
the computed property is used as the {{#each filteredArticles... argument and setting a property beforehand (which raised a deprecation warning) is not required anymore.

Components act pretty similar to controllers in some ways. You can think of them as a combination of controller+view in older ember versions. They still work in current ember. But if you want to make your issue work via components, I would suggest the following:
Lest say, you have a /templates/components/list.hbs:
{{input type='text' placeholder='Filter' size='50' valueBinding='searchKeyword'}}
<ol>
{{#each filteredArticles as |article|}}
<li>{{{article.title}}</li>
{{/each}}
</ol>
In one of your templates that should use the components, e.g. /templates/my-list.hbs:
{{list articles=model.articles}}
Then for your component to work, add a /components/list.js:
Ember.Component.extend({
searchKeyword: null,
articles: null,
filteredArticles: null,
updateList: Ember.computed('searchKeyword', function(){
var keyword = this.get('searchKeyword');
var filtered = this.get('articles').filterBy('title', searchKeyword);
this.set('filteredArticles', filtered);
}),
didInsertElement(){
this.set('filteredArticles', this.get('articles'));
}
})

Related

How Can i order a model?

I'm a newbie in ember and I don't know how to sort a list of movies by title.
I have a route index with a model hook:
export default Route.extend(RealtimeRouteMixin,{
model() {
return this.store.findAll('movie');
}
});
I render a component when this route is loaded. I set the model in the template
{{landing-page
add=(action 'addMovie')
movies=model
}}
Inside this component, there are other child's components. In one of them is a list of movie where I want to show the list of the movies sort by title.
//landing-page template
{{movie-list-header}}
{{movie-list
movies=this.movies
}}
{{add-movie-form add=this.add
}}
//movive-list template
{{#each movies as |movie|}}
{{movie-list/movie-list-item
movie=movie
}}
<hr />
{{/each}}
I don't know what the best approach to do it. I've thought to sort the model in the route but I don't know If I have to do it something like this
export default Route.extend(RealtimeRouteMixin,{
model() {
return this.store.findAll('movie').then(funcion(data){
// sort here ???
});
}
});
Your approach would work. Then sortBy would do the trick.
The other approach is to put it somewhere in your component hierarch starting with the controller. A simple computed property will do the trick. There is also a computed property macro.
I prefer to handle simple ordering in the template using ember-composable-helpers as it sorts strings well with the least amount of boilerplate. After installing that addon and restarting your ember server you can do this in your template with the sort-by helper.
//movie-list template
{{#each (sort-by "title" movies) as |movie|}}
{{movie-list/movie-list-item
movie=movie
}}
<hr />
{{/each}}
In the controller/component where your model you want to sort with property, you can mention following computed property:
sortedModel: sort('model', 'title:desc'),
you can import sort as follows:
import { sort } from '#ember/object/computed';
Now use sortedModel instead of model in templates/handlebars. I hope that make sense. Please let me know if you have any query?

I have some questions about Sapper/Svelte

I just started using Sapper (https://sapper.svelte.technology) for the first time. I really like it so far. One of the things I need it to do is show a list of the components available in my application and show information about them. Ideally have a way to change the way the component looks based on dynamic bindings on the page.
I have a few questions about using the framework.
First, I'll provide a snippet of my code, and then a screenshot:
[slug].html
-----------
<:Head>
<title>{{info.title}}</title>
</:Head>
<Layout page="{{slug}}">
<h1>{{info.title}}</h1>
<div class="content">
<TopBar :organization_name />
<br>
<h3>Attributes</h3>
{{#each Object.keys(info.attributes) as attribute}}
<p>{{info.attributes[attribute].description}} <input type="text" on:keyup="updateComponent(this.value)" value="Org Name" /></p>
{{/each}}
</div>
</Layout>
<script>
import Layout from '../_components/components/Layout.html';
import TopBar from '../../_components/header/TopBar.html';
let COMPONENTS = require('../_config/components.json');
export default {
components: {
Layout, TopBar
},
methods: {
updateComponent(value) {
this.set({organization_name: value});
}
},
data() {
return {
organization_name: 'Org Name'
}
},
preload({ params, query }) {
params['info'] = COMPONENTS.components[params.slug];
return params;
}
};
</script>
Now my questions:
I notice I can't #each through my object. I have to loop through its keys. Would be nice if I could do something like this:
{{#each info.attributes as attribute }}
{{attribute.description}}
{{/each}}
Before Sapper, I would use Angular-translate module that could do translations on strings based on a given JSON file. Does anyone know if a Sapper/Svelte equivalent exists, or is that something I might need to come up with on my own?
I'm not used to doing imports. I'm more use to dependency injection in Angular which looks a bit cleaner (no paths). Is there some way I can create a COMPONENTS constant that could be used throughout my files, or will I need to import a JSON file in every occurence that I need access to its data?
As a follow-up to #3, I wonder if there is a way to better include files instead of having to rely on using ../.. to navigate through my folder structure? If I were to change the path of one of my files, my Terminal will complain and give errors which is nice, but still, I wonder if there is a better way to import my files.
I know there has got to be a better way to implement what I implemented in my example. Basically, you see an input box beside an attribute, and if I make changes there, I am calling an updateComponent function which then does a this.set() in the current scope to override the binding. This works, but I was wondering if there was some way to avoid the function. I figured it's possible that you can bind the value of the input and have it automatically update my <TopBar> component binding... maybe?
The preload method gives me access to params. What I want to know if there is some way for me to get access to params.slug without the preload function.
What would be really cool is to have some expert rewrite what I've done in the best possible way, possibly addressing some of my questions.
Svelte will only iterate over array-like objects, because it's not possible to guarantee consistent behaviour with objects — it throws up various edge cases that are best solved at an app level. You can do this sort of thing, just using standard JavaScript idioms:
{{#each Object.values(info.attributes) as attr}}
<p>{{attr.description}} ...</p>
{{/each}}
<!-- or, if you need the key as well -->
{{#each Object.entries(info.attributes) as [key, value]}}
<p>{{attr.description}} ...</p>
{{/each}}
Not aware of a direct angular-translate equivalent, but a straightforward i18n solution is to fetch some JSON in preload:
preload({ params, query }) {
return fetch(`/i18n/${locale}.json`)
.then(r => r.json())
.then(dict => {
return { dict };
});
}
Then, you can reference things like {{dict["hello"]}} in your template. A more sophisticated solution would only load the strings necessary for the current page, and would cache everything etc, but the basic idea is the same.
I guess you could do this:
// app/client.js (assuming Sapper >= 0.7)
import COMPONENTS from './config/components.json';
window.COMPONENTS = COMPONENTS;
// app/server.js
import COMPONENTS from './config/components.json';
global.COMPONENTS = COMPONENTS;
Importing isn't that bad though! It's good for a module's dependencies to be explicit.
You can use the resolve.modules field in your webpack configs: https://webpack.js.org/configuration/resolve/#resolve-modules
This would be a good place to use two-way binding:
{{#each Object.values(info.attributes) as attr}}
<p>{{attr.description}} <input bind:value=organization_name /></p>
{{/each}}
Yep, the params object is always available in your pages (not nested components, unless you pass the prop down, but all your top-level components like routes/whatever/[slug].html) — so you can reference it in templates as {{params.slug}}, or inside lifecycle hooks and methods as this.get('params').slug, whether or not a given component uses preload.

Ember loop through an array of checkboxes and get checked items?

I have an Ember.js application (using the Ember CLI). I have a component called sideMenu with the files sideMenu.js and sideMenu.hbs. Within the .js file, I have an array of vehicle names like this:
export default Ember.Component.extend({
vehicles: ['Audi', 'Chrysler', 'Harley Davidson'],
vehicleCheckboxes: { 'Audi': true, 'Chrysler': true, 'Harley Davidson': true }
});
In my template for this component, I am looping through these vehicles and creating a checkbox for each item like this:
{{#each vehicles as |vehicle|}}
{{input type='checkbox' checked='true'}} {{vehicle}}
{{/each}}
This gives me what I want - a few checkboxes with the vehicle names next to it. I want to be able to know what the user unchecked / checked in this list. I tried to do this by creating some Ember properties dynamically and in the template:
{{input type='checkbox' checked="{{vehicleCheckboxes}}.{{vehicle}}"}}
This didn't seem to work though. How can I accomplish this? There doesn't seem to be any indication on the Ember documentation that it is possible from within the framework in any way.
checked expects boolean value (true/false)
you can either use this addon like :
{{multiselect-checkboxes options=vehicles selection=selectedVehicles}}
or you can have your loop like this:
{{#each vehicles as |vehicle|}}
{{input type='checkbox' checked=(get vehicleCheckboxes vehicle)}} {{vehicle}}
{{/each}}
and you need your vehicleCheckboxes converted to Ember.Object I think if it does not work as it is
vehicleCheckboxes: Ember.Object.create({ 'Audi': true, 'Chrysler': true, 'Harley Davidson': true })

Using ui-select2 with ng-repeat does not set the model correctly

First, some background:
When using ui-select2, you have to supply an initSelection function in the select2 config object. If you don't, you'll get an error (but the functionality won't be affected. Everything will still work as expected).
To illustrate, see this plunkr. When you select an item from the dropdown menu, it'll work, but you'll get the following error:
Error: cannot call val() if initSelection() is not defined
Adding an empty function to initSelection fixes the error. You can uncomment it in the above plunkr to see that.
The problem:
When using ui-select2 in conjunction with ng-repeat, it just doesn't update the model.
Controller:
// for this demo, `users` is injected into the controller
$scope.users = users.slice(0, 2);
$scope.select2Config = {
placeholder: 'Select User...',
query: function ( options )
{
// `users` in this demo is injected into the controller.
// in the real world this would be an ajax request
options.callback({ results: users });
},
// Without initSelection, I get the above error.
// Regardless, the model isn't updated.
initSelection: angular.noop,
formatSelection: select2format,
formatResult: select2format,
};
function select2format ( user )
{
return user.first + ' ' + user.last;
}
View:
<ul>
<li ng-repeat="user in users">
<input type="text" ng-model="user" ui-select2="select2Config">
</li>
</ul>
When selecting an item from the dropdown list, the model isn't updated. If there's no initSelection in the config I get the above error, but adding it still doesn't update the model.
Here's a plunkr demonstrating the above.
The question:
How do I make ui-select2 update the model in an ng-repeat?
Try using inheritance:
http://plnkr.co/edit/jyJaYU4DQX1LROD6nQaw?p=preview
Also I you were calling "user in users" on the ng-repeat but then also setting the ng-model to "user". I am not sure why.
As to initSelection behavior, I do not have an answer.
edit
I updated the plunker. The ng-repeat directive creates a new scopes - true, but all of its children (or repeated items) are siblings. You had bound "user" (that was being defined/transcluded by ng-repeat) to the ng-model of each select (and expecting that the "users" array would be updated if you changed the model).
I believe ng-repeat is a one way binding top down
Therefor what I showed was correct but admittedly lazy. I bound both selects to the same model but I just as easily bound them to seperate properties on the "selected" object using $index.
Point is: if you want two way binding then write a new directive, but it is easier to do something similar to what I show.
PS: In practice I populate a "lookups" object that has some arrays for the select/pull-downs and use a seperate object called "user" that holds what the user has selected. This is pretty much pulled right out of the (older) documentation.

How can I get an instance of a model's controller inside a handlebars template?

I've got a sticky situation that I keep on running into: The need for a new instance of a controller inside a handlebars template.
Here is a brief example of my situation. (Please excuse my use of coffeecript)
In Ember, I have a model:
App.Foo = DS.Model.extend
attr: DS.attr()
...
Which I load from an endpoint etc.. And place into an array controller:
App.FooArray = Ember.ArrayController.extend
###*
* Array of App.Foo
* #type {Array}
*/
content:
method: ->
...
Finally, I have an 'instance' controller for this model, which implements further methods (i.e. this is not a singleton controller as would be found at the router level, but a decorator (or proxy) that augments the model with added methods and event handlers):
App.FooController = Ember.ObjectController.extend
###*
* Content
* #type {App.Foo}
*/
content: null
action: ->
...
In handlebars, I want to iterate over items in an App.FooArray:
{{#each myFooArray}}
Hi! My attr is {{attr}}
{{/each}}
etc.. This works splendidly for parameters and such.
However, the trouble starts when I want to use actions (or other properties which would belong to a FooController)
{{#each myFooArray}}
Hi! My attr is {{attr}} <a {{action 'action'}}>Action me!</a>
{{/each}}
Suddenly my actions are not working. That's because the action helper doesn't apply the action to 'this' but rather to a controller higher up, possibly even at the Route level!
So to work around this, I need to pass a target (i.e. a controller):
{{action 'action' target=**********}}
Well, the controller I want is an instance of App.FooController.
Up until now, I've been instantiating controllers inside the model (yuck!):
App.Foo = DS.Model.extend
attr: DS.attr()
...
attrn: DS.attr()
myController: Ember.computed (->
App.FooController.create
content: this
)
and thus iterating as follows:
{{#each myFooArray}}
Hi! My attr is {{attr}} <a {{action 'action' target=myController}}>Action me!</a>
{{/each}}
I know this is bad, but I can't think of a better way. Somebody, please help me see the light!
You can explicitly set the itemController in your each loop.
{{#each myFooArray itemController="foo"}}
Hi! My attr is {{attr}}
{{/each}}
This question poses an important and longstanding question about ArrayControllers, CollectionViews, Models and ObjectControllers.
At the time of writing, my knowledge of the inner workings of Ember was limited. However, I can rephrase my question more concisely as follows:
Given an ArrayController, CollectionView and instance controllers for a model, how can one leverage the itemControllerClass property of the ArrayController to iterate over its content and wrap each item in a unique (i.e. non-singleton) instance of itemController?
Turns out this problem is longstanding and the solution echoes #jeremy-green but I will expand on things here.
First off though: the Ember support thread that encapsulates the problem: https://github.com/emberjs/ember.js/issues/1637
I think the discussion there points very clearly to the need for non-singleton controllers in certain situations.
As well, here is the documentation for ArrayController that indicates the presence of an 'itemController' property on the ArrayController: http://emberjs.com/api/classes/Ember.ArrayController.html#property_itemController
Looking further into the docs, you will also note the presence of an 'lookupItemController' function: http://emberjs.com/api/classes/Ember.ArrayController.html#method_lookupItemController
These functions are for the express purpose of returning the content as an array of Controllers but how?
Well the first requirement is to use the ArrayController directly as the content in a loop. Unfortunately this is where things start to fall apart.
You may think it would be simply the case that a CollectionView can be used:
{{view myCollectionView controllerBinding=myArrayController}}
or
{{view myCollectionView contentBinding=myArrayController}}
But unfortunately this is not the case. More so in the situation where you are rendering a Controller on another Controller's route. CollectionView does not preserve the relationship between the ArrayController and its 'itemControllerClass`
The only way to make this work, as #jeremy-green points out:
{{#each myFooArray itemController="foo"}}
Hi! My attr is {{attr}}
{{/each}}
A more complete example would be:
<ol class="foo-item-list">
{{#each controllers.foo_items}}
<li>{{ view "foo" }}</li>
{{/each}}
</ol>
Wherein App.FooItemsController has either the property itemController or lookupItemController defined.
Unfortunately in this situation we lose the benefits of using the tagName or emptyView properties of CollectionView. Hopefully if an 'ArrayView' is ever created, it will bring the best of both worlds to this situation!

Categories

Resources