Document

Build a document

alias StatifierBlocks.{Block, Document}

document =
  Document.new(
    Block.new("core.sequence",
      id: "blk_root",
      slots: %{
        "body" => [
          Block.new("myapp.check_details", id: "blk_check_details")
        ]
      }
    ),
    id: "bdoc_patron_registration"
  )

Block.new/2 takes :id, :type_version, :config, :slots and :note. See How to build a block document and compile it to a chart.

Document options

OptionDefaultMeaning
:ida minted idThe document's id
:revision0The revision number
:metadata%{}Not compiled
:datamodel[]Document.DatamodelEntry structs; each names a root
:accepts[]Event names the document accepts

Store and read it back

json = Document.to_json(document)
{:ok, document} = Document.from_json(json)

to_json/1 raises ArgumentError on an invalid document. from_json/1 answers {:error, reason} and never raises.

Declare a datamodel root

Document.new(root,
  datamodel: [
    %Document.DatamodelEntry{
      id: "visitor_age",
      description: "The visitor's age, from the details they entered."
    }
  ]
)

Palette

Build a palette from your block types

palette =
  StatifierBlocks.Palette.from_modules(
    [{"myapp.check_details", MyApp.Blocks.CheckDetails}],
    core: true
  )

core: true adds the core.* vocabulary. See How to register your own block types.

Build a palette from a map

palette =
  StatifierBlocks.Palette.new(
    Map.merge(StatifierBlocks.Palette.core_types(), %{
      "myapp.check_details" => MyApp.Blocks.CheckDetails
    })
  )

A palette is a value you build for one operation and pass explicitly.

Declare a step that names an invoke type

defmodule MyApp.Blocks.SendWelcome do
  @moduledoc "myapp.send_welcome: sends the new patron a welcome message."
  use StatifierBlocks.InvokeStep,
    invoke_type: "myapp:send_welcome",
    palette: %{label: "Send welcome", group: "Registration"}
end

The block type names the invoke type and never runs it; a handler you register runs it.

Type a palette

def io(_config), do: %{kinds: [:step], produces: "myapp.patron_application"}

produces is a write and consumes a read at the document's subject path. See How to type a palette.

Check the palette can express a stored document

:ok = StatifierBlocks.Plan.expressible(document, palette)
true = StatifierBlocks.Plan.expressible?(document, palette)

An unexpressible document answers {:no, reasons}.

Compile

Compile a document

{:ok, compiled} = StatifierBlocks.Compiler.compile(document, palette)

It answers {:ok, %Compiled{}} or {:error, findings}, never a raise and never a partial success.

What Compiled carries

FieldHolds
scxmlThe chart Statifier runs as-is
provenanceThe map from generated states back to blocks
recordCompilationRecord: the revision, the chart and the compiler version
invoke_typesThe invoke types the chart names
warningsCompiler.Findings from a successful compile
acceptsThe document's accepts list
interfaceDeclared outcomes, done-data keys and referenced documents

Compile options

OptionMeaning
:datamodelThe host's declared datamodel
:declare{id, expr} roots for the chart's <data>
:known_invoke_typesLint invoke types against this set
:entry_typeThe type the flow's entry is checked against
:terminateCompile a root document that finishes: one top-level <final> per root outcome
:child_useCompile for use as a child: each <final> carries its outcome as donedata

Point a running position back at a block

compiled.provenance
|> StatifierBlocks.Provenance.owners_of_states(active_state_ids)
|> Enum.map(& &1.block_id)

A state id the map does not know is dropped.

Publish

Check before you keep a revision

findings =
  StatifierBlocks.Publish.findings(document, palette, %{
    datamodel: datamodel,
    declare: declare
  })

Every context key is optional: :datamodel, :declare and :chart_outcomes. It emits nothing. See How to publish a document.

Refuse on a finding's severity

refuses? = Enum.any?(findings, &(&1.severity == :error))

:error refuses the publish, :warning is your call and :info never refuses.

Adapt the compile's findings

{adapted, _no_anchor} =
  StatifierBlocks.Finding.from_compiler_all(compiled.warnings)

A compiler finding that names no block has no anchor and comes back in the second list.

Check a parent against its children

findings =
  StatifierBlocks.Graph.check(parent_compiled, fn document_id ->
    {:ok, compiled_child}
    # or {:error, :not_published}
  end)

Check a child revision against its parents

[{parent_document_id, finding}] =
  StatifierBlocks.Graph.consumers_broken(child_next, [parent_compiled])

The calls judge pairs; walking the graph is yours.

Editor

Mount the component

<.live_component
  module={StatifierBlocks.Editor}
  id="registration-editor"
  document={@registration}
  palette={@palette}
  on_change={&save_registration_draft/1}
/>

id, document and palette are required. See How to embed the editor in a LiveView host.

Register the hooks

import StatifierBlocks from "statifier_blocks";

let liveSocket = new LiveSocket("/live", Socket, {
  hooks: { ...StatifierBlocks },
});

Register both hooks, StatifierBlocksDrag and StatifierBlocksMeasure. Without the measure hook no connectors are drawn.

Import the stylesheet

@import "../../deps/statifier_blocks/assets/css/statifier_blocks.css";

Optional assigns

AssignMeaning
on_selectCalled with %{id:, type:, label:}, or nil
selected_idMoves the selection on the update that carries it
findingsFindings you supply, merged with the two the view model derives
datamodelDeclared paths; nil turns the undeclared-path advisories off
declareThe {id, expr} roots you pass the compiler as :declare
profileWhich surfaces this mount draws, and whether it edits

See Editor assigns and host seams for every assign.

Without LiveView

phoenix_live_view is optional. Without it no StatifierBlocks.Editor.* module compiles, and the rest of the package does.