Dynamically render components in Ember for ember-i18n - ember.js

First of all, ember's component helper doesn't help in this case. That would only help if I knew how many components I needed to render, and in what order.
I need to be able to render components based on a string like:
{{user}} has made a bid of {{bid}}, where:
{{user}} and {{bid}} will be replaced with components.
the given string is unknown, with an unknown number of dynamic sections representing components (The components would be passed in with the given string).
If these dynamic sections were helpers, this would be easy - but helpers just don't cut it for certain items in my game.
Ideally I could do something like this:
{{translated-content
content='{{user}} has made a bid of {{bid}}'
user=(component 'user-ui')
bid=(component 'bid-ui') }}
Is this possible with ember?

With some help, I've come up with the following component which works with ember-i18n, and ember 1.11 or later.
It could likely be optimised further, but it works nice and fast the way it is.
Create a new component
ember g component t-t
template.hbs
{{#each parts as |part|}}
{{#if part.isComponent}}
{{component part.content}}
{{else}}
{{part.content}}
{{/if}}
{{/each}}
component.js
import Ember from 'ember';
const { $ } = Ember;
export default Ember.Component.extend({
tagName: 'span',
updateComponents: Ember.on('didReceiveAttrs',function(opts){
let newAttrs = opts.newAttrs;
let components = {};
$.each(newAttrs,(key,val)=>{
if( key !== 't' && typeof val === 'object' ){
let keys = Object.keys(val);
if(keys.length && keys[0].indexOf('COMPONENT_')>=0){
components[key] = val;
}
}
});
this.set('_components',components);
}),
parts: Ember.computed('_components','t','i18n.locale',function(){
let attrs = [];
let components = this.get('_components');
let componentKeys = Object.keys(components);
$.each(this.attrs,(key,val)=>{
if( key !== 't'){
if( componentKeys.indexOf(key)<0 ){
attrs[key] = val;
} else {
attrs[key] = `{{${key}}}`;
}
}
});
let content = this.get('i18n').t(this.get('t'),attrs).toString();
content = content.replace(/\{\{(\w+?)\}\}/g,(fullMatch)=>{
return `{{split}}${fullMatch}{{split}}`;
});
let parts = content.split('{{split}}');
parts.forEach((val,i)=>{
let isComponent;
let key = val.replace(/\{\{(\w+?)\}\}/g,(fullMatch,key)=>{
isComponent = true;
return key;
});
if(isComponent && components[key]){
parts[i] = {
isComponent: true,
content: components[key]
};
} else {
parts[i] = {
content: Ember.String.htmlSafe(val)
};
}
});
return parts;
}),
}).reopenClass({
positionalParams: ['t']
});
Usage
{{t-t
'your-ember-i18n-path'
key1='Normal Content (example)'
key2=(component 'your-component') }}

Related

Ember editable recursive nested components

I'm currently trying to build a component that will accept a model like this
"values": {
"value1": 234,
"valueOptions": {
"subOption1": 123,
"subOption2": 133,
"subOption3": 7432,
"valueOptions2": {
"subSubOption4": 821
}
}
}
with each object recursively creating a new component. So far I've created this branch and node components and its fine at receiving the data and displaying it but the problem I'm having is how I can edit and save the data. Each component has a different data set as it is passed down its own child object.
Js twiddle here : https://ember-twiddle.com/b7f8fa6b4c4336d40982
tree-branch component template:
{{#each children as |child|}}
{{child.name}}
{{tree-node node=child.value}}
{{/each}}
{{#each items as |item|}}
<li>{{input value=item.key}} : {{input value=item.value}} <button {{action 'save' item}}>Save</button></li>
{{/each}}
tree-branch component controller:
export default Ember.Component.extend({
tagName: 'li',
classNames: ['branch'],
items: function() {
var node = this.get('node')
var keys = Object.keys(node);
return keys.filter(function(key) {
return node[key].constructor !== Object
}).map(function(key){
return { key: key, value: node[key]};
})
}.property('node'),
children : function() {
var node = this.get('node');
var children = [];
var keys = Object.keys(node);
var branchObjectKeys = keys.filter(function(key) {
return node[key].constructor === Object
})
branchObjectKeys.forEach(function(keys) {
children.push(keys)
})
children = children.map(function(key) {
return {name:key, value: node[key]}
})
return children
}.property('node'),
actions: {
save: function(item) {
console.log(item.key, item.value);
}
}
});
tree-node component:
{{tree-branch node=node}}
Anyone who has any ideas of how I can get this working would be a major help, thanks!
Use:
save(item) {
let node = this.get('node');
if (!node || !node.hasOwnProperty(item.key)) {
return;
}
Ember.set(node, item.key, item.value);
}
See working demo.
I think this would be the perfect place to use the action helper:
In your controller define the action:
//controller
actions: {
save: function() {
this.get('tree').save();
}
}
and then pass it into your component:
{{tree-branch node=tree save=(action 'save')}}
You then pass this same action down into {{tree-branch}} and {{tree-node}} and trigger it like this:
this.attrs.save();
You can read more about actions in 2.0 here and here.

How can I port a bound date input helper to HTMLBars?

I'm trying to port a 'DateInputView' TextField extension with a bound helper to HTMLBars. It appears that 'call()' has been removed from 'Handlebars.helpers.view' so the helper doesn't work anymore. I've tried several syntax changes based on forums I've read but nothing works. The field is a datepicker which switches to the native calendar picker when using a mobile device. I see an example where the bound helper is integrated into the view helper code, so uses one js file rather than two, so am trying to go that route now. Code is below. If anyone who knows to to rework it for HTMLBars please let me know.
// ../templates/avail/navigation.hbs
{{date-input id="checkin" valueBinding="controllers.avail.arrival" class="form-control date-picker"}}
// ../views/date-input.js
var DateInputView = Ember.TextField.extend({
didInsertElement: function(){
Ember.run.scheduleOnce('afterRender', this, this._setupDateInput);
},
_setupDateInput: function(){
var _this = this;
var type = Ember.$(this.get('element')).attr('type');
// Set up Date Picker for object
if( type === "input" ) {
Ember.$(this.get('element')).datepicker({
format: "yyyy-mm-dd",
autoclose: true
});
}
}
});
export default DateInputView;
// ../helpers/date-input.js
import DateInputView from '../views/date-input';
export default Ember.Handlebars.makeBoundHelper(function(options) {
Ember.assert('You can only pass attributes to the `input` helper, not arguments', arguments.length < 2);
var hash = options.hash,
types = options.hashTypes,
inputType = hash.type,
onEvent = hash.on;
delete hash.type;
delete hash.on;
hash.type = "input";
if( /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ) {
hash.type = "date";
}
hash.onEvent = onEvent || 'enter';
return Ember.Handlebars.helpers.view.call(this, DateInputView, options);
});
I wound up re-working this viewHelper as a Component.

Bindings for nested component not working in ember-qunit

We have an ember component (let's call it component B), and the template for that component contains another component (component A). If we have computed properties in component B bound to properties in component A, the bindings are not working completely when we're testing using ember-qunit, but the bindings are working in the real application. In the tests, the bindings are working if we programmatically set values in components A or B, but if we use ember helpers (e.g. fillIn) to set component values, the bindings aren't getting fired. We don't experience this problem with non-nested components.
A jsfiddle that demonstrates the problem is here: http://jsfiddle.net/8WLpx/4/
Please ignore that parent component below could have just been an extension of the nested component. This is just to demonstrate the issue.
Code below if you'd rather:
HTML/handlebars
<!-- URL input -->
<script type="text/x-handlebars" data-template-name="components/url-input">
<div {{ bind-attr class=":input-group showErrors:has-error:" }}>
{{input value=web_url class="form-control"}}
</div>
</script>
<!-- video URL input -->
<script type="text/x-handlebars" data-template-name="components/video-url-input">
{{url-input class=class value=view.value selectedScheme=view.selectedScheme web_url=view.web_url}}
</script>
Component Javascript
//=============================== url input component
App.UrlInputComponent = Ember.Component.extend({
selectedScheme: 'http://',
value: function(key, value, previousValue) {
// setter
if (arguments.length > 1) {
this.breakupURL(value);
}
// getter
return this.computedValue();
}.property('selectedScheme', 'web_url'),
computedValue: function() {
var value = undefined;
var web_url = this.get('web_url');
if (web_url !== null && web_url !== undefined) {
value = this.get('selectedScheme') + web_url;
}
return value;
},
breakupURL: function(value) {
if(typeof value === 'string') {
if(value.indexOf('http://') != -1 || value.indexOf('https://') != -1) {
var results = /^\s*(https?:\/\/)(\S*)\s*$/.exec(value);
this.set('selectedScheme', results[1]);
this.set('web_url', results[2]);
} else {
this.set('web_url', value.trim());
}
}
},
onWebURLChanged: function() {
// Parse web url in case it contains the scheme
this.breakupURL(this.get('web_url'));
}.observes('web_url'),
});
//=============================== video url input component
App.VideoUrlInputComponent = Ember.Component.extend({
value: "http://",
selectedScheme: 'http://',
web_url: "",
});
Test Code
emq.moduleForComponent('video-url-input','Video URL Component', {
needs: ['component:url-input',
'template:components/url-input'],
setup: function() {
Ember.run(function() {
this.component = this.subject();
this.append();
}.bind(this));
},
});
emq.test('Test fill in url programmatically', function() {
var expectedScheme = 'https://';
var expectedWebURL = 'www.someplace.com';
var expectedURL = expectedScheme + expectedWebURL;
Ember.run(function() {
this.component.set('selectedScheme', expectedScheme);
this.component.set('web_url', expectedWebURL);
}.bind(this));
equal(this.component.get('value'), expectedURL, "URL did not match expected");
});
emq.test('Test fill in url via UI', function() {
var expectedURL = 'https://www.someplace.com';
fillIn('input', expectedURL);
andThen(function() {
equal(this.component.get('value'), expectedURL, "URL did not match expected");
}.bind(this));
});
The this.append() cannot happen in the test setup; it must happen in the "test" method because the ember qunit "test" wrapper clears all of the views before calling the standard qunit "test" method.

return value from computed properties callback did not update templates

Sorry for the long title. Its quite hard to put into words.
Ember version: 1.2.0
here goes:
My components:
App.AutocompleteComponent = Ember.Component.extend({
searchResults: function() {
var returnValue
var service = new google.maps.places.AutocompleteService();
service.getPlacePredictions({options},callback);
function callback(results){
returnValue = results;
}
return returnValue;
}.property('searchText')
My Templates:
{{input type="text" value=searchText placeholder="Search..."}}
<ul >
{{#each itemResults}}
<li>{{this.name}}</li>
{{/each}}
</ul>
When i debug using ember chrome debug tool, i can see the component holding the searchResults values correctly. But it is not being updated accordingly in the template.
Any ideas?
if this way of handling/using computed property is not suggested, can suggest any other ways?
Thank you in advance.
You probably want to debounce this (and I don't know what options is, is it a global var?). And the template is itemResults instead of searchResults. http://emberjs.com/api/classes/Ember.run.html#method_debounce
watchSearchResults: function() {
var self = this;
var service = new google.maps.places.AutocompleteService();
var callback= function(results){
self.set('searchResults', results);
}
service.getPlacePredictions({options},callback);
}.observes('searchText')
Thank you kingpin2k for your response,
I have found other way of dealing with returns that obviously didn't work on callbacks and since computed property which in a way requires 'returns' making it not feasible on this use case.
Instead i have opt to using Observers.
By the way, this code is was meant to be dealing with auto complete.
here is the final code:
WebClient.AutocompleteFromComponent = Ember.Component.extend({
searchTextChanged: Em.observer('searchText',function(){
var service = new google.maps.places.AutocompleteService();
service.getPlacePredictions({
input: this.get('searchText'),
types: ['(regions)'],
componentRestrictions: {
country: 'my'
}
}, function(predictions, status) {
//Start callback function
if (status != google.maps.places.PlacesServiceStatus.OK) {
alert(status);
return;
}
for (var i = 0, prediction; prediction = predictions[i]; i++) {
console.log(prediction.description);
mapItem = {};
mapItem.name = prediction.description;
mapItem.type = 'map'
mapItem.reference = prediction.reference;
itemResults.push(mapItem);
}
//console.log(itemResults)
self.set('itemResults', itemResults)
});
})
The template code is still the same.

Collection of objects of multiple models as the iterable content in a template in Ember.js

I am trying to build a blog application with Ember. I have models for different types of post - article, bookmark, photo. I want to display a stream of the content created by the user for which I would need a collection of objects of all these models arranged in descending order of common attribute that they all have 'publishtime'. How to do this?
I tried something like
App.StreamRoute = Ember.Route.extend({
model: function() {
stream = App.Post.find();
stream.addObjects(App.Bookmark.find());
stream.addObjects(App.Photo.find());
return stream;
}
}
where the resource name is stream
But it doesn't work. I am using the latest released Ember 1.0.0 rc 2 and handlebars 1.0.0 rc 3 with jQuery 1.9.1 and ember-data.
Probably the way I am trying to achieve this whole thing is wrong. The problem is even if I am able to use the collection of objects of multiple models to iterate in the template, I would still need to distinguish between the type of each object to display its properties apart from the common property of 'publishtime'.
You can use a computed property to combine the various arrays and then use Javascript's built in sorting to sort the combined result.
Combining the arrays and sorting them
computed property to combine the multiple arrays:
stream: function() {
var post = this.get('post'),
bookmark = this.get('bookmark'),
photo = this.get('photo');
var stream = [];
stream.pushObjects(post);
stream.pushObjects(bookmark);
stream.pushObjects(photo);
return stream;
}.property('post.#each', 'bookmark.#each', 'photo.#each'),
example of sorting the resulting computed property containing all items:
//https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/sort
streamSorted: function() {
var streamCopy = this.get('stream').slice(); // copy so the original doesn't change when sorting
return streamCopy.sort(function(a,b){
return a.get('publishtime') - b.get('publishtime');
});
}.property('stream.#each.publishtime')
});
rendering items based on a property or their type
I know of two ways to do this:
add a boolean property to each object and use a handlebars {{#if}} to check that property and render the correct view
extend Ember.View and use a computed property to switch which template is rendered based on which type of object is being rendered (based on Select view template by model type/object value using Ember.js)
Method 1
JS:
App.Post = Ember.Object.extend({
isPost: true
});
App.Bookmark = Ember.Object.extend({
isBookmark: true
});
App.Photo = Ember.Object.extend({
isPhoto: true
});
template:
<ul>
{{#each item in controller.stream}}
{{#if item.isPost}}
<li>post: {{item.name}} {{item.publishtime}}</li>
{{/if}}
{{#if item.isBookmark}}
<li>bookmark: {{item.name}} {{item.publishtime}}</li>
{{/if}}
{{#if item.isPhoto}}
<li>photo: {{item.name}} {{item.publishtime}}</li>
{{/if}}
{{/each}}
</ul>
Method 2
JS:
App.StreamItemView = Ember.View.extend({
tagName: "li",
templateName: function() {
var content = this.get('content');
if (content instanceof App.Post) {
return "StreamItemPost";
} else if (content instanceof App.Bookmark) {
return "StreamItemBookmark";
} else if (content instanceof App.Photo) {
return "StreamItemPhoto";
}
}.property(),
_templateChanged: function() {
this.rerender();
}.observes('templateName')
})
template:
<ul>
{{#each item in controller.streamSorted}}
{{view App.StreamItemView contentBinding=item}}
{{/each}}
</ul>
JSBin example - the unsorted list is rendered with method 1, and the sorted list is rendered with method 2
It's a little complicated than that, but #twinturbo's example shows nicely how to aggregate separate models into a single array.
Code showing the aggregate array proxy:
App.AggregateArrayProxy = Ember.ArrayProxy.extend({
init: function() {
this.set('content', Ember.A());
this.set('map', Ember.Map.create());
},
destroy: function() {
this.get('map').forEach(function(array, proxy) {
proxy.destroy();
});
this.super.apply(this, arguments);
},
add: function(array) {
var aggregate = this;
var proxy = Ember.ArrayProxy.create({
content: array,
contentArrayDidChange: function(array, idx, removedCount, addedCount) {
var addedObjects = array.slice(idx, idx + addedCount);
addedObjects.forEach(function(item) {
aggregate.pushObject(item);
});
},
contentArrayWillChange: function(array, idx, removedCount, addedCount) {
var removedObjects = array.slice(idx, idx + removedCount);
removedObjects.forEach(function(item) {
aggregate.removeObject(item);
});
}
});
this.get('map').set(array, proxy);
},
remove: function(array) {
var aggregate = this;
array.forEach(function(item) {
aggregate.removeObject(item);
});
this.get('map').remove(array);
}
});