Hello dear SO users and contributers. I'm unsure on how to manipulate compositeViews using the JS framework MarionetteJS based on BackboneJS. Hopefully someone will be able to give me some advice on how to continue.
The application context: The application I'm building is a GRID that can accept draggable items. This grid is being build using two CompositeView's to build the rows within the page section and an ItemView for each field that's actually represented on the website.
The application views: I posses a gridView that has a collection of gridModels which get split over multiple rows in the initialize method. This generates a section in the HTML that will hold the GRID. The gridRowView will generate a div with the class 'day' for each row within the gridView build from all of the gridItems.
It has been mentioned that in a previous question relating my GRID that a treeView is probably more suitable for the purposes. But I feel this does not concern the scope of my question.
planboard.gridItemView = Backbone.Marionette.ItemView.extend({
model: planboard.gridModel,
collection: planboard.gridCollection,
template: "#grid-item-template",
onShow: function() {
//code to plan dropped orders
}
});
planboard.gridRowView = Backbone.Marionette.CompositeView.extend({
template: "#grid-row-template",
itemView: planboard.gridItemView,
itemViewContainer: "div.day",
initialize: function() {
this.collection = new Backbone.Collection(_.toArray(this.model.attributes));
}
});
planboard.gridView = Backbone.Marionette.CompositeView.extend({
template: "#grid-template",
itemView: planboard.gridRowView,
tagName: "section",
initialize: function() {
var grid = this.collection.groupBy(function(list, iterator) {
return Math.floor(iterator / collums); // 4 == number of columns
});
this.collection = new Backbone.Collection(_.toArray(grid));
}
});
The application templates:
<!-- Grid templates -->
<script type="text/template" id="grid-item-template">
<div class="gridItem droppable" id="{{cid}}"><b>{{data}}</b></div>
</script>
<script type="text/template" id="grid-row-template">
<div class="day"></div>
</script>
<script type="text/template" id="grid-template">
<section></section>
</script>
The problem: The code below (minus the event wrapper) resembles code as structured in the DOM. As you can see the views generate the expected result. I am however in a spot where I need to use positioning to place items on the GRID. To do this I've taken the start of div container around the section (the start of the GRID).
Although this works, this becomes a problem once scroll-bars are introduced. Since the start position of the GRID does not actualy change you can scroll down but the orders will always remain on the same position off the screen.
Possible solution: Watching the Google Calendar I've realized that they use an additional event-wrapper inside of their rows for positioning. This seems logical to me since then the offset to determine the position from would no longer be the start of your GRID, but rather the start of row (class 'day'). This should solve the problem that scroll-bars could pose.
In my mind this would look something like the code below:
<section>
<div> <!-- wrapper generated by framework -->
<div class="day">
<div class="event-x"> <!-- used as offset for positioning -->
<div id="c39" class="gridItem droppable"> <!-- grid column --></div>
</div>
</div>
<div>
</section>
My question: Since my row (the div with class 'day') is automatically generated by the MarionetteJS CompsositeView I am unable to give this some unique value to identify it with that does not change. Having an additional event wrapper with a unique identifier would most likely solve my problem, since then I could use JQuerySelectors to get the positioning from that element.
However... Marionette practically generates the wrapper for me and I'm unsure how to add a wrapper inside of this row with a unique on top of this. Do any of you (who have experience with MarionetteJS or Backbone) have any idea how to go about this?
Am I even looking in the right direction? Any help would be much appreciated.
If you only need to assign unique id / class to wrapper, generated by View, following answer might help. Note, that it actually doesn't any additional wrapper, it is just way to add attributes to automatically generated wrappers.
You can define attributes for View (or anything, that extends it e.g. CompositeView). These attributes will be automatically assigned to wrapper element. You can define them in extend or as a function or hash
If you need just static identifier, use first approach:
planboard.gridRowView = Backbone.Marionette.CompositeView.extend({
template: "#grid-row-template",
itemView: planboard.gridItemView,
itemViewContainer: "div.day",
id: "some-id",
initialize: function() {
this.collection = new Backbone.Collection(_.toArray(this.model.attributes));
}
});
If you need to generate id based on model, use second approach:
planboard.gridRowView = Backbone.Marionette.CompositeView.extend({
template: "#grid-row-template",
itemView: planboard.gridItemView,
itemViewContainer: "div.day",
attributes: function() {
return {
id: this.model.get("id") // or something like this
};
},
initialize: function() {
this.collection = new Backbone.Collection(_.toArray(this.model.attributes));
}
});
Related
I am using a Masonry JavaScript grid layout library https://masonry.desandro.com/. I have an issue when appending new items to the grid directly from the DOM using Angular *ngfor which iterate through an a array as follows:
<div class="grid">
<div class="grid-item grid-item--height{{i}}" *ngFor="let i of array">
{{i}}
</div>
</div>
Because I am not appending them using the "appended masonry method", masonry does not layout them out. The array is getting new elements every time a user scroll down. So a JavaScript method is called:
onScrollDown() {
// add another 20 items
this.sum += 20;
for (let i = start; i < this.sum; ++i) {
this.array.push(i); }
}
When the array get more elements, it automatically creates new html items. So i need to find the way masonry layout them out again after the new items are being created. I tried calling $grid.masonry('layout') after the elements have being added to the array in the OnScrollMethod but it did not work.
I am trying $('.grid').masonry('reloadItems') as well, but calling this method the new items are overlapping the previous ones.
I would appreciate any help.
Update:
I am initializing masonry using my angular initialize component method as follows:
ngOnInit() {
this.$grid = jQuery('.grid').masonry({
// options
itemSelector: '.grid-item',
columnWidth: 384,
gutter: 24
});
UPDATED
You might try reloadItems. If your loading images, use imagesLoaded.js to avoid overlaps:
ngOnInit() {
this.$grid = jQuery('.grid').imagesLoaded( function() {
jQuery('.grid').masonry({
// options
itemSelector: '.grid-item',
columnWidth: 384,
gutter: 24
});
});
onScrollDown() {
// add another 20 items
this.sum += 20;
for (let i = start; i < this.sum; ++i) {
this.array.push(i); }
jQuery('.grid').imagesLoaded( function() {
jQuery('.grid').masonry('reloadItems');
});
}
It's been a lot of years since I had to do this, but basically:
Masonry runs a lot of JS, in order to figure out optimal bin-packing for the elements you are creating.
That means that it hard-codes a lot of style information on each "card" inside of your area, and after it calculates it, it sets it.
If you use Masonry to add new "cards" to that view, then it will continue to update the view, and change the layout and the sizes of those cards.
If you don't use the Masonry instance you had before (you use the element, not the Masonry object), then Masonry doesn't have a way to track the changes. At that point, you either have to add them to the DOM and then add them to the object, through addItems, and THEN call layout() (or masonry())... or you have to rewrap the DOM element in Masonry again, now that the element has updated.
This might have changed, but I doubt it.
There are things you could do by writing angular directives, I guess, but the cheap and cheerful way of accomplishing this would be to save the instance of your component's element (or the element that controller is bound to... whatever), and also save the reference to the Masonry view you made, and on update, add all of the items, and call layout again...
...or just, on each render, make a new Masonry view, using your root element.
PS: make sure to load a lot of new elements at once, because if you're just loading 3 at a time it'll be messy...
...also, make sure you pre-load the images in the elements, because if the images load afterwards your Masonry layout is going to get very, very messy.
I used masonry-layout in Vue.js and had the same problem. The way that solved my issue is:
onScrollDown() {
// ...
// after add another 20 items
this.msnry.reloadItems();
this.msnry.layout();
}
That this.msnry is my masonry-layout instance.
EDIT: Here is the solution sample based on the first answer. You're still welcome to offer further suggestions and fill some gaps in my knowledge (see comments).
plnkr.co/edit/mqGCaBHptlbafR0Ue17j?p=preview
ORIGINAL POST
Hello all Angular heads,
I’m trying to build a tool for building simple web pages from a set of predefined elements. The end user designs the layout by choosing which elements appear in which order (there would be a selection of maybe 20-30 different elements). An example layout:
Heading
Paragraph
Paragraph
Subheading
Barchart
Paragraph
…and so forth
Under the hood, there would be an array of Javascript objects:
var page_structure = [
{type: ”heading”, content: ”The final exam - details” },
{type: ”paragraph”, content: ”Bla bla bla bla bla…” },
{type: ”paragraph”, content: ”Bla bla bla bla bla…” }
…
{type: ”bar chart”, y: ”Grades”, colour: ”blue” }
…
];
The end user inputs parameters like content, variable, colour, etc, for each individual element.
THE PROBLEM
I can’t figure how to draw these elements on the page so that the dynamic parameters for each element are properly included.
Each element type (heading, paragraph, bar chart, other possible elements) has it’s own HTML template, which has to be able to dynamically display user-defined parameters. I’m thinking I need something like this:
var templates = {
heading: ”<h1>USER_CHOSEN_PARAMETER_HERE</h1>”,
paragraph: ”<p>USER_CHOSEN_PARAMETER_HERE</p>”
…
};
I’m using Angular’s ng-repeat directive to draw each element. I have tried using either ng-html-bind…
<div ng-repeat="x in page_structure track by $index">
<div ng-bind-html="templates[x.type]"></div>
</div>
…or a custom directive, which I call mycomponent:
<div ng-repeat="x in page_structure track by $index">
<div mycomponent component='x'></div>
</div>
Both methods work just dandy when there are no user defined parameters in templates. I can’t, however, wire up the parameters. With ng-bind-html, I have tried using an expression markup like this:
var templates = {
heading: ”<h1>{{x.content}}</h1>”,
paragraph: ”<p>{{x.content}}</p>”,
…
};
(I actually define templates in app controller constructor, so it’s $scope.templates = { bla bla } to be precise.)
This is just showing curlies and ”x.content” in the actual web page. How do I refer to a dynamically variable parameter inside ng-html-bind template, or is it even possible?
I also tried the custom directive route, defining the directive as
.directive('mycomponent', function() {
return {
scope: { component: "=" },
template: templates[component.type]
}; )
This was even more messed up, since I actually couldn’t figure out how I should even try to refer to a dynamic parameter here inside the template. So I apologise my inability to offer a meaningful example of what I tried to do.
Any help or working examples of the methods I should use here are greatly appreciated. I’m happy to provide more details if needed (this is my first post in SO, so I’m still trying to get the jist of how to flesh out the questions).
I suggest using ngInclude, in which you can have a funciton that check for the proper template to load based on the passed page structure.
<div ng-repeat="structure in page_structure">
<div ng-include="getMyTemplate(structure)"></div>
</div>
$scope.getMyTemplate = function(structure) {
switch (structure.type) {
case 'heading':
return '/Views/Heading.html' // or id of a pre-loaded template
case 'paragraph':
return '/Views/Paragraph.html' // or id of a pre-loaded template
}
}
I'm looking for a way to integrate something like ng-repeat with static content. That is, to send static divs and to have them bound to JS array (or rather, to have an array constructed from content and then bound to it).
I realize that I could send static content, then remove and regenerate the dynamic bits. I'd like not to write the same divs twice though.
The goal is not only to cater for search engines and people without js, but to strike a healthy balance between static websites and single page applications.
I'm not sure this is exactly what you meant, but it was interesting enough to try.
Basically what this directive does is create an item for each of its children by collecting the properties that were bound with ng-bind. And after it's done that it leaves just the first child as a template for ng-repeat.
Directive:
var app = angular.module('myApp', []);
app.directive('unrepeat', function($parse) {
return {
compile : function (element, attrs) {
/* get name of array and item from unrepeat-attribute */
var arrays = $parse(attrs.unrepeat)();
angular.forEach(arrays, function(v,i){
this[i] = [];
/* get items from divs */
angular.forEach(element.children(), function(el){
var item = {}
/* find the bound properties, and put text values on item */
$(el).find('[ng-bind^="'+v+'."]').each(function(){
var prop = $(this).attr('ng-bind').split('.');
/* ignoring for the moment complex properties like item.prop.subprop */
item[prop[1]] = $(this).text();
});
this[i].push(item);
});
});
/* remove all children except first */
$(element).children(':gt(0)').remove()
/* add array to scope in postLink, when we have a scope to add it to*/
return function postLink(scope) {
angular.forEach(arrays, function(v,i){
scope[i] = this[i];
});
}
}
};
});
Usage example:
<div ng-app="myApp" >
<div unrepeat="{list:'item'}" >
<div ng-repeat="item in list">
<span ng-bind="item.name">foo</span>
<span ng-bind="item.value">bar</span>
</div>
<div ng-repeat="item in list">
<span ng-bind="item.name">spam</span>
<span ng-bind="item.value">eggs</span>
</div>
<div ng-repeat="item in list">
<span ng-bind="item.name">cookies</span>
<span ng-bind="item.value">milk</span>
</div>
</div>
<button ng-click="list.push({name:'piep', value:'bla'})">Add</button>
</div>
Presumable those repeated divs are created in a loop by PHP or some other backend application, hence why I put ng-repeat in all of them.
http://jsfiddle.net/LvjyZ/
(Note that there is some superfluous use of $(), because I didn't load jQuery and Angular in the right order, and the .find on angular's jqLite lacks some features.)
You really have only one choice for this:
Render differently for search engines on the server, using something like the approach described here
The problem is you would need to basically rewrite all the directives to support loading their data from DOM, and then loading their templates somehow without having them show up in the DOM as well.
As an alternative, you could investigate using React instead of Angular, which (at least according to their website) could be used to render things directly on the web server without using a heavy setup like phantomjs.
I have a model Category that has many Documents. When rendering an individual Category I want to list all the the child documents in a drag and drop sortable list. I also want double clicking on any individual document to allow inline editing for that document.
I got both parts working on there own, but can't seem to figure out how to merge them together.
For the sortable list I'm using a custom subclass of CollectionView to render the documents, and after inserting the element I call the html5sortable jquery plugin.
For the inline editing I set an itemController for each document being rendered. Inside the DocumentController I maintained application state of editing the document.
I'm looking for insight on how to combine the two approaches. What I think I need is a way to setup an itemController for each itemView in the CollectionView. I've put the relevant code below.
App.SortableView = Ember.CollectionView.extend({
tagName: 'ul',
itemViewClass: 'App.SortableItemView',
didInsertElement: function(){
var view = this;
Ember.run.next(function() {
$(view.get('element')).sortable();
});
}
});
App.SortableItemView = Ember.View.extend({
templateName: 'sortable-item',
doubleClick: function() {
//This should ideally send 'editDocument' to controller
}
});
App.DocumentController = Ember.ObjectController.extend({
isEditing:false,
editDocument: function () {
this.set('isEditing', true);
},
finishedEditing: function() {
var model = this.get('model');
model.get('store').commit();
this.set('isEditing', false);
}
});
<script type="text/x-handlebars" data-template-name="category">
<h1>{{ name }}</h1>
<h2>Documents</h2>
<!-- This makes a sortable list -->
{{view App.SortableView contentBinding="documents"}}
<!-- This makes an editable list -->
{{#each documents itemController="document"}}
<!-- change markup dependent on isEditing being true or false -->
{{/each}}
<!-- How do I combine the two -->
</script>
Thanks for any help. Really appreciate it.
The secret is to set itemController on your ArrayController instead of trying to set it on the view. Then, any views that bind to that ArrayController will get a controller back instead of whatever content is behind it.
To do that, you'll have to make an explicit DocumentsController:
App.DocumentsController = Ember.ArrayController.extend({
needs: ['category'],
contentBinding: 'controllers.category.documents',
itemController: 'document'
});
and then in your categories:
App.CategoryController = Ember.ObjectController.extend({
needs: ['documents']
Now, in your templates, bind to controllers.documents instead of documents.
I think this is a bug in Ember, which is about to be resolved:
https://github.com/emberjs/ember.js/issues/4137
I just realized I was misunderstanding the el attribute of a Backbone.View. Basically my views require dynamic id attributes based on its model's attribute. I thought I had this working fine because I simply specified it in my template:
<script type="text/template" id="item_template">
<li class="item" id="{{identifier}}">
<span class="name">{{name}}</span>
</li>
</script>
However, I realized that what Backbone was actually doing was putting this compiled template into another element, div by default. I learned more about this by reading the documentation, but I'm still confused on how to create a dynamic id.
Preferably, I would love to find a way to make it such that the stuff in the above template serves as my el, since it already has everything I want, but I don't know if that is possible. So I'm wondering if, quite simply, there is a way to specify a dynamic id attribute.
I tried setting it within the initialize method, this.id = this.model.get('attr') but it didn't seem to have any effect, possibly because by this time it is already too late.
What I'm currently doing is just using jQuery to add the id in during render():
this.el.attr(id: this.model.get('identifier'));
it works, but of course, I'm simply asking if there is a preferred way to do it through Backbone.
Yes there is a standard way to do this in Backbone. You can pass id to the View constructor. You can also refactor your template so that Backbone creates the parent <li> element for you. Try this simpler template:
<script type="text/template" id="item_template">
<span class="name">{{name}}</span>
</script>
And add these to your view:
myView = Backbone.View.extend({
className: "item",
tagName: "li"
})
And instantiate it like this:
var view = new YourView({
model: mymodel,
id: mymodel.get('identifier') // or whatever
})
Good luck!
There is one more approach. I found it more convenient than passing id every time you create an instance of your view.
Template:
<script type="text/template" id="item_template">
<span class="name">{{name}}</span>
</script>
View:
var MyView = Backbone.View.extend({
tagName: 'li',
attributes: function(){
return {
id: this.model.get('identifier'),
class: 'item'//optionally, you could define it statically like before
}
}
})
When you create your view, pass in a selector that will let the view find your existing pre-rendered DOM element:
var id = "1234";
var view = YourView({el: '#'+id});