backbone, javascript mvc - styling views with javascript
Asked Answered
S

4

6

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
}
Slug answered 15/6, 2011 at 0:23 Comment(5)
How did this turn out? Did you find a reasonable solution?Ivett
Well, I ended up going with the code in the edit above, which works okay. Am going to award the bounty to strongriley since his answer is closest to what I ended up with. Will leave the question unanswered for a little while though as I'm not sure about my own solutionSlug
Fair enough. I was curious as to whether you had tried my answer which seemed the cleanest. I would not want to have to place wrapping code in every view which may have a textarea; I would want it to 'just work', basically. Anyway, good question, enjoyed it.Ivett
After closer examination - you're absolutely right!! That's actually a really great plugin. Shame on me but I didn't actually check it out because you mentioned it hadn't been tested and I had some recollection of trying it for a very similar task some time back and it just operating in a similar fashion to jquery's live events. Sorry about the bounty, because this solves the problem in a single line of code.Slug
No problem; glad you arrived at a solution which suits you.Ivett
I
1

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.

Ivett answered 19/6, 2011 at 19:3 Comment(2)
This is great, how did I not know about this before! Do you have any idea what the performance implications are?Slug
I don't think its substantial. We use this technique in several places in our app to assign date and time pickers and we have not really noticed a performance drain. The fact that we used this technique successfully was why I've been commenting so much; all these other answers seem suboptimal to me (including the one which won the bounty!).Ivett
R
1

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.

Rh answered 16/6, 2011 at 2:42 Comment(2)
The problem here though is that if a view renders itself as part of a parent view before the parent view is in the DOM it cant initialize the RTE - that has to happen after its parent view gets inserted into the pageSlug
This won the bounty, but requires specific code in each view whose DOM has a textarea. I would recommend people take a look at my solution which will work for any textarea assigned the appropriate class. That solution does not require any more code than the one-liner provided.Ivett
I
1

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.

Ivett answered 19/6, 2011 at 19:3 Comment(2)
This is great, how did I not know about this before! Do you have any idea what the performance implications are?Slug
I don't think its substantial. We use this technique in several places in our app to assign date and time pickers and we have not really noticed a performance drain. The fact that we used this technique successfully was why I've been commenting so much; all these other answers seem suboptimal to me (including the one which won the bounty!).Ivett
E
0

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();
      }
    });

  }
});
Excoriation answered 15/6, 2011 at 5:7 Comment(1)
I like this but unfortunately I can't make a background for every item that will be processed. e.g. jquery menus also must be in the dom when initialized. Also, views may re-render themselves when the page is already in focus..Slug
C
0

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

Coremaker answered 20/6, 2011 at 21:5 Comment(5)
Stopping short of down-voting this answer to be polite and respect the effort. However, basing object extensions on the need for textarea handling seems very wrong. Honestly, only when I got to the end of the answer was I sure that this answer was intended for the question.Ivett
Thanks for being polite! :) I'm fairly new to jquery / js / but have pretty good experience with backbone. Can you expand on your reasons as to why this answer seems wrong? As I see it, we are talking about a view here. I have a feeling that some of what I was trying to get across got lost cause of my 'pseudocode'. Regardless.. I mean... if the jquery plugin team suggests doing a similar thing it can't be that wrong?Coremaker
Heh... I'm not cool enough to comment on the answer from Bill Eisenhaur just yet, so I'll drop it here. I have to disagree with mixing javascript with your template, as that goes against the idea of separation of responsibilities, and unobtrusive js. Throwing js into templates rendered by js just seems a step backwards in maintainability. However, the plugin he refers to (plus example) seems like the best answer yet (imho). For those stumbling onto this for similar reasons: It does duplicate some of the built-in '.live()' functionality (api.jquery.com/live)...Coremaker
Put it in the view or the template, makes no difference. I think its actually okay if you have Javascript that is doing DOM-related things to stay in the template, but either way is okay. The built-in functionality you speak of may actually be the same or at least very similar to the LiveQuery plugin.Ivett
yea, ".live()" lets you bind a callback to an event for any matchers that exist now OR in the future (the in the future part is what's different than ".bind()"). However, it does not (can not?) bind to 'new' selectors appearing, which the plugin does. Though, you may be able to get the same functionality by using ".live()" and firing a '.live()'-bound event on view render. Wish I had time to work on the answer! I really love the question!Coremaker

© 2022 - 2024 — McMap. All rights reserved.