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 ourindex
action we don't need to add anything. Ouredit
action is nothing extraordinary, just finding thestyle
for the current user, so we would know what to edit. And then there is ourupdate
action, 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 callingcompile
on 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.