Skip to content

Instantly share code, notes, and snippets.

@hi94740
Last active February 17, 2024 11:04
Show Gist options
  • Star 12 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save hi94740/de40798423b093edb6473556fde252b5 to your computer and use it in GitHub Desktop.
Save hi94740/de40798423b093edb6473556fde252b5 to your computer and use it in GitHub Desktop.
Chainable widget API for Scriptable App.

Scriptable Chainable Widget API

Wraps Scriptable’s ListWidget API with a chainable interface. Enables writing widget syntax that is closer to the actual view hierarchy.

Copy the createWidget() function to your scripts to get started.

Create and render a widget

Calling the createWidget() function will return a new wrapper object representing the widget view with chainable methods:

createWidget()
  .render()

Equivalent to:

const widget = new ListWidget()
Script.setWidget(widget)

Note that you cannot pass the wrapper object directly to the Script.setWidget() method. Instead you should call the .render() method of the wrapper object.

Preview widget

createWidget()
  .presentMedium()

Set property on view

Instead of assigning the value to the property directly, you should call the method that has same name as the property with the value you want to assign as the argument:

createWidget()
  .backgroundColor(new Color("#F77"))
  .render()

Equivalent to:

const widget = new ListWidget()
widget.backgroundColor = new Color("#F77")
Script.setWidget(widget)

Call methods on view

All methods are chainable, except .present...() methods, which will return a promise that resolves when preview is dismissed:

createWidget()
  .setPadding(10, 10, 10, 10)
  .backgroundColor(new Color("#F77"))
  .presentMedium()
  .then(() => console.log("dismissed"))

Equivalent to:

const widget = new ListWidget()
widget.setPadding(10, 10, 10, 10)
widget.backgroundColor = new Color("#F77")
widget.presentMedium()
  .then(() => console.log("dismissed"))

Add view

To add view, call the same .add...() methods:

createWidget()
  .addText("hello world")
  .render()

Equivalent to:

const widget = new ListWidget()
const text = widget.addText("hello world")
Script.setWidget(widget)

But there's something different here. Since all methods are chainable, the .add...() methods will return the parent view itself instead of the added view.

So how to modify the added view? You can do that by providing a callback as the last argument, which will receive the added view as argument. You can then modify the added view in that callback:

createWidget()
  .addText("hello world", text => text.centerAlignText())
  .render()

Equivalent to:

const widget = new ListWidget()
const text = widget.addText("hello world")
text.centerAlignText()
Script.setWidget(widget)

The added view received by the callback is also a chainable wrapper object, so you can do this:

createWidget()
  .addText("hello world", text => text
    .centerAlignText()
    .textColor(new Color("#F77"))
  )
  .render()

Equivalent to:

const widget = new ListWidget()
const text = widget.addText("hello world")
text.centerAlignText()
text.textColor = new Color("#F77")
Script.setWidget(widget)

You can also embed views in stacks the same way:

createWidget()
  .addStack(stack => stack.layoutHorizontally()
    .addText("hello world", text => text
      .centerAlignText()
      .textColor(new Color("#F77"))
    )
  )
  .render()

Equivalent to:

const widget = new ListWidget()
const stack = widget.addStack()
stack.layoutHorizontally()
const text = stack.addText("hello world")
text.centerAlignText()
text.textColor = new Color("#F77")
Script.setWidget(widget)

Add additional logic to the chain

Sometimes we need to render views conditionally or render a list of views. The chaining methods are not very good at this but you can call .doTo() with a callback that receives the view itself as argument to embed some additional logic in the chain:

const showDate = true
const list = ["Maina", "Aya"]
createWidget()
  .doTo(widget => showDate && widget.addDate(new Date))
  .addStack(stack => stack.layoutVertically()
    .doTo(stack => list.forEach((item) => stack.addText(item)))
  )
  .render()

Access the raw view instance

You can access the raw view instance by the raw property of the wrapper object:

Script.setWidget(createWidget().raw)
// Example Usage
createWidget()
.backgroundColor(new Color("#F77"))
.addStack(stack => stack.layoutVertically()
.addDate(new Date(), date => date.applyTimerStyle())
.addSpacer(20)
.addStack(stack => stack.layoutHorizontally()
.doTo(stack => Array(7).fill().forEach((e, i) => stack
.addText((i + 1) + "", text => text.textColor(Color.white()))
))
)
)
.render()
// Copy the function below to use in your scripts.
function createWidget() {
const widget = new ListWidget()
const wrap = (view, isWidget) => {
let wrapped
const wrapWith = (key, exec) => [key, (...args) => (exec(...args), wrapped)]
wrapped = Object.fromEntries(Object.keys(view).map(key => {
if (typeof view[key] === "function") {
if (key.startsWith("add")) return wrapWith(key, (...args) => {
let cb
if (args.length > 0 && typeof args[args.length - 1] === "function") cb = args.pop()
const addee = view[key](...args)
if (cb) cb(wrap(addee))
})
else if (key.startsWith("present")) return [key, () => view[key]()]
else return wrapWith(key, (...args) => view[key](...args))
} else return wrapWith(key, newValue => view[key] = newValue)
}).concat([
wrapWith("doTo", cb => cb(wrapped)),
...(isWidget ? [wrapWith("render", () => Script.setWidget(widget))] : []),
["raw", view]
]))
return wrapped
}
return wrap(widget, true)
}
// Copy the function below to use in your scripts.
function createWidget() {
const widget = new ListWidget()
const wrap = (view, isWidget) => {
let wrapped
const wrapWith = (key, exec) => [key, (...args) => (exec(...args), wrapped)]
wrapped = Object.fromEntries(Object.keys(view).map(key => {
if (typeof view[key] === "function") {
if (key.startsWith("add")) return wrapWith(key, (...args) => {
let cb
if (args.length > 0 && typeof args[args.length - 1] === "function") cb = args.pop()
const addee = view[key](...args)
if (cb) cb(wrap(addee))
})
else if (key.startsWith("present")) return [key, () => view[key]()]
else return wrapWith(key, (...args) => view[key](...args))
} else return wrapWith(key, newValue => view[key] = newValue)
}).concat([
wrapWith("doTo", cb => cb(wrapped)),
...(isWidget ? [wrapWith("render", () => Script.setWidget(widget))] : []),
["raw", view]
]))
return wrapped
}
return wrap(widget, true)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment