Is it possible to dynamically add and remove regions to a layout with Marionette? My app needs to be able to push and pop regions from a layout. This is similar to how GitHub pushes and pops views when you drill down in the source code of a project. They have the slide over animation when presenting the next view and then it slides back when you're backing out. The idea is that I need to keep the previous views around. Another analogy would be how UINavigationControllers work on iOS.
Or maybe I should just define a custom layout that is able to handle adding and removing regions on the fly?
I ended up implementing a container view to fit my needs. It cleans up event refs like you'd expect in Marionette.
https://github.com/ayoung/backbone-vs-marionette/blob/marionette/public/js/views/mainContainer.js
I'm not sure but you may be getting confused with the existence of some html and the displaying of that html?
I mean you can make a CompositeView of Items and only show one of the items at a time. Then use jQuery animate or another animation library to move through the CompositeView's Items.
Yes it is possible. Here is the code I use.
The layout:
var Layout = Marionette.LayoutView.extend({
initialize: function(options) {
options = _.extend({ regionTag: 'div' }, options);
this.mergeOptions(options, ['regionTag', 'regionName']);
},
template: false,
regions: {},
append: function(view) {
var viewClass = 'dynamic-layout-' + this.regionName,
viewCount = $('.' + viewClass).length + 1,
viewId = this.regionName + '-view-' + viewCount,
$el = $('<' + this.regionTag + '/>', {
id: viewId,
class: viewClass
});
this.$el.append($el);
var region = Marionette.Region.extend({
el: '#' + viewId
});
this.regionManager.addRegion(viewId, region);
this.regionManager.get(viewId).show(view);
},
appendEmpty: function(id, className, tag) {
tag = tag || 'div';
var data = { id: id, className: className, tag: tag };
var $el = Marionette.Renderer.render('#append-layout-template', data);
this.$el.append($el);
var region = Marionette.Region.extend({
el: '#' + id
});
this.regionManager.addRegion(id, region);
},
customRemove: function(regionId) {
this.regionManager.removeRegion(regionId);
}
});
A helpful template:
<script type="text/template" id="append-layout-template">
<<%= tag %> id='<%= id %>' class='<%= className %>'></<%= tag %>>
</script>
The controller:
var view = new SomeView();
// the region name will be a part of a unique id
var layout = new Layout({ regionName: 'myRegion' });
// add a dynamic region to the layout and a view to that region
layout.append(view);
// same as above (you have to name the id and class yourself)
var regionId = 'myRegionId';
layout.appendEmpty(regionId, 'someClassName', 'span');
layout.getRegion(regionId).show(view);
// remove a region
layout.customRemove(regionId);
Related
Let me know if you can help me out somehow, i'm kind of struggling to get my head around.
Starting with some Marionette application logics:
app.js
//basic setup
this.Graph = new joint.dia.Graph;
this.Paper = new joint.dia.Paper({ width: 640, height: 480, model: this.Graph });
// [...] lots of code
//adding elements
app.Elements.add(element);
So far so good. Now the tricky part. I want a collection.
JointCollectionView.js
module.exports = Marionette.CollectionView.extend({
tagName: 'div',
className: 'row',
childView: JointView,
addChild: function(child, ChildView, index){
//does that make sense?
app.Graph.addCell(child);
//should i add it to collection?
if (child.shouldBeShown()) {
return Marionette.CollectionView.prototype.addChild.call(this, child, ChildView, index);
}
},
getChildView: function(item) {
return app.Graph.getCell(item);
}
//[...]
})
Now even more tricky. How do i handle the joint-view to make it work with collections and also display html elements?
JointView.js
module.exports = joint.dia.ElementView.extend({ /* ?!?!?! */ });
//OR ?
module.exports = Marionette.ItemView.extend({
jointElementView: null, //this will be like above somewhere else...
initialize: function(options){
jointElementView = new JointElementView({ /* ... */ });
}
})
I'm no JointJS expert, but your implementation looks perfect.
You want to use the second option:
JointView.js
module.exports = Marionette.ItemView.extend({
template: _.template("");
jointElementView: null, //this will be like above somewhere else...
initialize: function(options){
this.jointElementView = new JointElementView({ /* ... */ });
}
});
since a Marionette.CollectionView expects a Marionette view (sp. a Marionette.ItemView or descendent [LayoutView/CompositeView]).
What I would add to JointView.js is a method to inject the result from this.jointElementView into the JointView.js html. So, add a property to it, like:
onRender: function () {
this.$el.append(this.jointElementView.el); // Where this.jointElementView.el is the JointJS view html
}
With the help of #seebiscuit i looked much deeper into jointjs and narrowed down how i should approach this problem (You didn't seem to be interested in the points though, so i'll answer myself)
The following files were edited:
app.js (changed, important!)
//this step is necessary to create these element before the paper is created.
//Make jointjs GLOBAL!!!!!!
joint.shapes.html = {};
joint.shapes.html.Element = require('views/Element'); //this dude im gonna use to create the elements
joint.shapes.html.ElementView = require('views/ElementView'); //this badboy will fire when i create those elements. MAGIC!
//basic setup
this.Graph = new joint.dia.Graph;
this.Paper = new joint.dia.Paper({ width: 640, height: 480, model: this.Graph });
// [...] lots of code
//adding elements
app.Elements.add(element);
JointCollectionView.js (beauti-/simplyfied)
module.exports = Marionette.CollectionView.extend({
tagName: 'div',
className: 'row',
childView: JointView,
onRender: function(){
// jointjs' paper is added here long after jointjs custom element init.
this.el.appendChild(app.Paper.el);
},
onDestroy: function(){
// removal of the paper is done here
this.el.removeChild(app.Paper.el);
},
buildChildView: function(child, JointView, childViewOptions){
// add model, jointjs' paper and graph into JointView's options
var options = _.extend({model: child}, childViewOptions);
options = _.extend({paper: app.Paper, graph: app.Graph}, options);
return new JointView(options);
}
//[...]
})
JointView.js (magic here!)
module.exports = Marionette.ItemView.extend({
tagName: 'div',
className: 'html-element',
template: "#agentTmpl",
// store here just in case
cell: null,
// [...]
initialize: function() {
// initialize joinjs element-shape here
Marionette.bindEntityEvents(this, this.model, this.modelEvents);
if(this.cell == null){
//notice `el: this.el` This will actually pass the option el to ElementView. Surprised?
//Yeah me too. From there i can do with the dom-element whatever i want
this.cell = new joint.shapes.html.Element({ el: this.el, position: { x: 80, y: 80 }, size: { width: 250 } });
}
},
onRender: function(){
// after rendering add cell to graph
this.options.graph.addCell(this.cell);
},
onDestroy: function(){
// after removal remove cell from graph
this.cell.remove();
}
});
Element.js
ElementView.js
For simplicity more or less like here: http://www.jointjs.com/tutorial/html-elements
To summarize what actually happens is: whenever a new Element is created ElementView will fire all necessary event (initialize, render & whatnot). From there you can manipulate the drawn svg elements or overlap (similar to the tutorial) with my previously created JointView's html. I basically put my JointView dom-element over the SVG which is drawn by jointjs.
There you go. Fixed!
Is it possible, with BackGrid, to build a custom Cell vía formatter, composing values from hidden columns?
var grid = new Backgrid.Grid({
columns: [
{
name:"half_value_1",
cell:"string",
rendered: false
},
{
name:"half_value_2",
cell:"string",
rendered: false
},
{
name: "composite",
cell: "string",
formatter: _.extend({}, Backgrid.CellFormatter.prototype, {
fromRaw: function (half_value_1, half_value_2) {
return half_value_1 + '/' + half_value_2;
}
})
}],
collection: col
});
Can I get the half_value_1 and half_value_2 inside the fromRaw function?
I think the best way to get the result you want is to use a custom cell rather than a custom formatter. You could do something like this for that particular column:
{
name: "composite",
cell: Backgrid.Cell.extend({
render: function(){
// You have access to the whole model - get values like normal in Backbone
var half_value_1 = this.model.get("half_value_1");
var half_value_2 = this.model.get("half_value_2");
// Put what you want inside the cell (Can use .html if HTML formatting is needed)
this.$el.text( half_value_1 + '/' + half_value_2 );
// MUST do this for the grid to not error out
return this;
}
})
}
That should work perfectly for you - I use it for a handful of grids in my projects. I didn't test this code though, so I may have typos :)
Key
I am new to Ext Js so forgive me if my terminology for certain things is not correct. What I need is text counter. Here is what I have so far...
http://jsfiddle.net/gfrobenius/WxNDf/1/
What I need to do is make this code reusable so that all textareas get this feature but I still have the ability to change the normal settings like label, name, id, maxLength, etc... Does this mean turning it into a class, plugIn, component (sorry still trying to learn terminology)? Can my afterLabelTextTpl be done simpler? I've seen examples like:
var tpl = new Ext.Template('Name: {name}, Age: {age}');
tpl.apply({name: 'John', age: 25});
in the documentation but am not sure how to incorporate it into my code. Also in my three functions for updating the text count you can see I have the line for getting the component hard-coded to 'gtext' and the line for getting the 'countDownSpan' hard-coded as well. How do I go about making them just know the parent item?
You can make your own component and extend from a textarea. Also you're doing 3 times the same thing in your callback function. And the id of your textarea should be unique.
Here's an example of your component:
Ext.define('MyApp.ux.form.field.TextArea', {
alias: 'widget.textareacounter',
extend: 'Ext.form.field.TextArea',
labelAlign: 'top',
enableKeyEvents: true,
enforceMaxLength: false,
maxLength: 1000,
constructor: function(config) {
var me = this;
Ext.apply(me, config);
me.afterLabelTextTpl = ['<span style="margin-left:5px;">(' + me.maxLength + ' char. left)</span>'];
me.callParent()
},
initComponent: function () {
var me = this;
me.on({
keydown: me.calculateCharsLeft,
keyup: me.calculateCharsLeft,
afterrender: me.calculateCharsLeft,
scope: me
});
me.callParent();
},
calculateCharsLeft: function (textfield, e) {
var me = this;
var span = Ext.dom.Query.select('span', me.getEl().dom)[0];
var remaining = me.maxLength - me.getValue().length;
span.innerHTML = '(' + remaining + ' char. left)';
}
});
And a working fiddle: http://jsfiddle.net/johanhaest/cpLvK/2/
In my json data, i am getting projectName and assignedTo values(it accrues in all data),
example data:
[ {projectName:'project1',assignedTo:'some1'},{projectName:'project2',assignedTo:'some2'}]
my template is: (including my confused stuff)
<script id="listTemplate" type="text/template">
<%= projectName === projectName ? projectName : taskStatus%>//not works how can i mange?
</script>
In my view, i am converting the model "toJSON()", but i am getting the project name alone in the 2 links what i am use.
my request, Is it possible to use a single template to print two different values? - as a first time i need to print project name, later taskStatus with some condition, if so any suggestion please
The reason why it is constantly printing project name is because, in the template, you have...
<%= projectName === projectName ? projectName : taskStatus%>
projectName would always equal to projectName. What you'll want to do in this case is, in your Backbone.View, when you serialize the Model to be consumed by the template, you can do this...
Backbone.View.extend({
events: {
// An example on how to change the display
'click button.change-display': 'onChangeDisplayClicked'
},
template: _.template(...),
// Controls whether project name should be shown or not.
showProjectName: true,
onChangeDisplayClicked: function() {
// Flip the switch
this.showProjectName = !this.showProjectName;
// Re-render the View
this.render();
},
serialize: function() {
// Grab the data from model
var data = this.model.toJSON();
// Pass this data to the template to control what to be displayed.
data.showProjectName = this.showProjectName;
return data;
},
render: function() {
this.$el.html(this.template(this.serialize()));
}
});
... and in your template, you would...
<script id="listTemplate" type="text/template">
<%= showProjectName ? projectName : taskStatu s%>
</script>
I'm trying to use Backbone.localStorage as a backend to an app.
I wrote a Model called Era, and a corresponding EraView. Pressing enter in the EraView causes the model to be saved, and this is where I get the error:
Uncaught Error: A "url" property or function must be specified
urlError backbone.js:1509
_.extend.url backbone.js:515
_.result underscore-min.js:1060
Backbone.sync backbone.js:1410
Backbone.sync backbone.localStorage.js:188
_.extend.sync backbone.js:276
_.extend.save backbone.js:476
karass.EraView.Backbone.View.extend.close era.js:61
karass.EraView.Backbone.View.extend.updateOnEnter era.js:75
Here is the code to the EraView
var karass = karass || {};
// Era Item View
// the DOM element for an era item
karass.EraView = Backbone.View.extend({
tagName: 'li',
className: 'era',
template: _.template( $('#era-template').html() ),
// The DOM events specified to an item
events: {
'dblclick .edit-input': 'edit',
'keypress .edit-input': 'updateOnEnter',
//'blur .edit': 'close',
},
// The EraView listens for changes to its model, re-rendering. Since there's
// a one-to-one correspondence between an era and a EraView in this karass,
// we set a direct reference on the model for convenience.
initialize: function(){
_.bindAll(this);
this.model.on('change', this.render, this);
},
// Re-renders the era item to the current state of the model and
// updates the reference to the era's edit input within the view
render: function(){
this.$el.html( this.template(this.model.attributes));
this.$era_start = this.$('.era-start');
this.$era_end = this.$('.era-end');
this.$era_start.attr('disabled', true);
this.$era_end.attr('disabled', true);
return this;
},
// Switch this view into editing mode, displaying the input field
edit: function(){
this.$('.edit-input').removeAttr('disabled');
this.$el.addClass('editing');
this.$('.edit-input').addClass('editing');
},
// Close the editing mode, saving changes to the era
close: function(){
this.$('.edit-input').attr('disabled', true);
var start = this.$era_start.val().trim();
var end = this.$era_end.val().trim();
if(start && end){
this.model.save({from: start, until: end});
}
this.$el.removeClass('editing');
this.$('.edit-input').removeClass('editing');
this.trigger('close');
},
updateOnEnter: function(e){
if(e.which !== ENTER_KEY && (!this.$era_start.val().trim() || !this.$era_end.val().trim())){
return;
}
this.close();
}
});
And this is the code for the era model:
var karass = karass || {};
karass.Era = Backbone.Model.extend({
defaults: {
from: '',
until: ''
},
});
I thought I didn't need a url while using localStorage.
Edit: I forgot to mention that while this behavior occurs in the Era/EraView itself, it also occurs in the Name model, which extends Era. Name in turn belongs in a Names collection. I don't know if this makes a difference, but I figured I add it.
Edit 2: The Names collection looks like this:
karass.Names = History.extend({
model: karass.Name,
localStorage: new Backbone.LocalStorage('karass-names'),
});
Edit 3: I posted all the code on jsfiddle: http://jsfiddle.net/herrturtur/CRv6h/
You don't need an url while using localStorage. But you need to set the localStorage property on your model or on your collection (if you set the localStorage on a collection the models inside the collection will "inherit" this setting):
karass.Era = Backbone.Model.extend({
localStorage: new Backbone.LocalStorage("EraCollection"),
// the EraCollection should be an unique name within your app.
defaults: {
from: '',
until: ''
},
});
If you don't setup the localStorage property the plugin falls back to the default ajax sync so you get the uri missing exception.