Coder Social home page Coder Social logo

gr0uch / simulacra Goto Github PK

View Code? Open in Web Editor NEW
540.0 540.0 24.0 1.8 MB

A data-binding function for the DOM.

Home Page: https://simulacra.js.org/

License: MIT License

JavaScript 73.94% CSS 12.03% Shell 0.33% HTML 13.70%
data-binding dom-builder meta-programming

simulacra's People

Contributors

dependabot[bot] avatar gr0uch avatar james2doyle avatar jeffcarp avatar

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

simulacra's Issues

Allow return value

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:

  • Return a string or number: valid for all elements. Generic elements should have textContent assigned, and input & textarea should get the value assigned.
  • Return a boolean: special case for input checkboxes which sets checked attribute.
  • Return anything that isn't a primitive: warning message (?)

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.

How to add change binding

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?

Example use cases

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).

Enable Array mutators

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:

  • Array.prototype.pop()
  • Array.prototype.push()
  • Array.prototype.shift()
  • Array.prototype.unshift()
  • Array.prototype.splice()

These ones can just use the setter function:

  • Array.prototype.reverse()
  • Array.prototype.sort()
  • Array.prototype.copyWithin() - ES6
  • Array.prototype.fill() - ES6

Special case:

  • Array index (array[i])

Optimize closures

The way it works is heavily reliant on closures, using WeakMaps may be faster.

Current pitfalls:

  • Functions are created on each binding.
  • Scope chain is some levels deep.

Rework path

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:

  • Remove flow helper
  • Remove setDefault helper

Match existing DOM nodes

This 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.

Fix animate helper in Firefox

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.

Add option to use comment nodes

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

Ultra-lightweight DOM implementation for server rendering

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)

Deferred rendering mode

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.

Reactive programming helpers

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' ])
})

Consider adding built-in change functions

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))

Investigate performance bottlenecks

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.

Replace path with index

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.

Allow change function without explicit binding

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.

Improve safety checks

  • When setting an array that is already bound to another key, it should throw an error.
  • When setting an object that is bound to a key, it should throw an error.
  • Prefer WeakMap over hidden property, guarantees no collision. may not be as performant.

event bindings lost in simulacra

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.

Allow to defer removeChild

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.

Split helpers into separate build

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.

Lisp rewrite

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.

Computed properties

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.

String output

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.

Add different rendering modes

Currently, the way rendering is done is immediate: a value change immediately affects the DOM, which might affect performance. There could be:

  • Immediate: currently how it's done.
  • Delayed: run all possible DOM operations in requestAnimationFrame, could be the new default.
  • Deferred: manually call simulacra.flush() to render changes.

Setting a property, i.e. simulacra.renderMode could switch between them.

Merge "mount" with "change" function

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

Test multiple binding

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.

try animationFrames to improve Painting time

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/

Add option to use comment nodes

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

Consolidate code in a single file

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.

Mutate array of objects doesn't work properly

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.

Replace definition function with data structure

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 () { ... } ]
} ])

Link data from one place to another

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.

Improve object assignment

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.

Add string selector support

This would make the public API slightly more friendly. However, it would defer input validation until trying to bind an object.

Array of objects?

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!

Personalization via appending URL & updating value

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

Allow bindings to be re-used

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.

Array return value

Currently, the return value in a change function must be a single value.

In addition, it could also accept an array:

  • 0: value to replace textContent, value, checked
  • 1: event listeners

There are some complications with update/remove, not sure how to resolve this.

Initialize Change Function

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.

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.