[tip box yellow] Many of these are considered to be experimental features.
Imba is moving extremely quickly and documenting every feature becomes challenging. We intend to continually revise the documentation as much as possible, but instead of not documenting features at all until we have time, it's better to at least expose them to power users who want to learn as much as possible at the cost of reading something slightly less structured and polished as the rest of the docs.
This also serves as a list of things that need to be documented more formally, which is better than having all of them in our personal notes. Contributions are highly appreciated, especially:
- Polishing any of these examples or creating your own examples.
- Moving examples to a relevant location on the documentation.
- Coming up with proper names for ambiguous features.
If you have questions about any of these, don't hesitate to reach out on Discord.
instanceof
is shortened to isa
in imba:
[] isa Array
But we've added more functionality. If the right hand side is a string,
it will check typeof
:
let s = 'hello'
typeof s is 'string' # true
s isa 'string' # true
It also works similarly to is
:
7 isa ('string' or 'number') # true
You can await desired changes to observable properties with imba.awaits
,
which takes a callback that will be called every time the property is changed until the callback returns a truthy value.
let user = new class User
@observable first_name = 'first'
setTimeout(&,100ms) do
user.first_name = 'updated'
setTimeout(&,200ms) do
user.first_name = 'again'
await imba.awaits do
console.log user.first_name
user.first_name is 'updated'
If you want to bundle with no hashing, for now you need to use Vite as a bundler and specify the output filename:
export default defineConfig({
...
build: {
rollupOptions: {
output: {
entryFileNames: 'assets/[name].js',
chunkFileNames: 'assets/[name].js',
assetFileNames: 'assets/[name].[ext]'
}
}
}
})
In Imba you can emit and listen to events without needing the DOM:
imba.emit 'hello'
imba.listen 'hello'
Element
is extended with an emit
function.
some_element.emit('event_name')
rd:full
compiles to
border-radius:9999px
If you name a file os.node.imba
and another os.imba
and import os
it will import the one for the relevant platform.
@media(hover: hover)
Imba will automatically traverse up the file tree starting from
the dirname of the file you ran with imba in search for a .env
file.
The contents will be accessable from process.env
.
The .env
file has the following syntax:
S3_BUCKET="YOURS3BUCKET"
SECRET_KEY="YOURSECRETKEYGOESHERE"
Accessing these variables in an imba file is as simple as process.env.S3_BUCKET
. No configuration necessary.
Note that, unlike the dotenv library, Imba will not use the cwd to search for the .env
file,
but rather the dirname of the file you ran.
In line with $1
, $2
, etc. positional params, $0
refers to the arguments object.
# [preview=console]
let x = [1, 2, 3]
console.log(*x)
console.log(...x)
This is a common pattern:
# [preview=console]
def example
let obj
try
obj = JSON.parse("this will error")
catch e
obj = {}
console.log obj
example!
Instead we can do:
# [preview=console]
def example
let obj =
try
JSON.parse("this will error")
catch e
{}
console.log obj
example!
Or with less lines:
# [preview=console]
def example
let obj =
try JSON.parse("this will error")
catch e {}
console.log obj
example!
Or in one line:
# [preview=console]
def example
let obj = try JSON.parse("this will error") catch e {}
console.log obj
example!
Or without a catch:
# [preview=console]
def example
let obj = try JSON.parse("this will error")
console.log obj
example!
This can also be done with if/else statements.
if $node$
'Node'
if $browser$
'Browser'
if $worker$
'Worker'
if $web$
'Web'
tag app
def mount
dataset.payload = "test" # sets the `data-payload` attribute on <app>
The cooldown event modifier will flag the cooldown
class on the target element.
<button @click.cooldown(1s)=handler>
css @.cooldown bg:blue
"On click, will flash blue and not be clickable for 1s"
The css
modifier for the @resize
event will set the units elw
and elh
on the element on resize:
tag app
<self @resize.css>
css s:50px bg:rose4
If you inspect <app>
:
--u_elw: 50;
--u_elh: 50;
You can use them in css as units:
css w:1elw h:1elh
You can also pass in custom element names:
tag app
<self @resize.css(1sectionwidth,1sectionheight)>
css s:50px bg:rose4
Results in:
--u_sectionwidth: 50;
--u_sectionheight: 50;
You can pass an object to class constructors to set instance properties:
# [preview=console]
global.L = console.log
class Note
content = 'hello'
let note = new Note({content:'world'})
L note
let obj = JSON.parse(JSON.stringify(note))
L new Note(obj)
# [preview=console]
global.L = console.log
tag app-button < button
def on$click mods, context, handler, o
L "self", self
L "mods", mods
L "context", context
L "handler", handler
L "o", o
self.addEventListener('click',handler,o)
tag app
<self>
<app-button @click.log('clicked')> "log"
imba.mount <app>
o
refers to options
/useCapture
: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#parameters
@observable one = ''
@observable two = ''
@autorun def my-function stop-observing
one
stop-observing! if stop-observing
two # this will not trigger this autorun function even though it is observable
Autorun functions will run in the order they are declared.
If you want to add a function to an object, you don't have to do:
let obj =
add: do(a, b)
a + b
You can just use the normal def
syntax:
let obj =
def add a, b
a+ b
Imba supports flex layouts that follow a 3-character pattern: vh
tbcs
lrcs
.
v
and h
stand for vertical
and horizontal
respectively.
t
and b
stand for top
and bottom
respectively.
l
and r
stand for left
and right
respectively.
c
and s
stand for center
and stretch
/space-between
(depending on the flex-direction) respectively.
It sounds a bit complicated but it's much more intuitive once you get a feel for it.
For example, to create a container whose content is centered, you can use d:vcc
or d:hcc
.
If you want the content to be to the top left, you can do d:vtl
or d:htl
.
Imba allows you to delegate an object as the accessor for a field. Accessors
must conform to an interface. Namely, they must have a $get
method and a $set
method.
class Uppercase
def $set value, target, key
target[key] = value
def $get target, key
target[key].toUpperCase!
let upcase = new Uppercase
class User
name as upcase
let user = new User
user.name = "sindre"
console.log user.name
# SINDRE
Here, we are essentially delegating the upcase
object as the
manager for getting and setting name
.
We've started to implement some value aliases, such as pos:abs
and pos:rel
. One caveat is that these shorthands can't be interpolated since
they're determined at compile time.
You can't use css variables to dynamically assign durations, you can either use custom units (1animtime:10s
) or an interpolated value (ease:{10}s
).
Because Imba's css is so quick to write and flexible, the majority of the time it makes sense to put css directly under elements as opposed to making a class or separate stylesheet:
tag app
<self>
<div>
css bg:blue
But then everything that's not a custom element ends up being named div
.
So you might put a semantic class name there that doesn't actually have any styles:
tag app
<self>
<.blue-thing>
css bg:blue
It's nice because it's much easier to see what a certain layout is by reading an actual name.
The issue with this is that you have to worry about .blue-thing
being defined somewhere else.
At Scrimba we've adopted the convention of using %
:
tag app
<self>
<%blue-thing>
css bg:blue
This assumes you're using the Vite bundler.
You can create an imba project that uses the vite bundler with npx imba create
.
It's insanely easy to get caching with service workers using Vite. Literally all you have to do is install a vite plugin and add it to the plugin list:
npm i vite-plugin-pwa -D
vite.config.js:
import { imba } from 'vite-plugin-imba';
import { defineConfig } from 'vite';
import { VitePWA } from 'vite-plugin-pwa';
export default defineConfig({
base: '',
plugins: [imba(),VitePWA()],
});
When you run vite build
, it'll automatically create a service worker for you
and cache requests so your page will load offline and instantly.
css @hover @focus @checked
The above essentially gets compiled to
css @hover@focus@checked
The same with @.collapsed @.show-more
If you want to nest them you'd do
css @hover
*@focus
This is usually what you want with psuedo selectors.
In any inheritor class or tag, calling super!
will call the inherited object's function.
tag modal
def test
console.log "hello"
tag my-modal < modal
def test
super!
console.log "hello again"
Writing super
without ()
or !
will essentially call the super function with ...args
You can also call other functions by using super
:
super.setup!
If, for example, you have a modal, and want to prevent all hotkeys:
@hotkey('*').global
You can select a specific parent up the chain:
css ^.collapsed # if the direct parent has class .collapsed
css ^^^.collapsed # if the parent's parent's parent has class .collapsed
This is most useful for pseudo states:
css c^@hover:blue
This will apply opacity 0 if the .collapsed
class is found anywhere
in the parent heirarchy chain, all the way to root.
css o..collapsed:0
css c:blue4
css c:blue440
css c:blue450
css c:blue465
css c:blue5
Writing console.log
all the time feels quite verbose.
You might fancy writing global.L = console.log
in your main/shared file.
This way you expose L
to your entire app and can just do:
L "hello"
instead of
console.log "hello"
We use a capital name because lowercase names in Imba imply self
.
fs:md-
css #site-bg:gray85
css bg:#site-bg
If the current element has class .blue
:
css &.blue bg:blue
If parent element has class use existing rich selector:
css @.blue bg:blue
@error.trap
const p = console.log
import * as fui from '@floating-ui/dom'
extend class Event
def @confirm msg
let view = <app-confirm anchor=target template=msg>
p target
imba.mount view
view.promise
tag app-confirm
anchor
template
promise = new Promise do #resolve = $1
def confirm
#resolve yes
hide!
def mount
{ x, y } = await fui.computePosition(anchor, self)
def reject
#resolve no
hide!
def hide
imba.unmount self
self
<self>
css pos:absolute w:max-content top:{y}px left:{x}px
<button @click=reject> 'Cancel'
<button @click=confirm> 'OK'
tag app
<self>
<button@click.confirm.log('Confirmed!')> 'Click to confirm'
imba.mount <app>
global css .dark bg:black c:white
tag app
def mount
self.flags.add('dark')
<self> 'manual flags'
imba.mount <app>
css bg:blue3-3
Imba provides a way of interacting with a class when it is declared as the parent of another class. If your parent class has a static inherited
method, it will be called with the subclass as an argument whenever some class inherits from it.
class Base
static def inherited subclass
console.log 'was inherited by',subclass
class User < Base
# => was inherited by [User]
This is in undocumented because it is very likely be removed for the final release. Currently it is possible to declare both min and max width/height with >val
and <val
in css props.
css div w:100px <80vw # width:100px max-width:80vw
css div h:80% >100px # height:80% min-height:100px
When you use an undefined @modifier
in styles, it defaults to check for a mod-nameofmodifier
class on the root element. So it is easy to use / add your own.
if state.user
document.flags.add('mod-logged-in')
css div
# you can use the @logged-in modifier anywhere in your app
# it is true when html.mod-logged-in matches
d:block @logged-in:none
Let's say you have a PWA and want specific styles when the app is opened outside of browser:
# set mod-standalone if we match that displayMode
document.flags.toggle('mod-standalone',
window.matchMedia('(display-mode: standalone)').matches
)
global css body
# different background when opening in standalone mode
bg:white @standalone:gray
Add a modifier whenever a certain element is opened
tag base-page
get ref do nodeName.toLowerCase!
def mount do document.flags.add("mod-{ref}")
def mount do document.flags.remove("mod-{ref}")
tag home-page < base-page
tag about-page < base-page
# Whenever a page is mounted the tag-name of the page
# can be used as a modifier globally in your styles
global css
body hue:blue @home-page:indigo
header d:hflex @home-page:none
@click.cooldown.confirm
If you don't provide a time for the cooldown, it will not allow any more events through until the promise of the callback has finished.