Giter Club home page Giter Club logo

sunmao-ui's Introduction

Sunmao

Apache-2.0 GitHub issues Github Stars Join the chat at Slack

Sunmao(榫卯 /suən mɑʊ/) is a front-end low-code framework. Through Sunmao, you can easily encapsulate any front-end UI components into low-code component libraries to build your own low-code UI development platform, making front-end development as tight as Sunmao("mortise and tenon" in Chinese).

中文

DEMO

The offcial website of Sunmao is developed by Sunmao, try it from here: Sunmao website editor

We also provide an open-to-use template: Sunmao Starter Kit

Why Sunmao?

Reactive rendering low-code framework

Sunmao chooses a reactive rendering solution that is easy to understand and has excellent performance, making Sunmao intuitive and quick to start.

Powerful low-code GUI editor

Sunmao has a built-in GUI editor, which almost includes all the capabilities that a complete low-code editor should have.

Extremely Extensible

Both the UI component library itself and the low-code editor support custom extensions. Developers can register various components to meet the needs of application and continue to use the existing visual design system.

Type Safety

You are in type safety both when developing Sunmao components and when using the Sunmao editor. Sunmao heavily uses Typescript and JSON schema for a great type system.

For more details, read Sunmao: A truly extensible low-code UI framework.

Tutorial

Sunmao users are divided into two roles, one is developer and the other is user.

The responsibilities of developers are similar to those of common front-end developers. They are responsible for developing UI components and encapsulating common UI components to Sunmao components. Developers need to write code to implement the logic of components.

The user's responsibility is to use the Sunmao components encapsulated by developers to build front-end applications in the Sunmao low-code editor. Users do not need front-end knowledge and programming skills. They can finish building the application through UI interaction only.

We have prepared two tutorials for user and developer. The user only needs to read the user's tutorial, while the developer must read both.

local development

Start

yarn
cd packages/editor
yarn dev

Test

yarn test:ci

Build

yarn

When you run the runtime or editor locally, if you modify the code of other packages, you must rebuild the modified package, otherwise, the runtime and editor will still run the old code.

License

Apache-2.0

sunmao-ui's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

sunmao-ui's Issues

Two hanlders in one event trait bug

I have 2 hanlders in a event trait. The first hanlder sets a value. The second handler alert this value.
The expectation is to alert the latest value. But actually, it will alert the old value.
This is because there is no eval process between 2 hanlders, so the second hanlder can only use last evaled value not the new value.

截屏2021-11-04 下午1 44 33

.

Implement fetch trait

  • fetch data in the trait
  • set loading, error, data to the store
  • required props:
    • url
    • method
  • optional props:
    • lazy
    • headers
    • querys
    • body
  • callback:
    • onSuccess
    • onFailure
  • trigger method

Copy paste component

What should be copied when copy a component?

  • properties
  • trait(except slot trait)
  • children

re-branding: sunmao-ui

In the previous discussion, the team agreed with the new branding: sunmao-ui(Chinese '榫卯' in PinYin).

Re-branding checklist

  • rename the repo
  • rename packages
  • refactoring code

Better drag interaction

  • can drag component into stack or grid to create component
  • can drag component to move component

Module Mechanism

Use cases

  • You build a complicated form by meta-ui and want to reuse it in multiple meta-ui application.
  • You build a fancy listItemView and want to use it in list.

module vs component

Module is made up of meta-ui component. Component is created in react.

Features of module

Basicly, module can be used like a component. Module can receive properties and expose state & events.

Tasks

  • define spec and schema
  • render module as item of table td and listItem
  • render module individually
  • create module in editor
  • edit module in editor

Should string number be converted to number when eval?

When evaling number string, we automatically convert it to real number.
But sometimes, we need it to be number string. For example, in chakra radio, chakra will change number to string number.
Is it nessesary to keep this logic?

Schema text editor

Although we are making a GUI editor, a text editor may be also very useful in some use cases.

Add transformer to fetch trait

In most cases, the response of api cannot be used directly. So a transformer function is needed to do some convert job.

Inject custom dependency in application

Sometime I need to use some custom dependecies or functions in expression. So a mechanism is needed to inject dependency to eval scope. It may work like registry.

expression evaluation v2

The design of expression evaluation

Status Quo

Expression is a core feature of sunmao-ui. For example, when a user defines an expression like this:

{{ btn_1.value.toUpperCase() }}

sunmao-ui will:

  1. Look up the state store and find the state of the component with id btn_1.
  2. Get the state of btn_1 and evaluate .value.toUpperCase(), which will access the key value and call toUpperCase to the value.

Beyond this, we still have another two design goals to accomplish:

  1. Reactive. The UI uses the result of this expression should re-render when btn_1.value changed.
  2. Performant. Other changes in the state store should not trigger this UI re-render, for example, btn_2.value and btn_1.other_key.

So in the first version of expression evaluation, we use zustand under the hood, but this implementation soon encountered some problems.

non-transparent usage
Currently, components need to use the useExpression hook to get the reactive result from the state store.
There are two main drawbacks:

  1. It costs more time to 'wrap' an existing UI component to a sunmao-ui one. Especially for those who have many props and all of them want to support expression.
  2. It breaks the type system of sunmao-ui. For example, props with type 'array of numbers' in the runtime need to declare it also accepts a string.

performance
Zustand builds its reactive model with the broadcasting approach. We will dive into a comparison of different designs of reactive models later. For now, the conclusion is it's hard to optimize performance with broadcasting-based implementations.

Reactive model

There are two kinds of implementations of the JS reactive model.

Broadcasting

In this implementation, all the consumers will subscribe to the state store. And whenever the state store was modified, it will broadcast to all the subscribers with the latest state.

This makes all the subscribers 'reactive' but is not performant because they may get notified of the changes they do not need.

Two common patterns to solve the performance issue of broadcasting are equality-checking and partition.

equality-checking
The reactive model will check whether the current value is equal to the previous value. If they are the same, the broadcasting will be skipped.

But equality-checking also has an overhead with itself. So most reactive models are using 'strict equality' or 'shadow equality' for checking. Both of them lose effectiveness when the values have 'deep equality' but have different references, like [{ v: 1 }] !== [{ v: 2 }].

partition
Partition means to split the state store into many small state stores. After partition, the range of broadcasting will be limited by one store.

But partition is not friendly to the expression usage. Because it's hard to extract which store has been used from a raw expression.

Dependency Tracking

Dependency tracking means collecting the dependencies during consumption. So it constructs an accurate dependency map between consumers and states in the store.

But tracking is not a simple thing in JS, so we'd better using some battle-tested library instead of building one from the scratch.

build time tracking
Svelte uses this approach by statically analyze code and tracking dependencies by 'assign statements'.

runtime tracking
Vue uses this approach by wrapping values into JS Proxy and hijacked all the accesses.

Since Vue3 provides a standalone library @vue/reactivity just do what we want without introducing build time work, we decided to implement the new expression evaluation with it.

Evaluation scope

Previously, we copy store states to the global scope before evaluation and tear down after it.

But the copy process will access all the states which lets the dependency tracker think this evaluation depends on all the states. So we need to change the scope evaluation to a custom scope without shadow copy.

A solution with the with keyword looks like this:

function maskedEval(expression, mask) {
  return new Function(`with(this) { return ${expression} }`).call(mask);
}

maskedEval('console.log(a)', { a: 1 }) // 1

implement editor component form

  • can edit component id
  • can edit properties(include nested properties)
  • ignore some special fields(like layout in grid, columns in table...)
  • use input

Update Spec

Component Spec

  1. remove acceptTrait
  2. add styleSlots and events. Both of them are simple string array.
 type Component = {
  version: string;
  kind: 'Component';
  metadata: Metadata;
  spec: ComponentSpec;
};

type ComponentSpec = {
  properties: JSONSchema;
  state: JSONSchema;
  methods: MethodSchema[];
  styleSlots: string[];
  slots: string[];
  events: string[]
};

Trait Spec

  1. add returnProps. returnProps are keys of the object returned by trait. For now, there are only 2 keys: styles and callbackMap. Component can only deal with these 2 props.
 type Trait = {
  version: string;
  kind: 'Trait';
  metadata: Metadata;
  spec: TraitSpec;
};

type TraitSpec = {
  properties: JSONSchema;
  state: JSONSchema;
  methods: MethodSchema[];
  returnProps: string[]// styles, callbackMap...
};

implement List component

  • support custom listItem template
  • support access itemRow data in expression
  • support pagination
  • enhance expression to support both {{ }} and itemRow

new trait implementation

Chinese Version: https://thoughts.teambition.com/share/6124b6fdf1962900423f2e5e

  • New Trait Implementation
  • Refactor Validation Trait
  • Refactor Event Trait
  • Refactor State Trait
  • Refactor Hidden Trait
  • Global Event Handler
  • Adaptor between Component & Trait

Why we need new implementation

  1. Trait is now implemented based on executing hooks in a loop, which is not the correct usage of hooks.
  2. Dynamic creating component in Trait hook will cause unnecessary mount & unmount. see demo on branch validation-bug

New implementation

  1. Components will received props from Traits. Components should implement some defined capabilities of components (such as how to deal with style, data, etc.)

  2. Trait is an ordinary function, not a Hook.

  3. Trait accepts the parameters from the Schema, and then returns an object. This object is implemented in accordance with defined capabilities of component.

  4. Merge all the objects returned by Trait and pass them to the component.

截屏2021-08-27 上午11 25 32

# Trait restrictions
  1. Each component must implement some interface to interact with traits

  2. Hook cannot be used in Trait (or the number of Trait cannot be changed dynamically?)

  3. Cannot use HOC in Trait

How to implement the existing Trait?

  1. Hidden

    1. Return style, use display: none
  2. State

    1. Return data, and the component calls mergeState to bind it to the store
  3. event

    1. Return the callback function. The timing of unsubscribe is controlled by the component.

'{{[]}}' expression causes infinite loop

Because every time '{{[]}}' is evaled, a new [] will generate, so that watch function treat is as a different value then trigger callback.

By the way, '{{{}}}' cannot be parsed correctly.

Schema validator

Before saving schema, we need validate in advance. If the broken schema is saved, sunmao-ui editor cannot render it any more.

ComponentWrapper in editor will broke style

Because ComponentWrapper wraps component with a div, the style may broke in some cases. For example, components in HStack will not auto stretch to fill the width of HStack.
And also because of this, applications may look different in editor and runtime.

implement hidden trait

Currently, we supported trait pass props to the component to which it attached.

To extend the usage scenario of trait, we will let the trait return a HOC as the wrapper of the component. With this feature, we can implement traits like hidden.

Support nested expression

When using module as template in list, we may write something like this {{ moduleName{{$listItem.id}}.stateValue }}. This expression cannot be parsed correctly now.

support multiple expression in single string

current implementation with multiple expression:

expression: `"constantA"+{{ variableA }} + "constantB" + {{ variableB }}`

expected:

expression: `constantA{{ variableA }}constantB{{  variableB }}`

implement editor structure tree

  • can display components structure
  • can remove component
  • can select component
  • can reorder component(use button)
  • can add new component in slot

Move node across levels

Now we can move node in the same level. But sometime we need move node across level.
I guess it will not be difficult since we just need modify slot name. The difficult part may be the UI.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.