I'm new to Mithril and trying to wrap my head around this issue.
I have a view that creates a DOM element and has a onClick method declared. Unfortunately when I tried to launch a lightbox such as magnificPopup, jqueryui dialog, fancybox or you name it, it calls the method undefined, even though the lightbox library is loaded.
Is this an issue with Mithril?
Here is my code:
m('a.view-link', {
onclick: function() {
$.colorbox(); // this fails
alert('here is the code'); // alerts do work
$('.view-link').css('border', '2px solid red'); // this works
// $('.white-popup').magnificPopup({ // this claims .magnificPopup is undefined
// type:'inline',
// midClick: true
// });
// $('#dialog').dialog('open'); //jqueryUI dialog claims that .dialog is undefined
},
href: '#'
} , ' Show link in lightbox')
The way Mithril works is by creating a virtual DOM (i.e. a plain-javascript data structure that represents a template), and then rendering this virtual DOM to create a real DOM tree.
When you call m(), you're building this virtual DOM, so at that point the DOM elements don't exist yet in the document. In order to integrate third party libraries, the correct way to do it is via the config attribute (see http://lhorie.github.io/mithril/mithril.html#accessing-the-real-dom-element )
In a nutshell, the config function gets called after rendering, and it takes the element as an argument, along with some other parameters to help control its lifecycle.
The syntax looks like this:
m("a.view-link", {config: function(el) {
$(el).colorbox() //initialize jquery plugin
}})
Might it be that $.colorbox(); is missing the 'subject' element?
$(this).colorbox(); should work better.
Related
In my website, the main index.php have a drop-down menu, and if I click on an item, an Ajax loader work and writes in the main div of the main index.
But if this import use foundation function (reveal, callout, tabs..), this import doesn't work.
If in web explorer console I insert again this command '$(document).foundation();', import works but I receive in the log a warning
Tried to initialize drill down on an element that already has a Foundation plugin.
foundation.min.js:65 Tried to initialize dropdown-menu on an element that already has a Foundation plugin.foundation.min.js:65
Tried to initialize off-canvas on an element that already has a Foundation plugin.foundation.min.js:65
Tried to initialize responsive-toggle on an element that already has a Foundation plugin.foundation.min.js:65
Tried to initialize reveal on an element that already has a Foundation plugin.
Can somebody help me to find a clean solution to reload foundation?
In Foundation 6.2.3,
$(document).foundation()
will call
reflow: function(this, plugins)
with plugins undefined, so every plugin will be checked: The function will look at each child of $(document) with a [data-plugin_name] attribute and show this warning if that element already has been initialized:
if ($el.data('zfPlugin')) {
console.warn("Tried to initialize " + name + " on an element that already has a Foundation plugin.");
return;
}
So you can either call foundation() directly on the new element after your ajax request :
$('#your_element').foundation();
or call reflow with a specified plugin (eg. reveal)
Foundation.reflow($('document'), 'reveal')
provided you don't already have an initialized 'reveal' element in your DOM
Mixed: (best solution I think)
Foundation.reflow($('#your_element'), 'reveal');
Firstly, do not call $(document).foundation(); more than once.
If you've updated an element already initialized with a plugin, you'll need to use reInit.
If you've added or completely replaced an element with a new element you'll need to use $(element).foundation();
I use the following technique:
First, when foundation is initialized I store a global:
$(document).foundation();
__foundationRun = true;
Then, no matter when, the following util will handle initialization of new/updated elements:
function foundationUpdate(el) {
if (__foundationRun) {
if (el.data('zfPlugin'))
// existing element already has a plugin - update
Foundation.reInit(el);
else
// new element - initialize
el.foundation();
}
// else leave for foundation to init
}
So you could use like this:
$(".accordion").append("<li>Item</li>");
foundationUpdate($(".accordion"));
or
var newAccordion = $("<ul class='vertical menu accordion-menu'"
+ " data-accordion-menu><li>Item</li></ul>");
$(".container").append(newAccordion);
foundationUpdate(newAccordion);
Here is an example of using getContent to display Dojo tooltip based on the element the mouse hovers over:
require(["dijit/Tooltip", "dojo/query!css2", "dojo/domReady!"], function(Tooltip){
new Tooltip({
connectId: "myTable",
selector: "tr",
getContent: function(matchedNode){
return matchedNode.getAttribute("tooltipText");
// What if we want to load this asynchronously???
}
});
});
However this doesn't work if you try to load data asynchronously via getContent(). Does anyonw knows how this can be solved?
When you set 'async: true' in the data-dojo-config property, dojo loader will load modules asynchronously. This means only the modules that you require will be loaded, therefore you won't have dojo/* or dijit/* .
If you were running into this issue on JsFiddle, all you need to do is to change the Framework & Extensions from 'onLoad' to 'No wrap - in < head>', so dojo loader is called in before require is. Here is an example. Please note the fiddle option:
data-dojo-config="async:true"
We're using CFLayout to create a tab structure in our web application. After creation of that layout we call this function:
mytabs = ColdFusion.Layout.getTabLayout("#attributes.cflayoutName#");
mytabs.on('tabchange',
function(tablayout,tab) {
var tabtitle = tab.title;
alert(tabtitle); // Actual code does various useful 'stuff' here.
}
);
That piece of code works very well, and the alert will show each time the user clicks on a tab.
The problem is that we are now trying to do the same thing with a CFLayout type of "accordion", and I cannot get an event to fire when the user switches which accordion pane they are looking at. We've tried leaving the above as is, as well as changing the "tabchange" attribute to "expand", "beforeexpand", "activate", and "collapse".
For this testing I'm using the following simple JS function to avoid issues arising from the JS within the onchange event:
mytabs = ColdFusion.Layout.getAccordionLayout("#attributes.cflayoutName#");
mytabs.on('expand',
function(tablayout,tab) {
console.log('test');
}
);
We do not receive any errors. Nothing is logged to the console at all. I've tried replacing the console.log to an alert to rule out any problems with that line.
I found that the Ext library documentation to be very helpful with finding a solution to this problem: here.
The Ext library has a getComponent method that allows you to reference the accordion layout panel that you are trying to add the expand event to. Once you have this, you can use the "on" method you are using above to assign the expand event to each panel individually.
for (x=1; x<accordionLayoutArray.length; x++) {
mytabs.getComponent(accordionPanelName).on('expand',
function(tab) { ... });
}
This became too long for a comment so adding as an answer
After some Google searches I found what I think are some related posts. It appears as though the accordion in Ext JS does not have the same events as the tab. Instead you need to add a listener in order to catch the expanding.
See this post - in case something happens to that page here is the relevant piece:
You'd need to listen to the expand event of the child panels in the accordion, you could do something like:
Code:
myAccordion.add(myFunc('myTitle'));
function myFunc(title)
{
return new Ext.Panel(
{
title: title,
listeners: { 'expand': {fn: something, scope: foo}}
}
);
}
And I also found another similar post here on SO - see both answers
Once you know that the accordion needs a listener you can find a number of results on Google. Such as How do I attach an event handler to a panel in extJS?
This Google search will give you lots of examples.
Hope that helps.
I'm having trouble figuring out a clean way to do this. Let's take for an example a code snippet from the example todo app that comes with backbone:
addOne: function(todo) {
var view = new TodoView({model: todo});
$("#todo-list").append(view.render().el);
},
So the ToDo view is being rendered and then it's being appended to #todo-list. But let's suppose we want to add a jQuery plugin to ToDo view. Where should we place the $(".todo").plugin() snippet? If we place it inside the ToDo view render function the HTML element is not set on the page, so the plugin won't 'latch' to any DOM element. If we place this inside the addOne function it will look ugly.
So, what's the best way?
The answer largely depends on the plugin you're talking about.
For example, most of the jQueryUI controls and the KendoUI controls allow you to call the plugin method from the render of the view, directly, before the HTML is attached to the DOM. You simply call it on the view's el.
For example, if I wanted to use KendoUI's menu on a view that generated:
Backbone.View.extend({
tagName: "ul",
render: function(){
var html = "<li>foo</li><li>bar</li>";
this.$el.html(html);
this.$el.kendoMenu();
}
});
There are some plugins that require the HTML to be a part of the DOM already, for whatever reason. In this case, I typically provide an onShow function in my views, and have the code that is displaying the view check for this and call it if it exists.
For example, there's a "Layout" plugin that I've used a few times. This plugin requires the HTML to be part of the DOM before it can work:
MyView = Backbone.View.extend({
render: function(){
var html = "generate some html here...";
this.$el.html(html);
},
onShow: function(){
this.$el.layout();
}
});
// .... some other place where the view is used
var view = new MyView();
view.render();
$("#someElement").html(view.el);
if (view.onShow) {
view.onShow();
}
FWIW: I've written the code for onShow and other common methods and events dozens of times, and have consolidated it all into a Backbone add-on called Backbone.Marionette, so that I don't have to write it anymore.
http://github.com/derickbailey/backbone.marionette
There's a lot more than just this available in this add-on, of course.
You can use the backbone $ method like so this.$('todo') to use context scoped jquery querying which will allow you to search in the current view DOM fragment which wasn't added to the document DOM tree yet.
From my experience adding jquery plugin binding in either render method or some kind of helper function if there was more custom bindings which would be then called from render method after the template was created.
A few of my views need their textareas converted to rich text editors.
I'm using jwysiwyg as the editor. It requires that the element it is being attached to is in the page when the editor is initialized i.e. when I call $(this.el).wysiwyg(), this.el is already in the document.
Most of my views do not actually attach themselves to the dom - their render methods simply set their elements html content using the apps templating engine e.g. $(this.el).html(this.template(content)
Views/Controllers further up the chain look after actually inserting these child views into the page. At the same time, views do re-render themselves when their models change.
How do I ensure that the editor is attached to the element every time its rendered and still ensure that the editor is not attached until the element is already in the page?
Obviously I could hack something together that would work in this particular case but I would like an elegant solution that will work for all cases.
Any help would be much appreciated.
Edit: The main point here is that the solution must scale gracefully to cover multiple elements that must be styled after rendering and must not be styled until they are in the DOM
Edit: This is not an issue if I do top-down rendering but this is slow, I'd like a solution whereby I can render from the bottom up and then insert the complete view in one go at the top
Edit:
Using a combination of some of the techniques suggested below I'm thinking of doing something like the following. Any comments/critique would be appreciated.
app/views/base_view.js:
initialize: function() {
// wrap the render function to trigger 'render' events
this.render = _.wrap(this.render, function() {
this.trigger('render')
});
// bind this view to 'attach' events.
// 'attach' events must be triggered manually on any view being inserted into the dom
this.bind('attach', function() {
this.attached();
// the refreshed event is only attached to the render event after the view has been attached
this.bind('render', this.refreshed())
// each view must keep a record of its child views and propagate the 'attach' events
_.each(this.childViews, function(view) {
view.trigger('attach')
})
})
}
// called when the view is first inserted to the dom
attached: function() {
this.style();
}
// called if the view renders after it has been inserted
refreshed: function() {
this.style();
}
style: function() {
// default styling here, override or extend for custom
}
What if you used the JQuery LiveQuery Plugin to attach the editor? Such code could be a part of your template code, but not as HTML, but as Javascript associated with the template. Or you could add this globally. The code might look like this (assuming you've included the plugin itself):
$('textarea.wysiwyg').livequery(function() {
$(this).wysiwyg();
});
I have not tested this code, but in theory it should match an instance of a textarea element with a class of 'wysiwyg' when it appears in the DOM and call the wysiwyg function to apply the editor.
To adhere to DRY principle and get an elegant solution, you'll want a function dedicated to determining if a textarea has wysiwyg, let's say wysiwygAdder and add it if necessary. Then you can use underscore's wrap function to append your wysiwyg adder to the end of the render function.
var View = Backbone.View.extend({
el: '#somewhere',
initialize: function(){
_.bind(this, "render");
this.render = _.wrap(this.render, wysiwygAdder);
},
render: function(){
//Do your regular templating
return this;//allows wysiwygAdder to pick up context
}
});
function wysiwygAdder(context){
$('textarea', context).doYourStuff();
//check for presence of WYSIWYG, add here
}
When the view is initialized, it overwrites your render function with your render function, followed by wysiwygAdder. Make sure to return this; from render to provide context.
One solution would be to use event delegation and bind the focus event to check whether the rich text editor had been loaded or not. That way the user would get the text editor when they needed it (via the lazy loading, a minor performance improvement) and you wouldn't have to load it otherwise. It would also eliminate needing to worry about when to attach the rich text editor and that being dependent on the rendering chain.
If you're worried about the FOUC (flash of unstyled content) you could simply style the un-modified text areas to contain an element with a background image the looked just like the wysiwyg controls and have your focus binding toggle a class to hide the facade once the rich text editor had taken over.
Here's a sample of what I had in mind:
var View = Backbone.View.extend({
el: '#thing',
template: _.template($("#template").html()),
render: function() {
// render me
$(this.el).html(this.template(context));
// setup my textareas to launch a rich text area and hide the facade
$(this.el).delegate('focus', 'textarea', function() {
if(!$(this).hasRichTextEditor()) { // pseudocode
$(this).wysiwyg();
$(this).find('.facade').toggle();
}
});
}
});
Great problem to solve! Not too sure I've got the entire jist but... You may be able to get away with a 'construction_yard' (I just made that term up) that's way off to the left, build and place items there, then just move them when they're ready to be placed. Something along the lines of:
.construction_yard {
position: absolute;
left: -10000000000px;
}
This solution may fix several problems that might crop up. For example jquery height and width attributes on something that's 'hidden' are 0, so if you are styling along those lines, you'd have to wait till it was placed, which is more complicated, and jumbles things up.
your views would then need to do something along the lines of (pseudo-code):
//parent
//do all your view-y stuff...
foreach (child = this.my_kids) {
if child.is_ready() {
put_that_child_in_its_place();
}
}
Similarly, for children, you'd do a similar thing:
//child
foreach(parent = this.my_parents) {
if parent.isnt_ready() {
this.go_play_in_construction_yard();
} else {
this.go_to_parents_house();
}
}
... and, since backbone is pretty easy to extend, you could wrap it up in a more generalized class using:
var ParentsAndChildrenView = Backbone.View.extend({blah: blah});
and then
var Wsywig = ParentsAndChildrenView.extend({blah: blah});
hope that helps!
Almost forgot to note my source:
http://jqueryui.com/demos/tabs/#...my_slider.2C_Google_Map.2C_sIFR_etc._not_work_when_placed_in_a_hidden_.28inactive.29_tab.3F