# Role Model

```mermaid
---
title: Keeping the Layers Separate
---
flowchart LR

HTML[HTML & Styling] --> Logic[Business Logic]
Logic --> Data
Logic --> HTML
Data  --> Logic

```
**The philosophy**

Separation of duties

* Styles
  * Elements should avoid using style="..."
  * Normally styles should not be set by code directly
* HTML
  * HTML structure should be as semantic as possible
  * HTML should not contain business logic or styles
* Business Logic
  * Business logic connects the DOM elements to the data
  * DOM manipulation in the business logic should be as limited as possible 
* Data
  * Data is presented to the UI via the business logic
  * Data should never contain business logic functions
  * Styles and HTML should only rarely appear in data and only in special situations (i.g. color swatches, etc.)

**The UI contract via data roles**

Data roles provide landmarks for the logic layer to connect data to the HTML.

```HTML
...
<fieldset data-role="totals">
    ...
    <label data-role="subtotal-label">Subtotal</label>
    <input type="text" data-role="subtotal-input"/>
    ...
</fieldset>
...
```
```javascript
const data = observable({
    ...
    subtotal: 123.45
    ...
})

...

const totals = role("totals");

totals.role("subtotal-input")
    .attribute("id", id("subtotal"))
    .model(prop(() => data, "subtotal"));

totals.role("subtotal-label")
    .attribute("for", getId("subtotal"));

...

```


## Data

The data layer is just data. It never contains logic, and it should only contain styles and HTML under very special circumstances.

### observable

The observable function turns a plain old JS object into an observable object.

```javascript
const state = observable({
    property1: null,
    prop2: "test",
    prop3: {
        A: 1.001
    }
    ...
});
```

### prop

The prop function creates a getter and setter for object properties. It is used anytime you want to pass a getter and setter for a property as a single parameter where there receiver does not have to know any details of the object model.

```javascript
const state = observable({
    ...
    prop3: {
        A: 1.001
    }
    ...
});

const p3 = prop(() => state.prop3, "A");

console.log(p3());  
// 1.001

console.log(p3.value);
// 1.001

p3.value = 32;

console.log(p3.value)
// 32

console.log(state.A)
// 32
```

The first argument of prop does not have to be a function. It may be a constant, but be careful as deep changes to the object model will not be followed.

```javascript

const p3 = prop(state.prop3, "A");

console.log(p3.value);
// 1.001

state.prop3 = {
    A: 1
}

console.log(p3.value);
// 1.001

console.log(state.A)
// 1

```

### reactTo

The reactTo function lets you react to changes in a value with a handler function. It can take a function as input,

```javascript
const state = observable({
    ...
    prop3: {
        A: 1.001
    }
    ...
});

reactTo(() => prop3.A).with((value) => console.log(value));
// 1.001

prop3.A = 12;
// 12

```

or a constant,

```javascript
...

reactTo(prop3.A).with((value) => console.log(value));
// 1.001

prop3.A = 12;
// nothing is logged

```
or a property.

```javascript
...

reactTo(prop(() => prop3, "A")).with((value) => console.log(value));
// 1.001

prop3.A = 12;
```

#### withReset

The function reactTo has an additional reaction mode `withReset` that does the same thing as `with` except it cleans up any disposable objects (like event listeners) created inside the handler each call. This allows you to have conditional initialization inside the handler that will be reinitialized every time the value being reacted to changes.

```javascript

reactTo(() => state.isLoaded).withInit(loaded => {
    const section = role("section");

    if(loaded) {
        
        // do conditional initialization steps 
        
        ...

        section.class("loading", true);
    } else {

        section.class("loading", false);
    }
})

```

Calling `with` is less costly, performance wise, than calling `withReset` so only use the reset version when necessary and appropriate.

You can also chain as many `with` calls together as you like.

```javascript
reactTo(() => state.data)
    .with(boilerPlate) // delegate repetitive initialization
    .with((data) => {
        // do bespoke one-time initialization 
    })
    .withInit((data) => {
        // do conditional initialization here
        if(...) {
            ...
        } else {
            ...
        }
    });

```

### debouncing

The `with` handlers are always debounced to the next tick. If more delay is needed just provide the delay as a second parameter in milliseconds.

reactTo(() => state.data)
    .with(() => {
        // delayed operation such as auto-save
        ...
    }, 5000) 


## DOM Connection

Three bootstrap connector functions allow you to do selects as if using querySelector or querySelectorAll on the document itself.

### role

Find the role by role name, i.e. `[role-name='<roleName>']`.

```html
<!DOCTYPE html> 
<html>
    ...
    <body>
        ...
        <section data-role="account">
            ...

```

```javascript

role("account")
    .class("loading", () => context.account.isLoading)
    ...

```

#### optionality

Roles are assumed to exist on the html and if a role is not found, an exception is raise. You can override this behavior by adding a configuration option { optional: true }.

```javascript

role("account", { optional: true})
    .class("loading", () => context.account.isLoading)
    ...

```

If a role is optional and not found, any api functions called on that role will be no-ops.

### select 

Select works exactly the same way as role but uses a bespoke selector instead of a role selector.

```html
<!DOCTYPE html> 
<html>
    ...
    <body>
        ...
        <section>
            ...

```


```javascript

select("body section")
    .class("loading", () => context.account.isLoading)
    ...

```

#### optionality

Optionality for `select` works the same way as it does for `role`.

```javascript

select(".input-helper", { optional : true })
    .class("invalid", () => !state.input.isValid)
    ...

```

### selectAll

The `selectAll` function lets you initialize multiple elements that have the same selector.

```javascript

selectAll(".currency", item => {
    item.attribute("format", () => ctx.currency.format);
});

```

## Role API

All role API methods are chainable.

### role, select, selectAll

Selector functions can be chained, the scope of the selection is hierarchical.

```javascript

// find the cart totals element
const main = select("aside.cart-totals");

// select the subtotal role's, span element
main.role("subtotal").select("span")
    .text(() => ctx.totals.subtotal);

main.role("tax")
    // show the tax role conditionally
    .show(() => !ctx.wholesale)  
    // within the tax role connect the text of the span
    .select("span").text(() => ctx.totals.tax);

main.role("total").select("span")
    .select("span").text(() => ctx.totals.total);

```

### class

Use the class method to dynamically set a class on the selected element.

```javascript

current.role("input")
    .class("highlight", () => data.isHighlighted)

```

### attribute

Use the attribute method to dynamically set the value an attribute.

```javascript

current.role("input")
    .attribute("aria-invalid", () => data.input.valid ? null : "true")

```

### style

Use the style method to dynamically set a style.

```javascript

product.role("swatch")
    .style("color", () => product.swatchColor)

```

### show

The show method is a shortcut to hide or show the current element. By default it hides an element by adding `display: none;` to the elements styles.

```javascript

cart.role("tax")
    .show(() => totals.tax > 0)

```

#### hideWithClass

The show method's behavior can be overridden to hide by adding a class that hides the element. This is a global setting.

```javascript

// call once to change the show/hide behavior
// pick any class name the makes sense given your existing css.
hideWithClass("cloak"); 

...

cart.role("tax")
    .show(() => totals.tax > 0) 

```

### text

Use text to connect the textContent of the current element.

```javascript

role("message")
    .text(() => ctx.message)
    .show(() => ctx.message?.length > 0)

```

### html

Use html to connect the innerHTML of the current element.

```javascript

role("message")
    .html(() => ctx.message)
    .show(() => ctx.message?.length > 0)

```

### value

Use value to connect the value of the current element.

```javascript

role("message")
    .value(() => ctx.message)
    .show(() => ctx.message?.length > 0)

```

### on

Use the on method to create an event listener

```javascript

const validate = () => {
    ...
}

role("input")
    .on("blur", validate)

```

The on method uses an internal on helper to do its work. This helper is expose as `on` and is available outside the role api.

```javascript

on(window, "resize", event => {
    // react to a window resize event
})

```

#### event options

Event listener options work exactly like the standard `addEventListener` method.

```javascript

const validate = () => {
    ...
}

role("input")
    .on("blur", validate, { capture: true; })

```

#### listen for multiple events

You can listen to multiple types of events on the same listener using one call.

```javascript

role("input")
    .on("focus blur", event => {
        if(event.type === "focus") {
            ...
        } else if(event.type === "blur") {
            ...
        }
    })

```

### onAttribute

Use the onAttribute listens for changes to attributes.

```javascript

role("input")
    .onAttribute("format", format => {
        // somebody changed the input's format attribute
        ...
    })

```

### model

Mode connects various DOM input element types to the data model. Values changes happen on change/blur depending on the element. The model is connected via the `prop` helper function.

```javascript

role("input")
    .model(prop(() => ctx.input, "value"))

```

#### radio buttons

Radio buttons require special handling to connect the model. But it is still very easy to do.

```html
<fieldset data-role="radio">
    <legend>Language preference:</legend>
    <label>
        <input type="radio" name="language" value="English" checked />
        English
    </label>
    <label>
        <input type="radio" name="language" value="French"/>
        French
    </label>
    <label>
        <input type="radio" name="language" value="Mandarin"/>
        Mandarin
    </label>
    <label>
        <input type="radio" name="language" value="Thai"/>
        Thai
    </label>
</fieldset>

```

```javascript

root.role("radio")
    .selectAll("[name='language']", rb => 
        rb.model(prop(() => ctx, "languageName"))
    );

```

### for

The for method manages a iterable list of elements for you utilizing a HTML template for connecting to the DOM.

```html
<select aria-label="Color" data-role="color-theme-picker">
    <template>
        <option></option>
    </template>
</select>
```

```javascript

root.role("color-theme-picker")
    .model(prop(() => ctx, "themeColor"))
    .for(() => ctx.themes, (option, theme) => option
        .attribute("selected", () => theme.color == ctx.themeColor ? "" : null)
        .text(theme.color)
    );

```

The for method manages changes to the list just as one would expect. Splicing, sorting, and total replacement or deletion are all handled gracefully.

### connect

Use connect to call reusable components in a standard way.

```html
...
<input type="text" aria-describedBy="input-helper=text" data-role="text-input" /> 
<small id="input-helper=text">Must be a number.</small>
...
```
```javascript

// connecting the component

root.role("text2-input")
    .connect(currencyInput(prop(state, "text2")))
    .attribute("format", () => ctx.format || null);

```
```javascript

// the reusable currencyInput component, a complex example using typescript 

const formatCurrency = (amount : number | undefined, format: string | null) => {
    if(amount === undefined) {
        return "";
    }
    if(format === "k") {
        return new Intl.NumberFormat('en-US', {
            style: 'currency',
            currency: 'USD',
            minimumFractionDigits : 3
        }).format(amount/1000) + "k";
    } else if(format === "M") {
        return new Intl.NumberFormat('en-US', {
            style: 'currency',
            currency: 'USD',
            minimumFractionDigits : 3
        }).format(amount/1000000) + "M";
    } else {
        return new Intl.NumberFormat('en-US', {
            style: 'currency',
            currency: 'USD',
        }).format(amount);
    }
}

const currencyInput = (
    amount : Property<number>
) => (root : Role) => {
    const usdRegex = /^\-?\$?(\d{1,3}(\,\d{3})*|(\d+))(.[0-9]*)?k?M?$/

    const state = observable({ 
        value: amount.value?.toString() ?? "",
        format: null,
        valid: true
    }) as { 
        value : string; 
        format : string | null; 
        valid : boolean;
    };

    reactTo(amount).with((value? : number) => state.value = formatCurrency(value, state.format));
    reactTo(() => state.format).with(format => state.value = formatCurrency(amount.value, format ?? null));

    root.on("keydown", ((e : KeyboardEvent) => {
            if (!e.ctrlKey && (!"$1234567890.-,".includes(e.key) && e.key.length === 1) 
                || e.key === "Enter") {
                e.preventDefault();
            }
        }) as (e : Event) => e is KeyboardEvent)
        .onAttribute("format", (format) => state.format = format)
        .model(prop(state, "value"))
        .on("focus", () => {
            if(state.valid) {
                state.value = amount.value?.toString() ?? "";
            }
        })
        .on("blur", () => {
            if(state.value.match(usdRegex)) {
                state.valid = true;
                amount.value = Number(state.value.replace(/\,|\$*/g, ""));
                state.value = formatCurrency(Number(state.value.replace(/\,|\$*/g, "")), state.format);
            } else {
                state.valid = false;
            }
        })
        .attribute("aria-invalid", () => state.valid ? null : "true");
}

```

## Utilities 

### id, getId

The id function helps create unique html IDs. You can optionally name them or change their default format. 

The getId retrieves the most recently created ID (optionally by name).

```javascript

root.role("text-input")
    .model(prop(ctx, "text"))
    .attribute("id", id());

root.role("input-label")
    .attribute("for", getId());

```

Fancier ID creation example with all the optional parameters.

```javascript

root.role("text-input")
    .model(prop(ctx, "text"))
    .attribute("id", id("input", (id) => `ID-${id}`));

root.role("input-label")
    .attribute("for", getId("input"));

```

### log

A global value logger is provided for debugging purposes. It logs to the console when a value changes.

```javascript
log(() => data.prop.value);
```

### setDebug

Use setDebug to log detailed debugging messages to the console. Acceptable configuration values are true (the default when setDebug is called), false, or "verbose".

```javascript
setDebug("verbose");
```

## Lifetime Helpers

Internally the library uses lifetime helpers to handle disposing of event and object change listeners. These methods are exposed in case you need to use them.

### shouldDispose

As a general practice, if you create something that needs disposed, you should call shouldDispose immediately with a disposer method that should be called to clean up the related resource.

```javascript

reactTo(() => data.doBackgroundJob).withReset((doJob) => {

    if(doJob) {
        shouldDispose(setInterval(() => {
            ... 
        }, 10000));
    }
})


```

### disposable/dispose

Disposable "blocks" allows finer grained control over what gets disposed when.

Disposable blocks are nestable. 

```javascript

// you can use any object to collect disposer methods
// use whatever makes sense. 
const myDisposeContext = Symbol("context");

// inside disposable you become responsible for the lifetime of disposable resources
// you are telling the system that you are going to dispose myDisposeContext 
// at some point in the future
disposable(myDisposeContext, () => {

    // disposer associated with myDisposeContext
    shouldDispose(setInterval(() => {
        ... 
    }, 10000));

    ...

    if(createResource) {
        const resource = createResource(...);

        // disposer associated with myDisposeContext
        shouldDispose(() => resource.dispose());

        ...
    }
})

...

// dispose setInterval and possibly resource as well
dispose(myDisposeContext);

```



