ember component based on model type - javascript

I know this is somewhat duplicated, but All my efforts to create a dynamic component renderer are failing possibly due to my lack of knowledge in ember concepts.
My Scenario is a multi purpose search bar which will search for models in the cache. I would like each search result to be rendered below the search input according to the model's type key. the handlebars file will be named according to the model type with the syntax components/app-search-<model-type-key>.hbs e.g. the template name for a customer model should be components/app-search-customer.hbs
my search template looks like this:
<div class="well well-md">
<div class="input-group">
{{input value=searchTerm class="form-control"}}
</div>
{{#if searchTerm}} <!-- Updating searchTerm causes results to populate with models -->
{{#if results.length}}
<ul class="list-group search-results">
{{#each result in results}}
<!-- todo -->
{{renderSearchComponent model=result}}
{{/each}}
</ul>
{{else}}
Nothing here Captain
{{/if}}
{{/if}}
</div>
And my attempt at a renderSearchComponent helper looks like this:
Ember.Handlebars.registerHelper('renderSearchComponent', function(model, options) {
var modelType = options.model.constructor.typeKey,
componentPath,
component,
helper;
if (typeof modelType === 'undefined') {
componentPath = "app-search-default";
} else {
componentPath = "app-search-" + modelType;
}
component = Ember.Handlebars.get(this, componentPath, options),
helper = Ember.Handlebars.resolveHelper(options.data.view.container, component);
helper.call(this, options);
});
When this runs the options.model throws: TypeError: options.model is undefined and additionally i have the following error:
Error: Assertion Failed: Emptying a view in the inBuffer state is not allowed and should not happen under normal circumstances. Most likely there is a bug in your application. This may be due to excessive property change notifications.
I have been batting my eyelids for what seems hours now trying to get this right. Is what I am asking for even possible?
Thank you in advance.

I know this is a year old question, but Ember since version 1.11+ has the new component helper to dynamically render components.
{{#each model as |post|}}
{{!-- either foo-component or bar-component --}}
{{component post.componentName post=post}}
{{/each}}

You were on the right path, a working example could be like:
import {handlebarsGet} from "ember-handlebars/ext";
Ember.Handlebars.registerHelper('renderSearchComponent', function(value, options) {
var propertyValue;
if (options) {
if ( options.types[0] !== 'STRING' ) {
var context = (options.contexts && options.contexts.length) ? options.contexts[0] : this;
propertyValue = handlebarsGet(context, value, options);
propertyValue = propertyValue.constructor.typeKey;
} else {
propertyValue = value;
}
} else {
options = value;
propertyValue = 'default';
}
var property = 'app-search-'+propertyValue;
var helper = Ember.Handlebars.resolveHelper(options.data.view.container, property);
if (helper) {
return helper.call(this, options);
}
});
This helper allows to pass either string, nothing or binding property.
{{renderSearchComponent}}
{{renderSearchComponent 'book'}}
{{renderSearchComponent result}}
Helper internals are not completed documented, i think because they are not a public API. But you could take inspiration by looking at the helper source code.

Related

Unable to view data on an oservable

I have a View model, which has a loaddata function. It has no constructor. I want it to call the loadData method IF the ID field has a value.
That field is obtained via:
self.TemplateId = ko.observable($("#InputTemplateId").val());
Then, at the end of my ViewModel, I have a bit of code that checks that, and calls my load function:
if (!self.CreateMode()) {
self.loadData();
}
My load method makes a call to my .Net WebAPI method, which returns a slighly complex structure. The structure is a class, with a few fields, and an Array/List. The items in that list, are a few basic fields, and another List/Array. And then THAT object just has a few fields. So, it's 3 levels. An object, with a List of objects, and those objects each have another list of objects...
My WebAPI call is working. I've debugged it, and the data is coming back perfectly.
self.loadData = function () {
$.get("/api/PlateTemplate/Get", { id: self.TemplateId() }).done(function (data) {
self.Data(ko.mapping.fromJS(data));
});
}
I am trying to load the contents of this call, into an observable object called 'Data'. It was declared earlier:
self.Data = ko.observable();
TO load it, and keep everything observable, I am using the Knockout mapping plugin.
self.Data(ko.mapping.fromJS(data));
When I breakpoint on that, I am seeing what I expect in both data (the result of the API call), and self.Data()
self.Data seems to be an observable version of the data that I loaded. All data is there, and it all seems to be right.
I am able to alert the value of one of the fields in the root of the data object:
alert(self.Data().Description());
I'm also able to see a field within the first item in the list.
alert(self.Data().PlateTemplateGroups()[0].Description());
This indicates to me that Data is an observable and contains the data. I think I will later be able to post self.Data back to my API to save/update.
Now, the problems start.
On my View, I am trying to show a field which resides in the root class of my complex item. Something I alerted just above.
<input class="form-control" type="text" placeholder="Template Name" data-bind="value: Data.Description">
I get no error. Yet, the text box is empty.
If I change the code for the input box to be:
data-bind="value: Data().Description()"
Data is displayed. However, I am sitting with an error in the console:
Uncaught TypeError: Unable to process binding "value: function
(){return Data().Description() }" Message: Cannot read property
'Description' of undefined
I think it's due to the view loading, before the data is loaded from the WebAPI call, and therefore, because I am using ko.mapping - the view has no idea what Data().Description() is... and it dies.
Is there a way around this so that I can achieve what I am trying to do? Below is the full ViewModel.
function PlateTemplateViewModel() {
var self = this;
self.TemplateId = ko.observable($("#InputTemplateId").val());
self.CreateMode = ko.observable(!!self.TemplateId() == false);
self.IsComponentEditMode = ko.observable(false);
self.IsDisplayMode = ko.observable(true);
self.CurrentComponent = ko.observable();
self.Data = ko.observable();
self.EditComponent = function (data) {
self.IsComponentEditMode(true);
self.IsDisplayMode(false);
self.CurrentComponent(data);
}
self.loadData = function () {
$.get("/api/PlateTemplate/Get", { id: self.TemplateId() }).done(function (data) {
self.Data(ko.mapping.fromJS(data));
});
}
self.cancel = function () {
window.location.href = "/PlateTemplate/";
};
self.save = function () {
var data = ko.mapping.toJS(self.Data);
$.post("/api/PlateTemplate/Save", data).done(function (result) {
alert(result);
});
};
if (!self.CreateMode()) {
self.loadData();
}
}
$(document).ready(function () {
ko.applyBindings(new PlateTemplateViewModel(), $("#plateTemplate")[0]);
});
Maybe the answer is to do the load inside the ready() function, and pass in data as a parameter? Not sure what happens when I want to create a New item, but I can get to that.
Additionally, when I try save, I notice that even though I might change a field in the view (Update Description, for example), the data in the observed view model (self.Data) doesn't change.
Your input field could be this:
<div data-bind="with: Data">
<input class="form-control" type="text" placeholder="Template Name" data-bind="value: Description">
</div>
I prefer using with as its cleaner and should stop the confusion and issues you were having.
The reason that error is there is because the html is already bound before the data is loaded. So either don't apply bindings until the data is loaded:
$.get("/api/PlateTemplate/Get", { id: self.TemplateId() }).done(function (data) {
self.Data(ko.mapping.fromJS(data));
ko.applyBindings(self, document.getElementById("container"));
});
Or wrap the template with an if, therefore it won't give you this error as Data is undefined originally.
self.Data = ko.observable(); // undefined
<!-- ko if: Data -->
<div data-bind="with: Data">
<input class="form-control" type="text" placeholder="Template Name" data-bind="value: Description">
</div>
<!-- /ko -->
Also if you know what the data model is gonna be, you could default data to this.
self.Data = ko.observable(new Data());
Apply Bindings Method:
var viewModel = null;
$(document).ready(function () {
viewModel = new PlateTemplateViewModel();
viewModel.loadData();
});

If HTML response is null, display alternate content

My api call returns html, but if that html is empty e.g. I get a console html response of "", I want to display a default message using knockout. So I'm guessing that it needs to recognise that "" is empty and then display my alternate content.
View model -
var MyText = ko.observable();
var company = shell.authenticatedCompany();
hazibo.helpTextGet(company.name, company.userName, company.password).then(function (data) {
MyText(data);
});
return {
MyText: MyText
};
View -
<section class="help-text">
<div class="flex-container">
<div class="flex-item" data-bind="html: MyText">This is my alternate message if the html response is ""</div>
</div>
</section>
There are a few ways you could go about it. Personally I like to keep as much code out of the markup as possible so I would check your response data in the api callback and set it there. No need to create messy looking data bindings if you just update the observable appropriately.
hazibo.helpTextGet(company.name, company.userName, company.password).then(function (data) {
if(!data) {
MyText("This is my alternate message...");
}else{
MyText(data);
}
});
If you need to preserve what the api call actually returned you could place the logic in a computed instead, and bind to that.
One way to achieve this is to use a computed observable to determine which set of html to display:
https://jsfiddle.net/dw1284/ucnewzwo/
HTML:
<section class="help-text">
<div class="flex-container">
<div class="flex-item" data-bind="html: ItemHtml()"></div>
</div>
</section>
JavaScript:
function ViewModel() {
var self = this;
// Html populated from API call
self.MyText = ko.observable('');
// Default Html
self.Default = ko.observable('This is my alternate message if the html response is ""');
// Computed observable chooses which HTML to display (bind this to view)
self.ItemHtml = ko.computed(function() {
if (!self.MyText() || self.MyText() === '') {
return self.Default();
} else {
return self.MyText();
}
});
}
ko.applyBindings(new ViewModel());

Ember.js route problems with filter

My aim is to pass filtered data to my controller and then to my template. I've tried not using the filter and everything works as expected. If I even try to use a filter that lets everything through, I don't get any data. I've even tried using false instead of true and fiddling with the argument list of the filter.
I'm using ember-data fixtures to test this. I'm following the name conventions so much of the work is done for me under the hood. That all seems to be working though (otherwise the first example should also have a problem).
Works (arrives in the controller and eventually gets rendered on the page):
App.DomainDirRoute = Ember.Route.extend({
model: function(params) {
return this.store.find('domain_dir');
}
});
Fails (controller gets an empty array):
App.DomainDirRoute = Ember.Route.extend({
model: function(params) {
return this.store.filter('domain_dir', function(item){
return true;
});
}
});
UPDATE (ATTEMPT 1):
Okay, so I've tried a couple of things based on Sam Selikoff's answer. I've defined 4 properties (2 filters, one map, one plain copy) in the controller and tried to display each in the mockup page. Only the property copyDomain gives a result.
Properties in controller:
filteredDomains: Ember.computed.filterBy('domain', 'domain', true),
upperCaseDomains: Ember.computed.map('domain', function(domain, index) {
return domain.toUpperCase() + '!';
}),
filteredDomains2: function() {
return this.get("model").filterBy('domain', true);
}.property('model.#each.domain'),
copyDomains: function(){
result = [];
this.forEach(function(item) {
result.pushObject(item);
})
console.log(result);
return result;
}.property('model.#each.domain')
Mockup:
<ul>
<li>filteredDomains</li>
{{#each domainDir in controller.filteredDomains}}
<li>domainDir.domain</li>
{{/each}}
</ul>
<ul>
<li>filteredDomains2</li>
{{#each domainDir in controller.filteredDomains2}}
<li>domainDir.domain</li>
{{/each}}
</ul>
<ul>
<li>upperCaseDomains</li>
{{#each domainDir in controller.upperCaseDomains}}
<li>domainDir.domain</li>
{{/each}}
</ul>
<ul>
<li>copyDomains</li>
{{#each domainDir in controller.copyDomains}}
<li>domainDir.domain</li>
{{/each}}
</ul>
Filtering is generally done at the controller/component level. store.find makes an AJAX request. Is your goal to only retrieve the filtered subset of data from the server, or to filter the data you already have at the view layer?
Typically if you're just wanting to do some live filtering, you'll do it in the controller. Leave your model hook as this.store.find('domain_dir') and add a filter in your controller:
App.DomainDirController = Ember.Controller.extend({
filteredDomains: function() {
return this.get("model").filterBy('someProp', true);
}.property('model.#each.someProp')
});
You should also check out the computed macros for some shorthands:
App.DomainDirController = Ember.Controller.extend({
filteredDomains: Ember.computed.filterBy('model', 'someProp');
});
Now in your template, you can do
{{#each domain in filteredDomains}}
...

Knockout foreach binding not working

I'm following John Papa's jumpstart course about SPA's and trying to display a list of customers loaded via ASP.NET Web API the knockout foreach binding is not working. The Web API is working fine, I've tested it on it's own and it is returning the correct JSON, because of that I won't post the code for it. The get method simply returns one array of objects, each with properties Name and Email. Although not a good practice, knockout is exposed globaly as ko by loading it before durandal.
I've coded the customers.js view model as follows
define(['services/dataservice'], function(ds) {
var initialized = false;
var customers = ko.observableArray();
var refresh = function() {
return dataservice.getCustomers(customers);
};
var activate = function() {
if (initialized) return;
initialized = true;
return refresh();
};
var customersVM = {
customers: customers,
activate: activate,
refresh: refresh
};
return customersVM;
});
The dataservice module I've coded as follows (I've not wrote bellow the function queryFailed because I know it's not being used)
define(['../model'], function (model) {
var getCustomers = function (customersObservable) {
customersObservable([]);
var options = {url: '/api/customers', type: 'GET', dataType: 'json'};
return $.ajax(options).then(querySucceeded).fail(queryFailed);
function querySucceeded(data) {
var customers = [];
data.forEach(function (item) {
var c = new model.Customer(item);
customers.push(c);
});
customersObservable(customers);
}
};
return {
getCustomers: getCustomers
};
});
Finaly the model module was built as follows:
define(function () {
var Customer = function (dto) {
return mapToObservable(dto);
};
var model = {
Customer: Customer
};
return model;
function mapToObservable(dto) {
var mapped = {};
for (prop in dto)
{
if (dto.hasOwnProperty(prop))
{
mapped[prop] = ko.observable(dto[prop]);
}
}
return mapped;
}
});
The view is then simply a list, it is simply:
<ul data-bind="foreach: customers">
<li data-bind="text: Name"></li>
</ul>
But this doesn't work. Any other binding works, and I've looked on the console window, and it seems the observable array is being filled correctly. The only problem is that this piece of code doesn't show anything on screen. I've reviewed many times the files but I can't seem to find the problem. What's wrong with this?
You can use the knockout.js context debugger chrome extension to help you debug your issue
https://chrome.google.com/webstore/detail/knockoutjs-context-debugg/oddcpmchholgcjgjdnfjmildmlielhof
Well, I just spent a lot of time on an local issue to realize that the ko HTML comment format, if used, should be like this:
<!-- ko foreach: arrecadacoes -->
and NOT like this:
<!-- ko: foreach: arrecadacoes -->
: is NOT used after ko...
I know this question is a little old but I thought I'd add my response in case someone else runs into the same issue I did.
I was using Knockout JS version 2.1.0 and it seems the only way I can get the data to display in a foreach loop was to use:
$data.property
so in the case of your example it would be
$data.Name
Hope this helps
I don't see anywhere in your code that you've called ko.applyBindings on your ViewModel.
KO has a known issue while using foreach in a non-container element like the one above <ul> so you have to use containerless control flow syntax.
e.g.
<ul>
<!-- ko foreach: customers-->
<li data-bind="text: Name"></li>
<!-- /ko -->
</ul>
Ref: http://knockoutjs.com/documentation/foreach-binding.html

handlebars - is it possible to access parent context in a partial?

I've got a handlebar template that loads a partial for a sub-element.
I would need to access a variable from the parent context in the calling template, from within the partial. .. doesn't seem to resolve to anything inside the partial.
Simplified code goes like this:
the template
{{#each items}}
{{> item-template}}
{{/each}}
the partial
value is {{value}}
(obviously the real code is more complicated but it's the same principle, within the partial .. appears to be undefined.)
To show it's undefined, I've used a very simple helper whatis like this:
Handlebars.registerHelper('whatis', function(param) {
console.log(param);
});
and updated the above code to this:
updated template
{{#each items}}
{{whatis ..}} <-- Console shows the correct parent context
{{> item-template}}
{{/each}}
updated partial
{{whatis ..}} <-- Console shows "undefined"
value is {{value}}
Is there a way to go around that issue? Am I missing something?
EDIT: There's an open issue relating to this question on handlebars' github project
Just in case anyone stumbles across this question. This functionality exists now in Handlebars.
Do this:
{{#each items}}
{{! Will pass the current item in items to your partial }}
{{> item-template this}}
{{/each}}
Working fiddle (inspired by handlebars pull request #385 by AndrewHenderson)
http://jsfiddle.net/QV9em/4/
Handlebars.registerHelper('include', function(options) {
var context = {},
mergeContext = function(obj) {
for(var k in obj)context[k]=obj[k];
};
mergeContext(this);
mergeContext(options.hash);
return options.fn(context);
});
Here's how you'd setup the parent template:
{{#each items}}
{{#include parent=..}}
{{> item-template}}
{{/include}}
{{/each}}
And the partial:
value is {{parent}}
As of 2.0.0 partials now supports passing in values.
{{#each items}}
{{> item-template some_parent_var=../some_parent_var}}
{{/each}}
Took me awhile to find this, hope it's useful for someone else too!
The easiest way to pass the parent context to the partial is to do the loop inside the partial. This way the parent context is passed by default and when you do the loop inside the partial the {{../variable}} convention can access the parent context.
example fiddle here.
The Data
{
color: "#000"
items: [
{ title: "title one" },
{ title: "title two" },
]
}
The Template
<div class="mainTemplate">
Parent Color: {{color}}
{{> partial}}
</div>
The Partial
<div>
{{#each items}}
<div style="color:{{../color}}">
{{title}}
</div>
{{/each}}
</div>
You can use some of the proposed solutions on the comments from the link to github:
https://github.com/wycats/handlebars.js/issues/182#issuecomment-4206666
https://github.com/wycats/handlebars.js/issues/182#issuecomment-4445747
They create helpers to pass the info to the partial.
I created an each Helper function that includes the parent key/values within the subcontext under the key parentContext.
http://jsfiddle.net/AndrewHenderson/kQZpu/1/
Note: Underscore is a dependency.
Handlebars.registerHelper('eachIncludeParent', function ( context, options ) {
var fn = options.fn,
inverse = options.inverse,
ret = "",
_context = [];
$.each(context, function (index, object) {
var _object = $.extend({}, object);
_context.push(_object);
});
if ( _context && _context.length > 0 ) {
for ( var i = 0, j = _context.length; i < j; i++ ) {
_context[i]["parentContext"] = options.hash.parent;
ret = ret + fn(_context[i]);
}
} else {
ret = inverse(this);
}
return ret;
});
To be used as follows:
{{#eachIncludeParent context parent=this}}
{{> yourPartial}}
{{/eachIncludeParent}}
Access parent context values in your partial using {{parentContext.value}}
I needed dynamic form attributes for something like this...
{{#each model.questions}}
<h3>{{text}}</h3>
{{#each answers}}
{{formbuilder ../type id ../id text}}
{{/each}}
{{/each}}
and a helper like so...
Handlebars.registerHelper('formbuilder', function(type, id, qnum, text, options)
{
var q_type = options.contexts[0][type],
a_id = options.contexts[1].id,
q_number = options.contexts[0][qnum],
a_text = options.contexts[1].text;
return new Handlebars.SafeString(
'<input type=' + q_type + ' id=' + a_id + ' name=' + q_number + '>' + a_text + '</input><br/>'
);
});
Which produces...
<input type="checkbox" id="1" name="surveyQ0">First question</input>
My model is a big blob of arrays and objects mixed together. What's noteworthy is that using '../' like so '../type', passes in the parent model as the context, and without it, such as with 'id', it passes in the current model as the context.
To get specifically the parent of the partial (where you may be several partials deep) then follow the other answers like SeanWM.
If you know that the parent is the main template then you can use #root which resolves to the top-most context no matter how deep you are.
e.g. {{#root.rootObject.rootProperty}}
It is a pity that ../../.. does not go up past a partial.

Categories

Resources