I'm trying to append HTML elements to the UI upon user clicks, such elements would be assembled and precompiled within a view component like this:
NewElementView = Ember.View.extend(
tagName: 'li',
click: ((event)->
this.insertElement()
),
blueprint: (->
'<div id="{{dom_id}}">
<textarea></textarea>
<button {{action "submit"}}>Save</button>
</div>'
).property()
insertElement: (->
html = this.get("blueprint")
template = Ember.Handlebars.compile(html)
context = { dom_id: "foo" }
Ember.$(template(context)).appendTo("#container")
)
)
When running Ember.Handlebars.compile(html) line it raises Uncaught TypeError: Cannot read property 'push' of undefined
Any idea of why?
Thanks in advance!
Ember.Handlebars.compile expects an object with a data property.
What you are trying to do can be achieved with the following
template
<script type="text/x-handlebars" data-template-name="my-custom-view">
<div {{bind-attr id='view.dom_id'}}>
<textarea></textarea>
<button {{action "submit"}}>Guardar</button>
</div>
</script>
code
App.NewElementView = Ember.View.extend
tagName: 'li'
click: (event) ->
#insertElement()
insertElement: ->
view = Ember.View.create
templateName: "my-custom-view"
container: #container
dom_id: "foo"
view.appendTo("#container")
Here a working example http://emberjs.jsbin.com/rapepepogi/17/edit?html,js,output
Related
I would like to append an Ember Component ComponentB to a DOM element which is generated by some non-Ember UI library on didInsertElement of ComponentA, resulting in something like
<div class='ember-view component-a'>
<div class='i-know-nothing-of-ember'>
<div class='ember-view component-b'></div>
</div>
</div>
I am aware of appendTo(element) method, however it fails with assertion
You cannot append to an existing Ember.View. Consider using Ember.ContainerView instead.
I also tried calling createElement on component B and then appending it to DOM via jQuery - which kind of works, however in the end it fails with error
Cannot set property '_elementInserted' of null
See http://emberjs.jsbin.com/cofebo/2/
What is the proper way to achieve the above; if possible actions and other behaviours should be as if i-know-nothing-of-ember would be generated by Component A template.
I suggest using the container to lookup the component and append it anywhere whenever you need to.
Approach 1 - retrieve container within route
http://emberjs.jsbin.com/libipazavu/1/edit?html,js,output
js
App = Ember.Application.create();
App.IndexRoute = Em.Route.extend({
setupController:function(controller,model){
controller.set("container",this.container);
}
});
App.IndexView = Em.View.extend({
appendNonEmberUILibrary:function(){
callNonEmberUILibrary();
var componentB = this.get("controller.container").lookup("component:component-b");
componentB.appendTo(".non-ember-ui");
}.on("didInsertElement")
});
App.ComponentBComponent = Em.Component.extend({
layoutName:"components/component-b",
prop1:"test-option-1"
});
function callNonEmberUILibrary(){
$("body").append("<div class='non-ember-ui' style='border:1px solid;'>element from non-ember ui lib</div>");
}
hbs
<script type="text/x-handlebars">
<h2>Welcome to Ember.js</h2>
<h3>Adding Ember Component to non-Ember DOM Element using <u><i>container</i></u></h3>
{{outlet}}
</script>
<script type="text/x-handlebars" data-template-name="components/component-b">
<br/>
<div style="border:1px dashed #F5664D">
This is componentB -> {{prop1}}
</div>
<br/>
</script>
Approach 2 - retrieve container within an initializer
http://emberjs.jsbin.com/hijedowacu/1/edit?html,js,output
js
App = Ember.Application.create();
App.initializer({
name:"main",
initialize:function(container,application){
application.register('main:compB', container.lookup("component:component-b"), { instantiate: false });
application.inject("controller:index","theComponentB","main:compB");
}
});
App.IndexView = Em.View.extend({
appendNonEmberUILibrary:function(){
callNonEmberUILibrary();
var componentB = this.get("controller.theComponentB");
componentB.appendTo(".non-ember-ui");
}.on("didInsertElement")
});
App.ComponentBComponent = Em.Component.extend({
layoutName:"components/component-b",
prop1:"test-option-1"
});
function callNonEmberUILibrary(){
$("body").append("<div class='non-ember-ui' style='border:1px solid;'>element from non-ember ui lib</div>");
}
hbs
<script type="text/x-handlebars">
<h2>Welcome to Ember.js</h2>
<h3>Adding Ember Component to non-Ember DOM Element using <u><i>container</i></u></h3>
{{outlet}}
</script>
<script type="text/x-handlebars" data-template-name="components/component-b">
<br/>
<div style="border:1px dashed #F5664D">
This is componentB -> {{prop1}}
</div>
<br/>
</script>
Approach3 - reallocate added component with jquery (update from comments)
Alternatively you could just add the component-b in the template as non visible and within didInsertElement you could reallocate and display it wherever required using jquery.
example http://jsbin.com/maginovexa/1/edit?html,js,output
js
App.IndexView = Em.View.extend({
prop2:"prop-from-index-view",
appendNonEmberUILibrary:function(){
callNonEmberUILibrary();
//var componentB = this.get("controller.container").lookup("component:component-b");
//componentB.appendTo(".non-ember-ui");
var componentBDOM = this.get("componentB").$().detach();
$(".non-ember-ui").append(componentBDOM);
}.on("didInsertElement"),
click:function(){this.set("prop2",Date.now());}
});
App.ComponentBComponent = Em.Component.extend({
layoutName:"components/component-b",
prop1:"test-option-1"
});
function callNonEmberUILibrary(){
$(".inner").append("<div class='non-ember-ui' style='border:1px solid;'>element from non-ember ui lib</div>");
}
hbs
<script type="text/x-handlebars">
<h2>Welcome to Ember.js</h2>
<h3>Adding Ember Component to non-Ember DOM Element using <u><i>container</i></u></h3>
<div class='inner'></div>
{{outlet}}
</script>
<script type="text/x-handlebars" data-template-name="index">
this is index (click here to change prop2 of component)
<div style="display:none">
{{component-b prop2=view.prop2 viewName="componentB"}}
</div>
</script>
....
This is a fully working solution for reallocating an ember controlled element to a non-ember element that is within an ember view, as requested by Simon Jesenko (the op).
Have you tried the run loop? Like this,
//after comp.createElement();
parent = this;
Ember.run.schedule('afterRender', this, function() {
parent.$('.i-know-nothing-of-ember').append($(comp.element));
});
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.
Normally, we can access a method declared in the controller from the views using: this.get('controller').send('method');. In this simple jsfiddle, that is failing with: Uncaught TypeError: Cannot read property 'send' of undefined.
Here is the entire code in the jsfiddle.
App = Ember.Application.create();
App.YesController = Ember.ArrayController.extend({
cow: function(){
console.log('say mooooo') ;
}
});
App.YesView = Ember.View.extend({
templateName: 'yes',
um: function(){
this.get('controller').send('cow');
}
});
The entire view:
<script type='text/x-handlebars' data-template-name='application'>
{{render yes}}
{{outlet}}
</script>
<script type='text/x-handlebars' data-template-name='yes'>
<h1> Can't use send to call controller method from view</h1>
<button {{action 'um' target='view.content'}}> can't call controller from view</button>
<button {{action 'cow'}}> controller action works</button>
</script>
As far as I know it's possible to call the cow() method in your controller like this:
App.YesView = Ember.View.extend({
templateName: 'yes',
um: function() {
this.get('controller').cow();
//this.controller.cow() // <-- this should work also
}
})
Try target=view rather that target=view.content, otherwise you're trying to call send on the model, which is undefined.
In my application I display a list of accounts like so:
<script type="text/x-handlebars" data-template-name="accounts">
{{#each account in controller}}
{{#linkTo "account" account class="item-account"}}
<div>
<p>{{account.name}}</p>
<p>#{{account.username}}</p>
<i class="settings" {{ action "openPanel" account }}></i>
</div>
{{/linkTo}}
{{/each}}
</script>
Each account has a button which allows users to open a settings panel containing settings just for that account. as you can see in this quick screencast:
http://screencast.com/t/tDlyMud7Yb7e
I'm currently triggering the opening of the panel from within a method located on the AccountsController:
Social.AccountsController = Ember.ArrayController.extend({
openPanel: function(account){
console.log('trigger the panel');
}
});
But I feel that it's more appropriate to open the panel from within a View that I've defined for this purpose. This would give me access to the View so that I can perform manipulations on the DOM contained within it.
Social.MainPanelView = Ember.View.extend({
id: 'panel-account-settings',
classNames: ['panel', 'closed'],
templateName: 'mainPanel',
openPanel: function(){
console.log('opening the panel');
}
});
<script type="text/x-handlebars" data-template-name="mainPanel">
<div id="panel-account-settings" class="panel closed">
<div class="panel-inner">
<i class="icon-cancel"></i>close
<h3>Account Settings</h3>
Disconnect Account
</div>
</div>
</script>
The problem I'm encountering is that I don't see how I can trigger a method on the Social.MainPanelView from the context of the AccountsController. Is there a better solution?
UPDATE 1
I've worked up a Fiddle to illustrate what I'm talking about:
http://jsfiddle.net/UCN6m/
You can see that when you click the button it calls the showPanel method found on App.IndexController. But I want to be able to call the showPanel method found on App.SomeView instead.
Update:
Approach One:
Simplest of all
Social.AccountsController = Ember.ArrayController.extend({
openPanel: function(account){
/* we can get the instance of a view, given it's id using Ember.View.views Hash
once we get the view instance we can call the required method as follows
*/
Ember.View.views['panel-account-settings'].openPanel();
}
});
Fiddle
Approach Two:(Associating a controller, Much Cleaner)
Using the Handlebars render helper: what this helper does is it associates a controller to the view to be displayed, so that we can handle all our logic related to the view in this controller, The difference is
{{partial "myPartial"}}
just renders the view, while
{{render "myPartial"}}
associates App.MyPartialController for the rendered view besides rendering the view, Fiddle
now you can update your code as follows
application.handlebars(The place you want to render the view)
{{render "mainPanel"}}
accounts controller
Social.AccountsController = Ember.ArrayController.extend({
openPanel: function(account){
this.controllerFor("mainPanel").openPanel();
}
});
main panel view
Social.MainPanelView = Ember.View.extend({
id: 'panel-account-settings',
classNames: ['panel', 'closed']
});
main panel controller
Social.MainPanelController = Ember.Controller.extend({
openPanel: function(){
console.log('opening the panel');
}
})
Approach Three:
This one is the manual way of accomplishing Approach Two
Social.MainPanelView = Ember.View.extend({
id: 'panel-account-settings',
controllerBinding: 'Social.MainPanelController',
classNames: ['panel', 'closed'],
templateName: 'mainPanel'
});
Social.MainPanelController = Ember.Controller.extend({
openPanel: function(){
console.log('opening the panel');
}
})
use this.controllerFor("mainPanel").openPanel()
You need to use the action helper rather than directly coding the links. The action helper targets the controller by default, but you can change it to target the view instead:
<a {{action openPanel target="view"}}></a>
Your second link should be a linkTo a route, since you are specifying a link to another resource. The whole snippet, revised:
Social.MainPanelView = Ember.View.extend({
id: 'panel-account-settings',
classNames: ['panel', 'closed'],
templateName: 'mainPanel',
openPanel: function(){
console.log('opening the panel');
}
});
<script type="text/x-handlebars" data-template-name="mainPanel">
<div id="panel-account-settings" class="panel closed">
<div class="panel-inner">
<a {{action openPanel target="view"} class="button button-close"><i class="icon-cancel"></a></i>
<h3>Account Settings</h3>
{{#linkTo "connections"}}Disconnect Account{{/linkTo}}
</div>
</div>
</script>
If anyone can put me out of my misery with this I would greatly appreciate it, drving me mad and I know it's gonna be something stupidly simple.
I have an array:
Data
var test = [{"name":"Kober Ltd","town":"Bradford","type":"Z1CA","number":3650645629},
{"name":"Branston Ltd.","town":"Lincoln","type":"Z1CA","number":3650645630}]
and I want to render this info as child elements inside a collectionView:
collectionView
App.ThreeView = Ember.CollectionView.extend({
itemViewClass: Ember.View.extend({
click: function(){
alert('hello')
},
classNames: ['element','foobar'],
templateName: 'foo'
})
})
and here is my controller:
controller
App.ThreeController = Ember.ArrayController.extend({
content: [],
init: function(){
var me = this;
$.each(test,function(k,v){
var t = App.ThreeModel.create({
name : v.name,
town: v.town,
type: v.type,
number: v.number
})
me.addObject( t )
})
console.log( me.content )
}
})
Templates:
<script type="text/x-handlebars" data-template-name="application">
{{outlet}}
</script>
<script type="text/x-handlebars" data-template-name="three">
</script>
<script type="text/x-handlebars" data-template-name="foo">
<div class="symbol"> {{ view.content.type }} </div>
<div class="number"> {{ view.content.number }} </div>
<div class="name"> {{ view.content.name }} </div>
<div class="town"> {{ view.content.town }} </div>
</script>
I am using the latest Ember so...V2 router that syncs up all the parts with the 'Three' name. Every will work if I put the array directly into the view:
App.ThreeView = Ember.CollectionView.extend({
content: test, // manually added a pure array into view content
itemViewClass: Ember.View.extend({
click: function(){
alert('hello')
},
classNames: ['element','foobar'],
templateName: 'foo'
})
})
But when I try and do this 'properly', using Ember.js Objects, I get no rendered views ( aside from an empty application view ).
I have tried work arounds, like adding a 'contentBinding' from the view to the controller just to see if I can force a connection but still nothing. It is important that I render the view through the container as I am using Three.js to pick up on the rendered content and manipulate further.
So, to summarise: I can render pure arrays in view, but nothing passed from controller. Incidentally, the controller is definitely being instituted as I can console log its contents on init. If i change the view name, the controller is not instantiated so I know the namespacing is working.
thanks in advance!
I'm not sure to embrace the whole problem, but for now, when you define your controller, in the init() function, first don't forget to call this._super() (it will go through the class hierarchy and call the constructors). Maybe that's just the missing thing.
Edit: it seems like with the new router, defining a view as a CollectionView does not work.
so I replaced it with a normal Ember.View, and use an {each} helper in the template.
<script type="text/x-handlebars" data-template-name="three">
{{each controller itemViewClass="App.FooView" tagName="ul"}}
</script>
here is a minimal working fiddle: http://jsfiddle.net/Sly7/qCdAY/14/
EDIT 2:
By re-reading the question, and seeing you try to bind the CollectionView content's property to controller, I tried it, because it just work fine :)
http://jsfiddle.net/Sly7/qCdAY/15/