Navigation Menu

Skip to content

Commit

Permalink
open source repo
Browse files Browse the repository at this point in the history
  • Loading branch information
ryansolid committed Apr 25, 2018
0 parents commit a194f02
Show file tree
Hide file tree
Showing 18 changed files with 1,409 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
@@ -0,0 +1,3 @@
node_modules/
dist/
lib/
2 changes: 2 additions & 0 deletions .npmignore
@@ -0,0 +1,2 @@
src/
.rollup.config.js
21 changes: 21 additions & 0 deletions LICENSE
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2018 Ryan Carniato

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
142 changes: 142 additions & 0 deletions README.md
@@ -0,0 +1,142 @@
# Solid.js

Solid.js is yet another declaritive Javascript library for creating user interfaces. It is unopinionated about how you organize your code and focuses on synchronizing local state and keeping the DOM up to date. Unlike make popular recent libraries it does not use the Virtual DOM. Instead it opts to compile it's templates down to real DOM nodes and wrap updates in fine grained computations.

The project started as trying to find a small performant library to work with Web Components, that had easy interopt with existing technologies. This library is very inspired by Knockout.js and Surplus.js, but seeks to provide an interface that is more familiar to people used to using virtual DOM libraries.

To that end this library is based on solely 3 things: JSX precompilation, ES2015 Proxies, and the TC-39 Observable proposal.

A Simple Component could look like:

import Solid, { State } from 'solid-js'

MyComponent() {
state = new State({
users: [{
id: 1, firstName: 'John', lastName: 'Smith'
}, {
id: 2, firstName: 'Jane', lastNameL 'Smith'
}]
});

return (<>
<h1>Welcome</h1>
<ul>{
state.users.map(user => {
<li>{user.firstName} {user.lastName}</li>
}
}</ul>
</>);
}

Solid.root(=>
mountEl.appendChild(MyComponent())
);

## Solid State

It all starts with a State object. State objects look like plain javascript options except to control change detection you call their set method.

var state = new State({counter: 0});
state.set({
counter: state.counter + 1
});

You can also deep set:

var state = new State({
user: {
firstName: 'John'
lastName: 'Smith'
}
});

state.set('user', {firstName: 'Jake', middleName: 'Reese'});

But where the magic happens is with making computations. This done through State's select method which takes an Observable, a Promise, or a Function and maps it to a state property. The simplest form of passing a function will wrap it in our Selector Observable which is a computation that automatically tracks dependencies.

state.select({
displayName: () => {
return `${state.user.firstName} ${state.user.lastName}`;
}
})

console.log(state.displayName); // Jake Smith

The way this works is that the State instance is the entry point. When working in your setup code and event handlers you will be just dealing with plain POJO javascript objects. However inside a computation the State instance will return nested proxies to track gets, and so on. In the case of a render tree even if the State root object doesn't make it all the way down as long as you fetch it originally off your state from with the render it will start tracking.

Truthfully for something as trivial as a display name you wouldn't necessarily need a selector and could just inline the expression in the template. But this really comes in handy for reuse and more complicated scenarios. And can be the primary mechanism to interopt with store technologies like Redux, Apollo, RxJS which expose themselves as observables. When you hook up these selectors you can use standard methods to map the observables to grab the properties you want and the State object will automatically diff the changes to only affect the minimal amount.

state.select({
myCounter: Observable.from(store).map(({counter}) => counter)
})

There is also a replace method for state which instead of merging values swaps them out.

## Solid Rendering

So to accomplish rendering we use JSX for templating that gets compiled to native DOM element instructions. To do that we take advantage of the babel-plugin-jsx-dom-expressions which while converting JSX to DOM element instructions wraps expressions to be wrapped in our computeds.

JSX as a templating language brings a lot of benefits. The just being javascript goes beyond just not needing a DSL, but setting up closure based context instead of creating context objects. This is both much more performant and uses considerable less memory. The well defined AST lends well to precompilation. This works so well it almost feels like cheating. I believe it's a big part of bringing the same level of tooling to fine grained change detection libraries already enjoyed by Virtual DOM libraries.

## Components

Solid.js doesn't have an opinion how you want to modularize your code. You can use objects, classes, or composable functions. Since the core render routine only runs once function closures are sufficient to maintain state. The library was made in mind for Web Components though.

You could imagine making a base Component class that creates a State instance for the internal state and props, which the child then inherits. In that model Solid would look very similar to somthing like React.

class Component {
constructor () {
this.state = new State({})
this.props = new State({});
}

connectedCallback() {
this.attachShadow({mode: 'open'});
Solid.root(() => this.shadowRoot.appendChild(this.render());
}

attributeChangedCallback(attr, oldVal, newVal) {
this.props.replace(attr, newVal);
}
}

class MyComponent extends Component {
constuctor () {
@state.set({greeting: 'World'});
}
render() {
<div>Hello {state.greeting}</div>
}
}

But functional composition is just as fair game.

Component(fn) {
state = new State({});
props = new State({});
fn({state, props});
}

MyComponent({state}) {
state.set({greeting: 'World'});
return (<div>Hello {state.greeting}</div>);
}

Solid.root(() =>
element.appendChild(Component(MyComponent))
);

## Why?

For all the really good things that came with React and the Virtual DOM evolution of declarative JS UI frameworks it also felt like a bit of a step backwards. And I don't mean the backlash people had with JSX etc..

The thing is for all it's differences the VDOM libraries like React still are based around having a special data object even if they push the complexity to under the hood of rendering. The trade off is lifecycle functions that break apart the declarative nature of the data. At an extreme relying on blacklisting changes in multiple places for shouldComponentUpdate. Imposed boundaries on components to sate performance concerns. A UI model that rerenders every loop that while relatively performant conceptually is at odds with what you see is what you get (it's not a mutable declaration but a series of imperative functions).

So the proposition here is if the data is going to be complicated anyway can we use Proxies to move the complexity into it rather than the rendering. And through using standard reactive interopt we can play we can invite playing nice with others rather push the interopt point in userland.

## Status

This project is still a work in progress. Although I've been working on it for the past 2 years it's been evolved considerably. I've decided to open source this at this point to share the concept. It took discovering the approaches used by Surplus.js to fill the missing pieces this library needed to prove out it's concept. And now I believe we can have performance and Proxies.

I will be publishing some examples. And need to work more on Tests/Compatibility/Documenting the rest of the libary.
22 changes: 22 additions & 0 deletions package.json
@@ -0,0 +1,22 @@
{
"name": "solid",
"description": "A declarative JavaScript library for building user interfaces.",
"version": "0.0.1",
"author": "Ryan Carniato",
"main": "lib/solid.js",
"module": "dist/solid.js",
"scripts": {
"build-es": "rollup -f \"es\" -c -o dist/solid.js",
"build-cjs": "rollup -f \"cjs\" -c -o lib/solid.js",
"prepublishOnly": "npm run build-es && npm run build-cjs"
},
"devDependencies": {
"coffeescript": "2.x",
"rollup": "^0.58.1",
"rollup-plugin-coffee2": "^0.1.15",
"rollup-plugin-node-resolve": "^3.3.0"
},
"dependencies": {
"symbol-observable": "^1.2.0"
}
}
13 changes: 13 additions & 0 deletions rollup.config.js
@@ -0,0 +1,13 @@
import coffee2 from 'rollup-plugin-coffee2';
import nodeResolve from 'rollup-plugin-node-resolve';

export default {
input: 'src/index.coffee',
output: {
exports: 'named'
},
plugins: [
coffee2(),
nodeResolve({ extensions: ['.js', '.coffee'] })
]
};
92 changes: 92 additions & 0 deletions src/Core.coffee
@@ -0,0 +1,92 @@
comparer = (v, k, b, is_array, path, r) ->
new_path = path.concat([k])
if is_array and not ((v?.id and v?.id is b[k]?.id) or (v?._id and v?._id is b[k]?._id)) or not(v? and b?[k]? and (v instanceof Object))
return r.push(new_path.concat([v]))
r.push.apply(r, Core.diff(v, b[k], new_path))

__next_index = 0
__next_handle = 1

export default Core = {
context: null
tasks: []
clock: 0

queueTask: (task) ->
Promise.resolve().then(Core.processUpdates) unless Core.tasks.length
Core.tasks.push(task)
__next_handle++

cancelTask: (handle) ->
index = handle - (__next_handle - Core.tasks.length)
Core.tasks[index] = null if __next_index <= index

processUpdates: ->
count = 0; mark = 0
while __next_index < Core.tasks.length
unless task = Core.tasks[__next_index]
__next_index++
continue
if __next_index > mark
if count++ > 5000
console.error 'Exceeded max task recursion'
__next_index = 0
return Core.tasks = []
mark = Core.tasks.length
try
task(task.value)
task.handle = null
task.value = null
catch err
console.error err
__next_index++
__next_index = 0
Core.tasks.length = 0
Core.clock++
return

setContext: (newContext, fn) ->
context = Core.context
Core.context = newContext
ret = fn()
Core.context = context
ret

root: (fn) ->
Core.setContext {disposables: d = []}, ->
fn(-> disposable() for disposable in d; d.length = 0; return)

ignore: (fn) ->
{ disposables } = (Core.context or {})
Core.setContext { disposables } , fn

isObject: (obj) -> obj isnt null and typeof obj in ['object', 'function']
isFunction: (val) -> typeof val is 'function'

diff: (a, b, path=[]) ->
r = []
if not Core.isObject(a) or not b?
r.push(path.concat([a])) unless a is b
else if Array.isArray(a)
comparer(v, k, b, true, path, r) for v, k in a when b?[k] isnt v
if b?.length > a.length
l = a.length
while l < b.length
r.push(path.concat([l, undefined]))
l++
else
comparer(v, k, b,false, path, r) for k, v of a when b?[k] isnt v
r.push(path.concat([k, undefined])) for k, v of b when not (k of a)
r

clone: (v) ->
return v unless Core.isObject(v)
return v.slice(0) if Array.isArray(v)
Object.assign({}, v)

unwrap: (item, deep) ->
return result if result = item?._state
return item unless deep and Core.isObject(item) and not Core.isFunction(item) and not (item instanceof Element) and not (item instanceof DocumentFragment)
item[k] = unwrapped for k, v of item when (unwrapped = Core.unwrap(v, true)) isnt v
item
}

1 comment on commit a194f02

@mdmundo
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome!

Please sign in to comment.