WhackDS
WhackDS (Design Suite) is the primary method through which creators extend the Document Object Model, the closed UiComponent hierarchy, with their own reactive UI components.
Relationship with other technologies
WhackDS can be thought as the "React.js" counterpart of the Whack framework. Certain advantages compared to React.js include:
- No stale captures of states, fixtures (or React.js "refs") and context reflections.
- Built-in style sheet integration.
- Default memoization
Memoization
Components are memoized by default, unlike in React.js.
To customize the equality of a field during memoization, use #[eq(...)]:
#![allow(unused)] fn main() { #[eq(a == b)] x: f64, }
Example
#![allow(unused)] fn main() { use whack::{components::*, ds}; #[ds::component] pub fn Component1( /// To which fixture to bind the contained `Label`. #[fixture_option] bind: UiComponent, ) { let x = ds::use_state::<f64>(|| 0.0); let button = ds::use_fixture<Option<UiComponent>>(|| None); // Effect ds::use_effect!({ (*button).unwrap().focus(); }); ds::xml! { <w:VGroup s:color="orange"> // Style sheet scoped to that VGroup tag. <w:Style> r###" :host { background: red; } "### </w:Style> <w:Label bind={bind}>"counter: {x.get()}"</w:Label> <w:Button bind={button} click&={x.set(x.get() + 1);}> "increase x" </w:Button> </w:VGroup> } } }
<w:Style> tag
A component tag accepts <w:Style> tags only if it defines a stylesheets: Option<whack::ds::StyleSheets> field.
The creator can separate a large CSS into a file. This uses include_str!() under the hood and still uses lazy parsing for efficiency.
#![allow(unused)] fn main() { <w:Style source="skins/button.css"/> }
Style tags accept parameters...
#![allow(unused)] fn main() { <w:Style color="red"> r###" :host { background: param(color); } "### </w:Style> }
Or use b:x= for passing a Binary (common for icons).
The creator may spread existing style sheets:
#![allow(unused)] fn main() { <w:Style extend={stylesheets}/> // stylesheets: Option<whack::ds::StyleSheets> }
The creator may pass a HashMap<String, String> as arguments to a style sheet...
#![allow(unused)] fn main() { <w:Style map={m}> r###" :host { background: param(color); } "### </w:Style> }
... and/or a HashMap<String, Binary> through b:map=.
Shorthand attributes
#![allow(unused)] fn main() { // bool x // x={true} // Event handlers event&={println!("hi");} // equivalent to `event=fn{|event, detail|{println!("hi");}}` // Functions f=fn{f} // equivalent to `f={Handler(Rc::new(f))}}` // Inline styles s:style_property="css exp" }
Fixtures
Fixtures are used similiarly to states, except they do not trigger a re-render of an UI component when the value is overwritten.
Note: For who is familiar with React.js, fixtures are the WhackDS equivalent of refs.
Option<whack::ds::Fixture<T>> is used for passing fixtures down to components. An actual fixture is represented by whack::ds::FixtureFixture<T>.
It is common to do the following in whack::ds::xml!:
#![allow(unused)] fn main() { bind={|val| { my_ref.set(val.clone()); match params.bind { Some(whack::ds::Fixture::Fixture(ref r)) => { r.set(val); }, // Receiver(Rc<dyn Fn(T) + 'static>) Some(whack::ds::Fixture::Receiver(ref f)) => { f(val); }, } }} }
Fields
Component fields are the parameters it receives in the whack::ds::component attribute macro.
String
A field
#![allow(unused)] fn main() { x: String, }
accepts anything impl AsRef<str>. The program might panic if unspecified.
String (optional)
A field
#![allow(unused)] fn main() { x: Option<String>, }
accepts anything impl Into<Option<impl AsRef<str>>>.
In this case a more complex setter is generated, like:
#![allow(unused)] fn main() { pub fn set_x<S: AsRef<str>>(&self, s: impl Into<Option<S>>) { let s = s.into().as_ref().map(|s| s.as_ref()); self.x.replace(s.map(|s| s.to_owned())) } }
Function
#![allow(unused)] fn main() { x: impl Fn(...), x: impl Fn(...) -> T, }
yields a Handler<dyn Fn(...) + 'static> field.
During memoization, equality, by default, always returns true to avoid re-rendering the component. It is assumed that these functions are always never changing.
For event handlers, you may want to have a signature like |event, detail| { ... }, so it can be used with the &= attribute notation.
Function (optional)
#![allow(unused)] fn main() { x: Option<impl Fn(...)>, x: Option<impl Fn(...) -> T>, }
yields an Option<Handler<dyn Fn(...) + 'static>> field.
During memoization, the equality comparison of the function objects always returns true by default to avoid re-rendering the component. It is assumed that these functions are always never changing.
For event handlers, you may want to have a signature like |event, detail| { ... }, so it can be used with the &= attribute notation.
Implicit
A field
#![allow(unused)] fn main() { x: T, }
... accepts anything impl Into<T>. The program might panic if it is unspecified.
For primitives (like f64 and bool), generates a Cell; for other types, generates a RefCell.
A setter like this is generated:
#![allow(unused)] fn main() { pub fn set_x(&self, v: impl Into<T>) { self.x.replace(Some(v.into())); } }
Implicit (optional)
A field
#![allow(unused)] fn main() { x: Option<T>, }
... unlike implicit fields, doesn't panic if unspecified, defaulting to None.
A setter like this is generated:
#![allow(unused)] fn main() { pub fn set_x<U: Into<T>>(&self, v: impl Into<Option<U>>) { let v = v.into().as_ref().map(|v| v.into()); self.x.replace(v); } }
Fixture
A field
#![allow(unused)] fn main() { #[fixture] x: T, }
is equivalent to a whack::ds::Fixture<T> field, which is either a fixture fixture (a physical fixture) or a fixture receiver function. The program might panic if unspecified.
Fixture (optional)
A field
#![allow(unused)] fn main() { #[fixture_option] x: T, }
is equivalent to a Option<whack::ds::Fixture<T>> field, which is either a fixture fixture (a physical fixture), or a fixture receiver function, or unspecified (None).
Field default
Specifying a default value for a field prevents the program from panicking for omitting the field. Option-typed fields have None as default, so they do need an explicit default.
#![allow(unused)] fn main() { #[default = v] x: T, }
Hooks
use_state(...), use_fixture(...), use_context(...) and use_effect!(...) are examples of built-in hooks.
Hooks must only appear at the top-level of a WhackDS component, and must not be run conditionally.
Effects
The use_effect! hook (effect) is used for either:
- Running code when component mounts and unmounts (the cleanup)
- Running code when a list of values change (e.g. states, derived values and parameters)
The effect must explicitly state its dependencies (i.e. it should run when a specific value changes), or it has no dependencies by default.
Here are some examples:
#![allow(unused)] fn main() { whack::ds::use_effect!({ // runs whenever component mounts }); whack::ds::use_effect!({ // runs whenever component mounts // (...) || { // cleanup. // runs when component unmounts } }); whack::ds::use_effect!({ // runs whenever the `x` state changes }, [x.get()]); whack::ds::use_effect!({ // runs whenever the `variant` parameter changes // (...) || { // cleanup. // runs either before re-running this effect, // or when component is unmounted. } }, [variant]); }
Item key
When interpolating a list of nodes inside the xml! macro, make sure to specify the special key attribute with an unique ID for each item.
Data attributes
Native <w:*> tags constructing UI components may include arbitrary data- prefixed attributes.