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
.
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
:
CKJS
produces a (potentially deeply) nested tree ofblocks
in JSON format.- A
block
has the following fields:type
,content
, andblocks
(the latter represents any nested child blocks). - When you define a
CKJS
block 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.
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.
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)
- (See
ColonelKurtz.FormHelpers.block_editor/2
)
- (See
-
block_editor(form, field, opts)
- (See
ColonelKurtz.FormHelpers.block_editor/3
)
- (See
-
blocks_json(form, field)
- (See
ColonelKurtz.FormHelpers.blocks_json/2
)
- (See
-
blocks_json(form, field, opts)
- (See
ColonelKurtz.FormHelpers.blocks_json/3
)
- (See
-
render_blocks(blocks)
- (See
ColonelKurtz.Renderer.render_blocks/1
)
- (See
-
validate_blocks(changeset, field)
- (See
ColonelKurtz.Validation.validate_blocks/2
)
- (See
-
validate_blocks(changeset, field, opts)
- (See
ColonelKurtz.Validation.validate_blocks/3
)
- (See
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 BlockTypeView
s.
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.Blocks
module.). -
Create a new subfolder inside
lib/your_app/
. Again, the name doesn't matter so long as you configureColonelKurtzEx
correctly (more information in the following section). For example, you might create a folder namedlib/your_app/block_types/
and to contain theYourApp.BlockTypes
namespace.
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
CKJS
data: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 end
Note:
validate_blocks/2
can 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
-
CKBlocks
expects the underlying field to be a:map
which is implemented as ajsonb
column 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/1
to 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.ex
definition forview
(orview_helpers
if 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/1
too. 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_editor
helper outputs some markup that you must mountCKJS
on.
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.ex
whereimage
is 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
ColonelKurtzEx
with the location of yourblock_views
andblock_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
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.
To help maintain high code quality this project uses dialyxir and credo for static code analysis, ExUnit for testing and ExCoveralls for test coverage.
mix dialyzer
mix credo --strict
mix test
- 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
- Solomon Hawk (@solomonhawk)
- Dylan Lederle-Ensign (@dlederle)
ColonelKurtzEx is released under the MIT License. See the LICENSE file for further details.