Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit a194f02
Showing
18 changed files
with
1,409 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
node_modules/ | ||
dist/ | ||
lib/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
src/ | ||
.rollup.config.js |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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'] }) | ||
] | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
Oops, something went wrong.
a194f02
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome!