ampersandjs / ampersand-model Goto Github PK
View Code? Open in Web Editor NEWObservable objects, for managing state in applications.
License: MIT License
Observable objects, for managing state in applications.
License: MIT License
The model is removed from the collection when category.destroy()
is invoked, yet no xhr request is made.
// categories.js
import Collection from 'ampersand-rest-collection'
import xhr from 'xhr'
import Category from './category'
import config from '../config'
export default Collection.extend({
url: config.apiUrl + '/categories',
model: Category,
mainIndex: '_id',
create(category) {
xhr({
uri: this.url,
json: category,
method: 'post',
}, (err, res, body) => {
if (err) return alert(err)
if (body && body.success === false) return alert(body.error)
category._id = body._id
this.add(category)
})
},
fetchWithTags() {
xhr({
uri: this.url + '?tags=true',
method: 'get',
json: true,
}, (err, res, body) => {
if (err) return alert(err)
if (body && body.success === false) return alert(body.error)
this.set(body)
})
},
})
// category.js
import Model from 'ampersand-model'
import cfg from '../config'
import Categories from './categories'
export default Model.extend({
// urlRoot: cfg.apiUrl + '/categories',
// url() {
// return cfg.apiUrl + '/categories/' + this._id
// },
props: {
_id: 'string',
name: 'string',
tags: 'array',
},
})
// pages/categories.jsx
import app from 'ampersand-app'
import AmpMixin from 'ampersand-react-mixin'
import React from 'react'
import xhr from 'xhr'
import { preAuth } from '../util/helpers'
import cfg from '../config'
import Category from '../models/category'
import Topbar from '../components/topbar.jsx'
import CategoryItem from '../components/category-item.jsx'
export default React.createClass({
displayName: 'CategoriesPage',
mixins: [AmpMixin],
getInitialState() {
return {
categoryInput: '',
}
},
categoryInputChanged(e) {
this.setState({ categoryInput: e.target.value })
},
addCategory(e) {
e.preventDefault()
if (this.state.categoryInput.trim() == '') return
preAuth(() => {
app.categories.create({ name: this.state.categoryInput })
})
},
deleteCategory(category) {
console.log('category', category)
category.destroy({
success(model, res, opts) {
console.log('category destroyed!')
console.log('model, res, opts', model, res, opts)
},
error(model, res, opts) {
console.log('There was an error destroying the category')
console.log('model, res, opts', model, res, opts)
},
})
},
render() {
var {categoryInput} = this.state
var {categories} = this.props
var content = categories
? (<ul>
{categories.map((cat) => {
if (cat.name) return <CategoryItem key={cat._id} deleteCat={this.deleteCategory} cat={cat}></CategoryItem>
})}
</ul>)
: <h4>Loading categories...</h4>
return (
<div className='form-wrap'>
<header>
<h1>Categories</h1>
<form onSubmit={this.addCategory}>
<input onChange={this.categoryInputChanged} value={categoryInput} type='text' placeholder='Add category'/>
<button type='submit' className='commit'>Add</button>
</form>
</header>
{content}
</div>
)
},
})
We are using ampersand-models for a project and noticed that when the model called .fetch()
it would return a success call even when the url route was a 404 (returned html not json or null
).
This creates a redundant item without any data.
I created a test repo to explain the issue: https://github.com/stevelacy/ampersand-fetch-test
Could a param be added to determine what type of data was needed to be returned, and if not throw an error? Or check the body type to make sure it is json or null. (ampersand-sync)
4.0.3 has some nice bug fixes, namely AmpersandJS/ampersand-sync#72
Hello,
My app have an offline mode and all of data is saved in cdvfile://localhost/persistent/...
When offline mode is activate the urls of models data change for json local url.
On ios its ok but on android the status of the json files is 'pending' on google dev tools network when the app is starting and the app is blocked.
Any idea?
In the upcoming release of ampsersand-sync
, xhr2.js will be used. There is already a bug and pull request tracking this AmpersandJS/ampersand-sync#32 This gives us a great new feature we didn't previously have: we can test the interfaces outside of the browser, using Mocha. It also raises a question..
var model = BaseModel.extend({
urlRoot: "/login"
});
What this does inside the browser is logical: it's a simple relative URL and all of it is handled behind the scenes. However, we'll have to hack ampersand-model.url()
https://github.com/AmpersandJS/ampersand-model/blob/master/ampersand-model.js#L128 so we can reference some kind of global root path or something. If we don't have a reference to a global root path, or some way to emulate this functionality transparently everywhere ampersand-models
are instantiated then we can we either
Add a computed lazy property that emulates something like baseURI, we'll use baseURL
as an example here.
*. Default baseURL property with
1. window.document.location.href
(if this exists)
2. if it doesn't exist default it to global.document.location.href
3. throw fatal error.
This would permit us to always calculate the base for the current urlRoot
.
*. Add a convenience function, isRelativeURL
to check whether or not urlRoot
is relative.
*. Add a conditional here using the new isRelativeURL
. If the root is relative, use a URL library to make the urlRoot
relative to the baseURL
.
Suggestions? Ideas? One consideration we may have is the ambiguity in names. Under the current scheme urlRoot
is often not the root at all. It's often relative to the implicit url root of document.window.location.href
. This doesn't fix this bad property name.
I've noticed that destroy is only successful when the server responds with a 200
status code (vs. an arguably more appropriate 204
).
Wouldn't it be simple for the code to test if the status code is >= 200 && < 300
and determine success thusly?
We are planning to use ampersand-model
on the server, with the requirement that different users need different authentication tokens added to the headers without using some magic to propagate them down to all models that need to know how to fetch themselves.
To solve this problem, the idea was to remove a model's ability to fetch itself, and use a different component that knows everything about how to fetch models.
The idea is similar to this:
var syncManager = require('sync-manager')({
authToken: 1243
});
var Model = require('our-model');
var m = new Model({
id: 123,
urlRoot: '/foo'
});
var fetched = syncManager.fetch(m);
var changed = fetched.then(function (model) {
model.set('foo', 'bar');
return model;
});
var saved = changed.then(syncManager.save);
Our model implementation will not contain any of the save
, fetch
and destroy
methods.
That way we enforce that updates need to go through the syncManager
and can slowly work our way towards single-directional data flow. We also don't need to propagate user data down into randomly required models and collections, something quite painful to do in event-loop based async programming on node. We currently use https://github.com/othiym23/node-continuation-local-storage instead to magically create something similar to thread locals which allow us to basically treat the auth data as globals over the lifetime of the request.
Of course we do not want to copy-paste all the code from ampersand-model
over and create maintainence and compatibility overhead. At first I wanted to delegate through to ampersand-model
's prototype method, but there's one annoying problem: sync
aka ampersand-sync
is accessed from the closure scope in ampersand-model
, making it necessary to create a proxy object passing through all this.XXX
and model.XXX
data so we can have our models be clean of the sync related methods.
I would propose instead to turn the code into a mixin factory which you can then put on the prototype of Model
and we can use in our sync-manager
.
// ampersand-model.js
var syncMethodFactory = require('ampersand-sync-methods');
var sync = require('ampersand-sync');
...
_.extend(Model.prototype, syncMethodFactory(sync));
// sync-manager.js
var syncMethodFactory = require('ampersand-sync-methods');
var sync = require('our-isomorphic-sync');
var syncManager = function (options) {
var customizedSync = sync({
authToken: options.authToken
});
var syncMethods = syncMethodFactory(customizedSync);
return {
save: function (model, key, val, options) {
return syncMethods.save.call(model, key, val, options);
}
};
};
This way it's very easy to create customized model classes that still rely on the sync code maintained by the project, and also allowing developers to replace sync
inside of ampersand-model
without having to rely on monkey patching the prototype.
Using ampersand-model
with webpack creates an invalid bundle.
The error require is not defined
occurs on line 14654 (caused by module.exports = require("net");
).
Below is a snippet that reproduces the problem.
Surprisingly, the exact same config works perfectly when using only ampersand-view
.
Bonus point: this way, the (broken) bundle is made of 54756 lines. With browserify it's a working bundle made of 8406 lines. Any idea on the huge difference of file weight?
app.js
var Model = require('ampersand-model');
module.exports = Model.extend({});
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
</body>
<script src="js/bundle.js"></script>
</html>
package.json
{
"dependencies": {
"ampersand-view": "6.x",
"ampersand-model": "6.x"
},
"devDependencies": {
"webpack": "x",
"webpack-build": "x",
"babel-core": "x",
"babel-loader": "x",
"babel-preset-es2015": "x",
"handlebars": "x",
"handlebars-loader": "x",
"json-loader": "x",
"hbsfy": "x",
"watchify": "x",
"browserify": "x"
},
"scripts": {
"failing": "webpack",
"working": "browserify -t [hbsfy -e hbs] js/app.js -o js/bundle.js"
}
}
webpack.config.js
var webpack = require('webpack');
module.exports = {
context: __dirname + "/js",
entry: {
"bundle" : "./app"
},
output: {
path: __dirname + "/js",
filename: "[name].js"
},
module: {
loaders: [
{ test: /\.json$/, loader: 'json-loader'},
{
test: /\.js$/,
loader: 'babel-loader',
query: {
compact: false,
presets: ['es2015']
}
},
{
test: /\.hbs$/,
loader: 'handlebars-loader'
}
]
},
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': '"development"',
'global': {} // bizarre lodash(?) webpack workaround
})
],
target: 'node',
node: {
__dirname: false,
__filename: false,
}
};
New ampersand-sync uses version 2 of XHR instead of version 1
var Model = AmpersandModel.extend({ props: { timestamp: 'date' }})
model = new Model()
model.timestamp = new Date()
model.unset('timestamp')
// TypeError: Property 'timestamp' must be of type date. Tried to set NaN
I'd have expxted the TypeError to only be thrown when timestamp
property is required
Missing attributes from server are not removed in model:
const model = new Model();
model.save({name: 'Paul', age: 18});
// server replies {name: 'Paul'}
// model still has is age attributes equal to 18
// server replies {name: 'Paul', age: 20}
// model is correctly updated
I need some values from model property to determine what to render. It seems like rendering got done before fetch did.
looking to create alternative sync implementations (localstorage, firebase etc) and need ability and a consistent strategy. I think this merits some discussion since it spans models & collections and there are varying layers of abstraction
When you create a model and try to use url in the list of props, an error is thrown when you try to save it.
Error: A "url" property or function must be specified
If you set the url it of coarse changes the url in which the model is saved.
model.save({ url: 'http://google.com' });
XMLHttpRequest cannot load http://google.com/.
Since url is a common field name used in databases I expected to be able to use it in an ampersand model.
Here is a simplified example: http://requirebin.com/?gist=2372c9322f4d2f5d5c3a
Hi,
In my application, I have an API endpoint that accepts URLs (and other parameters) in POST / PUT requests, and also returns them back in the GET request. Currently, the URL field has the name "url"
in the API, however, and it seems there's a conflict with the model's url()
method.
When I try to add "url"
to the props, like:
var MyModel = AmpersandModel.extend({
props: {
url: 'String'
}
...
});
I see that when I call .save()
to POST
the data ({"url": "my-url.com"}
), the URL that we hit is:
/my-url.com
But I really want to hit /api/urls
with the data.
Any recommendations?
I have released the 4.0.0 version of ampersand-sync.
README.md now has docs too.
The biggest change for you is that ajaxConfig can do much more and response object is not the XMLHttpRequest instance anymore. Also, ampersand-sync now works in nodejs, and ampersand-model should be able to do that too.
I'm getting errors of functions being undefined when I use the latest version of ampersand-state and the latest version of ampersand-model in the same project. This is due to the usage of an old ampersand-state version (4.6.0) as a dependency in ampersand-model. When is this going to be updated?
I have a model with properties as such:
var Profile = BaseModel.extend({
props: {
firstName: 'string',
lastName: 'string',
avatar: 'string'
}
});
My model initialization should denote that all attributes start as null
or undefined
:
var profile = new Profile();
From a React view, I am then updating the avatar to a local file URI (for when user captures photo or selects a photo in the context of a Cordova app), e.g.:
function onPhotoChanged: function (uri) {
this.props.profile.set('avatar', uri);
}
Later, when the user clicks the "Save" button, I actually persist to the server:
function onSubmit: function (event) {
this.props.profile.save({
firstName: this.refs.firstName.value(),
lastName: this.refs.lastName.value(),
avatar: this.refs.avatar.value()
}, {
success: this.onSuccess.bind(this),
error: this.onError.bind(this)
})
}
All the attributes are being propagated correctly. However, because of the set('avatar', uri)
call in the onPhotoChanged
handler, my Ampersand model thinks that the avatar value has NOT changed (via hasChanged()
) even though it has yet to be persisted to the server up until the point that I actually call save
. (FWIW, I'm calling hasChanged
from my Profile#sync
method prior to persisting, and I can see that all other attributes are properly marked as changed.) Looking at the previousAttributes
object shows that it's not considering "avatar" null or undefined, but is instead stating that its initial value is whatever last came back when calling set('avatar', uri)
.
For example:
if (this.profile.hasChanged('avatar')) {
// Separate handling logic for saving avatar image.
// This never gets called!
}
This seems like a bug to me, is this the expected behavior?
Hi There,
I have some data relating to authorization that needs to be included in every ajax request. How would I include this in a base class without having to include it in every single one of my models.
For example:
client_id: 'myAppId'
should be part of every ajax request.
I had a look at ajaxConfig .. but this only lets me set headers and and xhr options, but does not seem to provide a way to include data.
I also looked at the "request" module on npm, but even there I couldn't find a neat way to always inject some data into every request.
In AngularJS I saw an example of an "authentication injector service" ... how would I achieve this with Ampersand?
Thanks,
Oliver
https://github.com/AmpersandJS/ampersand-model/blob/master/ampersand-model.js#L52
The resp object is the JSON.parse(body)
output, not the actual HTTP request. The success and error functions are inconsistent with one another, the arguments are in different orders and even though they are called the same arg names the data is totally different. Not sure what the thought process was behind how these were arranged but I think it would make sense to normalize them and make them work the same way.
If I have a model with a property someDate
of type date
, I get two different serialization results when I save with {wait: true}
, depending on if I set someDate prior to invoking save, or if I pass it to the save function as follows:
model.someDate = new Date();
model.save(null, {wait: true})
// serialized to something like {"someDate": 143502072321}
model.save({someDate: new Date()}, {wait: true});
// serializes to {"someDate": "2015-06-23T01:59:21.780Z"}
Line 67 appears to be the culprit.
Perhaps I am wrong, but something akin to the requested enhancement in ampersand-state Issue #122 seems to be needed so solve this.
Perhaps the default headers passed to XHR
should be
{
"Accept": "application/json"
}
I think it makes sense since the model is expecting JSON. I've been otherwise having to add this to ajaxConfig
for all my models.
Thoughts?
See this:
https://github.com/AmpersandJS/ampersand-model/blob/master/ampersand-model.js#L126
While ampersand-sync passes the following information:
https://github.com/AmpersandJS/ampersand-sync/blob/master/ampersand-sync.js#L102
Is there a reason why body are send by default with a Content-Type to text/plain;charset=UTF-8
?
This is very painful.
Ok, this is on my side too.
Using ajaxConfig
is the reason of this, but I can't find why.
ajaxConfig: {
beforeSend: function(xhr) {
xhr.open(this.type, this.url + `?token=...`, true);
},
I am noticing a potential issue in the last lines of the model's save function. When I try to make a patch and I also want to wait to make the changes such as:
model.save({
/*attrs*/
}, {
patch: true,
wait: true
}
the attrs set in sync
are set to the serialization to the model.
if (method === 'patch') options.attrs = attrs;
// if we're waiting we haven't actually set our attributes yet so
// we need to do make sure we send right data
if (options.wait) options.attrs = _.extend(model.serialize(), attrs);
xhr = this.sync(method, this, options);
return xhr;
if I am sending a patch I definitely don't want to send over EVERYTHING that is attached to my model but maybe I am missing a point here.
function wrapError
is stripping ampersand-sync's err.msg. This is unfortunate as accessing it is often necessary. What about a patch to add the original contents of the err.message as options['ampersand-sync-error']
(setting that to arguments[2])?
I'm unsure if I'm supposed to notify the collection of a change?
The tags property is just an array of strings.
export default Model.extend({
props: {
_id: 'string',
name: 'string',
tags: 'array',
},
})
Method in a React Class:
addTag(e) {
e.preventDefault()
if (this.state.tagInput.trim() == '') return
var cat = this.props.cat
console.log(cat.tags) // food, shopping
if (!cat.tags) cat.tags = []
cat.tags.push(this.state.tagInput)
console.log(cat.tags) // food, shopping, entertainment (not updated in UI)
this.setState({ submitted: true })
var cat = this.props.cat
var self = this
cat.save({ tags: cat.tags }, {
success(model, res, opts) {
self.setState({
tagInput: '',
submitted: false,
})
// Tag now displayed in UI
},
error(model, res, opts) {
if (res.success === false) return alert(res.error)
}
})
},
I think if you have a collection and a prop by the same name, a fatal error should be thrown when one tries to override the other. My code only works as a collection..
module.exports = BaseModel.extend({
props: {
clientid: 'number'
// , events: 'object'
}
, collections: { events: NE }
We should probably also switch to tape so we can get https://ci.testling.com/ set up as well.
Suppose that I do the following:
model.save({foo: 1});
model.save({foo: 2});
Now suppose that the first save takes the server longer to complete than the second one. In that case, the callback to update the model with the server state will first update the model with foo: 2
, and then with foo: 1
, discarding the second save. Is this known? This seems to be an actual issue for me.
What's the way to handle parallel PUT operations with Ampersand? Is it at all possible?
When defining a beforeSend in the ajaxConfig it never gets called
I'll be releasing sync v4 soon, please take a look at master and at
AmpersandJS/ampersand-sync#66
I'm new here, so I have no idea what the process of adopting new sync version is. Let's talk.
Hi,
it would be nice to automatically setting collection and collection items parent for models initialized from JSON data. I have a model A that has a collection of items B and I'd like to access a property of the model A in a view rendered for item B.
Or maybe there's a way how to achieve this and if anybody can tell me how, that would be nice :)
Thanks,
Karel
the code in question:
// Wrap an optional error callback with a fallback error event.
var wrapError = function (model, options) {
var error = options.error;
options.error = function (model, resp, options) {
if (error) error(model, resp, options);
model.trigger('error', model, resp, options);
};
};
model.trigger(...)
is expecting the model
from wrapError
arguments, but instead is model
from options.error
. We can see from ampersand-sync that the first argument is the response and not the model in this scenario:
// Make the request. The callback executes functions that are compatible
// With jQuery.ajax's syntax.
var request = options.xhr = xhr(ajaxSettings, function (err, resp, body) {
if (err && options.error) return options.error(resp, 'error', err.message);
The save
code says:
// After a successful server-side save, the client is (optionally)
// updated with the server-side state.
From the code that follows, I was assuming this was controlled using options.parse
, but it seems i'm wrong. How do you make sure the model does not update itself using the server state?
FYI, I used the following to test this:
// Succeeds
test("`save` updates model with server response", function (t) {
t.plan(2);
var model = new Backbone.Model({props: {testing: 'string'}});
model.sync = function (method, model, options) { options.success({testing: 'bar'}); };
model.save({testing: 'foo'}, { parse: true });
t.equal(model.get('testing'), 'bar');
t.ok(true);
});
// Fails
test("`save` does not update model with server response", function (t) {
t.plan(2);
var model = new Backbone.Model({props: {testing: 'string'}});
model.sync = function (method, model, options) { options.success({testing: 'bar'}); };
model.save({testing: 'foo'}, { parse: false });
t.equal(model.get('testing'), 'foo');
t.ok(true);
});
If this is intentional, then close my issue, otherwise this needs a look.
https://github.com/AmpersandJS/ampersand-model/blob/master/ampersand-model.js#L64
and
https://github.com/AmpersandJS/ampersand-model/blob/master/ampersand-model.js#L67
Line 67 overrides what was done on 64 if both wait and patch are true.
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.