Best Way to Handle Dynamic CSS in a Rails App

How do I create dynamic CSS in Rails?

Currently there is a lot of options to generate dynamic css in rails.

You can use less css - is an extension to CSS with extra features.

Gem Less css for rails provides integration for Rails projects using the Less stylesheet language in the asset pipeline.

If you are using twitter bootstrap you may check this out less rails bootstrap.

Also you can use one more CSS extension language Sass for generating CSS. Here is a Saas rails gem.

Check out Dynamic CSS in Rails and Render Rails assets to string blog posts and article about Asset Pipeline

Related SO questions:

  • Best way to handle dynamic css in a rails app
  • Dynamic CSS in Rails asset pipeline, compile on fly
  • Rails: change CSS property dynamically?

How to use 'dynamic CSS' in Rails

Seems like an inline style would work fine here. In your ERB, in your student divs, just do:

<% @students.each do |student| %>
<div style="height: <%= student.height %>px; width: <%= student.width %>px;">
<!-- other student stuff -->
</div>
<% end %>

It's either this or generating a unique CSS class for every student first, then using it in each div.

ruby rails dynamic css class

The above scenario can be handled the following way:

<% positionRight||= false %>
<section class=<%= positionRight ? "custom-css-class" : "" %>>

and then

<%= render 'headers', positionRight: true, ...}  %>
OR
<%= render 'headers', ...} %>

Dynamic CSS in Rails asset pipeline, compile on fly

Here is the solution I finally landed on:

  • I ended up switching over to bootstrap-sass instead https://github.com/thomas-mcdonald/bootstrap-sass
  • Made the following changes to my application.rb file to ensure that the :asset group is always included despite the environment:

    if defined?(Bundler)
    # If you precompile assets before deploying to production, use this line
    # Bundler.require(*Rails.groups(:assets => %w(development test)))
    # If you want your assets lazily compiled in production, use this line
    Bundler.require(:default, :assets, Rails.env)
    end
  • Used the concepts provided by Manuel Meure (Thank you Manuel!) of Kraut Computing found at http://www.krautcomputing.com/blog/2012/03/27/how-to-compile-custom-sass-stylesheets-dynamically-during-runtime/ .

    • I made some adjustments to suit my own needs, but the core concepts illustrated by Manuel were the foundation for my compilation process.
  • In my model (lets call it "Site"), I have a snippet of code that looks like this:

    # .../app/models/site.rb
    ...

    BASE_STYLE = "
    @import \"compass/css3\";

    <ADDITIONAL_STYLES>

    @import \"bootstrap\";
    @import \"bootstrap-responsive\";
    ".freeze

    # Provides the SASS/CSS content that would
    # be included into your base SASS content before compilation
    def sass_content
    "
    $bodyBackground: #{self.body_background};
    $textColor: #{self.text_color};
    " + self.css # Any additional CSS/SASS you would want to add
    end

    def compile_css(test_only = false, force_recompile = false)

    # SassCompiler is a modification of the information made available at the Kraut Computing link
    compiler = SassCompiler.new("#{self.id}/site.css", {:syntax => :scss, :output_dir => Rails.root.join('app', 'assets', 'sites')})

    # Bail if we're already compiled and we're not forcing recompile
    return if compiler.compiled? && !force_recompile && !test_only

    # The block here yields the content that will be rendered
    compiler.compile(test_only) {
    # take our base styles, slap in there some vars that we make available to be customized by the user
    # and then finally add in our css/scss that the user updated... concat those and use it as
    # our raw sass to compile
    BASE_STYLE.gsub(/<ADDITIONAL_STYLES>/, self.sass_content)
    }
    end

I hope this helps. I know its a deviation from the original post, but its deviated because this seemed to be the most attainable solution to the problem.

If I haven't answered a specific question you have, feel free to comment so I can expand where possible.

Thanks!

Using stylesheet_link_tag to call css in a database [Rails 4]

Don't worry about caching right now. Just see if it renders at all:

class Admin::StylesheetsController < ApplicationController

respond_to :css

def show
@stylesheet = Stylesheet.find(params[:id])
render text: @stylesheet.code, content_type: "text/css"
end

end

Write a test for GET /stylesheets/:id.css. And in the markup:

<link rel="stylesheet" href="<%= stylesheet_path(@stylesheet, format: :css) %>" type="text/css" />

It seems like you're confusing the stylesheet for the forum. The stylesheet is a resource, and it is requested implicitly with the <link> tag. You put the link to the stylesheet as part of the forum html page.

Your stylesheets/:id.css path returns the css only, no markup. A test for this assuming the field in your stylesheet model is called :code would be:

describe '#show' do
let(:stylesheet) { Factory(:stylesheet) }
it 'should have the correct css' do
get :show, id: stylesheet.id
expect(response.body).to eq(stylesheet.code)
end
end

Seems like you're confused about the way Rails works with requests. I'll walk you through the timeline:

  1. Browser requests the forum page. This is handled by ForumsController#show or something like that.

  2. In a before filter or otherwise, you need to determine the id for the stylesheet somehow, for the <link> tag.

  3. Your layout application is wrapped around the page for the action, and the request ends.

  4. Browser notices it needs to request stylesheets / javascripts, one of which is your custom stylesheet.

  5. Your StylesheetsController#show is called, and the stylesheet is rendered back. This and all the other assets requests are completed.

  6. Now the user with the browser can see the custom css.

So you see, the @stylesheet is not set when the page is rendered, and istance variables are set per controller per request. Not globally. Setting @stylesheet in StylesheetsController#show only sets it for the route GET /stylesheets/:id.css.

You might want to consider using sublayouts - http://www.ajostrow.me/thoughts/sublayouts-with-rails-revised

Dynamic CSS using Sprockets

After a long day I was able to solve my problem thanks to John Feminella and his post on google. The challenging part for me was figuring out how to create a new Sprockets::Context. Luckily John's solution doesn't require a Context.

Updated gist here

Attempt #1

This code does not allow importing css files from the asset pipeline.

@import "foundation"; works because foundation is loaded as a compass module

@import "custom_css"; results in an error message saying the file could not be found

def call(template)
erb = ActionView::Template.registered_template_handler(:erb).call(template)

%{
options = Compass.configuration.to_sass_engine_options.merge(
:syntax => :scss,
:custom => {:resolver => ::Sass::Rails::Resolver.new(CompassRails.context)},
)
Sass::Engine.new((begin;#{erb};end), options).render
}
end

Attempt #2

This code fails to embed base64 urls using asset-data-url

  def call(template)
erb = ActionView::Template.registered_template_handler(:erb).call(template)

%{
compiler = Compass::Compiler.new *Compass.configuration.to_compiler_arguments
options = compiler.options.merge({
:syntax => :scss,
:custom => {:resolver => ::Sass::Rails::Resolver.new(CompassRails.context)},
})
Sass::Engine.new((begin;#{erb};end), options).render
}
end

Attempt 3

Turns out you can use empty values while creating the context. Code below works in development but throws an error in production.

ActionView::Template::Error (can't modify immutable index)

It appears the error occurs in Sprockets::Index which is used instead of Sprockets::Environment in production. Switching to Sprockets::Environment doesn't solve the problem either.

  def call(template)
erb = ActionView::Template.registered_template_handler(:erb).call(template)

%{
context = CompassRails.context.new(::Rails.application.assets, '', Pathname.new(''))
resolver = ::Sass::Rails::Resolver.new(context)

compiler = Compass::Compiler.new *Compass.configuration.to_compiler_arguments
options = compiler.options.merge({
:syntax => :scss,
:custom => {:resolver => resolver}
})

Sass::Engine.new((begin;#{erb};end), options).render
}
end

Generating dynamic CSS using templates

To answer your question: when erb is compiled, it only outputs ruby code contained in a <%= code %> wrapper. Your current code is probably running fine (I'm not familiar with Tilt or straight SASS), but you aren't telling it to output the result to the sass file. Change the first line of master.css.sass.erb from <% to <%= and then you can debug from there.

That being said, I would recommend against this process. You're going to be wasting resources compiling the stylesheet every time it is called.

An alternate method would be to just keep your stylesheets static as in your opening example so they can be precompiled and cacheable, and then create a partial like layouts/_conditional_styles.html.erb using stock html and css like:

<% unless @your_sanitized_style_object.nil? %>
<style>
body{
background: <%= @your_sanitized_style_object.background_color.html_safe %> !important;
}
</style>
<% end %>

Which could override the application stylesheet by being rendered after the application css file in your layouts/application.html.erb file such as:

<%= stylesheet_link_tag "application" %>
<%= render "layouts/conditional_styles" %>

To answer your question as to why you use less resources using precompiled assets and then overriding them, consider your example test.css.sass.erb

$color: <%= color %>
body
background:$color !imporant

Assuming the color variable is red, the process would go something like this... First your application will run through it with its erb compiler and give you a test.css.sass file such as:

$color: #ff0000
body
background:$color !important

Then your application will run through the code again with its sass compiler to give you a test.css file such as:

body{ background:#ff0000 !important; }

And after all of that, it will serve up the file. In your development environment you will not see that much of a performance difference as your application defaults to rebuilding the assets for every request. The difference comes when it's time to serve your application to the web in production.

If your assets aren't dependent on variables in the application, the stylesheets can be precompiled. What this means is that your application compiles the asset once, and after it compiles the asset you get a stylesheet like test-f25ab2b1286feb7cc98375sac732f7d0.css.

The stylesheets will be identical but what is unique is all that jargon that rails throws on the end of the file name when it precompiles something. That jargon is known as a fingerprint and its purpose is to tell the server on an incoming request whether or not there is a newer version of the file. If the file hasn't been modified, and the system making the request has already downloaded that file to its cache, then your browser will use the cached version instead of downloading the stylesheet over again.

Now lets say your test.css.sass.erb file is something huge like 50kB for example's sake.

  • Without precompilation your resource cost is:

    1. having to traverse that 50kB file 2 times on every request to compile it from erb > sass > css

    2. having to serve up that 50kB file on every request.

  • With precompiled assets that are overridden by conditional styles embedded in the html of the layout as explained in my first answer:

    1. Your compilation cost goes down to almost zero because the stylesheet is precompiled so there's nothing to do to it and your whatever.html.erb template that contains the conditional styles is already being compiled by your erb compiler so you're just adding the work of rendering variables.

    2. You only serve up the precompiled stylesheet once meaning you save ~50kB in bandwidth on every request, using only the bytes needed for the rendered dependent styles.

Hope this helps, for more information on how all of this works I would recommend starting with the Asset Pipeline Rails Guide.

Dynamic css - wrong content type

The problem seems to be in the requested url. <%= get_css_path %> will return /get_css and hence the controller action with handle this request by the format.html part. So you can try to append the format with the url as:

<link href="<%= get_css_path(:format => :css) %>" media="screen" rel="stylesheet" type="text/css" />

This will request /get_css.css and hence the action will handle with format.css part.



Related Topics



Leave a reply



Submit