Create Ember View from a jQuery object - ember.js

I'm looking into integrating Ember with an existing Rails application, to take advantage of Ember's bindings, events (didInsertElement, etc.) ...
Now I don't want to transfer my erb views to handlebars, but instead I want to create Ember View objects and attach them to various elements already in the DOM. For example, I might have
<html>
<body>
<div class="header">
</div>
<div class="content">
</div>
<div class="footer">
</div>
</body>
</html>
and (on DOM ready) create a View for each element:
App.HeaderView = Ember.View.create({
// capture $('.header') to this
// console.log(this.$().attr('class')) should then output `header`
});

use appendTo() on a view: App.HeaderView.appendTo('.header') see http://jsfiddle.net/yFke9/
UPDATE
I think this is currently not possible. Please proof me wrong! You could create a workaround for this, although this is a hack, see http://jsfiddle.net/jFTk5/. The workaround basically adds the view via append() and inside the didInsertElement callback it replaces the specific element via jQuery's replaceWith.
App.HeaderView = Ember.View.create({
template: Ember.Handlebars.compile('hello from HeaderView'),
classNames: ['header'],
didInsertElement: function() {
Ember.$('.header').replaceWith(this.$());
}
}).append();
If you're going with this solution you could write a Mixin which handles this for you, see http://jsfiddle.net/KFcgA/.
App.ReplaceWith = Ember.Mixin.create({
didInsertElement: function(){
var el = this.get('elementToReplace');
Ember.$(el).replaceWith(this.$());
}
});
App.HeaderView = Ember.View.create(App.ReplaceWith, {
template: Ember.Handlebars.compile('Hello from HeaderView'),
classNames: ['header'],
elementToReplace: '.header'
}).append();

Ok the following works but I haven't fully tested it.
Inspired by #pangratz's pull request I extend Ember.View with the following method for
Ember.View = Ember.Object.extend(
/** #scope Ember.View.prototype */ {
// ........
wrap: function(target) {
this._insertElementLater(function() {
// Set all attributes name/values from target
var target_attrs = {};
var $this = this.$();
for (var attr, i=0, attrs=$(target)[0].attributes, l=attrs.length; i<l; i++){
attr = attrs.item(i)
var attrName = attr.nodeName;
var attrValue = attr.nodeValue;
if(attrName === 'id') continue;
$this.attr(attrName, attrValue);
}
// Set HTML from target
$this.html($(target).html());
Ember.$(target).replaceWith($this);
});
return this;
},
// ........
});
Basically it copies the html content of the target element as well as its attributes. Then by just doing
App.HeaderView = Ember.View.create().wrap('.header');
the .header element (that is already in the DOM) is now in App.HeaderView.
See http://jsfiddle.net/KFcgA/4/

Related

Ember Dynamically Generated HTML

I have a requirement where I need to add html after the DOM has been rendered.
I was wondering if it is possible to manipulate the DOM after creation and dynamically add html and also specifying an associated ember action.
E.g. The intension of what I want to achieve:
$('.add').on("click", function(e){
e.preventDefault();
i += 1;
var content = "<div class=\"item dodgerBlue\"><h1>"+i+"</h1></div>";
var content + "{{action "owlItemClicked" titleModel.id titleModel.index titleModel on="click" }}"
owl.data('owlCarousel').addItem(content);
});
Specifically I want to add another Item to my carousel:
http://owlgraphic.com/owlcarousel/demos/manipulations.html
I'm not sure that the triple-stash, which just includes content without escaping it, will work with an {{action}}. In any case, it looks to me that you'd be better off simply defining the html within an each block and letting Ember handle the content addition.
{{#each model as |titleModel index|}}
<div class=\"item dodgerBlue\">
<h1>{{index}}</h1>
</div>
{{action "owlItemClicked" titleModel.id titleModel.index titleModel on="click" }}
{{/each}}
I noticed you have a titleModel.index property, so maybe you don't need the index in each block and can use the model's property instead.
That would be the Ember way to do it. However, it looks like this OwlCarousel widget wants to have the html passed directly. But there's also a reinit method, so maybe that would be sufficient to tell it that new content has been added through Ember. Something like the following:
carouselOptions: {
// ...
},
didInsertElement: function() {
Ember.run.scheduleOnce('afterRender', this, function() {
var $c = Ember.$('#the-carousel');
if ($c.length) {
$c.owlCarousel( this.get('carouselOptions') );
}
});
},
actions: {
addCarouselItem: function(e){
e.preventDefault();
// Add a new item to your array.
// Not shown because i have no idea of your code.
// Ember will handle DOM insert.
// Then reinit carousel.
$("#the-carousel").data('owlCarousel').reinit( this.get('carouselOptions') );
},
owlItemClicked: function(e) {
// ...
}
}
You can insert dynamically created HTML using the triple-stache in your template.
// controller.js
dynamicHtml: Ember.computed({
get() {
return `<div>Hello World!</div>`;
}
})
...
{{! template.hbs }}
{{{dynamicHtml}}}

Populating nested underscore templates in Backbone.js

I am attempting to create an html page from a complex JSON object. I have already successfully parsed the JSON object into a Collection of Models, Where each Model has a collection of another Model etc..
I therefore have nested views to cater for this.
To create my html page, I have two templates like the following:
<script type="text/template" id="template1">
<h1><%=heading1%></h1>
<h2><%=heading2%></h2>
<ul id="template2-list"></ul>
</script>
<script type="text/template" id='template2'>
<p class = "heading"><%-otherheading%></p>
<div class="content" id="tab">
.....
</div>
</script>
As you can see, I have a template (template1) that contains a list of template2. How would I go about populating these templates from my Backbone nested views?
This is what I have tried:
var CollectionView = Backbone.View.extend({
type: "CollectionView", //For debugging purposes
el: "#container",
initialize: function () {
},
render: function () {
_.each(this.model.models, this.process, this);
return this;
},
process: function(obj)
{
var childItemView = new View1({model: obj});
childItemView.render();
this.$el.append(childItemView.el); //This works fine since I only have one Model in the highest level collection
}
})
var View1 = Backbone.View.extend({
type: "View1",
template: _.template($("#template1").html()),
tagName: "div",
className: "tableRow",
initialize:function () {
this.model.on("change", this.modelChanged, this);
},
render: function () {
var outputHtml = this.template(this.model.toJSON());
this.$el.html(outputHtml);
this.model.get('nestedModel').each(this.process, this);
return this;
},
process: function(obj) {
var childItemView2 = new View2({model: obj});
childItemView2.render();
childItemView2.el = '#template2-list';
$(this.el).append(childItemView2.el); //This still results in template2 being placed after the entire template 1
},
modelChanged: function(model, changes) {
console.log("modelChanged: " + this.model.get('title'));
}
});
If it's just populating underscore, then you should convert the collection to json(including the submodels collections), and you can add a for loop inside of the template. <% for(var x... %>.
The other option is, to use a library like marionette which has a composite view which can hold collection views, you can see an example for a treeView here: http://lostechies.com/derickbailey/2012/04/05/composite-views-tree-structures-tables-and-more/
it basically shows how to render collections inside collections.
There are lots of ways to do this.
Template inside template
Pass the entire collection and do all recursive iteration logic in the template itself by calling the child template inside parent template itself. Only one view is involved.
<script type="text/template" id="template1">
<h1><%=heading1%></h1>
<h2><%=heading2%></h2>
<ul id="template2-list">
<!-- Your iteration logic goes here -->
<%= _.template($("#template2").html())({model: model}) %>
</ul>
</script>
<script type="text/template" id='template2'>
<p class = "heading"><%-otherheading%></p>
<div class="content" id="tab"></div>
</script>
Better way is:
In the collection view, create a child view instance(you have done that)
Do the recursive iteration logic to read the collection models in the collection view(parentview) and call the child view to render the child collections.
If you want a complete solution, create a fiddle with json and html. Will help you to make it work.
I realised my mistake. Not sure if this is entirely correct, but I rendered the parent view first, then found the new list element (template2-list) and appended the rendered child view to that.
i.e.
render: function () {
var outputHtml = ...
this.$el.html(outputHtml); //render parent view
this.model.get('nestedModel').each(this.process, this);
...
},
process: function(obj) {
var childItemView2 = new View2({model: obj});
childItemView2.render();
this.$('#template2-list').append(childItemView2.el);
},
Thanks for all the help!

Ember: how to set classNameBindings on parent element of view

See http://jsfiddle.net/4ZyBM/6/
I want to use Bootstrap for my UI elements and I am now trying to convert certain elements to Ember views. I have the following problem:
I embed an input element in a DIV with a given class (control-group). If a validation error occurs on the field, then I want to add an extra class "error" to the DIV.
I can create a view based on the Ember.TextField and specify that if the error occurs the ClassNameBinding should be "error", but the problem is that class is the set to the input element and not to the DIV.
You can test this by entering a non alpha numeric character in the field. I would like to see the DIV border in red and not the input field border.
HTML:
<script type="text/x-handlebars">
<div class="control-group">
{{view App.AlphaNumField valueBinding="value" type="text" classNames="inputField"}}
</div>
</script>
JS:
App.AlphaNumField = Ember.TextField.extend({
isValid: function () {
return /^[a-z0-9]+$/i.test(this.get('value'));
}.property('value'),
classNameBindings: 'isValid::error'
})
Can I set the classNameBindings on the parent element or the element closest to the input ? In jQUery I would use:
$(element).closest('.control-group').addClass('error');
The thing here is that without using jQuery you cannot access easily the wrapping div around you Ember.TextField's. Also worth mentioning is that there might be also a hundred ways of doing this, but the simplest solution I can think of would be to create a simple Ember.View as a wrapper and check the underlying child views for validity.
Template
{{#view App.ControlGroupView}}
{{view App.AlphaNumField
valueBinding="value"
type="text"
classNames="inputField"
placeholder="Alpha num value"}}
{{/view}}
Javascript
App.ControlGroupView = Ember.View.extend({
classNameBindings: 'isValid:control-group:control-group-error',
isValid: function () {
var validFields = this.get('childViews').filterProperty('isValid', true);
var valid = validFields.get('length');
var total = this.get('childViews').get('length')
return (valid === total);
}.property('childViews.#each.isValid')
});
App.AlphaNumField = Ember.TextField.extend({
isValid: function () {
return /^[a-z0-9]+$/i.test(this.get('value'));
}.property('value')
});
CSS
.control-group-error {
border:1px solid red;
padding:5px;
}
.control-group {
border:1px solid green;
padding:5px;
}
Working demo.
Regarding bootstrap-ember integration and for the sake of DRY your could also checkout this ember-addon: https://github.com/emberjs-addons/ember-bootstrap
Hope it helps.
I think that this is the more flexible way to do this:
Javascript
Boostrap = Ember.Namespace.create();
To simplify the things each FormControl have the properties: label, message and an intern control. So you can extend it and specify what control you want. Like combobox, radio button etc.
Boostrap.FormControl = Ember.View.extend({
classNames: ['form-group'],
classNameBindings: ['hasError'],
template: Ember.Handlebars.compile('\
<label class="col-lg-2 control-label">{{view.label}}</label>\
<div class="col-lg-10">\
{{view view.control}}\
<span class="help-block">{{view.message}}</span>\
</div>'),
control: Ember.required()
});
The Boostrap.TextField is one of the implementations, and your component is a Ember.TextField. Because that Boostrap.TextField is an instance of Ember.View and not an Ember.TextField directly. We delegate the value using Ember.computed.alias, so you can use valueBinding in the templates.
Boostrap.TextField = Boostrap.FormControl.extend({
control: Ember.TextField.extend({
classNames: ['form-control'],
value: Ember.computed.alias('parentView.value')
})
});
Nothing special here, just create the defaults values tagName=form and classNames=form-horizontal, for not remember every time.
Boostrap.Form = Ember.View.extend({
tagName: 'form',
classNames: ['form-horizontal']
});
Create a subclass of Boostrap.Form and delegate the validation to controller, since it have to be the knowledge about validation.
App.LoginFormView = Boostrap.Form.extend({
submit: function() {
debugger;
if (this.get('controller').validate()) {
alert('ok');
}
return false;
}
});
Here is where the validation logic and handling is performed. All using bindings without the need of touch the dom.
App.IndexController = Ember.ObjectController.extend({
value: null,
message: null,
hasError: Ember.computed.bool('message'),
validate: function() {
this.set('message', '');
var valid = true;
if (!/^[a-z0-9]+$/i.test(this.get('value'))) {
this.set('message', 'Just numbers or alphabetic letters are allowed');
valid = false;
}
return valid;
}
});
Templates
<script type="text/x-handlebars" data-template-name="index">
{{#view App.LoginFormView}}
{{view Boostrap.TextField valueBinding="value"
label="Alpha numeric"
messageBinding="message"
hasErrorBinding="hasError"}}
<button type="submit" class="btn btn-default">Submit</button>
{{/view}}
</script>
Here a live demo
Update
Like #intuitivepixel have said, ember-boostrap have this implemented. So consider my sample if you don't want to have a dependency in ember-boostrap.

Ember valueBinding to Redactor WYSIWYG

I'm using the Redactor WYSIWYG editor and it allows you to use minimal markup before initializing its code like this:
<textarea id="redactor" name="content">
…
</textarea>
However during initialization Redactor will wrap this textarea with the following content:
<div class="redactor_box">
<div class="redactor_ redactor_editor" contenteditable="true" dir="ltr">
…
</div>
<textarea id="redactor" name="content" style="display: none;">
…
</textarea>
</div>
I currently have done this in Ember
Template:
{{ view App.RedactorView valueBinding='contentAttributes.headerContent' class='header-redactor' name='headerContent' }}
View extending Ember.TextArea:
App.RedactorView = Ember.TextArea.extend({
didInsertElement: function() {
$("#"+this.elementId).redactor();
}
});
This still holds a binding to the textarea (now hidden), but I now need to bind the redactor_editor class instead. How can I do this?
After a bit of digging in the Redactor code I found out that if your element destined to be an editor is not a textarea element, Redactor will do the reverse thing and add the textarea if your a using a div instead for example.
Updated my view and tweaked it based on code from Ember.TextArea and Ember.TextSupport so it would get the correct value, this will probably work fine if you're using a contenteditable enabled element as well.
App.RedactorView = Ember.View.extend({
tagName: 'div',
init: function() {
this._super();
this.on("focusOut", this, this._elementValueDidChange);
this.on("change", this, this._elementValueDidChange);
this.on("paste", this, this._elementValueDidChange);
this.on("cut", this, this._elementValueDidChange);
this.on("input", this, this._elementValueDidChange);
},
_updateElementValue: Ember.observer(function() {
var $el, value;
value = Ember.get(this, "value");
$el = this.$().context;
if ($el && value !== $el.innerHTML) {
return $el.innerHTML = value;
}
}, "value"),
_elementValueDidChange: function() {
var $el;
$el = this.$().context;
return Ember.set(this, "value", $el.innerHTML);
},
didInsertElement: function() {
this.$().redactor();
this._updateElementValue();
}
});
Here's a JSBin demonstrating it: http://emberjs.jsbin.com/cefebepa/1/edit
I'm using Ember + Foundation, and kroofy's solution worked like a charm for me.
The height of the contentEditable were loading without paddings (were missing a paragraph), so ive changed to redactor's insertHtml method to update the value.
from:
return $el.innerHTML = value;
to:
return this.$().redactor('insertHtml', value);
thanks kroofy.
First, when you want to access the Ember View's DOM element, you should use this:
this.$()
instead of
$("#"+this.elementId)
About the Redactor issue... I am not sure how wise it is to tie up the Ember code with the WYSIWYG editor's functionality, but if you are determined about it, you can do the following:
App.RedactorView = Ember.TextArea.extend({
didInsertElement: function() {
var box = this.$().closest('.' + this.elementId + '_box');
box.find('.' + this.elementId + '_redactor_editor').redactor();
}
});

What's the best idiom for creating an EmberJS view that can show all its child-views, or just one?

Say I have a model App.Page, an ArrayController of App.Page, and an App.PageView to render each App.Page.
I'm trying to figure out how to best implement App.MyPagesView so it works like so:
if App.showAllPages is true: I want MyPagesView to contain an App.PageView(s) for displaying each of the App.Page in App.pages
Else: I want MyPagesView only show one App.PageView, bound to App.pages.currentPage.
The most straightforward implementation that occurs to me is using a template like so:
// MyPagesViewApproach1
{{#unless App.showAllPages}}
{{view App.PageView pageBinding="pages.currentPage"}}
{{else}}
{{#each pages}}
{{view App.PageView pageBinding="this"}}
{{/each}}
{{/unless}}
But won't this create new views for the existing models every time the user toggles showAllPages on and off? Also, I get emberJS warnings about performance issues when I try to use this template.
The PageView(s) could be quite complex and expensive to render. I'd really like to create a PageView once for each Page, and just remove/hide the irrelevant PageViews from the DOM when they're not in use.
App = Ember.Application.create({
showAllPages: false,
pages: Ember.ArrayController.create({
content: []
currentPage: null
}),
ready: function () {
this.pages.pushObject(App.Page.create({title: 'Page One'});
this.pages.pushObject(App.Page.create({title: 'Some Other Page'});
this.pages.pushObject(App.Page.create({title: 'Grrreatest Page Evar'});
this.pagesController.set('currentPage',
this.pagesController.get('firstObject'));
}
});
App.Page = Ember.Object.extend({
title: null
// etc, etc...
});
App.PageView = Ember.View.extend({
templateName: 'page',
page: null // should be bound to an App.Page
});
App.MyPagesView_Approach1 = Ember.View.extend({
pagesBinding: 'Elicitation.pages'
// ???
});
App.MyPagesView_Approach2 = Ember.ContainerView.extend({
// ???
});
And my HTML:
<script type="text/x-handlebars" data-template-name="page">
The title of this page is {{ page.title }}
</script>
{{view App.MyPagesView }}
To recap, what's the proper EmberJS-y way to implement MyPagesView so it responds to App.showAllPages without re-creating all the views each time its toggled?
Should it be some sort of ContainerView? Or should I use the unless/else template shown at the top of the question? Or something entirely different? I feel like a really simple solution exists in EmberJS, but its elluding me.
Here's the best I've come up with, encapsulated as a re-usable View class called "CurrentCollectionView". I'm using CollectionView, and using view.set('isVisible') to hide/show appropriate child views. Basically use it like a CollectionView, but you can set currentContent to hide all but one element of content, or use showAllContent to override currentContent.
App.CurrentCollectionView = Ember.CollectionView.extend({
showAllContent: false,
currentContent: null,
currentContentChanged: function () {
console.log("Elicitation.PagesView.currentContentChanged()");
var showAllContent = this.get('showAllContent');
if (Ember.none(showAllContent) || !showAllContent) {
var contents = this.get('content');
var currentContent = this.get('currentContent');
this.get('childViews').forEach(function (view, i) {
var isVisible = contents.objectAt(i) == currentContent;
view.set('isVisible', isVisible);
});
} else {
this.get('childViews').forEach(function (view) {
view.set('isVisible', true);
});
}
}.observes('currentContent', 'showAllContent', 'childViews')
});
An example of using CurrentCollectionView to implement MyPagesView:
App.MyPagesView = App.CurrentCollectionView.extend({
itemViewClass: App.PageView,
contentBinding: 'App.pages',
currentContentBinding: 'App.pages.currentPage',
showAllContentBinding: 'App.showAllPages',
});
or as using it inline as a template:
{{view App.CurrentCollectionView itemViewClass="App.PageView" contentBinding="App.pages" currentContentBinding="App.pages.currentPage" showAllContentBinding="App.showAllPages"}}
Hope somebody else finds this useful and/or can improve on it (please!)