Whats a good way or a pattern to creating modal forms in EmberJS. Something like this.
I will describe the way i manage modal views with a CSS approach:
Add CSS class names:
.modal {
-webkit-transition: -webkit-transform #modal-duration #ease-animation-style;
-webkit-transform: translate3d(0,0,0);
-webkit-backface-visibility: hidden;
}
.modal.from-left.is-hidden {
-webkit-transform: translate3d(-320px,0,0);
}
.modal.from-right.is-hidden {
-webkit-transform: translate3d(320px,0,0);
}
.modal.from-up.is-hidden {
-webkit-transform: translate3d(0,-1*#app-height,0);
}
.modal.from-down.is-hidden {
-webkit-transform: translate3d(0,#app-height,0);
}
Add custom events to your Application Namespace to receive the transitionEnd event in your view:
Yn.Application = Em.Application.extend( Em.Evented, {
customEvents: {
webkitTransitionEnd: 'transitionEnd',
......
}
});
Now create a mixin to be used in your view as:
Yn.Modal = Em.Mixin.create({
isHidden: true,
classNameBindings: ['isHidden'],
classNames: ['modal'],
transitionEnd: function(event) {
// transitionEnd triggers multiple times
// http://stackoverflow.com/questions/4062105/webkit-transitionend-event-grouping
if ( event.originalEvent.propertyName === '-webkit-transform' &&
$(event.target)[0] === this.$()[0] ) {
var eventName = this.get('isHidden') ? 'transitionHide' : 'transitionShow' ;
this.trigger(eventName);
}
}
});
You can now insert the view in the DOM via appendTo or any handlebars view template or whatever method you use, and manages your view with its isHidden property which can be bound for example to a controller UI property, you could also interact with the view with the view lifecycle hooks as didInsertElement or the new defined as transitionHide, transitionShow hooks.
You can use my modified Bootstrap.ModalPane
ex:
App.ContactView = Bootstrap.ModalPane.extend({
closeOnEscape: false,
showBackdrop: true,
showCloseButton: false,
heading: 'Contact',
primary: "Save changes",
secondary: "Cancel",
classNames: ["large-modal"],
bodyViewClass: Ember.View.extend({
templateName: 'contact-body'
}),
callback: function (opts, event) {
var controller = this.get('controller');
if (opts.primary) {
controller.save();
} else {
controller.cancel();
}
}
});
In you controller you can then do something like this:
editContact: function (contact) {
var contactController = this.controllerFor('contact');
contactController.set('content', contact);
var contactView = App.ContactView.create({
controller: contactController
});
contactView.append();
},
You can also define your own modalPaneTemplate with customizations. It's the principle of how the Boostrap.ModelPane works that matters, default it only supports 2 buttons at the bottom. If you want 5 buttons, or buttons in the header, start coding yourself and create a custom modalPaneTemplate.
Related
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.
I'm trying add a delete button with an ember action from a controller. For some reason Ember.Handlebars.compile('<button {{action "deletePerson"}}>Delete</button> returns a function and not the compiled string.
Here's a jsbin
Here's the relevant portion of code:
App.ApplicationController = Ember.Controller.extend({
columns: function() {
...
buttonColumn = Ember.Table.ColumnDefinition.create({
columnWidth: 100,
headerCellName: 'Action',
getCellContent: function(row) {
var button = Ember.Handlebars.compile('<button {{action "deletePerson" this}}>Delete</button>');
return button; // returns 'function (context, options) { ...'
}
});
...
}.property()
...
After looking through the link from #fanta (http://addepar.github.io/#/ember-table/editable) and a lot of trial and error, I got it working.
Here's the working jsbin.
Here are some key points:
Instead of using getCellContent or contentPath in the ColumnDefinition, you need to use tableCellViewClass and to create a view that will handle your cell
Pass in this to the action on your button — and modify content off that. One gotcha is to edit content, you need to copy it using Ember.copy
Here's the relevant code:
App.ApplicationController = Ember.Controller.extend({
columns: function() {
...
buttonColumn = Ember.Table.ColumnDefinition.create({
columnWidth: 100,
headerCellName: 'Action',
tableCellViewClass: 'App.PersonActionCell'
});
...
}.property(),
onContentDidChange: function(){
alert('content changed!');
}.observes('content.#each'),
...
});
App.PersonActionCell = Ember.Table.TableCell.extend({
template: Ember.Handlebars.compile('<button {{action "deletePerson" this target="view"}}>Delete</button>'),
actions: {
deletePerson: function(controller){
// Will NOT work without Ember.copy
var people = Ember.copy(controller.get('content'));
var row = this.get('row');
// For some reason people.indexOf(row) always returned -1
var idx = row.get('target').indexOf(row);
people.splice(idx, 1);
controller.set('content', people);
}
}
});
How do i change an elements class on click via ember.js, AKA:
<div class="row" {{bindAttr class="isEnabled:enabled:disabled"}}>
View:
SearchDropdown.SearchResultV = Ember.View.extend(Ember.Metamorph, {
isEnabled: false,
click: function(){
window.alert(true);
this.isEnabled = true;
}
});
The click event works as window alert happens, I just cant get the binding to.
The class is bound correctly, but the isEnabled property should be modified only with a .set call such as this.set('isEnabled', true) and accessed only with this.get('isEnabled'). This is an Ember convention in support of first-class bindings and computed properties.
In your view you will bind to a className. I have the following view in my app:
EurekaJ.TabItemView = Ember.View.extend(Ember.TargetActionSupport, {
content: null,
tagName: 'li',
classNameBindings: "isSelected",
isSelected: function() {
return this.get('controller').get('selectedTab').get('tabId') == this.get('tab').get('tabId');
}.property('controller.selectedTab'),
click: function() {
this.get('controller').set('selectedTab', this.get('tab'));
if (this.get('tab').get('tabState')) {
EurekaJ.router.transitionTo(this.get('tab').get('tabState'));
}
},
template: Ember.Handlebars.compile('<div class="featureTabTop"></div>{{tab.tabName}}')
});
Here, you have bound your className to whatever the "isSelected" property returns. This is only true if the views' controller's selected tab ID is the same as this views' tab ID.
The code will append a CSS class name of "is-selected" when the view is selected.
If you want to see the code in context, the code is on GitHub: https://github.com/joachimhs/EurekaJ/blob/netty-ember/EurekaJ.View/src/main/webapp/js/app/views.js#L100
Good answers, however I went down a different route:
SearchDropdown.SearchResultV = Ember.View.extend(Ember.Metamorph, {
classNameBindings: ['isSelected'],
click: function(){
var content = this.get('content');
SearchDropdown.SelectedSearchController.set('content', content);
var loadcontent = this.get('content');
loadcontent.set("searchRadius", $("select[name=radius]").val());
SearchDropdown.LoadMap.load(content);
},
isSelected: function () {
var selectedItem = SearchDropdown.SelectedSearchController.get('content'),
content = this.get('content');
if (content === selectedItem) {
return true;
}
}.property('SearchDropdown.SelectedSearchController.content')
});
Controller:
SearchDropdown.SelectedSearchController = Ember.Object.create({
content: null,
});
Basically stores the data of the selected view in a controller,
I have a statemachine and I am using the new currentViewBinding to swap out parts of an overall containerView whenever a new state is entered using currentViewBinding:
index: Ember.State.create({
enter: function(manager) {
App.get('appController').set('feedView', Ember.View.create({
templateName: 'dashboard_feed',
contentBinding: 'App.feedController.content',
controller: App.get('App.feedController')
}));
}
})
At this moment in time, the rendering of these view is quite slow. Is there a way I could keep the view in memory and avoid the re-rendering every time I enter the state?
I actually provided a solution to this for another question on StackOverflow, but it's super relevant here too. Avoiding re-rendering of a flash object from scratch when view is reactivated
Here's the jsFiddle: http://jsfiddle.net/EE3B8/1
I extend ContainerView with a flag to stop it from destroying the currentView upon it's destruction. You'll want to stash the view instance somewhere that it won't be destroyed.
App.ImmortalContainerView = Ember.ContainerView.extend({
destroyCurrentView: true,
willDestroy: function() {
if (!this.destroyCurrentView) { this._currentViewWillChange(); }
this._super();
}
});
App.immortalView = Ember.View.create({
template: Ember.Handlebars.compile(
'I WILL LIVE FOREVER!'
)
});
You could extend Ember.ContainerView to show/hide its currentView view like so:
App.FastContainerView = Ember.ContainerView.extend({
toggleCurrentViewFast: true,
_currentViewWillChange: function() {
var childViews = this.get("childViews"),
currentView = this.get("currentView");
if (this.toggleCurrentViewFast && childViews.indexOf(currentView) >= 0) {
currentView.set("isVisible", false);
} else {
this._super();
}
},
_currentViewDidChange: function() {
var childViews = this.get("childViews"),
currentView = this.get("currentView");
if (this.toggleCurrentViewFast && childViews.indexOf(currentView) >= 0) {
currentView.set("isVisible", true);
} else {
this._super();
}
}
});
I have an app which lists albums. When album is clicked on both AlbumView and App.overlay (also a view) are displayed.
App.overlay = Ember.View.create({...}) (Lightbox-like overlay).
and:
App.AlbumView = Ember.View.extend({
// close the selected album view by closing the overlay
close: function() {
App.overlay.close();
}
});
And here's the problem: I want to be able to close those both views by clicking on the overlay, but I want overlay to remain independent of AlbumView, so that I can use the overlay in other places (i.e. without introducing a coupling between the two). How can I do it?
Here is my current implementation, with tight coupling, which I really don't like:
App.overlay = Ember.View.create({
// handle clicking anywhere on the overlay
click: function() {
this.close();
},
// close the overlay (setting selectedAlbum's controller content to null hides the AlbumView)
close: function() {
App.selectedAlbumController.set('content', null); // this should not be here
this.remove();
}
});
I'm only just learning ember, so take this with a grain of salt...
You could add a 'visible' property to the overlay, and then observe it from the other AlbumView. Like this:
var overlay = Ember.View.create({
visible: true,
click: function() {
this.close();
},
close: function() {
this.set('visible', false);
this.remove();
}
});
App.AlbumView = Ember.View.extend({
overlayClosed: function() {
App.selectedAlbumController.set('content', null);
this.remove();
}.observes('overlay.visible')
});
What about extracting your close method in a mixin?
App.AlbumClosing = Ember.Mixin.create({
close: function() {
App.selectedAlbumController.set('content', null);
this.remove();
}
});
var overlay = Ember.View.create(App.AlbumClosing, {
click: function() {
this.close();
}
});