= sinatra_more == Preface This gem has been designed to work with Sinatra (http://www.sinatrarb.com). Sinatra is a DSL for quickly creating web applications in Ruby with minimal effort. The canonical example of how to create an entire simple web application with Sinatra is something like: # myapp.rb require 'rubygems' require 'sinatra' get '/' do 'Hello world!' end and then to run the application: $ ruby myapp.rb The extreme simplicity of the framework is quite refreshing. I have been using Sinatra a great deal for recent projects. First for small and simple json and xml web services and then even for more complex full-featured applications. This gem represents my attempt to make it as fun and easy as possible to code increasingly advanced view-heavy web applications in Sinatra. == Introduction Note: This library is still experimental and may not be ready for production just yet. That being said the gem is being actively used on a number of sinatra projects. In addition, the gem has fairly solid test coverage ensuring that everything works as expected. This will be a plugin which expands sinatra's capabilities in a variety of ways. Note that all extensions have been created to work with haml, erb, and erubis. This gem is intended to be template-agnostic in providing helpers wherever possible. Let me expand briefly on what I want to accomplish with this gem. I love sinatra but if I want to use it for any non-trivial application I very quickly miss a lot of the extra tools provided by rails. Now the obvious question is "Why not just use rails then?" Well, in many cases that might be the right decision. Still, at least until version 3 comes along, Rails is quite a large framework with a 'take it or leave it' attitude. Personally, I have come to love the spirit of sinatra which acts as a thin wrapper on top of rack often allowing middleware to do most of the work and pulling in additional complexity only as required. My goal with this extension is to match the spirit of Sinatra and at the same time create a standard library of tools, helpers and components that will make Sinatra suitable for more complex applications. Here is a small list of what sinatra_more contains: * Code generators for creating new sinatra applications (using sinatra_gen on command line) * Generic view and tag helpers (tag, content_tag, input_tag, ...) * Asset tag helpers (link_to, image_tag, javascript_include_tag, ...) * Full form helpers and builders support (form_tag, form_for, field_set_tag, text_field, ...) * Full url named route support to avoid hardcoding url paths in sinatra (map, url_for) * Generally useful formatting extensions (relative_time_ago, js_escape_html, sanitize_html) * Simple 'mailer' support for sinatra (akin to ActionMailer but simpler and powered by pony) * Plug and play setup for the excellent Warden authentication system Keep in mind, the user will be able to pull in these components seperately and leave out those that are not required. Please help me brainstorm and fork the project if you have any ideas to contribute. == Installation If you want to use the WardenPlugin component, then the 'warden' gem would need to be installed. To install sinatra_more, simply grab the latest version from gemcutter: $ sudo gem install sinatra_more --source http://gemcutter.org Now you are ready to use this gem in your sinatra project. == Usage This extension can be easily registered into any existing sinatra application. You can require different components based on which pieces are useful for your particular application. # app.rb require 'sinatra/base' require 'sinatra_more' # or require 'sinatra_more/markup_plugin' for precise inclusion class Application < Sinatra::Base register SinatraMore::MarkupPlugin register SinatraMore::RenderPlugin register SinatraMore::WardenPlugin register SinatraMore::MailerPlugin register SinatraMore::RoutingPlugin end This will then allow you to use the components that have been registered. A breakdown of components is below: === MarkupPlugin This component provides a great deal of view helpers related to html markup generation. There are helpers for generating tags, forms, links, images, and more. Most of the basic methods should be very familiar to anyone who has used rails view helpers. ==== Output Helpers * content_for(key, &block) * Capture a block of content to be rendered at a later time. * content_for(:head) { ...content... } * Also supports arguments passed to the content block * content_for(:head) { |param1, param2| ...content... } * yield_content(key, *args) * Render the captured content blocks for a given key. * yield_content :head * Also supports arguments yielded to the content block * yield_content :head, param1, param2 * capture_html(*args, &block) * Captures the html from a block of template code for erb or haml * capture_html(&block) => "...html..." * concat_content(text="") * Outputs the given text to the templates buffer directly in erb or haml * concat_content("This will be output to the template buffer in erb or haml") ==== Tag Helpers * tag(name, options={}) * Creates an html tag with the given name and options * tag(:br, :style => 'clear:both') =>
* tag(:p, :content => "demo", :class => 'large') =>

demo

* content_tag(name, content, options={}) * Creates an html tag with given name, content and options * content_tag(:p, "demo", :class => 'light') =>

demo

* content_tag(:p, :class => 'dark') { ...content... } =>

...content...

* input_tag(type, options = {}) * Creates an html input field with given type and options * input_tag :text, :class => "demo" * input_tag :password, :value => "secret", :class => "demo" ==== Asset Helpers * flash_tag(kind, options={}) * Creates a div to display the flash of given type if it exists * flash_tag(:notice, :class => 'flash', :id => 'flash-notice') * link_to(*args, &block) * Creates a link element with given name, url and options * link_to 'click me', '/dashboard', :class => 'linky' * link_to('/dashboard', :class => 'blocky') { ...content... } * mail_to(email, caption=nil, mail_options={}) * Creates a mailto link tag to the specified email_address * mail_to "me@demo.com" * mail_to "me@demo.com", "My Email", :subject => "Feedback", :cc => 'test@demo.com' * image_tag(url, options={}) * Creates an image element with given url and options * image_tag('icons/avatar.png') * stylesheet_link_tag(*sources) * Returns a stylesheet link tag for the sources specified as arguments * stylesheet_link_tag 'style', 'application', 'layout' * javascript_include_tag(*sources) * Returns an html script tag for each of the sources provided. * javascript_include_tag 'application', 'special' ==== Form Helpers * form_tag(url, options={}, &block) * Constructs a form without object based on options * Supports form methods 'put' and 'delete' through hidden field * form_tag('/register', :class => 'example') { ... } * field_set_tag(*args, &block) * Constructs a field_set to group fields with given options * field_set_tag(:class => 'office-set') { } * field_set_tag("Office", :class => 'office-set') { } * error_messages_for(record, options={}) * Constructs list html for the errors for a given object * error_messages_for @user * label_tag(name, options={}, &block) * Constructs a label tag from the given options * label_tag :username, :class => 'long-label' * label_tag(:username, :class => 'blocked-label') { ... } * hidden_field_tag(name, options={}) * Constructs a hidden field input from the given options * hidden_field_tag :session_key, :value => 'secret' * text_field_tag(name, options={}) * Constructs a text field input from the given options * text_field_tag :username, :class => 'long' * text_area_tag(name, options={}) * Constructs a text area input from the given options * text_area_tag :username, :class => 'long' * password_field_tag(name, options={}) * Constructs a password field input from the given options * password_field_tag :password, :class => 'long' * check_box_tag(name, options={}) * Constructs a checkbox input from the given options * check_box_tag :remember_me, :checked => true * radio_button_tag(name, options={}) * Constructs a radio button input from the given options * radio_button_tag :gender, :value => 'male' * select_tag(name, settings={}) * Constructs a select tag with options from the given settings * select_tag(:favorite_color, :options => ['1', '2', '3'], :selected => '1') * select_tag(:more_color, :options => [['label', '1'], ['label2', '2']]) * select_tag(:multiple_color, :options => [...], :multiple => true) * file_field_tag(name, options={}) * Constructs a file field input from the given options * file_field_tag :photo, :class => 'long' * submit_tag(caption, options={}) * Constructs a submit button from the given options * submit_tag "Create", :class => 'success' * button_tag(caption, options={}) * Constructs an input (type => 'button') from the given options * button_tag "Cancel", :class => 'clear' * image_submit_tag(source, options={}) * Constructs an image submit button from the given options * image_submit_tag "submit.png", :class => 'success' A form_tag might look like: - form_tag '/destroy', :class => 'destroy-form', :method => 'delete' do = flash_tag(:notice) - field_set_tag do %p = label_tag :username, :class => 'first' = text_field_tag :username, :value => params[:username] %p = label_tag :password, :class => 'first' = password_field_tag :password, :value => params[:password] %p = label_tag :strategy = select_tag :strategy, :options => ['delete', 'destroy'], :selected => 'delete' %p = check_box_tag :confirm_delete - field_set_tag(:class => 'buttons') do = submit_tag "Remove" ==== FormBuilders * form_for(object, url, settings={}, &block) * Constructs a form using given or default form_builder * Supports form methods 'put' and 'delete' through hidden field * Defaults to StandardFormBuilder but you can easily create your own! * form_for(@user, '/register', :id => 'register') { |f| ...field-elements... } * form_for(:user, '/register', :id => 'register') { |f| ...field-elements... } * fields_for(object, settings={}, &block) * Constructs fields for a given object for use in an existing form * Defaults to StandardFormBuilder but you can easily create your own! * fields_for @user.assignment do |assignment| ... end * fields_for :assignment do |assigment| ... end The following are fields provided by AbstractFormBuilder that can be used within a form_for or fields_for: * error_messages(options={}) * Displays list html for the errors on form object * f.errors_messages * label(field, options={}) * f.label :name, :class => 'long' * text_field(field, options={}) * f.text_field :username, :class => 'long' * check_box(field, options={}) * Uses hidden field to provide a 'unchecked' value for field * f.check_box :remember_me, :uncheck_value => 'false' * radio_button(field, options={}) * f.radio_button :gender, :value => 'male' * hidden_field(field, options={}) * f.hidden_field :session_id, :class => 'hidden' * text_area(field, options={}) * f.text_area :summary, :class => 'long' * password_field(field, options={}) * f.password_field :secret, :class => 'long' * file_field(field, options={}) * f.file_field :photo, :class => 'long' * select(field, options={}) * f.select(:state, :options => ['California', 'Texas', 'Wyoming']) * f.select(:state, :collection => @states, :fields => [:name, :id]) * f.select(:state, :options => [...], :include_blank => true) * submit(caption, options={}) * f.submit "Update", :class => 'long' * image_submit(source, options={}) * f.image_submit "submit.png", :class => 'long' A form_for using these basic fields might look like: - form_for @user, '/register', :id => 'register' do |f| = f.error_messages %p = f.label :username, :caption => "Nickname" = f.text_field :username %p = f.label :email = f.text_field :email %p = f.label :password = f.password_field :password %p = f.label :is_admin, :caption => "Admin User?" = f.check_box :is_admin %p = f.label :color, :caption => "Favorite Color?" = f.select :color, :options => ['red', 'black'] %p - fields_for @user.location do |location| = location.text_field :street = location.text_field :city %p = f.submit "Create", :class => 'button' There is also a StandardFormBuilder which builds on the abstract fields that can be used within a form_for: * text_field_block(field, options={}, label_options={}) * text_field_block(:nickname, :class => 'big', :caption => "Username") * text_area_block(field, options={}, label_options={}) * text_area_block(:about, :class => 'big') * password_field_block(field, options={}, label_options={}) * password_field_block(:code, :class => 'big') * file_field_block(field, options={}, label_options={}) * file_field_block(:photo, :class => 'big') * check_box_block(field, options={}, label_options={}) * check_box_block(:remember_me, :class => 'big') * select_block(field, options={}, label_options={}) * select_block(:country, :option => ['USA', 'Canada']) * submit_block(caption, options={}) * submit_block(:username, :class => 'big') * image_submit_block(source, options={}) * image_submit_block('submit.png', :class => 'big') A form_for using these standard fields might look like: - form_for @user, '/register', :id => 'register' do |f| = f.error_messages = f.text_field_block :name, :caption => "Full name" = f.text_field_block :email = f.check_box_block :remember_me = f.select_block :fav_color, :options => ['red', 'blue'] = f.password_field_block :password = f.submit_block "Create", :class => 'button' and would generate this html:

...omitted...

You can also easily build your own FormBuilder which allows for customized fields: class MyCustomFormBuilder < AbstractFormBuilder # Here we have access to a number of useful variables # # * template (use this to invoke any helpers)(ex. template.hidden_field_tag(...)) # * object (the record for this form) (ex. object.valid?) # * object_name (object's underscored type) (ex. object_name => 'admin_user') # # We also have access to self.field_types => [:text_field, :text_area, ...] # In addition, we have access to all the existing field tag helpers (text_field, hidden_field, file_field, ...) end Once a custom builder is defined, any call to form_for can use the new builder: - form_for @user, '/register', :builder => 'MyCustomFormBuilder', :id => 'register' do |f| ...fields here... The form builder can even be made into the default builder when form_for is invoked: # anywhere in the sinatra application set :default_builder, 'MyCustomFormBuilder' And there you have it, a fairly complete form builder solution for sinatra. I hope to create or merge in an even better 'default' form_builder in the near future. ==== Format Helpers * escape_html (alias h and h!) * (from RackUtils) Escape ampersands, brackets and quotes to their HTML/XML entities. * relative_time_ago(date) * Returns relative time in words referencing the given date * relative_time_ago(2.days.ago) => "2 days" * relative_time_ago(5.minutes.ago) => "5 minutes" * relative_time_ago(2800.days.ago) => "over 7 years" * time_in_words(date) * Returns relative time in the past or future using appropriate date format * time_in_words(2.days.ago) => "2 days ago" * time_in_words(100.days.ago) => "Tuesday, July 21" * time_in_words(1.day.from_now) => "tomorrow" * escape_javascript(html_content) * Escapes html to allow passing information to javascript. Used for passing data inside an ajax .js.erb template * escape_javascript("

Hey

")
See the wiki article for additional information: === RenderPlugin This component provides a number of rendering helpers for sinatra, making the process of displaying templates far smoother. This plugin also has support for useful additions such as partials (with support for :collection) into the templating system. * erb_template(template_path, options={}) * Renders a erb template based on the given path * erb_template 'users/new' * haml_template(template_path, options={}) * Renders a haml template based on the given path * haml_template 'users/new' * render_template(template_path, options={}) * Renders the first detected template based on the given path * render_template 'users/new' * render_template 'users/index', :template_engine => 'haml' * partial(template, *args) * Renders the html related to the partial template for object or collection * partial 'photo/_item', :object => @photo, :locals => { :foo => 'bar' } * partial 'photo/_item', :collection => @photos Using render plugin helpers is extremely simple. If you want to render an erb template in your view path: erb_template 'path/to/my/template' or using haml templates works just as well: haml_template 'path/to/haml/template' There is also a method which renders the first view matching the path and removes the need to define an engine: render_template 'path/to/any/template' It is worth noting these are mostly for convenience. When I had more complex view files in sinatra, I got tired of writing: haml :"the/path/to/file" erb "/path/to/file".to_sym Finally, we have the all-important partials support for rendering mini-templates onto a page: partial 'photo/_item', :object => @photo, :locals => { :foo => 'bar' } partial 'photo/_item', :collection => @photos This works as you would expect and also supports the collection counter inside the partial item_counter # /views/photo/_item.haml # Access to collection counter with _counter i.e item_counter # Access the object with the partial_name i.e item And that concludes the render plugin for now but I am open to adding more methods as time progresses. See the wiki article for additional information: === WardenPlugin This component provides out-of-the-box support for Warden authentication. With this plugin registered, warden will be automatically required, configured and helpers will be provided to make interacting with warden dead simple. * current_user * Returns the current authenticated user * authenticate_user! * Login the user through the default warden strategies * logout_user! * Signs out the user from the session * logged_in? * Returns true if the user has been authenticated * authenticated?(&block) * If a block is given, only yields the content if the user is authenticated * authenticated? { puts "I am authenticated!" } * must_be_authorized!(failure_path=nil) * Forces a user to return to a fail path unless they are authorized * Used to require a user be authenticated before routing to an action * must_be_authorized!('/login') There are a few configuration options and details you need to be aware of. By default, the WardenPlugin assumes you have a User class which represents the authenticating class type. If your user class has a different name then you need to specify that as follows: SinatraMore::WardenPlugin::PasswordStrategy.user_class = CustomUser In addition, the strategy used expects that you have an authenticate method with the specific signature below: class CustomUser # ... # Returns user record if user and password match; otherwise return false def authenticate(username, password) user = User.find(username) user.has_password?(password) ? user : false end # ... end Using this plugin you also do need to define your own routes for managing warden sessions. An example is below: get '/login/?' do haml_template 'session/login' end post '/login/?' do authenticate_user! redirect "/dashboard" end post '/unauthenticated/?' do flash[:notice] = "That username and password are not correct!" status 401 haml_template 'session/login' end get '/logout/?' do logout_user! redirect '/session/login' end I was made aware of other sinatra/warden plugins which work very similarly to the system I outline for this plugin. Most notably is the sinatra_warden plugin by Justin Smestad (http://github.com/jsmestad/sinatra_warden) Eventually I plan to vendor that gem or just remove support for this piece of my plugin completely. If anybody has any thoughts on this or the warden integration in general, please let me know. Nonetheless, there is a certain convenience for me having access to a plug-and-play warden solution directly from this gem. See the wiki article for additional information: === MailerPlugin This component uses an enhanced version of the excellent pony library (vendored) for a powerful but simple mailer system within Sinatra. There is full support for using an html content type as well as for file attachments. The MailerPlugin has many similarities to ActionMailer but is much lighterweight and (arguably) easier to use. Let's take a look at using the MailerPlugin in an application. By default, MailerPlugin uses the built-in sendmail functionality on the server. However, smtp is also supported using the following configuration: SinatraMore::MailerBase.smtp_settings = { :host => 'smtp.gmail.com', :port => '587', :tls => true, :user => 'user', :pass => 'pass', :auth => :plain } Once those have been defined, the default will become smtp delivery unless overwritten in an individual mail definition. Next, we should define a custom mailer extended from SinatraMore::MailerBase. # app/mailers/sample_mailer.rb class SampleMailer < SinatraMore::MailerBase def registration_email(name, user_email_address) from 'admin@site.com' to user_email_address subject 'Welcome to the site!' body :name => name type 'html' # optional, defaults to plain/text charset 'windows-1252' # optional, defaults to utf-8 via :sendmail # optional, to smtp if defined otherwise sendmail end end This defines a mail called 'registration_mail' with the specified attributes for delivery. The body method is passing the name attribute to the body message template which should be defined in [views_path]/sample_mailer/registration_email.erb as shown below: # ./views/sample_mailer/registration_email.erb This is the body of the email and can access the <%= name %> that was passed in from the mailer definition That's all there is to defining the body of the email which can be plain text or html Once the mailer definition has been completed and the template has been defined, the email can be sent using: SampleMailer.deliver(:registration_email, "Bob", "bob@bobby.com") or if you like the method_missing approach: SampleMailer.deliver_registration_email "Bob", 'bob@bobby.com' And that will then deliver the email according the the configured options. This is really all you need to send emails. A few variations are shown below for completeness. If we want to attach files to our email: # app/mailers/sample_mailer.rb class SampleMailer < SinatraMore::MailerBase def attachment_email(name, user_email_address) from 'admin@site.com' to user_email_address # ... attachments { "foo.zip" => File.read("path/to/foo.zip"), "file.txt" => "this is a text file!" } end end or perhaps we want to have a short body without the need for a template file: # app/mailers/sample_mailer.rb class SampleMailer < SinatraMore::MailerBase def short_email(name, user_email_address) from 'admin@site.com' to user_email_address subject 'Welcome to the site!' body "This is a short body defined right in the mailer itself" end end See the wiki article for additional information: === RoutingPlugin This component provides Sinatra with an enhanced url routing system which enables named route aliases to be defined and used throughout your application to refer to urls. The benefits of this is that instead of having to hard-code route urls into every area of your application, now we can just define the urls in a single spot and then attach an alias which can be used to refer to the url throughout the rest. Let's take a look at how to define named route mappings: # /app/routes/example.rb require 'sinatra_more' class RoutingDemo < Sinatra::Application register SinatraMore::RoutingPlugin # Define the named route mappings map(:account).to("/the/accounts/:name/and/:id") map(:accounts).to("/the/accounts/index") # Configure the routes using the named alias get(:account) { "name: params[:name] - id: params[:id]" } get(:accounts) { "I am the body for the url /the/accounts/index" } end Notice we simply create a route alias using the map function and then pass in the corresponding url into the to method. You can then define the routes using the named symbol representing the url. The route aliases can be accessed using url_for url_for(:accounts) url_for(:account, :id => 1, :name => 'first') You can also refer to the url in views using url_for # /app/views/index.erb

Go to the <%= link_to 'accounts dashboard', url_for(:accounts) %> to view your accounts

Go to account for <%= link_to 'first account', url_for(:account, :id => 1, :name => 'first') %> Simply invoking url_for(name, *parameters) will return the full mapped url for use in links or anywhere else that the url might be required. The routing system also supports url route configuration namespaces: # /app/routes/example.rb map(:admin, :show).to("/admin/:id/show") namespace :admin do get :show do "admin show for #{params[:id]}" end end You could also define the route aliases themselves using a namespace for convenience: # /app/routes/example.rb map :admin do |namespace| namespace.map(:show).to("/admin/:id/show") namespace.map(:destroy).to("/admin/:id/destroy") end namespace :admin do get :show do "admin show for #{params[:id]}" end get :destroy do "admin destroy for #{params[:id]}" end end You can then reference the urls using the same url_for method: <%= link_to 'admin page', url_for(:admin, :show, :id => 25) %> <%= link_to 'admin page', url_for(:admin, :update, :id => 25) %> <%= link_to 'admin page', url_for(:admin, :show, :id => 25) %> You can freely use both named route aliases and traditional Sinatra routes in the same application without conflict. See the wiki article for additional information: == Sinatra Generators In addition to the support provided above, sinatra_more also comes preloaded with flexible code generators for Sinatra powered in part by the excellent Thor gem (incidentally also used in the Rails 3 generators). These generators are intended to allow for easy code generation both in creating new applications and building on existing ones. The generators have been built to be as library-agnostic as possible, supporting a myriad of test frameworks, js libraries, mocking libraries, etc. The usage for the generator is quite simple: $ sinatra_gen -- The simplest possible command to generate a base application would be: $ sinatra_gen demo_app . This would construct a Sinatra application DemoApp (which extends from Sinatra::Application) inside the folder 'demo_app' at our current path. Inside the application there would be configuration and setup performed for the default components. You can also define specific components to be used: $ sinatra_gen demo_app . --test=rspec --renderer=haml --mock=rr --script=jquery --orm datamapper There is also support for aliases for each component within the command: $ sinatra_gen demo_app . -t=rspec -r=haml -m=rr -s=jquery -d=datamapper The available components and their default options are listed below: * test: bacon (default), shoulda, rspec, testspec, riot * renderer: erb (default), haml * mock: mocha (default), rr * script: jquery (default), prototype, rightjs * orm: datamapper (default), mongomapper, activerecord, sequel The generator uses the bundler gem to resolve any application dependencies when the application is newly created. The necessary bundler command can be executed automatically through the generator with $ sinatra_gen demo_app . --run_bundler # alias -b or this can be done manually through executing command gem bundle in the terminal at the root of the generated application. If not executed manually, the bundling will be performed automatically the first time the application attempts to boot. Note that this command only has to be performed when the application is first generated or when the Gemfile is modified. The generator framework within sinatra_more is extensible and additional components and tools can be added easily. This would be achieved through forking our project and reading through the code in /generators/sinatra_generator.rb and the setup instructions inside the relevant files within /generators/components/. We are happy to accept pull requests for additional component types not originally included (although helping us maintain them would also be appreciated). We are also planning to add generator actions such as: * Model generation (working for any of the available orms listed) * Routes generation * Mailer generation * Migrations generation See the wiki article for additional information: == Acknowledgements * Thanks to kelredd (http://github.com/kelredd) for the sinatra_helpers code that helped me to create erb capture and concat methods. * Thanks to sbfaulkner for the sinatra-helpers code that I browsed through many times while starting this library. * Thanks to vestel for the excellent modified pony fork which in part powers the mailer_plugin (http://github.com/vestel/pony) * Thanks to focat and sinatra-content-for library (http://github.com/focat/sinatra-content-for) for a good content_for starting point * Thanks to bcarlso for the snap sinatra library (http://github.com/bcarlso/snap) which was a starting point for named routes * Thanks to wycats and others for the awesome Thor gem which made creating the sinatra generator relatively painless * Thanks to wycats and others for the bundler gem which made bundling the required gems for an application easy == Contributors * Nathan Esquenazi - Project creator and code maintainer * Arthur Chiu - Forming the idea and various code contributions == Known Issues * No serious issues known == Links and Resources * EnvyCasts - * Ruby5 - == Note on Patches/Pull Requests * Fork the project. * Make your feature addition or bug fix. * Add tests for it. This is important so I don't break it in a future version unintentionally. * Commit, do not mess with rakefile, version, or history. * Send me a pull request. Bonus points for topic branches. == Copyright Copyright (c) 2009 Nathan Esquenazi. See LICENSE for details.