@kamranayub bit.ly/NDCFlexibleUI
background by saragnzalez
@kamranayub
@kamranayub
or better yet, show you one...
Any resemblance to actual UI, legacy or modern,
is purely coincidental. These views are my own.
Source: Threadless
Horst Rittel and Melvin Webber defined a "wicked" problem as one that could be clearly defined only by solving it, or by solving part of it. This paradox implies, essentially, that you have to "solve" the problem once in order to clearly define it and then solve it again to create a solution that works.
Code Complete, Steve McConnell
item
product_title
street_date
{
"product_title": "Borderlands 3 Super Deluxe Edition",
"street_date": "2019-09-13T00:00:00Z"
}
This is a "consumer" way of thinking
product_title
rules
display
values
street_date
rules
display
values
{
"product_title": {
"description": "A product's title",
"default_rules": {
"required": true,
"min_length": 1,
"max_length": 1337,
"read_only": false
},
"display": {
"component_type": "single_line_text",
"help_text": null
},
"item_values": [
{
"id": "item1",
"value_raw": "Borderlands 3",
"value_human": "Borderlands 3"
},
{
"id": "item2",
"value_raw": "Borderlands 3: Super Deluxe Edition",
"value_human": "Borderlands 3: Super Deluxe Edition",
"override_rules": {
"required": false,
"read_only": true
}
}
]
},
"street_date": {
"description": "The date this item will be available for purchase",
"default_rules": {
"required": true,
"min_date": 1555818103157,
"max_date": null,
},
"display": {
"component_type": "date",
"help_text": "Must be later than today's date"
},
"item_values": [
{
"id": "item1",
"value_raw": "2019-09-13T00:00:00Z",
"value_human": "Sep 13, 2019",
"override_rules": {
"min_date": null
}
}
]
}
}
handles
primary
data
external data
caching layer
GraphQL Backend
schema
engine
data
resolution
React Frontend
state
management
client rule engine
query {
itemData(attribute_ids: ["genres"]) {
attributes {
id
default_rules {
required
read_only
}
display {
component_type
}
values(item_ids: ["item1", "item2"]) {
item_id
raw_value
human_value
override_rules {
required
}
}
}
}
}
AuthN / AuthZ
Default schema
Mapping (resolvers)
Load raw data
Load external data
{
id: "genres",
default_rules: {
required: true,
read_only: false
},
display: {
component_type: "multi_select"
},
values: [
{
item_id: "item1",
raw_value: 2,
human_value: "First-Person Shooter",
override_rules: null
}
]
}
Run rules
Modify schema
Merge resolved data
Return final schema
{
id: "genres",
default_rules: {
required: true,
read_only: false
},
display: {
component_type: "multi_select"
},
values: [
{
item_id: "item1",
raw_value: 2,
human_value: "First-Person Shooter",
override_rules: null
}
]
}
{
id: "genres",
default_rules: {
required: true,
read_only: false
},
display: {
component_type: "multi_select"
},
values: [
{
item_id: "item1",
raw_value: 2,
human_value: "First-Person Shooter",
override_rules: {
required: false,
read_only: true
}
}
]
}
Business rules need (most) of the data loaded to work with
Human data needs to be cached within and across requests for performance
Not all attributes have the same performance profile
Resolve data before running rule engine
Introduce a caching layer (Redis, memcache, etc.)
Try to collate attributes from different data sources
which provides
type ItemAttribute {
id: String!
display: AttributeDisplay
}
type AttributeDisplay {
component_type: ComponentType!
}
enum ComponentType {
single_line_text
multi_line_text
number
date
datetime
multi_option
}
Static typing
Powerful query capabilities
query data($attribute_ids: [String!]!, $item_ids: [String!]!, hydrate: Boolean!) {
attributes(ids: $ids) {
id
title: description
default_rules {
max_length
}
values(items: $item_ids) {
raw_value
human_value @include(if: $hydrate)
override_rules {
max_length
}
}
}
item_metadata(ids: $item_ids) {
last_updated
}
}
type AttributeRules {
required: Boolean
read_only: Boolean
max_length: Int
max_chars: Int @deprecated(reason: "Use `max_length` instead")
}
One version, field-level reporting
subscription item_updates(ids: ["item1"]) {
item_id
values {
raw_value
human_value
}
}
Subscriptions
Caching infrastructure
Field-level timing and analytics
Custom directives
Extensible architecture
you could totally use REST too
{
"attributes": [
{
"id": "product_title",
"display": {
"component_type": "single_line_text"
}
}
]
}
{
"attributes": [
{
"id": "product_title",
"default_rules": {
"read_only": true
},
"display": {
"component_type": "single_line_text"
}
}
]
}
{
"attributes": [
{
"id": "product_title",
"default_rules": {
"read_only": true,
"max_length": 50
},
"display": {
"component_type": "single_line_text"
}
}
]
}
{
"attributes": [
{
"id": "street_date",
"display": {
"component_type": "date"
}
}
]
}
{
"attributes": [
{
"id": "genres",
"display": {
"component_type": "multi_select"
}
}
]
}
query {
lookup(attribute_id: "genres") {
page_size
total
options(filter: "act") {
raw_value
human_value
}
}
}
Attribute
Group
"Editor"
e.g. product_title
e.g. dimensions (WxHxD)
e.g. complex attributes
Graph theory and math comes in handy!
{
product_title: [
{ item_ids: ["item1", "item2"], value: "Borderlands 3" },
{ item_ids: ["item3"], value: "Borderlands 3 Super Deluxe Edition" }
],
street_date: [
{ item_ids: ["item1", "item2", "item3"], value: "2019-09-13T00:00:00Z" }
]
}
Send
Compare
Validate
Persist
Publish
The save flow reuses much of the same schema infrastructure
Utilize the same schema relationships to load data
If one item succeeds but another fails, do you fail the entire request? Probably not.
Optimistic UI, exception-based corrections
(not as I do)
First, decide whether this is something you'll find value from. Do you have dozens or hundreds of data points to support?
See if JSON schema will work for you!
If it doesn't, then roll your own schema
@kamranayub
kamranicus.com