Motion.cr
Motion is a framework for building reactive, real-time frontend UI components in your Amber application using pure Crystal that are reusable, testable & encapsulated.
Install / Use
/learn @AndiLavera/Motion.crREADME
Motion is a framework for building reactive, real-time frontend UI components in your Amber application using pure Crystal that are reusable, testable & encapsulated. For brevity, we will call them MotionComponents.
- Motion is an Object-Oriented View Layer
- Peacefully coexists with your existing frontend
- Real-time frontend UI updates from frontend user interaction AND server-side updates
- No more frontend models, stores, or syncing; your source of truth is the database you already have
- Write Less Javascript
Table of Contents
- Table of Contents
- Installation
- Documentation
- Component Guide
- Motions Guide
- Configuration
- Limitations
- Roadmap
- Contributing
- License
Installation
Add the shard to your dependencies:
dependencies:
motion:
github: andrewc910/motion.cr
version: 0.2.0
Create a file motion.cr in config/initializers and add:
require "motion"
# Adds a few helper methods
require "motion/monkey_patch/amber"
Create a socket:
amber g socket Motion
Add the route:
websocket "/cable", MotionSocket
Documentation
Component Guide
MotionComponents are Crystal objects that output HTML. MotionComponents are most effective in cases where view code is reused or benefits from being tested directly. The code itself was pulled & altered from Lucky Framework.
Why should I use components?
Testing
Unlike traditional views, Motion Components can be unit-tested.
Views are typically tested with slow integration tests that also exercise the routing and controller layers in addition to the view. This cost often discourages thorough test coverage.
With MotionComponents, integration tests can be reserved for end-to-end assertions, with permutations and corner cases covered at the unit level.
Data Flow
Traditional views have an implicit interface, making it hard to reason about what information is needed to render, leading to subtle bugs when rendering the same view in different contexts.
MotionComponents use defined props that clearly defines what is needed to render, making them easier (and safer) to reuse than partials.
Standards
Views often fail basic code quality standards: long methods, deep conditional nesting, and mystery guests abound.
MotionComponents are Crystal objects, making it easy to follow (and enforce) code quality standards.
Building components
Conventions
Components are subclasses of Motion::Base and live in views/components. It's common practice to create and inherit from an ApplicationComponent that is a subclass of Motion::Base. By doing so, not only can you share logic, you can share view templates.
Component names end in Component.
Component module names are plural, as for controllers and jobs: Users::AvatarComponent
Quick start
If you followed the installation guide above, you can start with you first component.
- Create a
componentsfolder inviews - Create your first component:
class MyFirstComponent < Motion::Base
def render
html_doctype
head do
css_link "/css/main.css"
utf8_charset
meta content: "text/html;charset=utf-8", http_equiv: "Content-Type"
title "My First Component"
end
body do
h1 { "My First Component!" }
end
end
end
- Render it in your controller:
render MyFirstComponent
DSL HTML Generation
For static html rendering, please review the lucky framework documentation
Note: Lucky uses the macro keyword
needs, motion usesprops
ECR & Slang
You can render html templates if you prefer. You have to mix in the amber module in to the components you would like to render from.
class MyFirstComponent < Motion::Base
include Amber::Controller::Helpers::Render
def render
render("users/new.ecr", layout: false)
end
end
In your controller:
render(MyFirstComponent)
Properties, Props & Type Safety
Props allow you to pass arguements to child components that are type safe. One of the problems with ecr views & partials is, it's hard to reason what variables & data the page requires to render because everything is within scope. Props explicity display what is required for a particular component.
class MyFirstComponent < Motion::Base
getter title : String
def initialize(@title); end
def render
html_doctype
head do
css_link "/css/main.css"
utf8_charset
meta content: "text/html;charset=utf-8", http_equiv: "Content-Type"
title "My First Component"
end
body do
h1 { @title }
end
end
end
Or you can use the props keyword:
class MyFirstComponent < Motion::Base
props title : String
def render
html_doctype
head do
css_link "/css/main.css"
utf8_charset
meta content: "text/html;charset=utf-8", http_equiv: "Content-Type"
title "My First Component"
end
body do
h1 { @title }
end
end
end
In your controller:
render(MyFirstComponent, title: "Hello World")
or rendering from a component:
m(MyFirstComponent, title: "Hello World") # m is shorthand for mount. mount is also acceptable
The if you use the props keyword, do not define an initialize method as it will do it for you. I recommend using the props macro when starting off and switching over to a custom initialization method when you need custom logic.
Blocks & Procs
Blocks & Procs can be passed to child components. This will allow you to create more generic & reusable components.
class MyFirstComponent < Motion::Base
props title : Proc(void)
def render
html_doctype
head do
css_link "/css/main.css"
utf8_charset
meta content: "text/html;charset=utf-8", http_equiv: "Content-Type"
title "My First Component"
end
body do
title.call
end
end
end
In your parent component:
title = Proc(void).new { h1 "Hello World!" }
m(MyFirstComponent, title: title)
Motions Guide
Motion.cr allows you to mount special DOM elements that can be updated real-time from frontend interactions, backend state changes, or a combination of both. Some features include:
- Websockets Communication - Communication with your Amber backend is performed via websockets
- No Full Page Reload - The current page for a user is updated in place.
- Fast DOM Diffing - DOM diffing is performed when replacing existing content with new content.
- Server Triggered Events - Server-side events can trigger updates to arbitrarily many components via WebSocket channels.
Motion.cr is similar to Phoenix LiveView (and even React!) in some key ways:
- Partial Page Replacement - Motion.cr does not use full page replacement, but rather replaces only the component on the page with new HTML, DOM diffed for performance.
- Encapsulated, consistent stateful components - Components have continuous internal state that persists and updates. This means each time a component changes, new rendered HTML is generated and can replace what was there before.
- Blazing Fast - Communication does not have to go through the full Amber router and controller stack. No changes to your routing or controller are required to get the full functionality of Motion.cr.
Installation
npm i @awcrotwell/motion
In main.js add:
import { createClient } from '@awcrotwell/motion';
const client = createClient();
