Socket
Socket
Sign inDemoInstall

ampersand-state

Package Overview
Dependencies
Maintainers
2
Versions
65
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

ampersand-state - npm Package Compare versions

Comparing version 4.2.7 to 4.3.0

44

ampersand-state.js

@@ -9,7 +9,8 @@ var _ = require('underscore');

options || (options = {});
if (!this.cid) this.cid = _.uniqueId('state');
this.cid = _.uniqueId('state');
this._values = {};
this._definition = Object.create(this._definition);
if (options.parse) attrs = this.parse(attrs, options);
if (options.parent) this.parent = options.parent;
this.parent = options.parent;
this.collection = options.collection;
this._keyTree = new KeyTree();

@@ -32,2 +33,8 @@ this._initCollections();

idAttribute: 'id',
namespaceAttribute: 'namespace',
typeAttribute: 'modelType',
// Stubbed out to be overwritten

@@ -38,2 +45,35 @@ initialize: function () {

// Get ID of model per configuration.
// Should *always* be how ID is determined by other code.
getId: function () {
return this[this.idAttribute];
},
// Get namespace of model per configuration.
// Should *always* be how namespace is determined by other code.
getNamespace: function () {
return this[this.namespaceAttribute];
},
// Get type of model per configuration.
// Should *always* be how type is determined by other code.
getType: function () {
return this[this.typeAttribute];
},
// A model is new if it has never been saved to the server, and lacks an id.
isNew: function () {
return this.getId() == null;
},
// get HTML-escaped value of attribute
escape: function (attr) {
return _.escape(this.get(attr));
},
// Check if the model is currently in a valid state.
isValid: function (options) {
return this._validate({}, _.extend(options || {}, { validate: true }));
},
// Parse can be used remap/restructure/rename incoming properties

@@ -40,0 +80,0 @@ // before they are applied to attributes.

2

package.json
{
"name": "ampersand-state",
"description": "An observable, extensible state object with derived watchable properties.",
"version": "4.2.7",
"version": "4.3.0",
"author": "Henrik Joreteg <henrik@andyet.net>",

@@ -6,0 +6,0 @@ "bugs": {

# ampersand-state
<!-- starthide -->
Part of the [Ampersand.js toolkit](http://ampersandjs.com) for building clientside applications.
<!-- endhide -->
An observable, extensible state object with derived watchable properties.

@@ -9,7 +13,4 @@

<!-- starthide -->
Part of the [Ampersand.js toolkit](http://ampersandjs.com) for building clientside applications.
<!-- endhide -->
For further explanation see the [learn ampersand-state](http://ampersandjs.com/learn/state) guide.
<!-- starthide -->
## browser support

@@ -19,3 +20,2 @@

](https://ci.testling.com/ampersandjs/ampersand-state)
<!-- endhide -->

@@ -28,61 +28,26 @@ ## Install

<!-- starthide -->
## In pursuit of the ultimate observable JS object.
## API Reference
So much of building an application is managing state. Your app needs a single unadulterated *source of truth*. But in order to fully de-couple it from everything that cares about it, it needs to be observable.
### extend `AmpersandState.extend({ })`
Typically that's done by allowing you to register handlers for when things change.
To create a **State** class of your own, you extend **AmpersandState** and provide instance properties an options for your class. Typically here you will pass any properties (`props`, `session` and `derived` of your state class, and any instance methods to be attached to instances of your class.
In our case it looks like this:
**extend** correctly sets up the prototype chain, so that subclasses created with **extend** can be further extended as many times as you like.
```js
// Require the lib
var State = require('ampersand-state');
Definitions like `props`, `session`, `derived` etc will be merged with superclass definitions.
// Create a constructor to represent the state we want to store
var Person = State.extend({
```javascript
var Person = AmpersandState.extend({
props: {
name: 'string',
isDancing: 'boolean'
}
});
// Create an instance of our object
var person = new Person({name: 'henrik'});
// watch it
person.on('change:isDancing', function () {
console.log('shake it!');
});
// set the value and the callback will fire
person.isDancing = true;
```
## So what?! That's boring.
Agreed. Though, there is some more subtle awesomeness in being able to observe changes that are set with a simple assigment: `person.isDancing = true` as opposed to `person.set('isDancing', true)` (either works, btw), but that's nothing groundbreaking.
So, what else? Well, as it turns out, a *huge* amount of code that you write in a project is really in describing and tracking relationships between variables.
So, what if our observable layer did that for us too?
Say you wanted to describe a draggable element on a page so you wanted it to follow a set of a rules. You want it to only be considered to have been dragged if it's total delta is > 10 pixels.
```js
var DraggedElementModel = State.extend({
props: {
x: 'number',
y: 'number'
firstName: 'string',
lastName: 'string'
},
session: {
signedIn: ['boolean', true, false],
},
derived: {
// the name of our derived property
dragged: {
// the properties it depends on
deps: ['x', 'y'],
// how it's calculated
fullName: {
deps: ['firstName', 'lastName'],
fn: function () {
// the distance formula
return Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)) > 10;
return this.firstName + ' ' + this.lastName;
}

@@ -92,384 +57,153 @@ }

});
var element = new DraggedElementModel({x: 0, y: 0});
// now we can just watch for changes to "dragged"
element.on('change:dragged', function (model, val) {
if (val) {
console.log('element has moved more than 10px');
} else {
console.log('element has moved less than 10px');
}
});
```
## You didn't invent derived properties, pal. `</sarcasm>`
### constructor/initialize `new AmpersandState([attrs], [options])`
True, derived properties aren't a new idea. But, being able to clearly declare and derive watchable properties from a model is super useful and in our case, they're just accessed without calling a method. For example, using the draggable example above, the derived property is just `element.dragged`.
When creating an instance of a state object, you can pass in the initial values of the **attributes** which will be [set](#ampersand-state-set) on the model. Unless [extraProperties](#amperand-state-extra-properties) is set to `allow`, you will need to have defined these attributes in `props` or `session`.
If you have defined an **initialize** function for your subclass of State, it will be invoked at creation time.
## Handling relationships between objects/models with derived properties
Say you've got an observable that you're using to model data from a RESTful API. Say that you've got a `/users` endpoint and when fetching a user, the user data includes a groupID that links them to another collection of groups that we've already fetched and created models for. From our user model we want to be able to easily access the group model. So, when passed to a template we can just access related group information.
Cached, derived properties are perfect for handling this relationship:
```js
var UserModel = State.extend({
props: {
name: 'string',
groupId: 'string'
},
derived: {
groupModel: {
deps: ['groupId'],
fn: function () {
// we access our group collection from within
// the derived property to grab the right group model.
return ourGroupCollection.get(this.groupId);
}
}
}
```javascript
var me = new Person({
firstName: 'Phil'
lastName: 'Roberts'
});
var user = new UserModel({name: 'henrik', groupId: '2341'});
// now we can get the actual group model like so:
user.groupModel;
// As a bonus, it's even evented so you can listen for changes to the groupModel property.
user.on('change:groupModel', function (model, newGroupModel) {
console.log('group changed!', newGroupModel);
});
me.firstName //=> Phil
```
Available options:
## Cached, derived properties are da shiznit
* `[parse]` {Boolean} - whether to call the class's [parse](#ampersand-state-parse) function with the initial attributes. _Defaults to `false`_.
* `[parent]` {AmpersandState} - pass a reference to a model's parent to store on the model.
So, say you have a more "expensive" computation for model. Say you're parsing a long string for URLs and turning them into HTML and then wanting to reference that later. Again, this is built in.
### idAttribute `model.idAttribute`
By default, derived properties are cached.
The attribute that should be used as the unique id of the model - typically the name of the property representing the model's id on the server. `getId` uses this to determine the id for use when constructing a model's url for saving to the server.
```js
// assume this linkifies strings
var linkify = require('urlify');
Defaults to `'id'`.
var MySmartDescriptionModel = State.extend({
// assume this is a long string of text
description: 'string',
derived: {
linkified: {
deps: ['description'],
fn: function () {
return linkify(this.description);
}
}
}
});
var myDescription = new MySmartDescriptionModel({
description: "Some text with a link. http://twitter.com/henrikjoreteg"
});
// Now i can just reference this as many times as I want but it
// will never run it through the expensive function again.
myDescription.linkified;
```
With the model above, the descrition will only be run through that linkifier method once, unless of course the description changes.
## Derived properties are intelligently triggered
Just because an underlying property has changed, *doesn't mean the derived property has*.
Cached derived properties will *only* trigger a `change` if the resulting calculated value has changed.
This is *super* useful if you've bound a derived property to a DOM property. This ensures that you won't ever touch the DOM unless the resulting value is *actually* different. Avoiding unecessary DOM changes is a huge boon for performance.
This is also important for cases where you're dealing with fast changing attributes.
Say you're drawing a realtime graph of tweets from the Twitter firehose, instead of binding your graph to increment with each tweet, if you know your graph only ticks with every thousand tweets you can easily create a property to watch.
```js
var MyGraphDataModel = State.extend({
var Person = AmpersandModel.extend({
idAttribute: 'personId',
urlRoot: '/people',
props: {
numberOfTweets: 'number'
},
derived: {
thousandTweets: {
deps: ['numberOfTweets'],
fn: function () {
return Math.floor(this.numberOfTweets / 1000);
}
}
personId: 'number',
name: 'string'
}
});
// then just watch the property
var data = new MyGraphDataModel({numberOfTweets: 555});
var me = new Person({ personId: 123 });
// start adding 'em
var increment = function () {
data.number += 1;
}
setInterval(increment, 50);
data.on('change:thousandTweets', function () {
// will only get called every time is passes another
// thousand tweets.
});
console.log(me.url()) //=> "/people/123"
```
## Derived properties don't *have* to be cached.
### getId `model.getId()`
Say you want to calculate a value whenever it's accessed. Sure, you can create a non-cached derived property.
Get ID of model per `idAttribute` configuration. Should *always* be how ID is determined by other code.
If you say `cache: false` then it will fire a `change` event anytime any of the `deps` changes and it will be re-calculated each time its accessed.
### namespaceAttribute `model.namespaceAttribute`
The property name that should be used as a namespace. Namespaces are completely optional, but exist in case you need to make an additionl distinction between models, that may be of the same type, with potentially conflicting IDs but are in fact different.
## State can be extended as many times as you want
Defaults to `'namespace'`.
Each state object you define will have and `extend` method on the constructor.
### getNamespace `model.getNamespace()`
That means you can extend as much as you want and the definitions will get merged.
Get namespace of model per `namespaceAttribute` configuration. Should *always* be how namespace is determined by other code.
```js
var Person = State.extend({
props: {
name: 'string'
},
sayHi: function () {
return 'hi, ' + this.name;
}
});
### typeAttribute
var AwesomePerson = Person.extend({
props: {
awesomeness: 'number'
}
});
The property name that should be used to specify what type of model this is. This is optional, but specifying a model type types provides a standard, yet configurable way to determine what type of model it is.
// Now awesome person will have both awesomeness and name properties
var awesome = new AwesomePerson({
name: 'henrik',
awesomeness: 8
});
Defaults to `'modelType'`.
// and it will have the methods in the original
awesome.sayHi(); // returns 'hi, henrik'
### getType `model.getType()`
// it also maintains the prototype chain
// so instanceof checks will work up the chain
Get type of model per `typeAttribute` configuration. Should *always* be how type is determined by other code.
// so this is true
awesome instanceof AwesomePerson; // true;
### extraProperties `AmpersandState.extend({ extraProperties: 'allow' })`
// and so is this
awesome instanceof Person; // true
Defines how properties that aren't defined in `props`, `session` or `derived` are handled. May be set to `'allow'`, `'reject'` or `'allow'`.
```
```javascript
var StateA = AmpersandState.extend({
extraProperties: 'allow',
});
## child models and collections
var stateA = new StateA({ foo: 'bar' });
stateA.foo === 'bar' //=> true
You can declare children and collections that will get instantiated on init as follows:
```js
var State = require('ampersand-state');
var Messages = require('./models/messages');
var ProfileModel = require('./models/profile');
var Person = State.extend({
props: {
name: 'string'
},
collections: {
// `Messages` here is a collection
messages: Messages
},
children: {
// `ProfileModel` is another ampersand-state constructor
profile: ProfileModel
}
var StateB = AmpersandState.extend({
extraProperties: 'ignore',
});
// When we instantiate an instance of a Person
// the Messages collection and ProfileModels
// are instantiated as well
var stateB = new StateB({ foo: 'bar' });
stateB.foo === undefined //=> true
var person = new Person();
// so meetings exists as an empty collection
person.meetings instanceof Meetings; // true
// and profile exists as an empty `ProfileModel`
person.profile instanceof ProfileModel; // true
// This also provides some additional capabilities
// when we instantiate a state object with some
// data it will apply them to the collections and child
// models as you might expect:
var otherPerson = new Person({
messages: [
{from: 'someone', message: 'hi'},
{from: 'someoneElse', message: 'yo!'},
],
profile: {
name: 'Joe',
hairColor: 'black'
}
var stateC = AmpersandState.extend({
extraProperties: 'reject'
});
// now messages would have a length
otherPerson.messages.length === 2; // true
// and the profile state object would be
// populated
otherPerson.profile.name === 'Joe'; // true
// The same works for `set`, it will apply it
// to children as well.
otherPerson.set({profile: {name: 'Mary'}});
// Since this a state object it triggers a `change:name` on
// the `profile` object.
// In addition, since it's a child that event propagates
// up. More on that below.
var stateC = new StateC({ foo: 'bar' })
//=> TypeError('No foo property defined on this model and extraProperties not set to "ignore" or "allow".');
```
## Event bubbling, derived properties based on children
### collection `state.collection`
Say you want a simple way to listen for any changes that are represented in a tempalate.
A reference to the collection a state is in, if in a collection.
Let's say you've got a `person` state object with a `profile` child. You want an easy way to listen for changes to either the base `person` object or the `profile`. In fact, you want to listen to anything related to the person object.
This is used for building the default `url` property, etc.
Rather than having to worry about watching the right thing, we do exactly what the browser does to solve this problem: we bubble up the events up the chain.
Which is why you can do this:
Now we can listen for deeply nested changes to properties.
And we can declare derived properties that depend on children. For example:
```js
var Person = State.extend({
children: {
profile: Profile
},
derived: {
childsName: {
// now we can declare a child as a
// dependency
deps: ['profile.name'],
fn: function () {
return 'my child\'s name is ' + this.profile.name;
}
}
}
});
// some ampersand-rest-collection instance
// with a `url` property
widgets.url //=> '/api/widgets'
var me = new Person();
// get a widget from our collection
var badWidget = widgets.get('47');
// we can listen for changes to the derived property
me.on('change:childsName', function (model, newValue) {
console.log(newValue); // logs out `my child's name is henrik`
});
// so when a property of a child is changed the callback
// above will be fired (if the resulting derived property is different)
me.profile.name = 'henrik';
// Without a `collection` reference this
// widget wouldn't know what URL to build
// when calling destroy
badWidget.destroy(); // does a DELETE /api/widgets/47
```
## A quick note about instanceof checks
### cid `state.cid`
With npm and browserify for module deps you can sometimes end up with a situation where, the same `state` constructor wasn't used to build a `state` object. As a result `instanceof` checks will fail.
A special property of states, the **cid**, or a client id, is a unique identifier automatically assigned to all states when they are first created. Client ids are handy when the state has not been saved to the server, and so does not yet have it's true **id** but needs a unique id so it can be rendered in the UI etc.
In order to deal with this (because sometimes this is a legitimate scenario), `state` simply creates a read-only `isState` property on all state objects that can be used to check whether or a not a given object is in fact a state object no matter what its constructor was.
<!-- endhide -->
```javascript
var userA = new User();
console.log(userA.cid) //=> "state-1"
## API Reference
var userB = new User();
console.log(userB.cid) //=> "state-2"
```
### extend `AmpersandState.extend({ })`
### isNew `model.isNew()`
To create a **State** class of your own, you extend **AmpersandState** and provide instance properties an options for your class. Typically here you will pass any properties (`props`, `session` and `derived` of your state class, and any instance methods to be attached to instances of your class.
Has this model been saved to the server yet? If the model does not yet have an id (using `getId()`), it is considered to be new.
**extend** correctly sets up the prototype chain, so that subclasses created with **extend** can be further extended as many times as you like.
### escape `model.escape()`
Definitions like `props`, `session`, `derived` etc will be merged with superclass definitions.
Similar to `get`, but returns the HTML-escaped version of a model's attribute. If you're interpolating data from the model into HTML, using **escape** to retrieve attributes will help prevent XSS attacks.
```javascript
var Person = AmpersandState.extend({
props: {
firstName: 'string',
lastName: 'string'
},
session: {
signedIn: ['boolean', true, false],
},
derived: {
fullName: {
deps: ['firstName', 'lastName'],
fn: function () {
return this.firstName + ' ' + this.lastName;
}
}
}
});
```
### constructor/initialize `new AmpersandState([attrs], [options])`
When creating an instance of a state object, you can pass in the initial values of the **attributes** which will be [set](#ampersand-state-set) on the model. Unless [extraProperties](#amperand-state-extra-properties) is set to `allow`, you will need to have defined these attributes in `props` or `session`.
If you have defined an **initialize** function for your subclass of State, it will be invoked at creation time.
```javascript
var me = new Person({
firstName: 'Phil'
lastName: 'Roberts'
var hacker = new PersonModel({
name: "<script>alert('xss')</script>"
});
me.firstName //=> Phil
document.body.innerHTML = hacker.escape('name');
```
Available options:
### isValid `model.isValid()`
* `[parse]` {Boolean} - whether to call the class's [parse](#ampersand-state-parse) function with the initial attributes. _Defaults to `false`_.
* `[parent]` {AmpersandState} - pass a reference to a model's parent to store on the model.
Check if the model is currently in a valid state, it does this by calling the `validate` method, of your model if you've provided one.
### extraProperties `AmpersandState.extend({ extraProperties: 'allow' })`
Defines how properties that aren't defined in `props`, `session` or `derived` are handled. May be set to `'allow'`, `'reject'` or `'allow'`.
```javascript
var StateA = AmpersandState.extend({
extraProperties: 'allow',
});
var stateA = new StateA({ foo: 'bar' });
stateA.foo === 'bar' //=> true
var StateB = AmpersandState.extend({
extraProperties: 'ignore',
});
var stateB = new StateB({ foo: 'bar' });
stateB.foo === undefined //=> true
var stateC = AmpersandState.extend({
extraProperties: 'reject'
});
var stateC = new StateC({ foo: 'bar' })
//=> TypeError('No foo property defined on this model and extraProperties not set to "ignore" or "allow".');
```
### dataTypes

@@ -476,0 +210,0 @@

@@ -216,1 +216,59 @@ /*jshint expr: true*/

});
test('custom id and namespace attributes', function (t) {
var NewPerson = State.extend({
props: {
name: 'string',
_id: 'number',
ns: 'string'
},
idAttribute: '_id',
namespaceAttribute: 'ns'
});
var person = new NewPerson({name: 'henrik', ns: 'group1', _id: 47});
t.equal(person.getId(), 47);
t.equal(person.getNamespace(), 'group1');
t.end();
});
test('customizable `type` attribute', function (t) {
var FirstModel = State.extend({
type: 'hello',
typeAttribute: 'type'
});
var SecondModel = State.extend({
modelType: 'second'
});
var first = new FirstModel();
var second = new SecondModel();
t.equal(first.getType(), 'hello');
t.equal(second.getType(), 'second');
t.end();
});
test('constructor should be defined', function (t) {
var Foo = State.extend({
props: { name: 'string' }
});
var foo = new Foo();
t.ok(foo.constructor);
t.end();
});
test('isValid is a thing', function (t) {
var Foo = State.extend({
props: { name: ['string', true] },
validate: function (attrs) {
if (attrs.name.length < 2) {
return "can't be too short";
}
}
});
var foo = new Foo();
t.notOk(foo.isValid());
foo.name = 'thing';
t.ok(foo.isValid());
t.end();
});
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc