ColonelKurtzEx (CKEX)
ColonelKurtzEx facilitates working with the block content editor Colonel Kurtz in Phoenix applications. The main faculties ColonelKurtzEx provides are focused on structured data, validation, and rendering.
Documentation is available on GitHub and will be on HexDocs soon!
Note on terminology: For clarity and conciseness, this document may refer to the elixir library as CKEX and the javascript library as CKJS.
Table of Contents
Overview
Colonel Kurtz (CKJS)
Colonel Kurtz is a block content editor implemented in JS.
It is recommended that you have a reasonable understanding of CKJS and how to use it before diving into CKEX. Specifically, how the data is structured and how to extend its functionality with new block types. Head over to the repo for more information.
Here's a brief summary of the basics to better orient you to some concepts relevant to CKEX:
CKJSproduces a (potentially deeply) nested tree ofblocksin JSON format.- A
blockhas the following fields:type,content, andblocks(the latter represents any nested child blocks). - When you define a
CKJSblock type, you implement it using a React Component which affords you great flexibility when it comes to the UI you present to users of your application.
Structured Data
Anyone familiar with Elixir and the surrounding community is likely to already understand the benefits of structured data. This isn't an essay on the subject but suffice to say that we believe in using named structs and predictable data wherever possible. One of the main motivations of ColonelKurtzEx is to convert CKJS JSON into named structs.
Validation
Data integrity is crucial to building robust software and validation is important for helping users create valid data through providing helpful error messages. ColonelKurtzEx gives developers the ability to validate CKJS JSON data by leveraging Ecto Changesets which should be familiar to many Elixir developers. If you've done anything with databases or validation you've likely used Ecto and will be familiar with how to implement validation rules for CK data using CKEX.
Rendering
ColonelKurtzEx provides a BlockTypeView macro that can be used in Phoenix Views. A block type view, aside from being a normal Phoenix View (used to handle presentation of data), controls whether a block can render by specifying an implementation for renderable?/1. The default is true, but modules that use the macro may override this method to enable more fine-grained control over whether a block should be rendered based on its current data.
For example, you might need to model a block that requires exactly 3 images to be defined in its data. If a greater or lesser number is specified, the block type view can disable rendering (e.g. to prevent invalid layouts from happening). However, you should try to implement these rules in your block type validation to prevent invalid data from reaching the database in the first place.
Installation
If available in Hex, the package can be installed by adding colonel_kurtz_ex to your list of dependencies in mix.exs:
def deps do
[
{:colonel_kurtz_ex, "~> 0.1.0"}
]
end
Documentation can be generated with ExDoc and published on HexDocs. Once published, the docs can be found at https://hexdocs.pm/colonel_kurtz_ex.
API
The root module for ColonelKurtzEx (ColonelKurtz) defines the most commonly used API methods when interacting with the library. It delegates all of the implementation to various other submodules. In most cases you won't have to think too hard about where to import things from unless you're reaching for a function that's less commonly used.
-
block_editor(form, field) -
block_editor(form, field, opts) -
blocks_json(form, field) -
blocks_json(form, field, opts) -
render_blocks(blocks) -
validate_blocks(changeset, field) -
validate_blocks(changeset, field, opts)
Getting Set Up
To get set up with ColonelKurtzEx, you'll need to install and configure Colonel Kurtz. Since the two libraries go hand in hand, you'll often jump back and forth between the two. For example, when you add a new block type to CKJS, you'll need to add the corresponding modules for CKEX (a BlockType, BlockTypeView, and template). In the future CKEX will provide generators to expedite the process of common tasks such as adding a new block type.
Note: The following sections are expandable (and are collapsed by default).
1. Add Folders for BlockTypes and BlockTypeViews
After adding the library to your dependencies, you'll want to define a few modules in the scope of your application. One for your custom BlockType definitions, and one for your BlockTypeViews.
Note: it is important that each of these concepts live inside a dedicated module namespace in your application so that the library can look up specific block type and view modules at runtime.
See an example
For example, assuming a standard phoenix project structure:
-
Create a new subfolder inside
lib/your_app_web/views/, using whatever name you'd like that corresponds with the module you'll be defining views inside (e.g.lib/your_app_web/views/blocks/folder andYourAppWeb.Blocksmodule.). -
Create a new subfolder inside
lib/your_app/. Again, the name doesn't matter so long as you configureColonelKurtzExcorrectly (more information in the following section). For example, you might create a folder namedlib/your_app/block_types/and to contain theYourApp.BlockTypesnamespace.
2. Configure ColonelKurtzEx
Add the following to your config/config.exs to allow CKEX to locate your custom BlockType and BlockTypeView modules:
config :colonel_kurtz_ex, ColonelKurtz,
block_views: YourAppWeb.Blocks,
block_types: YourApp.BlockTypes
3. Add a CK field to one of your app's schemas
-
First, amend the schema to add a new field that will hold your
CKJSdata:defmodule YourApp.Post do use Ecto.Schema # 1. alias the custom ecto type alias ColonelKurtzEx.CKBlocks # 2. import the validation helper import ColonelKurtzEx.Validation, only: [validate_blocks: 2] schema "posts" do field :title, :string # 3. add a field of this type, named whatever you like field :content, CKBlocks, default: [] end def changeset(post, params \\ %{}) do post # 4. make sure you cast the new field in your changeset |> cast(params, [:title, :content]) # 5. call `validate_blocks` passing the name of your field |> validate_blocks(:content) end endNote:
validate_blocks/2can take an atom or a list of atoms if you have more than one set of blocks fields to validate. -
Then create the migration to add the field to your database
mix ecto.gen.migration add_content_to_posts -
CKBlocksexpects the underlying field to be a:mapwhich is implemented as ajsonbcolumn in Postgres.# priv/repo/migrations/<timestamp>_add_content_to_posts.exs defmodule YourApp.Repo.Migrations.AddContentToPost do use Ecto.Migration def change do alter table("posts") do add :content, :map end end end -
Run your migration
mix ecto.migrate
4. Teach your Phoenix Views how to render blocks
-
Use
ColonelKurtz.render_blocks/1to render block content somewhere in a template.More information
You may import this method as needed in the views that will render blocks. Or, as a convenience, you may import this function automatically in all of your phoenix views by adding it to the
your_app_web.exdefinition forview(orview_helpersif you want it to be available for live views as well, example below). -
In addition, to render the block editor in your forms, you'll want to pull in
ColonelKurtz.render_blocks/1too. The example below shows how to do this for all Phoenix Views in your application.See an example
# lib/your_app_web.ex # ... defp view_helpers do quote do use Phoenix.HTML import Phoenix.LiveView.Helpers import BlogDemoWeb.LiveHelpers # 1. import `render_blocks/1` so that it is available for all views import ColonelKurtz, only: [render_blocks: 1, block_editor: 2] # 2. optional: import all of the form helpers if you want to use other functions # (such as `blocks_json/2` or `block_errors_json/2`) import ColonelKurtz.FormHelpers import Phoenix.View import BlogDemoWeb.ErrorHelpers import BlogDemoWeb.Gettext alias BlogDemoWeb.Router.Helpers, as: Routes end end # ... -
Render your blocks inside your view's show template:
# lib/your_app_web/templates/post/show.html.eex # ... <%= render_blocks @post.content %> # ... -
Render the block editor field inside your view's form:
# lib/your_app_web/templates/post/form.html.eex # ... <%= label f, :content %> <%= error_tag f, :content %> <%= block_editor f, :content %> # ...Note: The
block_editorhelper outputs some markup that you must mountCKJSon.
5. Add your custom BlockTypes
Note: As of this writing, there remains a lot of work to do in order to provide a default set of useful block types, some of which are already provided by CKJS, along with generators to aide in the creation of new block types.
-
Create your BlockType module: Continuing from the example scenario outlined above, create a new block type at
lib/your_app/block_types/image.exwhereimageis just an example of a descriptive name of the block you're modeling.# lib/your_app/block_types/image.ex # 1. optional, you may choose to define the `<type>Block` module if you need to add validation # at the block level (for most use cases you can skip this step; it's only necessary if # you need to validate e.g. that a block has a particular number of child `:blocks`). defmodule YourApp.BlockTypes.ImageBlock do use ColonelKurtz.BlockType def validate(_block, changeset) do changeset # e.g. this block must have at least 1 child block |> validate_length(:blocks, min: 1) end end # 2. define the `<type>Block.Content` module within your configured `:block_types` namespace defmodule YourApp.BlockTypes.ImageBlock.Content do # 3. use the BlockType macro use ColonelKurtz.BlockTypeContent # 4. use the `embedded_schema` macro to specify the schema for your block's content embedded_schema do field :src, :string field :width, :integer field :height, :integer end # 5. optional, but encouraged - define your validation rules for the block's content def validate(_content, changeset) do changeset |> validate_required([:src, :width, :height]) # ... any other custom validation rules you need ... end end -
Create your BlockView: create a new block view at
lib/your_app_web/blocks/image_view.ex.Note: make sure you've configured
ColonelKurtzExwith the location of yourblock_viewsandblock_types. See "ConfigureColonelKurtzEx" above.# lib/your_app_web/blocks/image_view.ex defmodule YourAppWeb.Blocks.ImageView do use YourAppWeb, :view use ColonelKurtz.BlockTypeView # optionally implement `renderable?/1` def renderable?(%ImageBlock{content: %{src: ""}} = block), do: false def renderable?(_block), do: true end
FAQs
How can I inspect the block data or errors in a nice format?
Take a look at the functions provided in ColonelKurtz.FormHelpers, specifically blocks_json/3 and block_errors_json/3. Both of these methods accept a third argument which is a list of options to pass to Jason.Encoder (hint: try pretty: true).
How does ColonelKurtzEx look up my custom block type and view modules?
You configure the :block_views and :block_types options for CKEX in your config.exs by providing the modules in your app that will contain your custom block types and views. When CKEX marshalls blocks JSON, it parses data into lists of maps using Jason and then looks up a block type based on the block's type field.
It does so by calling Module.concat with the module you specified for :block_types and Macro.camelize(type) <> "Block" (e.g. "image" => YourApp.BlockTypes.ImageBlock).
Similarly, to lookup your view modules CKEX calls Module.concat with the module you specified for :block_views and Macro.camelize(type) <> "View" (e.g. "image" => YourAppWeb.Blocks.ImageView).
My block type schema changed, how can I migrate my existing block data?
Congratulations, you've discovered an unsolved Hard Problemâ„¢.
We're currently working on a proposal for library changes that might better facilitate data migrations on CK block JSON.
In the meantime, it is recommended to leverage your RDBMS's capabilities for querying and modifying JSON. Our best advice for now is: As much as you can, try to avoid the need to migrate CK JSON data.
Development
Code Quality
To help maintain high code quality this project uses dialyxir and credo for static code analysis, ExUnit for testing and ExCoveralls for test coverage.
Typespecs
mix dialyzer
Code Style
mix credo --strict
Tests
mix test
Contributing
- Fork the library
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin my-new-feature) - Create new Pull Request
Authors
- Solomon Hawk (@solomonhawk)
- Dylan Lederle-Ensign (@dlederle)
License
ColonelKurtzEx is released under the MIT License. See the LICENSE file for further details.