I have a Layout, with a region. When the Layout is initialized, I want it to automatically initialize a pre-set view to do into it's region, and show/close it when the Layout itself is showed/closed.
Current example from https://github.com/marionettejs/backbone.marionette/blob/master/docs/marionette.layout.md:
AppLayout = Backbone.Marionette.Layout.extend({
template: "#layout-template",
regions: {
mainRegion: "#menu",
content: "#content"
}
});
var layout = new AppLayout();
ParentAppLayout.show(layout); // Render the Layout to a parent
layout.mainRegion.show(new SubView());
This example indicates that the Layout must first be shown, and after I can then init and show the child view. (above, if I show the SubView before the layout is itself shown, nothing will happen, I assume because the selector doesn't exist in the DOM?)
For a re-usable layout, I want to add this send view show into the Layout itself, rather than have to keep on adding it in manually everywhere the view is used. How can this be achieved?
AppLayout = Backbone.Marionette.Layout.extend({
template: "#layout-template",
regions: {
mainRegion: "#menu",
content: "#content"
},
initalize: function() {
this.mainRegion.attachView(new SubView());
},
onShow: function() {
this.mainRegion.show(this.mainRegion.currentView);
}
});
var layout = new AppLayout();
ParentAppLayout.show(layout); // Render the Layout to a parent, expecting the child view to also be created automatically
This approach however doesn't do anything either - no errors.
What about doing this
AppLayout = Backbone.Marionette.Layout.extend({
template: "#layout-template",
regions: {
mainRegion: "#menu",
content: "#content"
},
onShow: function() {
this.mainRegion.show(new SubView());
}
});
var layout = new AppLayout();
ParentAppLayout.show();
Otherwise if creating SubView is expensive you could do it in the initialize like this
initialize: function() {
this.subView = new SubView();
}
and later use it in the onShow.
Related
I have create a simple CKEditor widget that highlights the elements that have the class "pink".
I have also added a "Pinkify" button to the toolbar, which replaces the HTML of the selected element with some other elements that have the class "pink".
What I observe when I click the button is that widgets are not created for the freshly inserted elements. However, when I toggle between Source mode and WYSISYG mode, the widgets get created.
See the jsfiddle and its code:
CKEDITOR.replace('ck', {
allowedContent: true,
extraPlugins: 'pink'
});
CKEDITOR.plugins.add('pink', {
requires: 'widget',
init: function(editor) {
editor.widgets.add('pinkwidget', {
upcast: function(element) {
return element.hasClass('pink');
}
});
editor.addCommand('pinkify', {
editorFocus: 1,
exec: function(editor) {
var selection = editor.getSelection(),
selectedElement = selection.getStartElement();
if (selectedElement) {
selectedElement.setHtml("Let's have some <span class=\"pink\">pink</span> widget here!");
editor.widgets.checkWidgets(); // needed?
}
}
});
editor.ui.addButton('pinkify', {
label: 'Pinkify',
command: 'pinkify'
});
},
onLoad: function() {
CKEDITOR.addCss('.cke_widget_pinkwidget { background: pink; }');
}
});
I am aware of this question on Stackoverflow, but I can't get it to work with setHtml called on an element. Can you suggest how to modify the code so that widgets get created as soon as the HTML is updated?
According to the CKEditor team, it is normal that CKEDITOR.dom.element.setHtml does not instanciate widgets (see Widgets not initialised after calling setHtml on an element).
So the workaround they gave me was to rewrite the code that insert HTML in place of the selected element to:
if (selectedElement) {
selectedElement.setHtml("");
editor.insertHtml("Let's have some <span class=\"pink\">pink</span> widget here!");
}
For those like me who didn't know, editor.insertHTML inserts HTML code into the currently selected position in the editor in WYSIWYG mode.
Updated jsFiddle here.
I'm currently creating a 'smartobject' widget. In the widgets dialog, the user can choose a 'smartobject', which simply put, generates some html, which should be added to the editor. Here comes the tricky part: the html sometimes div elements and sometimes simply span elements. In the case of the div variant, the widget should be wrapped in a div 'template'. In the case of a span variant, the widget should be wrapped in a span and the html should be added 'inline'.
In the widgets API I see the following way to define a template:
editor.widgets.add('smartobject', {
dialog: 'smartobject',
pathName: lang.pathName,
template: '<div class="cke_smartobject"></div>', // <------
upcast: function(element) {
return element.hasClass('smartObject');
},
init: function() {
this.setData('editorHtml', this.element.getOuterHtml());
},
data: function() {
var editorHtml = this.data.editorHtml;
var newElement = new CKEDITOR.dom.element.createFromHtml(editorHtml);
newElement.copyAttributes(this.element);
this.element.setText(newElement.getText());
}
});
But in my case, the template is more dynamic: sometimes a div and sometimes the span will do the correct thing..
How can I fix this without needing to create two widgets which will do the exact same thing, with only the wrapping element as difference?
I've already tried to replace the entire element in the 'data' method, like:
newElement.replace(this.element);
this.element = newElement;
But this seemed not supported: resulted in undefined errors after calling editor.getData().
I'm using ckeditor v4.5.9
Thanks for your help!
It seems I got it working (with a workaround).
The code:
CKEDITOR.dialog.add('smartobject', this.path + 'dialogs/smartobject.js');
editor.widgets.add('smartobject', {
pathName: lang.pathName,
// This template is needed, to activate the widget logic, but does nothing.
// The entire widgets html is defined and created in the dialog.
template: '<div class="cke_smartobject"></div>',
init: function() {
var widget = this;
widget.on('doubleclick', function(evt) {
editor.execCommand('smartobject');
}, null, null, 5);
},
upcast: function(element) {
return element.hasClass('smartObject');
}
});
// Add a custom command, instead of using the default widget command,
// otherwise multiple smartobject variants (div / span / img) are not supported.
editor.addCommand('smartobject', new CKEDITOR.dialogCommand('smartobject'));
editor.ui.addButton && editor.ui.addButton('CreateSmartobject', {
label: lang.toolbar,
command: 'smartobject',
toolbar: 'insert,5',
icon: 'smartobject'
});
And in the dialog, to insert code looks like:
return {
title: lang.title,
minWidth: 300,
minHeight: 80,
onOk: function() {
var element = CKEDITOR.dom.element.createFromHtml(smartobjectEditorHtml);
editor.insertElement(element);
// Trigge the setData method, so the widget html is transformed,
// to an actual widget!
editor.setData(editor.getData());
},
...etc.
UPDATE
I made the 'onOk' method a little bit better: the smartobject element is now selected after the insertion.
onOk: function() {
var element = CKEDITOR.dom.element.createFromHtml(smartobjectEditorHtml);
var elementId = "ckeditor-element-" + element.getUniqueId();
element.setAttribute("id", elementId);
editor.insertElement(element);
// Trigger the setData method, so the widget html is transformed,
// to an actual widget!
editor.setData(editor.getData());
// Get the element 'fresh' by it's ID, because the setData method,
// makes the element change into a widget, and thats the element which should be selected,
// after adding.
var refreshedElement = CKEDITOR.document.getById(elementId);
var widgetWrapperElement = CKEDITOR.document.getById(elementId).getParent();
// Better safe then sorry: if the fresh element doesn't have a parent, simply select the element itself.
var elementToSelect = widgetWrapperElement != null ? widgetWrapperElement : refreshedElement;
// Normally the 'insertElement' makes sure the inserted element is selected,
// but because we call the setData method (to ensure the element is transformed to a widget)
// the selection is cleared and the cursor points to the start of the editor.
editor.getSelection().selectElement(elementToSelect);
},
So in short, I partially used the widget API for the parts I wanted:
- Make the html of the widget not editable
- Make it moveable
But I created a custom dialog command, which simply bypasses the default widget insertion, so I can entirely decide my own html structure for the widget.
All seems to work like this.
Any suggestions, to make it better are appreciated:)!
As suggested in this ckeditor forum thread, the best approach would be to set the template to include all possible content elements. Then, in the data function, remove the unnecessary parts according to your specific logic.
In the following code, I have a view which extends from another view (but does not inherit any functionality, only renders the template) and a model which I want to implement now. My view is for a like button, which I need to retrieve the state of the like button from the server each time the page is loaded. I am not sure how to do this using the model. Do I need to have an Ajax call in the model retrieving the state from the server or does that call fall into the view?
This is my code:
var likeButton = Backbone.Model.extend ({
initialize: function () {
this.isLiked = /* need something here! Ajax call to get state of button from server? */
}
});
var LikeButtonView = BaseButtonView.extend({ // extends form a previews view which simply extends from backbone and render's the template
template: _.template($('#like-button').html()),
sPaper: null,
sPolyFill: null,
sPolyEmpty: null,
isLiked: false,
events: {
"click .icon": "like",
},
model: new likeButton (),
initialize: function (options) {
BaseButtonView.prototype.initialize.apply(this, [options]); // inherit from BaseButtonView
this.likeButn = $("button.icon", this.$el);
this.svgNode = this.likeButn.find("svg").get(0); // find the svg in the likeButn and get its first object
this.sPaper = Snap(this.svgNode); // pass in the svg object into Snap.js
this.sPolyFill = this.sPaper.select('.symbol-solid');
this.sPolyEmpty = this.sPaper.select('.symbol-empty');
if (this.model.isLiked) {
this.likeButn.addClass("liked");
} else if (!this.model.isLiked) {
this.likeButn.addClass("unliked");
}
},
like: function() {
this._update();
},
_update: function () {
if ( !this.isLiked ) { // if isLiked is false, remove class, add class and set isLiked to true, then animate svg to liked position
this._like();
} else if ( this.isLiked ) { // is isLiked is false, remove class, add class, set isLiked to false, then animate svg to unliked position
this._unlike();
}
},
_like: function() {
this.likeButn.removeClass("unliked");
this.likeButn.addClass("liked");
this.isLiked = true;
this.sPolyFill.animate({ transform: 't9,0' }, 300, mina.easeinout);
this.sPolyEmpty.animate({ transform: 't-9,0' }, 300, mina.easeinout);
},
_unlike: function() {
this.likeButn.removeClass("liked");
this.likeButn.addClass("unliked");
this.isLiked = false;
this.sPolyFill.animate({ transform: 't0,0'}, 300, mina.easeinout);
this.sPolyEmpty.animate({ transform: 't0,0' }, 300, mina.easeinout);
}
});
There are three ways to implement the 'like' button's knowledge of the current state of the page: A hidden field delivered from the HTML, an Ajax call to the server, or generating your javascript server-side with the state of the like model already active.
Let's start with the basics. Your code is a bit of a mess. A model contains the state of your application, and a view is nothing more than a way of showing that state, receiving a message when the state changes to update the show, and sending messages to the model to change the state. The model and the view communicate via Backbone.Events, and the view and the DOM communicate via jQuery.Events. You have to learn to keep those two separate in your mind.
Here, I've turned your "like" model into an actual model, so that the Backbone.Event hub can see the changes you make.
var likeButton = Backbone.Model.extend ({
defaults: {
'liked': false
}
});
Now in your view, the initial render will draw the state in gets from the model. When a DOM event (described in the 'events' object) happens, your job is to translate that into a state change on the model, so my "toggleLike" only changes the model, not the view. However, when the model changes (explicitly, when the "liked" field of the model changes), the view will then update itself automatically.
That's what makes Backbone so cool. It's the way views automatically reflect the reality of your models. You only have to get the model right, and the view works. You coordinate the way the view reflects the model in your initialization code, where it's small and easy to reason about what events from the model you care about.
var LikeButtonView = BaseButtonView.extend({
template: _.template($('#like-button').html()),
events: {
"click .icon": "toggleLike",
},
initialize: function (options) {
BaseButtonView.prototype.initialize.call(this, options); // inherit from BaseButtonView
// A shortcut that does the same thing.
this.likeButn = this.$("button.icon");
this.model.on('change:liked', this._updateView, this);
},
render: function() {
BaseButtonView.prototype.render.call(this);
// Don't mess with the HTML until after it's rendered.
this.likeButn.addClass(this.model.isLiked ? "liked", "unliked");
},
toggleLike: function() {
this.model.set('liked', !this.model.get('liked'));
},
_updateView: function () {
if (this.model.get('liked')) {
this._showLikedState();
} else {
this._showUnlikedState();
}
}
});
How the like model gets initialized is, as I said above, up to you. You can set a URL on the model's options and in your page's startup code tell it to "fetch", in which case it'll get the state from some REST endpoint on your server. Or you can set it to a default of 'false'. Or you can set it in hidden HTML (a hidden div or something) and then use your page startup code to find it:
new LikeButtonView({model: new LikeButton({}, {url: "/where/page/state/is"}));
or
new LikeButtonView({model: new LikeButton({liked: $('#hiddendiv').data('liked')}, {}));
If you're going to save the liked state, I'd recommend the URL. Then you have someplace to save your data.
If have a little game where images have to be dragged on the right hotspots of a larger image.
The little images are inside a containerView, the hotspots also are ContainerViews.
When i drop the images on a hotspot i use the following code in my drag-n-drop mixin to move the image in the dom:
Player.Droppable = Ember.Mixin.create({
drop: function(event) {
//get the view that was dragged
var viewId = event.originalEvent.dataTransfer.getData('Text');
var view = Ember.View.views[viewId];
//log the parent-view: App.AnswerListView
console.log(view.get('parentView').constructor);
//detach the view from the original containerView
var parentView = view.get('parentView');
parentView.removeObject(view);
//attach it to the hot-spot-containerview
this.addObject(view);
//logging this gives a different result: App.HotspotView
console.log(view.get('parentView').constructor);
event.preventDefault();
return false;
}
});
The view i'm dragging is an App.AnswerView. What i'm expecting from the docs is that the function parentViewDidChange on the AnswerView is triggered, but that doesn't happen:
App.AnswerView = Ember.View.extend(App.Draggable, {
templateName: "answer",
classNameBindings: [':answer', 'this.content.isSelected:selected'],
click: function(evt){
this.get('controller').send('answerClicked', this.content);
},
parentViewDidChange: function(){
this.get('controller').send('answerMoved', this.content);
},
});
The docs say: Called when the parentView property has changed. In my case, it is changed. Is this a bug, or am I missing something here?
TIA
This issue was a bug and got resolved in the 1.0.0 final
https://github.com/emberjs/ember.js/issues/2423
pushObject and removeObject are methods, inherited from the Ember.MutableArray Mixin, that the Ember.ContainerView extends. If you look at Ember's source code for ContainerView (https://github.com/emberjs/ember.js/blob/v1.0.0-rc.2/packages/ember-views/lib/views/container_view.js#L15), you will see that ContainerView does not override those methods, thus they only manipulate its childViews array, not the view's parent view.
You should find methods that manipulate the '_parentView' property here instead: (https://github.com/emberjs/ember.js/blob/v1.0.0-rc.2/packages/ember-views/lib/views/view.js#L2018) - in the Ember.View's implementation.
So in short, use:
removeChild instead of removeObject, to delete a child view from your ContainerView
createChildView + pushObject instead of 'addObject', if you want to add a new child view to a ContainerView.
Example Code:
Player.Droppable = Ember.Mixin.create({
drop: function(event) {
//get the view that was dragged
var viewId = event.originalEvent.dataTransfer.getData('Text');
var view = Ember.View.views[viewId];
//log the parent-view: App.AnswerListView
console.log(view.get('parentView').constructor);
//detach the view from the original containerView
var parentView = view.get('parentView');
parentView.removeChild(view);
//attach it to the hot-spot-containerview
this.createChildView(view);
this.pushObject(view);
//logging this gives a different result: App.HotspotView
console.log(view.get('parentView').constructor);
event.preventDefault();
return false;
}
});
A simple and short question: If a view contains two or more sub views. Should the view container be a layout view?
If not, what are good alternatives?
Update:
my code:
var LikeButtonModal = Backbone.Model.extend({
url: 'api/profile/like/'
});
var LikeButton = Backbone.Marionette.ItemView.extend({
tagName: 'button',
className: 'like',
template: '<div>like</div>',
events: {
'click' : 'like'
},
initialize: function(userId){
this.model = new LikeButtonModal();
},
like: function(){
this.model.save();
}
})
var LeftProfileView = Backbone.Marionette.Layout.extend({
template: '#profile-left',
regions:{
extra : '.extra'
},
initialize: function(){
this.on("item:rendered", this.editable, this);
},
onRender: function(){
if(this.model.get('userid') != ActiveUser.get('userid')){
this.extra.show(new LikeButton(this.model.get('userid')));
}
}
});
Layouts are good for this if you will be replacing the sub-views at different times, or if the sub-views are very different types... for example, a layout might contain your header, your navigation and your main content region.
Other options are CollectionViews and CompositeViews.
Collection views will render a collection of items, using the same type of view for each item in your collection. This works well for lists of things.
CompositeViews are CollectionViews that can render a wrapper template around the collection. For example, an HTML table structure. The table, thead, tbody and tfooter tags can be rendered in the CompositeView's wrapper template, and then a collection of items can be rendered in to the tbody tag.
This might shed a little more light on the subject, too: https://github.com/derickbailey/backbone.marionette/wiki/Use-cases-for-the-different-views