How to load partials / views / templates dynamically in Ember.js - javascript

So I have the following setup.
On the main page, a list of generators is being displayed based on a list coming from a model using fixture data.
Now when one of the generator links is clicked, a new page is shown with some input fields that are dynamically generated based on that fixture data.
Until this point everything works perfectly.
Now when I change the input field's value in the generator page (after selecting one of the generators) to see the changes being updated in some sort of a preview div just below my input fields, it is easy. I can use {{generatorFields.0.value}} to bind the first input field, .1., and so on until I bind all of them.
But as you can imagine, each generator has its own format and its own input fields, and I want to create a new .hbs file for each and every one of them and then pass that file into the generator page to show the preview.
I solved 0.1% of the problem with a partial. In the generator.hbs file I entered {{partial "generator-1"}} and this loads my _generator-3.hbs file that contains that {{generatorFields.0.value}} bind, and it works. But that partial is not dynamic; I need to load a different partial each time I use a different generator. How can I achieve this?
How can I pass the partial name dynamically or load a template based on the model data that I have?
The code used so far is below:
idex.hbs looks like this:
<table class="table table-hover">
<thead>
<tr>
<th>#</th>
<th>Generator name</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{{#each model}}
<tr>
<td>{{id}}</td>
<td>{{title}}</td>
<td>{{#linkTo 'generator' this classNames="btn btn-mini pull-right"}}Create file{{/linkTo}}</td>
</tr>
{{/each}}
</tbody>
</table>
generator.hbs
{{#each generatorFields}}
<div class="row-fluid">
<div class="span4">{{name}}</div>
<div class="span8">{{view Ember.TextField valueBinding='value' class='span12' placeholder='Type value here…'}}</div>
</div>
{{/each}}
{{partial "generator-1"}}
_generator-1.hbs
<h1>Project: {{generatorFields.0.value}}</h1>
app.js
App.Store = DS.Store.extend({
revision: 13,
adapter: 'DS.FixtureAdapter'
});
App.Router.map(function () {
this.resource('index', { path: '/' });
this.resource('generator', {path: '/generator/:generator_id'});
});
App.IndexRoute = Ember.Route.extend({
model: function () {
return App.Generator.find();
}
});
App.Generator = DS.Model.extend({
title: DS.attr('string'),
templateName: DS.attr('string'),
generatorFields: DS.attr('generatorFields')
});
// Fixture data
DS.RESTAdapter.registerTransform('generatorFields', {
serialize: function(serialized) {
return Em.none(serialized) ? {} : serialized;
},
deserialize: function(deserialized) {
return Em.none(deserialized) ? {} : deserialized;
}
});
App.Generator.FIXTURES = [{
id: 1,
title: "test 1",
generatorFields: [
{id: 1, name: "name 1", value: ""}
],
templateName: "generator-1"
}, {
id: 2,
title: "test 2",
generatorFields: [
{id: 1, name: "name 1", value: ""},
{id: 2, name: "name 2", value: ""},
],
templateName: "generator-2"
}];

You can create a dynamic partial helper that uses the passed in name to render with the {{partial}} helper.
Ember.Handlebars.helper('dynPartial', function(name, options) {
return Ember.Handlebars.helpers.partial.apply(this, arguments);
});
Then use this dynamic partial, {{dynPartial}} instead.
{{#each item in controller}}
{{dynPartial item.templateName}}
{{/each}}
For a generator with templateName of generator-1. This would render with the partial _generator-1. Note that the name of the template's id/data-template-name must begin with an underscore.

You should be able to simply place your dynamic partial variable within the partial helper.
{{#each item in controller}}
{{partial item.templateName}}
{{/each}}
As #darshan-sawardekar pointed out if you have a generator with templateName of generator-1it would render the partial _generator-1.

While #Darshan's answer is simpler than the below and will work in many cases, I just ran into an issue where transitioning to a same route with a different model causes the partial to not re-render if the second model's partial name is the same as the first's (bug in ember?). Setting up a view that watches the model fixes this.
App.FooDynamicLayout = Ember.View.extend
rerenderOnModelChange: (->
#rerender()
).observes('model')
And call it with:
view App.FooDynamicLayout templateName=dynamicTemplateName model=model

#KamrenZ already mentioned this but I figured I'd cite chapter and verse for those looking into this. More recent versions of Ember gracefully accept bound property names and use them in the partial helper:
http://ember-doc.com/classes/Ember.Handlebars.helpers.html#method_partial
BOUND TEMPLATE NAMES The parameter supplied to partial can also be a
path to a property containing a template name, e.g.:
{{partial someTemplateName}}
The above example will look up the value of someTemplateName on the
template context (e.g. a controller) and use that value as the name of
the template to render. If the resolved value is falsy, nothing will
be rendered. If someTemplateName changes, the partial will be
re-rendered using the new template name.

Related

Backbone one to many relationschip

I'm a beginner with Backbone.js and i have trouble to make a one to many relationschip between two Backbone models, and showing this data into a html tabel row.
I'm working with a machine that can have multiple orders. the data will look like :
machine 10 has orders 1,2,5,9.
machine 14 has orders 3,4,6.
The machine and orders are coupled with a FK machine_id in order.
I'm trying to make the TR element with the first element TH to be the machine, and on the second/third TH i wanna show the orders that are belong to a machine.
So the questions i ask are :
how do i make this one to many relationship, within my backbone model.
how do i make this TR element within my underscore template to show orders that belong to a machine.
Below are the models for Order and Machine :
app.Machine = Backbone.Model.extend({
defaults : {
machine_id : "",
status : "",
description : "",
},
});
app.Order = Backbone.Model.extend({
defaults: {
order_id: "",
description: "",
amount: "",,
begin_date:"",
end_date:"",
machine_id: ""
},
});
app.MachineList = Backbone.Collection.extend({
model: app.Machine
});
Views:
app.MachineView = Backbone.View.extend({
className: 'MachineRow',
template: _.template($('#Machine_Template').html()),
render: function(){
this.$el.html(this.template(this.model.attributes));
return this;
}
});
app.MachineListView = Backbone.View.extend({
el: '#Machine',
initialize: function(initialMachine){
this.collection = new app.MachineList(initialMachine);
this.render();
},
render: function(){
this.collection.each(function(item){
this.renderMachine(item);
filterOrder(item.get('machine_id'));
}, this);
},
renderMachine: function(item){
var machineView = new app.MachineView({
model: item
});
$(machineView.render().el).appendTo(this.$el)
}
});
HTML code : will look something like this??
<script id="machine_template" type="text/template">
<th>
<%= machine_id %> //etc
</th>
<th>
<%= order_id %> //etc
</th>
<th>
<%= order_id %> //etc
</th>
</script>
Assuming you have a collection of orders called orders, you can do something like:
let renderData = machineList.map(machine -> {
return {
machine: machine,
orders: orders.where({machine_id: machine.get('id')}
}
});
this should give you a structure similar to
[{
machine: machineModel1,
orders: [orderModel1, orderModal2]
},{
machine: machineModel2,
orders: [orderModel3, orderModal4]
}]
You can now pass this to your template function which iterates over each entry and renders it.
I see you already have established one to many relationship by referencing your machine_id in your app.Order model
and as for your 2nd question,
how do i make this TR element within my underscore template to show
orders that belong to a machine.
I don't see the <tr></tr> in your template.
In backbone, template is just a dumb html component where logic is discouraged. All your logic will reside in your views, ideally you may have a collection for your orders and query your orders collection by the machine_id field.

Use child scope's data for parent scope's ng-repeat

Update 2 added, see below
First of all, this is the starting point of the framework I am working with (and needs to fix):
// index.html
<!doctype html>
<html ng-app="myApp">
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.7/angular.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
<script src="index.js"></script>
<body>
<div ng-controller="outerController">
<div id="copy"></div>
<hr>
<div id="src" ng-controller="innerController">
<table>
<th>Name</th>
<th>Type</th>
<tbody>
<tr ng-repeat="poke in pokemon">
<td>{{ poke.name }}</td>
<td>{{ poke.type }}</td>
</tr>
<tr>
<td>Pikachu</td>
<td>Electric</td>
</tr>
</tbody>
</table>
</div>
</div>
</body>
</html>
// index.js
var app = angular.module("myApp", []);
app.controller("innerController", function($scope) {
$scope.pokemon = [{
name: "Bulbasaur",
type: "Grass/Poison"
}, {
name: "Charmander",
type: "Fire"
}, {
name: "Squirtle",
type: "Water"
}];
});
app.controller("outerController", function($scope) {
$("#copy").html($("#src").html());
});
So as you can see, the child controller will generate a table from its scope's data via ng-repeat. This step is successful. The next step is for the parent controller to copy-paste the inner HTML from src to copy. The intent is for copy to contain a copy of the complete table fully generated by angularJS inside src.
This step has failed. Only the table headers and the static Pikachu row is visible. After doing some research I am certain that this is because pokemon is inside the child controller's scope which is inaccessible by the parent controller. The HTML copied into the copy container includes the entire ng-repeat directive. This copied directive is inside the parent scope, where $scope.pokemon does not exist/contains no data, which is why the ng-repeat in copy generated nothing.
I cannot put the data inside the parent controller. In my actual application, the system uses a modular design. Each inner controller represents a module which pulls its own set of data from the server. There are multiple web pages (represented by the outer controller) which have a many-to-many relationship with the modules, and the composition of modules in each web page needs to be modifiable. That means the data used by a module must be contained within itself.
How can I rectify this?
Update 1: Redacted. I posted an example of using $emit and $on but Robert's example should be assumed as correct, since I'm still very new to this. Refer to his answer.
Update 2: While testing Alvaro Vazquez's & Robert's solutions, I've identified the specific root cause. When $("#copy").html($("#src").html()); is executed, either the copied ng-repeat executed before any data transfer to outerController occurred, or it was never executed. In the end, modifying what I originally did above makes it fully working:
var app = angular.module("myApp", []);
$(function() {
$("#copy").html($("#src").html());
});
app.controller("innerController", function($scope) {
$scope.pokemon = [{
name: "Bulbasaur",
type: "Grass/Poison"
}, {
name: "Charmander",
type: "Fire"
}, {
name: "Squirtle",
type: "Water"
}];
});
app.controller("outerController", function($scope) {
$scope.pokemon = [{
name: "Bulbasaur",
type: "Grass/Poison"
}, {
name: "Charmander",
type: "Fire"
}, {
name: "Squirtle",
type: "Water"
}];
});
With the location of that particular statement changed, all that is left is to transfer the data to outerController, and at this point both Alvaro's and Robert's solutions work.
As an aside, I think some have advised against using $("#copy").html($("#src").html());. As I have partly described in the comments, the actual application I'm developing consists of multiple web pages, each containing its own outerController. Each innerController is in its own separate HTML file added via an include directive into src. The outerController copies the inner HTML of src, passes it to a third party library, which pastes it into copy and controls its visual layout. $("#copy").html($("#src").html()); is actually part of the third party library's implementation, so I can't change that. Using this statement is therefore a requirement.
I'll post the above as a solution when I get home and has the convenience of a PC keyboard. In the meantime feel free to recommend better solutions to what is found if you have one, thanks!
I think you should make use of angular services.
Declaring a service
First of all, you should declare a service which would 'serve' the data to the rest of your application. For the sake of simplicity, I will only show a method which returns a predefined array, but you could get the data from the server here.
app.service('pokemonService', function () {
return {
getPokemon: function () {
return [{
name: "Bulbasaur",
type: "Grass/Poison"
}, {
name: "Charmander",
type: "Fire"
}, {
name: "Squirtle",
type: "Water"
}];
}
};
});
Using the service in your controller
Then, you can use the service on any of your controllers, injecting it as any other predefined angular service:
app.controller('innerController', function($scope, pokemonService) {
$scope.pokemon = pokemonService.getPokemon();
});
app.controller('outerController', function($scope, pokemonService) {
$scope.outerPokemon = pokemonService.getPokemon();
});
Showing the data in your view
Finally, you can list all your pokémon in any template/part of the template you want:
<!doctype html>
<html ng-app="myApp">
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.7/angular.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
<script src="index.js"></script>
<body>
<div ng-controller="outerController">
<div id="copy">
<!-- Here you can also list the pokémon from your outerController, maybe this time in a list -->
<ul>
<li ng-repeat="poke in pokemonOuter">
{{ poke.name }} - <span class="type">{{ poke.type }}</span>
</li>
</ul>
</div>
<hr>
<div id="src" ng-controller="innerController">
<table>
<th>Name</th>
<th>Type</th>
<tbody>
<tr ng-repeat="poke in pokemon">
<td>{{ poke.name }}</td>
<td>{{ poke.type }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</body>
</html>
Wrap up
As you can see, there is no need of messing with the DOM at all. If you use AngularJS, you should do things the Angular way, and working directly with the DOM is not the Angular way at all. Instead, you should put all your data and business logic into services, then use those services in your controllers to retrieve that data and pass it to the view.
Scopes in Angular uses prototypal inheritance, so the child scope will have access to the parent properties but the parent will not have access to the child controller scope properties.
You can use a service to share data or use $emit to send events upwards (upwards until the root scope).
I created a plnkr for you to show you how to use emit (you can find it here)
var app = angular.module("myApp", []);
app.controller("outerController", ['$scope', function($scope) {
console.log('aici');
$scope.$on('pokemonEvent', function(event, mass) { console.log(mass); });
}]);
app.controller("innerController", ['$scope', function($scope) {
$scope.pokemon = [{
name: "Bulbasaur",
type: "Grass/Poison"
}, {
name: "Charmander",
type: "Fire"
}, {
name: "Squirtle",
type: "Water"
}];
$scope.$emit('pokemonEvent', $scope.pokemon);
}]);

Ember JS : Make relationships work with multiple files

I've just begun to work on Ember.js and I'm currently trying to figure out, on a simple example, how relationships work between two models and their associated fixtures.
The two models are contained in two different files :
The first one in a graph.js file:
App.Graph= DS.Model.extend({
title: DS.attr('string'),
elements: DS.hasMany('element',{ async: true })
});
App.Graph.FIXTURES = [
{
id:1,
title:'Test',
elements: 1
}
];
The second in an element.js file:
App.Element = DS.Model.extend({
x: DS.attr('string'),
y: DS.attr('string'),
graph: DS.belongsTo('graph',{ async: true })
});
App.Element.FIXTURES = [
{
id:1,
x:'10',
y:'10',
graph:1
}
];
I simply want to display the coordinates, here is the template :
<script type="text/x-handlebars" data-template-name="test">
{{#each item in model}}
{{#each item.elements}}
<ul>
<li>
<label>{{x}}</label>
<label>{{y}}</label>
</li>
</ul>
{{/each}}
{{/each}}
</script>
And the associated route :
App.TestRoute = Ember.Route.extend({
model: function() {
return this.store.find('graph');
}
});
The problem is that I get this error Error while processing route: test No model was found for 'graph' Error: No model was found for 'graph'
I suspect that the problem comes with the relationships and the fact that the models are in separate files included in the html file.
I've spent a long time on how to make this work but I don't really know how to handle it.
Any thoughts? Thanks for your time!

Ember.js: Where does this method go?

Coming to Ember from Rails, one of the places I'm struggling is trying to figure out Ember's definitions of models, views, and controllers.
I'm just testing out some sample Ember code. I'm getting my user events via the GitHub API, and I want to change the type name into something readable.
I have a jsbin here, but here's the gist:
App = Ember.Application.create();
App.IndexRoute = Ember.Route.extend({
model: function(){
return Ember.$.getJSON('https://api.github.com/users/thenickcox/events').then(function(data){
return data.splice(0,7);
});
}
});
I have a method that types a type and returns a string:
interpretType: function(type){
if (type === 'PushEvent') {
return 'Pushed';
}
return name;
}
In Rails, this would go on the model. But the only model here is the one that Ember created in memory by default (right?). So then I thought, it's something that each member of the array needs, because here's the view:
<h3> Some events</h3>
<ul>
{{#each}}
<li>I {{interpretType(type)}} to {{repo.name}}</li>
{{/each}}
</ul>
So is that something that goes on Ember.ArrayController? I tried that, like this:
App.IndexController = Ember.ArrayController.extend({
interpretType: function(type){
if (type === 'PushEvent') {
return 'Pushed';
}
return name;
}.property()
});
That just gave me an error. Where do I put this?
PS. So you don't have to look at the GitHub API, here's an example JSON object:
{
id: "1890853674",
type: "CreateEvent",
actor: {
id: 702327,
login: "thenickcox",
gravatar_id: "63f35d9e50dfd73281126b051a51668a",
url: "https://api.github.com/users/thenickcox",
avatar_url: "https://2.gravatar.com/avatar/63f35d9e50dfd73281126b051a51668a?d=https%3A%2F%2Fa248.e.akamai.net%2Fassets.github.com%2Fimages%2Fgravatars%2Fgravatar-user-420.png&r=x"
},
repo: {
id: 14463966,
name: "thenickcox/whiskey_taster",
url: "https://api.github.com/repos/thenickcox/whiskey_taster"
},
payload: {
ref: "master",
ref_type: "branch",
master_branch: "master",
description: "My first ember/rails app"
},
public: true,
created_at: "2013-11-17T09:00:17Z"
},
Here is an updated JSBin
Basically, the each can specify an itemController to decorate the model.
App.EventController = Ember.ObjectController.extend({
interpretType: function(){
var type = this.get('model.type');
if (type === 'PushEvent') {
type = 'Pushed';
}
return type;
}.property('model.type')
});
Handlebars doesn't have functions as you've written it, but since we are now using the event controller which wraps the single model, we just refer to interpretType to do the translation:
{{#each itemController='event'}}
<li>{{interpretType}} to {{repo.name}}</li>
{{/each}}
Put it inside an Ember.ObjectController
ArrayController's deal with methods related to the collection of data from the model, whereas ObjectController deals with methods related to the specific object.
I'm also learning Ember from a Rails background.
If you haven't already come across this, you will definetely want to check out ember-tools, it's a command line generator very similar to what we've got in rails. I cant imagine building an Ember app without something like it..

Ember.js: Proper way to iterate through object in model?

I'm developing my first EmberJS app after following some tutorials as practice. It simply contains a list of 'tables', 'columns', and 'rows' similar to a database.
Link to the problematic page: http://www.kangarooelectronics.com/fakeDB/#/tables/edit/2
My issue is that when I go to remove a column I get:
Object # has no method 'deleteRecord'
As I understand this is due to the object I'm iterating through having no references to the controller because of the way I am constructing the array that I use to create my list.
Removing tables works fine, which are listed in the following fashion:
{{#each model itemController='TableList'}}
<a {{action removeTable this}}>Delete</a>
{{/each}}
I'm iterating through the columns via:
{{#each column in currentColumns itemController='TablesEdit'}}
<a {{action removeColumn column}}>Drop</a>
{{/each}}
Snippet from FIXTURES object:
FakeDB.Table.FIXTURES = [
{
id: 1,
name: 'Users',
columns: {
1:{'colId':1, 'name':'name'},
2:{'colId':2, 'name':'favorite color'},
3:{'colId':3, 'name':'phone number'}
},
// ...snip... //
I am getting 'currentColumns' via:
FakeDB.Table = DS.Model.extend({
name: DS.attr('string'),
columns: DS.attr('object'),
rows: DS.attr('object'),
currentColumns: function() {
var newColumns = $.map(this.get('columns'), function(k, v) {
return [k];
});
return newColumns;
}.property('columns'),
// ..snip.. //
Here you can see my problem... it's obvious that my 'column' isn't going to have any methods from my controller. I tried something like this:
FakeDB.Adapter = DS.FixtureAdapter.extend();
FakeDB.Adapter.map('FakeDB.Table', {
columns: {embedded: 'load'},
rows: {embedded: 'load'}
});
FakeDB.Columns = DS.Model.extend({
colId: DS.attr('integer'),
name: DS.attr('string')
});
FakeDB.Rows = DS.Model.extend({
colId: DS.attr('integer'),
name: DS.attr('string')
});
But I couldn't get {{#each column in columns}} to work with that.
Any suggestions? I'm going to read the docs again and will post back if I find a solution.
Thanks!
EDIT:
So I think I found another solution, but I'm still running into a little issue.
FakeDB.Table = DS.Model.extend({
name: DS.attr('string'),
columns: FakeDB.Columns.find().filter(function(item, index, self) {
if(item.tableID == 1) { return true; }
})
});
Still not sure what to replace 'item.tableID == 1' with so that I get items with the tableID referencing to the current page...
Columns are structured as...
FakeDB.Columns.FIXTURES = [
{
id: 1,
tableID: 1,
name: 'name'
},
// ...snip... //
But now I get:
assertion failed: Your application does not have a 'Store' property defined. Attempts to call 'find' on model classes will fail. Please provide one as with 'YourAppName.Store = DS.Store.extend()'
I am in fact defining a 'Store' property...
I'm developing my first EmberJS app after following some tutorials as practice. It simply contains a list of 'tables', 'columns', and 'rows' similar to a database.
Most databases do contain a list of tables, rows and columns. Most web applications contain a fixed set of tables with pre-defined columns and a dynamic list of rows. If this is your first ember app i would recommend starting with something that keeps you on the happy path.
I am in fact defining a 'Store' property...
True but ember is complaining because store is not available before ember app is initialized. Anything that accesses the store should be in a framework hook of some kind. It can't be used when defining your objects, which wouldn't make a lot of sense anyway.
Probably what you meant to do was make a computed property called columns like this:
FakeDB.Table = DS.Model.extend({
name: DS.attr('string'),
columns: function() {
FakeDB.Columns.find().filter(function(item, index, self) {
if(item.tableID == 1) { return true; }
})
}.property('')
});

Categories