Skip to content
This repository

Add application helper #128

Closed
paulmillr opened this Issue September 23, 2011 · 27 comments

9 participants

Paul Miller Stefan Huber Ben Nolan Omar Khan Ryan McKillen Thomas Schranz Jökull Sólberg Auðunsson Kyle Mathews
Paul Miller
Owner

All brunch applications need to store imported models / views somewhere. In our TODO example, importing is done by this code:

window.app = {}
app.routers = {}
app.models = {}
app.collections = {}
app.views = {}

Todos = require('collections/todos_collection').Todos
MainRouter = require('routers/main_router').MainRouter
HomeView = require('views/home_view').HomeView
NewTodoView = require('views/new_todo_view').NewTodoView
TodosView = require('views/todos_view').TodosView
StatsView = require('views/stats_view').StatsView
TodoView = require('views/todos_view').TodoView

# app bootstrapping on document ready
$(document).ready ->
  app.initialize = ->
    app.collections.todos = new Todos()

    app.routers.main = new MainRouter()
    app.views.home = new HomeView()
    app.views.newTodo = new NewTodoView()
    app.views.todos = new TodosView()
    app.views.stats = new StatsView()

    Backbone.history.navigate('home', false) if Backbone.history.getFragment() is ''

I think it's extremely verbose. I propose to include a simple BrunchApplication class that would do all work for us. Importing is needed by all brunch apps, so I guess this would help them a lot.

Paul Miller
Owner

@MSNexploder what do you think about this? The syntax is kinda strange, but I didn't found better.

{BrunchApplication} = require "helpers"


window.app = new BrunchApplication

app.import
  collections:
    # Classname: initialize or not new object of this classname?
    Todos: no
  routers:
    # app.routers.Main -- class
    # app.routers.main -- object
    Main: yes
  views:
    Home: yes
    NewTodo: yes
    Stats: yes
    Todo: yes

$ ->
  Backbone.history.navigate("home", false) if Backbone.history.getFragment() is ""
Stefan Huber

Yeah, the syntax really doesn't feel right. :-(
And probably does to much magic.

Maybe we should build something more like a factory or generator.
And encourage people to follow sane naming conventions.
Eventually even provide a way to change these defaults.

Just a example I spontaneously made up and which probably needs more rethinking:

{Factory} = require "helpers"

window.app = new BrunchApplication

# automatically requires needed files and instantiates new object
app.collections.todos = Factory.collection 'Todo'

app.routers.main = Factory.router 'Main'
app.views.home = Factory.view 'Home'
app.views.newTodo = Factory.view 'NewTodo'
app.views.todos = Factory.view 'Todos'
app.views.stats = Factory.view 'Stats'

$ ->
    Backbone.history.navigate("home", false) if Backbone.history.getFragment() is ""
Paul Miller
Owner

Hmm, you're right. Too much magic.

Another idea: import all stuff automatically, like in Rails. Because I can't imagine the use-case when user creates a model in src/models/some_model.coffee and don't imports it later.

With this, user would just need to initialize necessary things with "new" keyword. This is much clearer.

{BrunchApplication} = require "helpers"

window.app = new BrunchApplication

# app.routers.Main exists because file app/routers/main exists too.
app.routers.main = new app.routers.Main
app.views.home = new app.views.Home
app.views.newTodo = new app.views.NewTodo
app.views.stats = new app.views.Stats
app.views.todo = new app.views.Todo

$ ->
  Backbone.history.navigate("home", false) if Backbone.history.getFragment() is ""
Stefan Huber

Hmm, I'm in general not a big fan of autoloading stuff within the javascript environment.
But I guess it would be ok in this case, as long as it can be optionally turned off.

Paul Miller
Owner

Related to auto-importing: #94.

Ben Nolan

I think auto loading everything is a good idea.

In my apps I do something like:

class Application
  constructor: ->
    _.extend(@, Backbone.Events)

  start: ->
    new HomeRouter
    new ExperiencesRouter
    new MapsRouter
    new SearchRouter

    # Create collections in the window scope
    ( ->
      @Experiences = new ExperienceCollection
    ).apply(window)

    Backbone.history.start()

@Application = Application

It's helpful to have the application object to have backbone events, since there are some app wide things you need to watch for (geolocation, authentication). I create my collections in the global scope, which is probably not a good idea.

Paul Miller
Owner

Another syntax idea. Still looking for compromise in verbosity and readability.

{BrunchApplication} = require "helpers"


class Application extends BrunchApplication
  init: ->
    models:
      todo: new app.models.Todo
    routers:
      main: new app.routers.Main
    views:
      home: new app.views.Home
      newTodo: new app.views.NewTodo
      stats: new app.views.Stats
      todo: new app.views.Todo

  onReady: ->
    Backbone.history.start()


window.app = new Application
Omar Khan

Personally I prefer this approach:

# main.coffee
App = require('routers/app').App

# app bootstrapping on document ready
$(document).ready ->
  app = new App()
  app.navigate 'home', true if Backbone.history.getFragment() is ''
  Backbone.history.start(pushState: true)
# routers/app.coffee
HomeView = require('views/home').HomeView

class exports.App extends Backbone.Router
  routes:
    'home': 'home'

  constructor: (options) ->
    super(options)
    this.collections = {}
    this.views = {}
    this.views.home = new HomeView()

  home: =>
    $('body').html this.views.home.render().el

I use this with a pubsub system based on Backbone.Events, it works quite nicely.

Paul Miller
Owner

@omarkhan problem with this solution is that user could have more than one router.

Ryan McKillen

I like the idea of auto-requiring all routers, collections, models and templates and making them available in the global app object. Routers could be auto-instantiated as well, but for everything else, I would just want the classes/constructors to be available. When views are instantiated, model instances need to be passed into the view constructors, so instantiating view instances onReady is not helpful.

Thomas Schranz
Owner

I also like the auto-loading idea. Imho the framework can do the heavy-lifting for things that are obviously needed. When we look at most applications we can observe this initialization pattern.

Omar Khan

If we want to automagically load all modules into a global namespace, is there really any point in using stitch? Why not just put everything under a global object at compile time?

Paul Miller
Owner

@omarkhan as I saw today, there's no point of using it. We'll need to implement stitch features in 0.10 anyway (e.g. for making templating modular & simple).

Jökull Sólberg Auðunsson

It would be much clearer if Brunch had a Modules class to group application parts together.

File app.coffee:

instagram = require('instagram/init')

window.app = {}
app.instagram = instagram.module

instagram.views, instagram.models etc is then ready. The Module class can be quite smart in the order it instantiates things and figures out what to include under what name.

File instagram/init.coffee:

Module = require('brunch/module').Module
module = new Module # Is aware of current module and [routes/collections/modules/views].coffee files

I'm trying to solve many problems with this suggestion. One is that the namespace app.views is going to get huge with big apps (app.views.instagramProfile, app.views.articleProfile, etc). A great benefit is being able to publish mature Modules and share them with the community.

Paul Miller
Owner

@jokull what about this reverse solution:

  • create subdirectory in views (views/instagram)
  • now you can get files from there by require "views/instagram/profile"
Jökull Sólberg Auðunsson

That's all well and good, but doesn't allow me to logically group all of instagram's communication, presentation etc. into a single folder. Maybe I just like that because originally I come from a Django background which emphasizes this quite a bit. Flask does this nicely with "blueprints" and indeed has a Blueprint class to encapsulate the place that particular "bit" of MVC has within the application structure (URL root foor example).

Ideally I'd just copy this module (folder) to another project if it needs similar functionality.

Paul Miller
Owner

That seems reasonable to me as I've came from Django background too.

Although I don't know about great resolution of this problem, that correlates greatly with new dir structure (#120).

One thing that came up to my mind is:

  • brunch could create apps directory in its projects with command brunch generate app instagram
  • any directory that's located inside apps considered as application.

So, some brunch app could have this directory structure:


Cakefile
README.md

/apps/
  helpers.coffee
  main.coffee
  twitter/
    collections/
    models/
    styles/
    routers/
    views/
      layouts/
  instagram/
    collections/
    models/
    styles/
    routers/
    views/
      layouts/
/build/
  web/
    index.html
      main.js (compile & compress)
      main.css (compile & compress)
      images/
  chrome/
    chrome.crx
/config/
  config.coffee
/doc/ (docco-generated documentation)
/test/
  helpers/
    spec_helper.coffee
/vendor/
  scripts/
    backbone.js
    jquery.js
    console_dummy.js
    underscore.js
  styles/
    normalize.css (html5boilerplate > reset css from blueprint)
    helpers.css

What do you think about it?

Jökull Sólberg Auðunsson

I love the basic idea. Some suggestions:
Encourage or at least allow twitter/collections.coffee instead of twitter/collections/something.coffee. Am I the only one that likes to have more then one "thing" in a file?

We should also steal the idea of having a "main" app too. This is what Flask does so well. Most projects start out small, and as it matures things move from a main app into modules.

Cakefile
README.md

/app/
  main.coffee
  helpers.coffee
  views.coffee
  styles/
    # .stylus 
  templates/
    # .eco 
  static/
    # Direct copy to build
  twitter/
    main.coffee # Kind of like the external API of this module
    collections.coffee
    models.coffee
    routers.coffee
    views.coffee
    static/
      # Vendor, images and other files, copies over to build
      # Might want to include urlize() for hashtags, mentions etc.
    styles/
      # .stylus 
    templates/
      # .eco 
/build/
  web/
    index.html
      main.js (compile & compress)
      main.css (compile & compress)
      images/
  chrome/
    chrome.crx
/config/
  config.coffee
/doc/ (docco-generated documentation)
/test/
  helpers/
    spec_helper.coffee
/vendor/
  scripts/
    backbone.js
    jquery.js
    console_dummy.js
    underscore.js
  styles/
    normalize.css (html5boilerplate > reset css from blueprint)
    helpers.css
Paul Miller
Owner

Am I the only one that likes to have more then one "thing" in a file?

Yea, this is "too django" approach. I prefer rails thing, where you have small file-per-class.

I would like to use brunch on a project but I'm having a hard time wrapping my head around the current directory structure. I prefer @paulmillr's rails-like approach, with small files (just one thing in a file).

But why not use a 'public' directory instead of the current 'build' directory? Then you would have:

public/
public/index.html
public/javascripts
public/stylesheets
public/images

With stylesheets and coffeescript files compiled to public/javascripts and public/stylesheets.

Kyle Mathews

+1 the idea of having a modular app-based approach. All my backbone/brunch projects are fairly small at the moment but I've been wandering myself how I'll keep them organized as the size/complexity grows and I think this would solve that problem very well.

It also fits itself nicely with situations where you're not building a single-page everything-backbone app but where you're supplementing existing pages with small backbone apps. I work a lot with Drupal and it'd be nice to be able to easily build many small backbone apps for inserting in different parts of the site e.g. on the user page, various admin pages, the search page, etc.

Also, on the one-thing per file vs. many things -- I'm a big fan of the rails model. With my Vim setup, I find it much easier to flip between files then to scroll up and down files looking for what I want.

Paul Miller
Owner

@ikezue it's because we're aiming towards multiple build targets.

E.g. build target for browsers, build target for chrome app store (that would additionally pack files in .crx package).

By the way, I see another trouble (#129): for the next release, we need to find a way to compile things file-to-file instead of current "all coffeescripts to app.js" for simplify debugging. I guess, the best way of implementing this is to make another build target, called "debug".

Is your propose to just rename "build" directory to "public" in that case?

Another open question is image storage. build/* is generated automatically, so I guess we need to make another directory for images etc. Any thoughts on this?

Jökull Sólberg Auðunsson

I got build/web/images/ and trust that the compile command be non destructive. Is this documented?

I've got a slight problem with the "web" directory. What exactly is it for?

Paul Miller
Owner

I got build/web/images/ and trust that the compile command be non destructive. Is this documented?

Today it's non-destructive, but, of course, I will document in bold when something like this would change.

web is a name of build target. Build targets are not supported properly today, though.

@paulmillr I hadn't thought about different builds for different applications. Yes, I could rename the 'build' directory to 'public', but I may also need to move things around. The 'web' directory would be redundant for web apps, unless I've misinterpreted its use. And in the current version of brunch (installed last weekend) I think index.html is outside the build directory.

How about using flags to support multiple application types:
brunch new app --type=chrome
brunch new app --type=web

Or specifying a target in config.yaml, or at the command line:
brunch build --target chrome

Just wanted to suggest these options, I'm happy to work around whatever is most convenient for others.

Re: images
I think Rails just copies assets/images/* over to public/images. I guess a similar pattern could work, copying some_dir/images/ over to build/images/.

I don't know much about Django or other web frameworks, but Rails 3.1 solves some of these same problems. It makes sense to steal a few ideas from them.

Thanks for responding, and your work.

Paul Miller
Owner

brunch build --target chrome

I think Rails just copies assets/images/* over to public/images. I guess a similar pattern could work, copying some_dir/images/ over to build/images/.

These are excellent ideas, I've created issues for them.

think index.html is outside the build directory.

I guess, the proper way here would be to place index.html in assets too, just as images.

Paul Miller
Owner

At this point I've implemented helper class without autoloading.

Here's an example of using helpers in twitter application:

{BrunchApplication} = require 'helpers'

class exports.Application extends BrunchApplication
  # This callback would be executed on document ready event.
  onReady: ->
    {Tweet} = require 'models/tweet'
    {TweetList} = require 'collections/tweet_list'
    {MainRouter} = require 'routers/main_router'
    {MentionsView} = require 'views/mentions_view'
    {MessagesView} = require 'views/messages_view'
    #{ProfileView} = require 'views/mine_view'
    {RetweetedView} = require 'views/retweeted_view'
    {RetweetsView} = require 'views/retweets_view'
    {TimelineView} = require 'views/timeline_view'
    {TwitterConnectorView} = require 'views/twitter_connector_view'

    # @tweets == window.app.tweets
    @tweets =
      mentions: new TweetList
      messages: new TweetList
      retweeted: new TweetList
      retweets: new TweetList
      timeline: new TweetList

    @views.mentions = new MentionsView @tweets.mentions
    @views.messages = new MessagesView @tweets.messages
    @views.retweeted = new RetweetedView @tweets.retweeted
    @views.retweets = new RetweetsView @tweets.retweets
    @views.timeline = new TimelineView @tweets.timeline

    # Attach the Twitter object to the namespace of our app
    # and then initialize the twitter connector.
    twttr.anywhere (twttr) =>
      @twttr = twttr
      @routers.main = new MainRouter
      @views.twitterConnector = new TwitterConnectorView
      Backbone.history.start()

window.app = new exports.Application

It could be explicit (though, not as verbose as today), but it has no magic and everything is transparent.

Paul Miller paulmillr closed this December 23, 2011
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Something went wrong with that request. Please try again.