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](https://github.com/vigetlabs/colonel-kurtz) for more information. Here's a brief summary of the basics to better orient you to some concepts relevant to `CKEX`: 1. `CKJS` produces a (potentially deeply) nested tree of `blocks` in JSON format. 2. A `block` has the following fields: `type`, `content`, and `blocks` (the latter represents any nested child blocks). 3. 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](https://hexdocs.pm/ecto/Ecto.Changeset.html#content) 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](https://hexdocs.pm/phoenix/Phoenix.View.html#content). 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"}
]
endDocumentation 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)-
(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
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 `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: 1. 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 and `YourAppWeb.Blocks` module.). 2. Create a new subfolder inside `lib/your_app/`. Again, the name doesn't matter so long as you configure `ColonelKurtzEx` correctly (more information in the following section). For example, you might create a folder named `lib/your_app/block_types/` and to contain the `YourApp.BlockTypes` namespace.
2. Configure ColonelKurtzEx
Add the following to your `config/config.exs` to allow `CKEX` to locate your custom `BlockType` and `BlockTypeView` modules:
```elixir
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
1. First, amend the schema to add a new field that will hold your `CKJS` data: ```elixir 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. 2. Then create the migration to add the field to your database ```bash mix ecto.gen.migration add_content_to_posts ``` 3. `CKBlocks` expects the underlying field to be a `:map` which is implemented as a `jsonb` column in Postgres. ```elixir # priv/repo/migrations/_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 ``` 4. Run your migration ```bash mix ecto.migrate ``` ---4. Teach your Phoenix Views how to render blocks
1. 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 for `view` (or `view_helpers` if you want it to be available for live views as well, example below).See an example
```elixir # 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 # ... ```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. 1. **Create your BlockType module**: Continuing from the example scenario outlined above, create a new block type at `lib/your_app/block_types/image.ex` where `image` is just an example of a descriptive name of the block you're modeling. ```elixir # lib/your_app/block_types/image.ex # 1. optional, you may choose to define the `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 `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 ``` 2. **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 your `block_views` and `block_types`. See "**Configure `ColonelKurtzEx`**" above. ```elixir # 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 dialyzerCode Style
mix credo --strictTests
mix testContributing
- 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.