Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

PikachuEXE
PikachuEXE

Posted on

     

Cells - Introduction

Background

GitHub has recently posted an article aboutview_component:
https://github.blog/2020-12-15-encapsulating-ruby-on-rails-views/
Before it gets too popular I think I should share my experience withcells

So that developers can have another chance to re-think and pick what to use for "encapsulated view components".

Scope of this post

(1) Only"concept cells", notview_component style cells.
The "default"/view_component naming style makes it difficult to just copy & rename a folder to bootstrap a new view component

(2) File Structure, Inputs, Rendering, View Inheritance & Caching
Testing - I am too lazy to do it (rarely beneficial for me)
Layout - Never used it.

File Structure

(Modified fromcells old README)
(Remember this is for concept cells)

app├── concepts│   ├── copyable_url_box│   │   ├── cell.rb│   │   ├── views│   │   │   ├── show.haml
Enter fullscreen modeExit fullscreen mode

File content

classCopyableUrlBox::Cell<Cell::Concept# ..end
Enter fullscreen modeExit fullscreen mode

If you want to put these files into a non-default folder likeapp/resources
You need to update theview_paths

classCopyableUrlBox::Cell<Cell::Conceptself.view_paths=Array("app/resources")+view_paths# ..end
Enter fullscreen modeExit fullscreen mode

Inputs

Theofficial doc suggests using this:

concept("comment/cell",@comment)#=> return a cell
Enter fullscreen modeExit fullscreen mode

This gives you access to an attributemodel (which is@comment) inside the cell object by default

But I prefer passing a hash so I do this:

classApplicationConceptCell<Cell::ConceptmoduleCells3HashInputBehaviourReplication# Since in cells 3.x# When a hash is passed in# Everything in hash is injected as ivars# So we want to replicate the behaviour here## This override works on cells 4.1.5definitialize(model,options={})ifmodel.is_a?(::Hash)model.each_pairdo|key,value|instance_variable_set("@#{key}",value)end# To avoid ivar `@model` being set in super with the Hash# We need to fetch value from hash and put it into `super`# Even usually it's `nil`super(model.fetch(:model){nil},options)elsesuperendendendprependCells3HashInputBehaviourReplicationend# Remember you have to inherit from the right cell!classCopyableUrlBox::Cell<ApplicationConceptCellprivate# region Inputs# Contract Something# attr_reader :input_nameContractAnd[::String,Send[:present?]]attr_reader:url# endregion Inputsend
Enter fullscreen modeExit fullscreen mode

This gives me access to input values as long as I defined them viaattr_reader.
Oh what's theContract XXX aboveattr_reader?
They are fromcontracts.ruby and completely optional and won't be explained in this post.
You can safely ignore those and maybe study that gem later.

I am also using another way to handle inputs which is to put input values intoinputs attribute via a value object.

require"contracted_value"classApplicationConceptCell2222<Cell::ConceptmoduleHashInputsAsImmutableInputsdefinitialize(model,options={})ifmodel.is_a?(::Hash)@inputs=self.class.inputs_class.new(model)# To avoid ivar `@model` being set in super with the Hash# We need to fetch value from hash and put it into `super`# Even usually it's `nil`super(model.fetch(:model){nil},options)elsesuperendendprivateattr_reader:inputsendprependHashInputsAsImmutableInputsclass_attribute(:inputs_class,instance_reader:false,instance_writer:false,)class<<selfdefdefine_inputs(&block)new_inputs_class=Class.new(::ContractedValue::Value)new_inputs_class.include(::Contracts::Core)new_inputs_class.include(::Contracts::Builtin)new_inputs_class.class_eval(&block)self.inputs_class=new_inputs_classendendend# Remember you have to inherit from the right cell!classCopyableUrlBox2222::Cell<ApplicationConceptCell2222private# region Inputsdefine_inputsdoattribute(:url,contract:And[::String,Send[:present?]],)end# endregion Inputsend
Enter fullscreen modeExit fullscreen mode

Above implemented via my own gemcontracted_value and feel free to swap it with any other value object implementation which can be gems or your own code.

Rendering

To use a cell, by default just invoke#call

And you can do it by:

  • Get result string directly
  • Create a cell, do something in the middle, get result string later (no need to invoke#call right after cell creation, and feel free to add new public methods as your own API)
# controllerrender(html:concept("copyable_url_box/cell, url: "...").call)
Enter fullscreen modeExit fullscreen mode

Since I usecells-rails,#call returns HTML safe string.
If you don't usecells-rails you might need to do extra work or include extra module to make that happen (check doc yourself).

In cell class, it looks like this:

classCopyableUrlBox::Cell<ApplicationConceptCell# This is actually optional if you use >= 4.1# BUT you can make this return something elsedefshowrenderend# inputs and other stuff...end
Enter fullscreen modeExit fullscreen mode

Why#show? If you read the doc you will know that
concept_cell.call == concept_cell.call(:show)
You are free to use other names but not recommended.

The templates can accessanything inside the cell.
Yes this includes

  • the inputs (yes even they are private attributes)
  • any public/protected/private methods
  • Some helper methods are delegated to controller ifcells-rails is used
  • Extra helper methods from helper modules included into the cell

No more "locals" when rendering. What you see (in cell objects) is what you get (in templates).

Having template file(s) is optional.
Returning a tag directly in#show also works.

classWebsiteLogo::Cell<ApplicationConceptCelldefshowimage_tag(website_logo_image_path,alt:"Logo",class:"...",)endprivatedefwebsite_logo_image_pathcaseinputs.variantwhen:small"..."else"..."endend# inputs and other stuff...end
Enter fullscreen modeExit fullscreen mode

View Inheritance

I generally prefer composition over inheritance (using a group of other cells instead a cell) but sometimes being able to use templates from parent cell class is more convenient.
I use it when I have several cells for page content of several page types but the layout is similar (same number of sections but different content).
Just read theview inheritance document yourself.

Caching

Best part comes last.
Caching is easy, cache invalidation is not.
But let's talk about how to enable caching first.

Enabling caching is quite easy according tocaching doc:

classCopyableUrlBox::Cell<ApplicationConceptCellcache:showend
Enter fullscreen modeExit fullscreen mode

Yup. The end.
Except when you pass different inputs into it, the cell will give you the same (cached) output. Which is why cache invalidation is the hard part.

First get some basic concept by looking at thecaching doc

cache:show,expires_in:10.minutes
Enter fullscreen modeExit fullscreen mode

This is just making the cached content expires after some time, nope not what we want.

cache:show{|model,options|"comment/#{model.id}/#{model.updated_at}"}cache:show,:if=>lambda{|*|has_changed?}cache:show,:tags:lambda{|model,options|"comment-#{model.id}"}
Enter fullscreen modeExit fullscreen mode

Looks messy? Let me give you my version

cache(:show,:cache_key,expires_in: :cache_valid_time_period_length,if: :can_be_cached?,)defcan_be_cached?# Use following code when child cell(s) is/are used# [#   child_cell_1,# ].all?(&:can_be_cached?)trueenddefcache_valid_time_period_length# Use following code when child cell(s) is/are used# [#   child_cell_1,# ].map(&:cache_valid_time_period_length).min# Long time100.yearsenddefcache_key{current_locale:::I18n.config.locale,url:Digest::MD5.hexdigest(url),}end
Enter fullscreen modeExit fullscreen mode

If language changed (::I18n.config.locale) or input value different (url), a different cache key would be used and the render result (cached or not) would be different

But hold on, what if I updated the logic/template and now I want it to return a result rendered with latest logic?

This is what Idid:

classApplicationConceptCell<Cell::ConceptDEFAULT_CLASS_LEVEL_CACHE_KEY={# ONLY change this when there are too many cells caching needs updated# Also only affects cells that has `#cache_key` binding to `super`# Yes I use cells since 2015design: :v2015_12_22_1001,}.freezeprivate_constant:DEFAULT_CLASS_LEVEL_CACHE_KEYdefself.cache_keyDEFAULT_CLASS_LEVEL_CACHE_KEYenddefcache_keyself.class.cache_keyendendclassCopyableUrlBox::Cell<ApplicationConceptCellcache(:show,:cache_key,expires_in: :cache_valid_time_period_length,if: :can_be_cached?,)defcan_be_cached?# Use following code when child cell(s) is/are used# [#   child_cell_1,# ].all?(&:can_be_cached?)trueenddefcache_valid_time_period_length# Use following code when child cell(s) is/are used# [#   child_cell_1,# ].map(&:cache_valid_time_period_length).min# Long time100.yearsenddefself.cache_keysuper.merge(logic: :v2020_12_18_1727,)enddefcache_keysuper.merge(current_locale:::I18n.config.locale,url:Digest::MD5.hexdigest(url),)endend
Enter fullscreen modeExit fullscreen mode

Notice that the above code all usecache_key.

ButRails 5.2 introducedcache versioning and we should use that too.

Caching - With Cache Versioning

The following code enablecache versioning

classApplicationConceptCell<Cell::ConceptDEFAULT_CLASS_LEVEL_CACHE_KEY={# Empty}.freezeprivate_constant:DEFAULT_CLASS_LEVEL_CACHE_KEYDEFAULT_CLASS_LEVEL_CACHE_VERSION={# ONLY change this when there are too many cells caching needs updated# Also only affects cells that has `#cache_key` binding to `super`# Yes I use cells since 2015design: :v2015_12_22_1001,}.freezeprivate_constant:DEFAULT_CLASS_LEVEL_CACHE_VERSIONdefself.cache_keyDEFAULT_CLASS_LEVEL_CACHE_KEYenddefcache_keyself.class.cache_keyenddefself.cache_versionDEFAULT_CLASS_LEVEL_CACHE_VERSIONenddefcache_versionself.class.cache_keyendendclassProductCardWide::Cell<ApplicationConceptCellcache(:show,:cache_key,expires_in: :cache_valid_time_period_length,if: :can_be_cached?,version: :cache_version,)defcan_be_cached?# Use following code when child cell(s) is/are used# [#   child_cell_1,# ].all?(&:can_be_cached?)trueenddefcache_valid_time_period_length# Use following code when child cell(s) is/are used# [#   child_cell_1,# ].map(&:cache_valid_time_period_length).min# Long time100.yearsend# def self.cache_key#   super# enddefcache_keysuper.merge(current_locale:::I18n.config.locale,# Assume product is an active record objectproduct:product.id,)enddefself.cache_versionsuper.merge(logic:    :v2017_12_22_1124,)enddefcache_versionsuper.merge(# Assume product is an active record objectproduct:product.updated_at,)endend
Enter fullscreen modeExit fullscreen mode

Point is when calling.cache in class, add optionversion.

Logic/Template/Translation/Images updated?

You can of course manually update the class level cache key/version.

But let's see how I make them auto update themselves.

Caching - Class Level Cache Key/Version Auto Update

1. Template Updates
This assumes every template update should cause cache invalidation (so even a refactor would invalidate the cache).

classSome::Cell<ApplicationConceptCelldefself.cache_versionsuper.merge(template:TEMPLATE_FILES_CONTENT_CACHE_KEY,)end# This generates the cache key/version from the content of all direct templates for this cell (templates from parent cells or custom paths NOT included)# Any change (even a refactor) would cause the value to changeTEMPLATE_FILES_CONTENT_CACHE_KEY=beginview_folder_path=File.expand_path("views",__dir__)file_paths=Dir.glob(File.join(view_folder_path,"**","*"))file_digests=file_paths.mapdo|file_path|::Digest::MD5.hexdigest(File.read(file_path))end::Digest::MD5.hexdigest(file_digests.join(""))endprivate_constant:TEMPLATE_FILES_CONTENT_CACHE_KEYend
Enter fullscreen modeExit fullscreen mode

2. Translation Updates
This requires you to have separate folders of translation files for different components.

classSome::Cell<ApplicationConceptCelldefself.cache_versionsuper.merge(translations:TRANSLATION_FILES_CONTENT_CACHE_KEY,)end# This generates the cache key/version from the content of all specified folder's files# Any change (even a refactor) would cause the value to changeTRANSLATION_FILES_CONTENT_CACHE_KEY=beginfolder_path=::Rails.root.join("config/locales/your/cell/translation/folder/path",)file_paths=Dir.glob(File.join(folder_path,"**","*"))file_digests=file_paths.mapdo|file_path|nextnilunlessFile.file?(file_path)::Digest::MD5.hexdigest(File.read(file_path))end::Digest::MD5.hexdigest(file_digests.join(""))endprivate_constant:TRANSLATION_FILES_CONTENT_CACHE_KEYend
Enter fullscreen modeExit fullscreen mode

3. Image Updates

classSome::Cell<ApplicationConceptCelldefself.cache_versionsuper.merge(images:IMAGE_FILES_CONTENT_CACHE_KEY,)end# This generates the cache key/version from the content of all specified folder's files# Any change (even a refactor) would cause the value to changeIMAGE_FILES_CONTENT_CACHE_KEY=beginfolder_paths=[# "app/assets/images/path/to/images",]asset_logical_paths=[# "app/assets/images/path/to/images.jpg",].mapdo|asset_logical_path|::Rails.root.join(asset_logical_path)endfile_paths=folder_paths.inject([])do|paths,folder_path|paths+::Dir.glob(::File.join(::Rails.root.join(folder_path),"**","*"))end+asset_logical_pathsfile_digests=file_paths.mapdo|file_path|nextnilunlessFile.file?(file_path)::Digest::MD5.hexdigest(File.read(file_path))end::Digest::MD5.hexdigest(file_digests.join(""))endprivate_constant:ASSET_FILES_CONTENT_CACHE_KEYend
Enter fullscreen modeExit fullscreen mode

4. Cell Logic Updates
Nope. You have to update cache key/version manually for this kind of updates.

The End

I still got some more to share about caching but let's save it for another post. (This post is for "beginners")

The code above is extracted from an existing project so let me know if there is any mistake.

Took me almost 3 months to finish this article (o.0)

Top comments(0)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

I am lazy.I seldom feel the need to write a blog post.
  • Joined

More fromPikachuEXE

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp