I'm trying to wrap the plugin Justified Gallery in an Ember component. The main problem that I'm facing is that the list of photos in the gallery come from an API, so they're part of the model. What I have so far:
App.JustifiedGalleryComponent = Ember.Component.extend({
_init: function() {
this.$().justifiedGallery({
rowHeight: 150,
fixedHeight: false,
margins: 7
});
}.on('didInsertElement')
});
Template
{{#each photo in items}}
<div>
<img src={{photo.thumbUrl}} />
</div>
{{/each}}
But I can't get that to work, probably because the list of photo is inside an each loop, and when the plugin is applied the photos are still not in the DOM? What would be the approach for this problem?
Thanks!
EDIT:
Taking as a reference the component for Masonry I've got this almost sorted, but the first time that I navigate to the URL nothing shows, if I go to a second route (inside the ember app) and go back to the gallery then it displays fine and justified. This is my component now:
import Ember from 'ember';
var getOptions = function (keys) {
var properties = this.getProperties(keys);
Object.keys(properties).forEach(function (key) {
if (properties[key] === "null") {
properties[key] = null;
}
if (properties[key] === undefined) {
delete properties[key];
}
});
return properties;
};
export default Ember.Component.extend({
classNames: ['justified-grid'],
options: null,
items: null,
setup: Ember.on('didInsertElement', function() {
this.set('options', getOptions.call(this, [
'rowHeight',
'fixedHeight',
'margins'
]));
this.justifyGrid();
}),
justifyGrid: Ember.observer('items.#each', function() {
var _this = this;
imagesLoaded(this.$(), function() {
_this.$().justifiedGallery(_this.get('options'));
_this.set('gridInitialized', true);
});
})
});
The problem wasn't in the component. It was that my model is loading the photos using async (Ember Data). For this reason, in the router, after setting the model, I had to force Ember Data to load my photos:
afterModel: function(model) {
return Em.RSVP.all([model.get('photos')]);
}
Related
I've been struggling hard with getting VueJS and TinyMCE to work together. I've come to the conclusion that using directives would be the way to go.
So far I've been able to pass in the body as a directive parameter, and tinyMCE sets the content. However, I can't get the two way binding to work. I'm also afraid that I'm doing things completely wrong based on the tinyMCE api.
The relevant tinyMCE functions I assume would be:
http://community.tinymce.com/wiki.php/api4:method.tinymce.Editor.setContent
// Sets the content of a specific editor (my_editor in this example)
tinymce.get('my_editor').setContent(data);
and
http://community.tinymce.com/wiki.php/api4:method.tinymce.Editor.getContent
// Get content of a specific editor:
tinymce.get('content id').getContent()
HTML
<div id="app">
<h3>This is the tinyMCE editor</h3>
<textarea id="editor" v-editor :body="body"></textarea>
<hr>
<p>This input field is properly binded</p>
<input v-model="body">
<hr>
<pre>data binding: {{ body }} </pre>
</div>
JS
tinymce.init({
selector:'#editor',
});
Vue.directive('editor', {
twoWay: true,
params: ['body'],
bind: function () {
tinyMCE.get('editor').setContent(this.params.body);
tinyMCE.get('editor').on('change', function(e) {
alert("changed");
});
},
update: function (value) {
$(this.el).val(value).trigger('change')
},
});
var editor = new Vue({
el: '#app',
data: {
body: 'The message'
}
})
Fiddle
https://jsfiddle.net/nf3ftm8f/
With Vue.js 2.0, the directives are only used for applying low-level direct DOM manipulations. They don't have this reference to Vue instance data anymore. (Ref: https://v2.vuejs.org/v2/guide/migration.html#Custom-Directives-simplified)
Hence I recommend to use Component instead.
TinymceComponent:
// Use JSPM to load dependencies: vue.js 2.1.4, tinymce: 4.5.0
import Vue from 'vue/dist/vue';
import tinymce from 'tinymce';
// Local component
var TinymceComponent = {
template: `<textarea class="form-control">{{ initValue }}</textarea>`,
props: [ 'initValue', 'disabled' ],
mounted: function() {
var vm = this,
tinymceDict = '/lib/jspm_packages/github/tinymce/tinymce-dist#4.5.1/';
// Init tinymce
tinymce.init({
selector: '#' + vm.$el.id,
menubar: false,
toolbar: 'bold italic underline | bullist numlist',
theme_url: tinymceDict + 'themes/modern/theme.js,
skin_url: tinymceDict + 'skins/lightgray',
setup: function(editor) {
// If the Vue model is disabled, we want to set the Tinymce readonly
editor.settings.readonly = vm.disabled;
if (!vm.disabled) {
editor.on('blur', function() {
var newContent = editor.getContent();
// Fire an event to let its parent know
vm.$emit('content-updated', newContent);
});
}
}
});
},
updated: function() {
// Since we're using Ajax to load data, hence we have to use this hook because when parent's data got loaded, it will fire this hook.
// Depends on your use case, you might not need this
var vm = this;
if (vm.initValue) {
var editor = tinymce.get(vm.$el.id);
editor.setContent(vm.initValue);
}
}
};
// Vue instance
new Vue({
......
components: {
'tinymce': TinymceComponent
}
......
});
Vue Instance (simplified)
new Vue({
el: '#some-id',
data: {
......
description: null
......
},
components: {
'tinymce': TinymceComponent
},
methods: {
......
updateDescription: function(newContent) {
this.description = newContent;
},
load: function() {
......
this.description = "Oh yeah";
......
}
......
},
mounted: function() {
this.load();
}
});
HTML (MVC view)
<form id="some-id">
......
<div class="form-group">
<tinymce :init-value="description"
v-on:content-updated="updateDescription"
:id="description-tinymce"
:disabled="false">
</tinymce>
</div>
......
</form>
The flows
First the data is loaded through remote resources, i.e., AJAX. The description got set.
The description got passed down to the component via props: initValue.
When the component is mounted, the tinymce is initialized with the initial description.
It also sets up the on blur event to get the updated content.
Whenever the user loses focus on the editor, a new content is captured and the component emits an event content-updated, letting the parent know that something has happened.
On Html you have v-on:content-updated. Since the parent is listening to the content-updated event, the parent method updateDescription will be called when the event is emited.
!!Couple Important Notes!!
By design, the component has 1 way binding, from parent to component. So when the description gets updated from Vue instance, the component's initValue property should be updated as well, automatically.
It would be nice if we can pass whatever the user types in tinymce editor back to the parent Vue instance but 2 ways bindings is not supposed. That's when you need to use $emit to fire up events and notify parents from components.
You don't have to define a function in parent and do v-on:content-updated="updateDescription". You can just directly update the data by doing v-on:content-updated="description = $event". The $event has the parameter you defined for the function inside the component - the newContent parameter.
Hope I explained things clearly. This whole thing took me 2 weeks to figure it out!!
Here's a Tinymce component for Vue.
http://jsbin.com/pucubol/edit?html,js,output
It's also good to know about v-model and custom input components:
https://v2.vuejs.org/v2/guide/components.html#Form-Input-Components-using-Custom-Events
Vue.component('tinymce', {
props: ['value'],
template: `<div><textarea rows="10" v-bind:value="value"></textarea></div>`,
methods: {
updateValue: function (value) {
console.log(value);
this.$emit('input', value.trim());
}
},
mounted: function(){
var component = this;
tinymce.init({
target: this.$el.children[0],
setup: function (editor) {
editor.on('Change', function (e) {
component.updateValue(editor.getContent());
})
}
});
}
});
<tinymce v-model="whatever"></tinymce>
Try this:
Vue.directive('editor', {
twoWay: true,
params: ['body'],
bind: function () {
tinyMCE.get('editor').setContent(this.params.body);
var that = this;
tinyMCE.get('editor').on('change', function(e) {
that.vm.body = this.getContent();
});
}
});
The trick was storing the directive in the temporary variable "that" so you could access it from within the change event callback.
There is now an npm package which is a thin wrapper around TinyMCE, making it easier to use in a Vue application.
It is open source with code on GitHub.
Installation:
$ npm install #tinymce/tinymce-vue
Usage:
import Editor from '#tinymce/tinyme-vue';
Templates:
<editor api-key="API_KEY" :init="{plugins: 'wordcount'}"></editor>
Where API_KEY is your API key from tiny. The init section is the same as the default init statement except you do not need the selector. For an example see the documentation.
I have a marionette compositeview which I am using to create a item list for a profile page on an app. For the child view, I extend from an already existing ItemView.
When I use this.setElement(this.el.innerHTML) in the compositeview onRender function, all the events set in the child view no longer are triggered and even more so, triggering them in the console on the inspector tool in the browser, does nothing.
However when I do not use setElement, the container div is added to my markup, but now all the events in the child view work.
Can someone help me understand this please.
The Collection I am using has a custom clone method.
I am using a global collection which is updated and stored in cache on each fetch.
When I actually instantiate my view, the collection has already been used and a region in the main layout view has been populated with a item list similar to the one I want to render.
This is how I instantiate my view:
var currentUser = Profile.get('username');
// Perform changes to global collection
Items.url = API + '/items/search?q=' + currentUser + '&size=20';
Items.parse = function (response) {
if (!response.results) {
return response;
} else {
return response.results;
}
};
Items.fetch(
{success: function (collection, response, options) {
this.listOfItems = new View.itemListProfilePage({
template: TemplIds.profilePagePostedItems,
parentClass: 'profile-cols',
collection: Items, // global collection
filterAttr: {user: currentUser},
isFiltered: true,
lazyLoad: true,
childViewContainer: '#profile-items',
childView: View.itemProfilePage.extend({
template: TemplIds.item
})
});
Backbone.trigger('main:show', this.listOfItems); //'main:show' is an event in layoutview which calls region.show
},
remove: false
});
My compositeview:
View.itemListProfilePage = Marionette.CompositeView.extend({
collection: null, //original collection cloned later for filtering
fetch: null, //promise for fetched items
lazyView: null,
options: {
parentClass: '',
filterAttr: {},
isFiltered: false,
lazyLoad: false
},
initialize: function () {
this.stopListening(this.collection);
//Change collection property and re-apply events
this.collection = this.collection.clone(this.options.filterAttr, this.options.isFiltered);
this._initialEvents();
this.collection.reset(this.collection.where(this.options.filterAttr), {reset: true});
this.listenTo(Backbone, 'edit:profileItems', this.addEditClassToSection);
},
onRender: function () {
this.setElement(this.el.innerHTML, true);
},
onShow: function () {
if (this.options.parentClass) {
this.el.parentElement.className = this.options.parentClass;
}
},
addEditClassToSection: function (options) {
if ( options.innerHTML !== 'edit' ) {
this.el.classList.add('edit-mode');
} else {
this.el.classList.remove('edit-mode');
}
},
}
The parent ItemView:
View.Item = Marionette.ItemView.extend({
model: null,
numLikes: null, //live set of DOM elements containing like counter
modalItem: null, //view class with further details about the item to be used within a modal
events: {
'click img.highlight': 'showModal'
},
initialize: function (options) {
var itemWithHeader; //extended item view class with header at the top and no footer
var addToCart;
//Set up all like-related events
this.listenTo(this.model, "change:numLikes", this.updateNumLikes);
this.listenTo(this.model, "change:liked", this.updateLiked);
//Set up the view classes to be used within the modal on click
itemWithHeader = View.ItemWithHeader.extend({
template: this.template,
model: this.model //TODO: move to inside itemDetails
});
itemAddToCart = View.ItemAddToCart.extend({
template: TemplIds.itemAddCart,
model: this.model //TODO: move to inside itemDetails
});
this.modalItem = View.ItemDetails.extend({
template: TemplIds.itemDetails,
model: this.model,
withHeader: itemWithHeader,
addToCart: itemAddToCart
});
},
onRender: function () {
var imgContainerEl;
var likeButtonEl;
//Get rid of the opinionated div
this.setElement(this.el.innerHTML);
this.numLikes = this.el.getElementsByClassName('num');
//Add the like button to the image
likeButtonEl = new View.LikeButton({
template: TemplIds.likeButton,
model: this.model
}).render().el;
this.el.firstElementChild.appendChild(likeButtonEl); //insert button inside img element
},
showModal: function (evt) {
var modalView = new View.Modal({
views: {
'first': {view: this.modalItem}
}
});
Backbone.trigger('modal:show', modalView);
},
});
The itemView for each individual item in my list:
View.itemProfilePage = View.Item.extend({
events: _.extend({},View.Item.prototype.events, {
'click .delete-me': 'destroyView'
}
),
onRender: function () {
View.Item.prototype.onRender.call(this);
this.deleteButtonEl = new View.itemDeleteButton({
template: TemplIds.deleteButton
}).render().el;
this.el.firstElementChild.appendChild(this.deleteButtonEl);
},
destroyView: function (evt) {
this.model.destroy();
}
});
The short answer is that you should not be using setElement.
Backbone specifically uses the extra container div to scope/bind the view's events. When you use setElement you are changing what the parent element is. Since you are doing this in the onRender function, which is called after the template has been rendered and the events have already been bound, you are losing your event bindings.
The correct thing to do if you are going to use Marionette and Backbone is to expect and utilize the "extra" div wrapper that is generated when you render a view. You can take control of the markup for that "wrapper" div by using className, id, and tagName view properties on your view classes.
I'm attempting to create a reusable typeahead component(?) for my app. I'm using twitter's typeahead javascript library and trying to create a custom component/view out of it.
I would like to be able to define the typeahead in my templates like so:
{{view App.TypeAhead name=ta_name1 prefretch=prefetch1 template=template1 valueHolder=ta_value1}}
I was thinking those variables would be located in the controllers:
App.ApplicationController = Ember.Controller.extend({
ta_name1: 'movies',
prefetch1: '../js/stubs/post_1960.json',
template1: '<p><strong>{{value}}</strong> - {{year}}</p>',
ta_value1: null
});
I don't know what i need to use to accomplish this, a component or a view. I would imagine it would something like this.
App.Typeahead = Ember.View.extend({
templateName: 'typeahead',
didInsertElement: function() {
$('.typeahead').typeahead([{
name: this.getName(),
prefetch: this.getPrefetch(),
template: this.getTemplate(),
engine: Hogan,
limit: 10
}]);
$('.typeahead').on('typeahead:selected', function(datum) {
this.set('controllers.current.' + this.getValueHolder()), datum);
});
}
});
With a template like
<script type="text/x-handlebars" data-template-name='typeahead'>
<input class="typeahead" type="text">
</script>
I don't know how to get away from the jQuery class selector. In reality, i will have more than one typeahead on a form so this class selection isn't going to cut it.
I also don't know how to get the values from the controller in the View. Obviously the getPrefetch(), getTemplate(), getValueHolder(), etc methods don't exist.
I know this is a TON of pseudo code but hopefully i can get pointed in the right direction.
You probably want to use a component for this.
The secret afterwards is that Ember components (and View) expose a this.$ function which is a jQuery selector scoped to the current view. So you only need to do this:
didInsertElement: function() {
this.$(".typeahead"); // ... etc
}
Take a look at my Twitter TypeAhead implementation for Ember.
you can use it like this:
APP.CardiologistsTypeAhead = Bootstrap.Forms.TypeAhead.extend({
dataset_limit: 10,
dataset_valueKey: 'id',
dataset_engine: Bootstrap.TypeAhead.HandlebarsEngine.create(),
dataset_template: '<strong>{{lastName}} {{firstName}}</strong>',
dataset_remote: {
url: '%QUERY',
override: function (query, done) {
$.connection.cardiologists.server.getAllByLastNameLike(query) //SignalR
.then(function (cariologists) {
done(cariologists);
});
}
},
didInsertElement: function () {
this._super();
var self = this;
Em.run.schedule('actions', this, function () {
var cardiologistFullName = this.get('controller.content.cardiologistFullName');
self.get('childViews')[1].$().val(cardiologistFullName);
});
},
valueChanged: function () {
this._super();
this.get('childViews')[1].$().typeahead('setQuery', this.get('controller.content.cardiologistFullName'));
}.observes('value'),
selected: function (cardiologist) {
var cardiologistFullName = '%# %#'.fmt(Em.get(cardiologist, 'lastName'), Em.get(cardiologist, 'firstName'));
this.set('controller.content.cardiologistFullName', cardiologistFullName);
this.set('value', Em.get(cardiologist, 'id'));
}
});
and the handlebars:
{{view APP.CardiologistsTypeAhead
classNames="col-sm-6"
label="Cardiologist:"
valueBinding="controller.content.referrerCardiologist"}}
Hello here is my little code :
i don't know how to make this more marionette ... the save function is too much like backbone...
self.model.save(null, {
success: function(){
self.render();
var vFormSuccess = new VFormSuccess();
this.$(".return").html(vFormSuccess.render().$el);
}
var VFormSuccess = Marionette.ItemView.extend({
template: "#form-success"
} );
http://jsfiddle.net/Yazpj/724/
I would be using events to show your success view, as well as using a layout to show your success view, if it's going into a different location.
MyLayout = Marionette.Layout.extend({
template: "#layout-template",
regions: {
form: ".form",
notification: ".return"
}
initialize: function () {
this.listenTo(this.model,'sync',this.showSuccess);
this.form.show(new FormView({model: this.model}));
},
showSuccess: function () {
this.notification.show(new VFormSuccess());
}
});
Or, you could do the same with just the one region, and having the FormView be the layout itself. You just need to ensure there is an element matching the notification region exists in the layout-template.
MyLayout = Marionette.Layout.extend({
template: "#layout-template",
regions: {
notification: ".return"
}
initialize: function () {
this.listenTo(this.model,'sync',this.showSuccess);
},
showSuccess: function () {
this.notification.show(new VFormSuccess());
}
});
What this allows you to do:
You can then show an error view quite easily, if you wanted. You could replace initialize with
initialize: function () {
this.listenTo(this.model,'sync',this.showSuccess);
this.listenTo(this.model,'error',this.showError);
},
and then add the following, ensuring you create a VFormError view.
showError: function () {
this.notification.show(new VFormError());
}
You should be able to write
self.model.save(null, {
success: function(){
self.render();
}
...
Why are you doing this
this.$(".return").html(vFormSuccess.render().$el);
If you define that template as the view template you could simply refer to it with $el, if you need two different templates then you might think about using a Controller, to decide what to use and who to use it.
If you use Marionette, you don't call render directly but instead use Marionette.Region to show your views.
I'm using the hottowel template and I'm trying to load views dynamically on to a dashboard type view.
The views should also be able to be viewed individually.
So lets say i have a graph view. I want to be able to navigate to it like this :"mysite.com/#/graph/1".
I should also be able to compose the view through ko bindings into the dashboard view.
What I'm doing right now is to on the activate method of the dashboard load a users saved dashboard views. Like this:
dashboard activate
function activate() {
return datacontext.getUserDashboardConfigurations(userId)
.then(function (data) {
_.each(data.elements(), function (view) {
system.acquire("viewmodels/" + view.viewType()).then(function (model) {
var newModel = model.create(); //factory method
newModel.__moduleId__ = model.__moduleId__;
newModel.viewSettingsId(view.SettingsId());
views.push(newModel);
});
});
vm.showLoad(false);
});
}
I probably haven't understood how durandal and/or require correctly. But the model returned by the acquire method is the same for every view of the same type. 3 graph views composed to the dashboard all gets the same instance of the graph model. So what I did was to create a sort of factory method for the model.
Graph model
define( function(logger, dataContext) {
function model() {
var viewSettingsId= ko.observable(0);
function activate() {
//omitted
}
function viewAttached(view) {
//omitted
}
return {
activate: activate,
title: 'Graph View',
viewSettingsId: viewSettingsId
};
}
return {
create: function() {
return new model();
}
};
});
ko binding
<div id="columns" data-bind=" foreach: views">
<div data-bind="compose: { model: $data, activate: true, alwaysAttachView: true, preserveContext: false } "></div>
</div>
router overridden
router.getActivatableInstance = function(routeInfo, params, module) {
if (typeof module == 'function') {
return new module();
} else {
if (module.create && typeof module.create == 'function') {
var newModule = module.create();
newModule.__moduleId__ = module.__moduleId__;
return newModule;
}
return module;
}
};
This sort of solves the problem for the dashboard. Navigation also work. But I feel that there must be a better solution. Maybe durandal widgets? But I want to have them as normal views and "widgets" on the dashboard. How can I solve this in a cleaner way?
I solved it. I found the samples, duh. I have overlooked that link in the documentation.
The master detail sample made it clear. Changed viewmodel to use the prototype pattern in the example. Then use the viewmodel.activateItem after system.acquire.
Then compose the views like the example. Works and feels cleaner!
dashboard activate
function activate() {
return datacontext.getUserDashboardConfigurations(userId)
.then(function (data) {
currentDashboardconfiguration = data;
_.each(data.elements(), function (view) {
system.acquire("viewmodels/" + view.viewType()).then(function (model) {
var newModel = new model();
viewModel.activator().activateItem(newModel, { id: view.viewSettingsId() }).then(function (suc) {
views.push(newModel);
});
});
});
vm.showLoad(false);
});
}
graph model
define( function(logger, dataContext) {
var ctor = function()
{
this.viewSettingsId = ko.observable(0);
this.title: 'Graph View',
}
ctor.prototype.activate = function() {
//omitted
}
ctor.prototype.viewAttached = function(view) {
//omitted
}
return ctor;
});
ko binding
<div id="columns" data-bind=" foreach: views">
<div class="widget column" data-bind="compose: $data"></div>
</div>