One of the latest challenges that I had, was to create “Custom style sheets for each user” that user can generate to style his page. This blog post will be right about how I managed it and what “underground rocks” I faced.
Basic Setup
First of all, we need some basic test app before we can start the interesting part of this post. I did a lot of digging to manage this one, I am using Rails version 4.2.4. I have already created User model with relation to Style model which will be responsible for managing custom CSS for the user.
We will be saving all the values in the database. Our Style table should look something like this:
create_table "styles", force: :cascade do |t| t.integer "user_id", null: false t.string "background_color" t.string "block_height" t.string "name_color" t.string "name_style" t.string "name_size" t.string "text_color" t.string "text_style" t.string "text_size" t.string "email_color" t.string "email_style" t.string "email_size" t.datetime "created_at", null: false t.datetime "updated_at", null: false end
Of course, we need a user who will own this new custom stylesheet file. Our User table should look something like this:
create_table "users", force: :cascade do |t| t.string "first_name", null: false t.string "last_name", null: false t.string "email", null: false t.text "text" t.datetime "created_at", null: false t.datetime "updated_at", null: false end
For this example app, I think, it will be enough, as you can see there will few styling options we will be able to edit. After we have generated these two models user and style. We need to add a few gems to make this happen. And they would be:
gem 'carrierwave', '~> 0.10.0'
for uploading our clogo file and
gem 'mini_magick', '~> 4.3', '>= 4.3.6'
for image manipulations. When we have successfully bundled our test app, we need to generate uploader for our logos.
rails generate uploader Logo
We don’t need to make huge changes to our models. The only thing for our user model is that it can own many styles.
class User < ActiveRecord::Base has_many :styles end
and for our style model we need to specify that it belongs to a specific user. Also, we need those two carrierwave uploaders.
class Style < ActiveRecord::Base mount_uploader :logo, LogoUploader belongs_to :user end
We are almost done with our basic side of this test app. Few more changes, and we will get to the good part. Now we need to make some changes to our Uploader that we have just generated.
For our LogoUploader we need to make a few changes as well. Of course, we need to specify store_dir. We will allow the user only to upload images, so we need to specify file extensions that will be allowed with the method extension_white_list. Finally, for this uploader, we will specify two versions. First thumb will be for our form so we could see the current logo we have saved and the other normal version for the uploaded file will be for index file, so the image is nice and big.
Don't forget to include CarrierWave::MiniMagick otherwise the image resizing will not work.
class LogoUploader < CarrierWave::Uploader::Base include CarrierWave::MiniMagick storage :file def store_dir "uploads/#{model.class.to_s.underscore}/ #{mounted_as}/#{model.id}" end def extension_white_list %w(jpg jpeg gif png) end version :thumb do process :resize_to_fit => [100, 100] end version :normal do process :resize_to_fit => [300, 300] end end
And finally, for the basic setup part for this post, let's have a look at ourcontroller. For ourindexaction we don't need to add anything. Oureditaction is nothing extraordinary, just finding thestylefor the current user, so we would know what to edit. And then there is ourupdateaction, as you can see below, there is nothing strange, except for two lines where I create a new objectGenerateCustomStyle.new(@style.id)and after that I am callingcompileon it. We will get to this in the next section of this post.
class StylesController < ApplicationController def index end def edit @style ||= current_user.styles.find(params[:id]) end def update @style = current_user.styles.new(style_params) if @style.save generate_stylesheet = GenerateCustomStyle.new(@style.id) generate_stylesheet.compile redirect_to root_path else render :edit end end private def style_params params.require(:style) .permit(:background_color, :block_height, :name_color, :name_style, :name_size, :text_color, :text_style, :text_size, :email_color, :email_style, :email_size, :logo) end end
The basic part in this sample App is pretty simple. Now, when this is done, we can go to the next part of this post.
Now, the Interesting Stuff
In lib folder I have created the file generate_custom_style.rb, that’s responsible for all css file manipulations we have to make.
After we have successfully saved the new params for our custom stylesheet we are calling on this class. The only thing we need to pass to make it work is style_id, so we can look up in the database what our users have saved.
We need to initialize several things:
attr_reader :custom_style, :body, :env, :filename, :scss_file def initialize(style_id) @custom_style = Style.find(style_id) @filename = "#{custom_style.user_id}_#{custom_style.updated_at.to_i}" @scss_file = File.new(scss_file_path, 'w') @body = ERB.new(File.read(template_file_path)).result(binding) @env = Rails.application.assets end
As you can see, we need to initialize five things before calling the compile method.
1) We need to find that User Style we just saved to the database. That is as simple as it gets Style.find(style_id).
2) We need some kind of filename. I am using user_id + timestamp. That timestamp is not necessary it would work as expected without it, but there might be a possibility we would need to save multiple user styles, so the user could switch between them.
3) We need to create scss file, as you can see we are calling method scss_file_path. Let’s see what that method is doing for us:
def scss_file_path @scss_file_path ||= File.join(scss_tmpfile_path, "#{filename}.scss") end
We are just creating a path to our temp scss file. Also, we are checking if we have a folder where we will create our temp files:
def scss_tmpfile_path @scss_file_path ||= File.join(Rails.root, 'tmp', 'generate_css') unless File.exists?(@scss_file_path) FileUtils.mkdir_p(@scss_file_path) end @scss_file_path end
And this method is just checking if we have generate_css folder in our tmp folder; if not – then we are creating it.
4) We need a body that we will write to our user stylesheet file. Method template_file_path, basically, is just a path to our template file with that body:
def template_file_path @template_file_path ||= File.join(Rails.root, 'app', 'assets', 'stylesheets', '_template.scss.erb') end
And in that template file, we have to define all the variables that we need:
$background_color : <%= custom_style.background_color %>; $block_height : <%= custom_style.block_height %>; $name_color : <%= custom_style.name_color %>; $name_style : <%= custom_style.name_style %>; $name_size : <%= custom_style.name_size %>; $text_color : <%= custom_style.text_color %>; $text_style : <%= custom_style.text_style %>; $text_size : <%= custom_style.text_size %>; $email_color : <%= custom_style.email_color %>; $email_style : <%= custom_style.email_style %>; $email_size : <%= custom_style.email_size %>; <% custom_style.logo.recreate_versions! if custom_style.logo.present? %> $logo_url : '<%= custom_style.logo.normal.url %>'; .logo-placeholder{ @if $logo_url !='' { background-image: url($logo_url); } } @import "application";
5) And finally, for the initializaton, we need that Rails.application.assets.
So, now we can run our compile method.
def compile find_or_create_scss begin scss_file.write generate_css scss_file.flush custom_style.update(css: scss_file) ensure scss_file.close File.delete(scss_file) end end
When this is called the first thing what is happening is that we are finding or creating our scss file.
def create_scss File.open(scss_file_path, 'w') { |f| f.write(body) } end
and writing that body to it.
Next, we need to generate our css file scss_file.write generate_css, so we can actually change the layout for each user separately.
def generate_css Sass::Engine.new(asset_source, { syntax: :scss, cache: false, read_cache: false, style: :compressed }).render end
At first, we need to find that file in the asset pipeline cache, otherwise, we will not be able to compile it.
def asset_source if env.find_asset(filename) env.find_asset(filename).source else uri = Sprockets::URIUtils.build_asset_uri(scss_file.path, type: "text/css") asset = Sprockets::UnloadedAsset.new(uri, env) env.load(asset.uri).source end end
Let’s dig more into this one. Locally we would be able to complete everything just using
env.find_asset(filename).source
and that would be it: our css file would compile without any problems. The problem started when I deployed my code to the server. Rails are caching assets, it’s completely normal, but that caused my problems. How to solve this?
uri = Sprockets::URIUtils.build_asset_uri(scss_file.path, type: "text/css") asset = Sprockets::UnloadedAsset.new(uri, env) env.load(asset.uri).source
The thing was that our ‘scss’ file was not found in env and that’s because of Rails cached assets. Basically, what we are doing here is creating that path to file ourselves.
Now we just need to finish what we have started.
To make our file appear immediately we need to ‘flush’ the file.
scss_file.flush
save this file to our model
custom_style.update(css: scss_file)
and finally we need to close and delete our temp scss file
scss_file.close File.delete(scss_file)
Now we should see the changes immediately after we will be redirected to the index action and it should also work in production mode without any problems.
END
Usually, I don’t work with the frontend, I prefer backend. But this was really interesting, at least for me, to make it work as it should. I hope this will make someone’s life easier.
You can check the full test app in Git.
Also, there is a small gem that Me and my Colleague are working on regarding this functionality, it’s still in progress, but I hope it will be finished soon.
More information about our Ruby developers you may find on our company’s web.