gr0uch / simulacra Goto Github PK
View Code? Open in Web Editor NEWA data-binding function for the DOM.
Home Page: https://simulacra.js.org/
License: MIT License
A data-binding function for the DOM.
Home Page: https://simulacra.js.org/
License: MIT License
Currently, returning anything in a mutator function doesn't do anything (with one exception, returning false
on a remove). This could probably be a bit more ergonomic by assuming what the return value should do:
textContent
assigned, and input & textarea should get the value
assigned.checked
attribute.Then return false
needs to be reworked as well, it should probably need to return a Symbol (or random value in case Symbol is not supported). Maybe expose it as simulacra.retainElement
. Also, unrelated but most checks for Node
should actually check for the more specific Element
type.
I have the following that works OK ...
template = document.createElement('template')
template.innerHTML = '<table border=1><tr><td><input type="text"></td></tr></table>'
data = { rows:[
{cells:['one','two','three']},
{cells:['four','five','six']}
]
}
bindings = [ template, {
rows: [ 'tr',
{cells: '[type="text"]'}
]
} ]
outlet = document.body
outlet.appendChild(simulacra(data, bindings))
How do I structure the bindings to add a change event to input box?
With Simulacra.js, each value in a bound object has a 1:1 correspondence to the DOM (in philosophical jargon this might be a first order simulacrum). Piling on more abstraction is often useful in real world use cases.
Suppose we have a list of products with prices:
const data = {
products: [
{ name: 'Foo', price: 1.21 },
{ name: 'Bar', price: 2.35 }
]
}
It would not be very useful to define this change function for price
:
(node, value) => value * tax
Because it won't be run again when tax
changes. This is not very reactive, I know.
Instead, consider tax
as a separate property of the data.
Object.defineProperty(data, 'tax', {
get: () => data.tax,
set: tax => data.products.forEach(product =>
product.total = product.price * tax)
})
Simulacra.js is agnostic about what sort of paradigm is used to mutate the bound object.
With higher order abstractions on top of Simulacra.js, data does not have to correspond directly to the DOM (this might be considered a second order simulacrum).
It says here that changing the prototype of an object is very slow, however it would be really nice to just push
on a bound array and have it update without having to assign itself. Not sure if there is a way to do this without taking a big performance hit.
To do:
These ones can just use the setter function:
Special case:
array[i]
)The way it works is heavily reliant on closures, using WeakMap
s may be faster.
Current pitfalls:
Currently the path
argument returns an array containing the path to the value. This is actually not reliable, since mutating an array in between the path will not update the path.
I think this argument should be an object containing the keys:
root
target
key
index
(if applicable)This might be a breaking change, or might not, I don't think it deserves a major version bump.
It's a breaking change. Some other things that might change:
flow
helpersetDefault
helperThis would be useful for rendering a page on the server side, and then initializing a client side app with the DOM nodes that are already on the page.
API strawman:
// where `node` is an optional parameter which will try to match existing nodes.
// return value would just be the `node` passed in.
simulacra(data, bindings[, node])
The way to do this would be to run all of the usual code, and then use isEqualNode
on the cloned node and the original nodes.
When inserting a DOM Node, sometimes the insert animation does not trigger in Firefox.
Using requestAnimationFrame
doesn't seem to work reliably, it would probably be better to use MutationObserver to detect when the element is inserted.
Currently, the mechanism uses empty text nodes for marking positions, which doesn't interact well with Node.normalize()
, but is the most minimal way.
There should be an option to use comment nodes instead, which is mostly for debugging purposes.
simulacra.useCommentNode = true
Currently, the top level object must not be an array. It should be possible to allow a top level array.
While domino
is a comprehensive solution to running DOM in Node.js, Simulacra.js uses only a subset of the DOM API. Essentially all it needs to do is build a string and support only the DOM API that Simulacra.js uses.
Strawman:
const simulacra = require('simulacra')
// DOM subset used by Simulacra.js
const DOM = require('simulacra/dom')
const bindObject = (data, binding, node) =>
simulacra.call(DOM, data, binding, node)
Or maybe even better:
// Use built-in DOM implementation.
// Return value is a string instead of DOM Node.
const renderHTML = require('simulacra/render')
const output = renderHTML(data, binding, templateString)
I am not happy that Simulacra.js is not among the very fastest in benchmarks >_<
The bottleneck is the "immediate mode" which it works in. When a property on a bound object is assigned, it calls the setter function immediately and makes changes to the DOM. This is also the bottleneck for similar libraries like Vue.js which also use object getters and setters.
The fastest libraries like Inferno and Kivi have a method to commit changes to the DOM manually, which does not have as much overhead as object getters/setters.
In the "deferred mode" the data object does not get getters/setters, the changes must be detected manually. This also means no array methods and optimizations around arrays are possible. The possible API could look like:
var render = require('simulacra/render')
var data = { ... }
var node = render(data, [ ... ])
render(data) // without bindings = commit changes
The only difference is the necessity of calling render
with the data object, to tell it when to commit changes to the DOM.
Some array mutators may be working sub-optimally due to array index setters being invoked first.
This would be a very nice to have. Currently when a nested object is assigned to a simulacra-bound key, it will get all of the defined properties, but it would be cool to also be able to define data flows as well.
Take 1:
$(data, {
firstName: $((target, value) =>
target.fullName = `${value} ${target.lastName}`
),
lastName: $((target, value) =>
target.fullName = `${target.firstName} ${value}`
),
fullName: $(node)
})
Take 2:
$(data, {
fullName: $(node, (node, target) =>
node.textContent = `${target.firstName} ${target.lastName}`,
[ 'firstName', 'lastName' ])
})
Some common use cases are left to the user. It would be nice to have optional built-in ways to do basic stuff, most commonly adding/removing event listeners and classes. None of it should be included as part of the default build but explicitly required. It could be new public API.
Strawman:
const simulacra = require('simulacra')
const { bindEvents, animate, setDefault, chain } = simulacra
const animateOptions = [
'fade-in' , // class to add on append
'bounce', // class to add on mutate
'fade-out', // class to add on remove
200 // how long the element should be retained after removal, in ms
]
// Events get attached on append, and removed automatically.
const events = { click (event) { console.log('clicked!', event) } }
// Change function defined like so:
const change = chain(
setDefault,
bindEvents(events),
animate(...animateOptions))
The fastest DOM libraries are about 50% faster than Simulacra.js in DBMonster. It would be nice to know where the bottlenecks are and make Simulacra.js on par with the fastest abstractions.
I think it may be too dangerous and unnecessary to include a full path
including the root object in the mutator function, it can easily lead to spaghetti code. The only missing information that might be important is the index
if its an array value.
This would be a reversion to previous behavior, and is intended to limit affecting the bound data object in mutator functions.
In the case of binding to the parent element, it's not necessary to repeat the parent element.
Strawman API:
{
// In this case, no array is needed.
foo (node, value, previousValue, path) { ... }
}
This is an incremental improvement.
Is it lack of support even for IE11?
while working with simulacra, I discovered that I was not able to add event listeners to elements before inserting them into the dom. Is this as-designed? If so, what would the suggested pattern be for dealing with event bindings? This is what I am attempting: https://gist.github.com/BenjaminVerble/a0cdfbced1d5865df670
this is not critical to a project or anything. I was just curious and interested in simulacra. I like working closer to the dom api.
Currently, there is no way to stop a node from being automatically removed. The motivation to allow to defer this is to make efficient remove animations easier.
I think that the mutator function may return any non-undefined value to prevent removeChild
, but I'm not sure what is the best interface to do this.
Just tried mutating a bound array item. The change is reflected, but the order of items in the rendered result does not match the one in the array anymore.
Here is a codepen that demonstrates the issue: http://codepen.io/anon/pen/XmwJBd
Is that the expected behavior or do I use the API in the wrong way?
They probably shouldn't be included by default.
require('simulacra/lib/flow')
require('simulacra/lib/set-default')
require('simulacra/lib/bind-events')
require('simulacra/lib/animate')
The browser window.simulacra
build should still include them, though.
It would be interesting to see if the code could simplified by writing it in Common Lisp via Parenscript. I don't expect any savings in output size, but I think it would reduce the lines of source code by using macros.
It is currently ~700 lines of JS, which is not trivial but also not that big. It might make sense incrementally try to reproduce the same JS code via Parenscript.
The motivation is to see how viable Lisp is as a language for authoring web front-ends. Parenscript is particularly suitable for authoring libraries with, since it does not rely on an external runtime. I guess very few people understand Lisp code so there are very few people doing it, but it's worth experimenting and I think it's feasible.
Not sure if this is possible but worth investigating, use user-defined getter/setters in the bound data object.
var store = {}
var data = {}
Object.defineProperty(data, 'computed', {
get: function () { return store.computed }
set: function (x) { store.computed = x + x }
})
var node = bind(data, bindings)
Would need to use Object.getOwnPropertyDescriptor
.
Perhaps there should be a reduced use case for server-side rendering. Basically, all that it needs to do is output a string, without worrying about data mutation. jsdom
could help here in keeping the API compatible in Node.js. A potential bottleneck with this approach is the initial time it takes to get a new instance of jsdom
.
You mentioned in the README that "it works in IE9+ with a WeakMap polyfill" but it seems like the template
tag requires a pretty new browser.
Did I miss something, or is the template
tag support not a problem?
This is sort of a micro-optimization but is nice to have, transparent to the user, and has no drawbacks.
https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment
Not sure if it would work in all cases though.
Currently, the way rendering is done is immediate: a value change immediately affects the DOM, which might affect performance. There could be:
requestAnimationFrame
, could be the new default.simulacra.flush()
to render changes.Setting a property, i.e. simulacra.renderMode
could switch between them.
The signature of mount
and change
differs for no good reason, other than making it slightly shorter by hiding the previousValue
.
This will make explaining the change
function in the docs much easier too.
This is unfortunately a breaking change, so it will require a major version release. actually doesn't break
I'm not sure if reusing the same binding works concurrently or not. Pretty sure it doesn't right now, because each binding is mapped to one marker regardless of data.
Hi @0x8890 , I really like this slim DOM focuesed thing!
As painting is the biggest remaining time block (in a zero-layout page!) It could be interesting to research anmationFrames to improve rendering performance due to DOM thrashing. I have experienced ( on mobile ) that it sometimes makes a relevant difference and sometimes not. It's interesting as simulacra "knows" DOM changes that belong together and should be wrapped in one rendering pass.
This guy did a good writeup: http://wilsonpage.co.uk/preventing-layout-thrashing/
Currently, the mechanism uses empty text nodes for marking positions, which doesn't interact well with Node.normalize()
, but is the most minimal way.
There should be an option to use comment nodes instead, which is mostly for debugging purposes.
simulacra.useCommentNode = true
It would be nice to also get the path in the event listener function.
{ click (event, path) { ... } }
Since Simulacra.js is so short and it's a design goal not to exceed a certain degree of complexity, it may be simpler to put it all in a single file. This has the added benefit of removing the build tool.
Some mutations like sort
, reverse
, fill
, and copyWithin
won't work with arrays of objects, because those objects are already bound. The internal defineIndex
setter needs to handle this.
Previously the definition function existed to validate inputs but now validation is handled at the end. It is possible to replace the second argument of bindObject
with a data structure:
$(data, [ fragment, {
name: [ '.name' ],
details: [ '.details', {
size: [ '.size', function mutator () { ... } ],
vendor: '.vendor'
}, function mount () { ... } ]
} ])
What does Loading and Scripting refer to in the benchmarks?
Howcome innerHTML is slower?
Would love to know!
Thanks :-)
Instead of denormalizing data everywhere it is displayed, it would be useful to come up with a way to link them together in one direction.
Strawman API:
// `path` is an array of strings. it must point to a path with a `change` function.
[ path, ... ]
This would make a linked field impossible to render, it has to link to fields that do render.
Currently, if a new object gets assigned over an old object, the DOM node of the old object will be removed and a new node will be appended for the new object.
This can be improved by reusing the old DOM node and just updating it with the fields of the new object.
Edit: not so sure if this can be done. Consider this data:
[ { b: '2' }, { c: '3' } ]
If I prepend a new object, so the new payload looks like:
[ { a: '1' }, { b: '2' }, { c: '3' } ]
Still the most efficient update can't be optimized for, which is just unshift
.
This allows for errors to be thrown in the mutator function which prevents the internal value from changing.
This would make the public API slightly more friendly. However, it would defer input validation until trying to bind an object.
The only caveat is that a different node can't be returned in the mount function for a nested binding.
Is it possible to render an array of objects? Something like this:
<template id="params">
<ul class="a">
<li>
<span class="k1"></span>
<span class="k2"></span>
</li>
</ul>
</template>
var data = {
a: [{k1:'a', k2:'1'},{k1:'b',k2:'2'}]
};
function $ (s) { return fragment.querySelector(s) };
var bindings = bind(fragment, {
a: bind($('.a'), [bind($('.k1')), bind($('.k2'))])
});
Thanks!
Hi there!
Newbie here. I've been thinking about a use case to pseudo-personalize a webpage where if the url is:
http://www.website.com/welcome=FirstName
Then the page can pick up the "FirstName" value and pass the data into a <p>
tag
Can the Simulacra plugin be used for this sort of use case?
Also, I've done some research and apparently JS based binding is prone to XSS vulnerabilities. Any thoughts on that?
Thanks
vilav
There needs to be another internal property for marking a binding as validated, and skipping safety checks. Right now, a binding can't be re-used which is contrary to one of the examples given in the readme.
Currently, the return value in a change function must be a single value.
In addition, it could also accept an array:
There are some complications with update/remove, not sure how to resolve this.
I would like to run a change function when my object is bound. Example:
let mainNode = bindObject( state, [ mainTemplate, {
messages: '#messages',
name: [ '#signin', function ( node, value, previousValue ) {
console.log('i ran');
if( value === null ) {
node.style.display = 'block';
return bindObject.retainElement;
}
else {
node.style.display = 'none';
}
}]
}]);
The change function associated with name
doesn't get run unless I actually change state.name
. While it is a function called change
, it would be nice if it ran at init time. messages
is automatically inserted as expected at init.
Is this a misguided approach, and if so, do you have any recommendations on how to get that function to run other than setting state.name
after init time?
Thank you.
Currently, the last argument is used only for array index, which is not that useful. It would be more useful to include a path from the root:
[ 'users', 5, 'name', 'first' ]
This would be a breaking change, but since it's 0.x
, YOLO.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.