Skip to content

Instantly share code, notes, and snippets.

@andybluntish
Last active April 26, 2018 04:32
Show Gist options
  • Save andybluntish/4e0b9fb429484901a61cf0c732e9a28d to your computer and use it in GitHub Desktop.
Save andybluntish/4e0b9fb429484901a61cf0c732e9a28d to your computer and use it in GitHub Desktop.
Bulk Save
import DS from 'ember-data'
const { JSONAPIAdapter } = DS
export default JSONAPIAdapter.extend({
namespace: 'api',
})
import Ember from 'ember'
const { Controller } = Ember
export default Controller.extend({
appName: 'Manage Students',
})
<h1>{{appName}}</h1>
{{outlet}}
import Ember from 'ember'
const {
A,
get,
isArray,
isPresent,
} = Ember
export function includes([obj, item]) {
if (isArray(obj)) {
return A(obj).includes(item)
}
return isPresent(get(obj, item))
}
export default Ember.Helper.helper(includes)
import Ember from 'ember'
const {
A,
Controller,
computed,
computed: { readOnly },
get,
} = Ember
export default Controller.extend({
teacher: readOnly('model.teacher'),
students: computed('model.students.@each', {
get() {
return A(get(this, 'model.students')).sortBy('id')
},
}).readOnly(),
teacherSchoolClasses: computed('teacher.schoolClasses.[]', {
get() {
return A(get(this, 'model.schoolClasses')).sortBy('id')
},
}).readOnly(),
schoolClasses: computed('model.schoolClasses.[]', {
get() {
return A(get(this, 'model.schoolClasses')).sortBy('id')
},
}).readOnly(),
})
import Ember from 'ember'
const {
Route,
RSVP: { hash },
get,
} = Ember
export default Route.extend({
model() {
const store = get(this, 'store')
return hash({
teacher: store.findRecord('teacher', 1),
students: store.findAll('student'),
schoolClasses: store.findAll('schoolClass'),
})
},
})
<div class="row">
<div class="col">
<h3>Teacher: {{teacher.fullName}}</h3>
{{#each teacherSchoolClasses as |schoolClass|}}
{{school-class-list teacher=teacher schoolClass=schoolClass}}
{{/each}}
</div>
<div class="col">
<h2>All Students ({{students.length}})</h2>
<table>
<thead>
<tr>
<th>Name</th>
<th>Grade</th>
<th>Actions</th>
</tr>
</thead>
{{#each students as |student|}}
{{student-record student=student schoolClasses=schoolClasses}}
{{/each}}
</table>
</div>
</div>
export default function() {
this.namespace = 'api'
this.get('/teachers')
this.get('/teachers/:id')
this.get('/students')
this.put('/students', (schema, req) => {
const { data } = JSON.parse(req.requestBody)
console.log(JSON.stringify(data, null, 2))
const updated = data.map((item) => {
const record = schema.students.find(item.id)
record.update(item.attributes)
return record
})
return updated
})
this.get('/students/:id')
this.patch('/students/:id')
this.get('/school-classes')
this.get('/school-classes/:id')
this.patch('/school-classes/:id')
}
import { Factory } from 'ember-cli-mirage'
export default Factory.extend({
name(i) { return `Class ${i + 1}` },
afterCreate(schoolClass, server) {
server.createList('student', 5, { schoolClasses: [schoolClass] })
},
})
import { Factory, faker } from 'ember-cli-mirage'
export default Factory.extend({
firstName() { return faker.name.firstName() },
lastName() { return faker.name.lastName() },
grade() {
return faker.random.number({
min: 1,
max: 5,
})
},
})
import { Factory, faker } from 'ember-cli-mirage'
export default Factory.extend({
firstName() { return faker.name.firstName() },
lastName() { return faker.name.lastName() },
afterCreate(teacher, server) {
server.createList('schoolClass', 2, { teachers: [teacher] })
},
})
import { Model, hasMany } from 'ember-cli-mirage'
export default Model.extend({
teachers: hasMany('teacher'),
students: hasMany('student'),
})
import { Model, hasMany } from 'ember-cli-mirage'
export default Model.extend({
schoolClasses: hasMany('schoolClass'),
})
import { Model, hasMany } from 'ember-cli-mirage'
export default Model.extend({
schoolClasses: hasMany('schoolClass'),
})
export default function(server) {
server.create('teacher')
server.createList('student', 20)
}
import { JSONAPISerializer } from 'ember-cli-mirage'
export default JSONAPISerializer.extend()
import ApplicationSerializer from './application'
export default ApplicationSerializer.extend({
include: ['teachers', 'students'],
})
import ApplicationSerializer from './application'
export default ApplicationSerializer.extend({
include: ['schoolClasses'],
})
import ApplicationSerializer from './application'
export default ApplicationSerializer.extend({
include: ['schoolClasses'],
})
import DS from 'ember-data'
const {
Model,
attr,
hasMany,
} = DS
export default Model.extend({
name: attr('string'),
teachers: hasMany('teacher', { async: true }),
students: hasMany('student', { async: true }),
})
import Ember from 'ember'
import DS from 'ember-data'
const {
computed,
get,
} = Ember
const {
Model,
attr,
hasMany,
} = DS
export default Model.extend({
firstName: attr('string'),
lastName: attr('string'),
grade: attr('number'),
schoolClasses: hasMany('school-class', { async: true }),
fullName: computed('firstName', 'lastName', {
get() {
return [
get(this, 'firstName'),
get(this, 'lastName'),
].join(' ').trim()
},
}),
})
import Ember from 'ember'
import DS from 'ember-data'
const {
computed,
get,
} = Ember
const {
Model,
attr,
hasMany,
} = DS
export default Model.extend({
firstName: attr('string'),
lastName: attr('string'),
schoolClasses: hasMany('school-class', { async: true }),
fullName: computed('firstName', 'lastName', {
get() {
return [
get(this, 'firstName'),
get(this, 'lastName'),
].join(' ').trim()
},
}),
})
import Ember from 'ember';
const {
A,
Component,
RSVP: { all },
computed,
computed: { readOnly, oneWay },
get,
isEqual,
set,
} = Ember
export default Component.extend({
selectedRecords: A(),
teacher: null,
schoolClass: null,
students: computed('schoolClass.students.[]', {
get() {
return A(get(this, 'schoolClass.students')).sortBy('id')
},
}).readOnly(),
studentCount: readOnly('students.length'),
gradeOptions: A([1, 2, 3, 4, 5]),
updateGradeTo: oneWay('gradeOptions.firstObject'),
noRecordsSelected: computed('selectedRecords.[]', {
get() {
return get(this, 'selectedRecords.length') < 1
},
}),
allRecordsSelected: computed('students.[]', 'selectedRecords.[]', {
get() {
return isEqual(get(this, 'students.length'), get(this, 'selectedRecords.length'))
},
}).readOnly(),
actions: {
toggleAllSelected(isChecked) {
if (isChecked) {
set(this, 'selectedRecords', A(get(this, 'students').toArray()))
} else {
set(this, 'selectedRecords', A())
}
},
selectRecord(student, isChecked) {
if (!student) return
if (isChecked) {
get(this, 'selectedRecords').addObject(student)
} else {
get(this, 'selectedRecords').removeObject(student)
}
},
// Remove a single Student from a single SchoolClass - 1 request
// - remove the SchoolClass from the Student model
// - save the Student model, sending its complete new data without the removed SchoolClass
removeFromClass(student) {
get(student, 'schoolClasses').removeObject(get(this, 'schoolClass'))
student.save()
get(this, 'selectedRecords').removeObject(student)
},
// Remove multiple Students from a single SchoolClass - 1 request
// - remove the SchoolClass from each selected Student model
// - save the SchoolClass model, sending its complete new data without the removed Students
removeSelectedFromClass() {
get(this, 'selectedRecords').forEach((student) => {
get(student, 'schoolClasses').removeObject(get(this, 'schoolClass'))
})
get(this, 'schoolClass').save()
get(this, 'selectedRecords').clear()
},
// Remove multiple Students from multiple (all) SchoolClasses - 1 request per affected SchoolClass
// - track a list of every SchoolClass the selected Students belong to
// - remove all SchoolClass relationships from each selected Student
// - save each of the affected SchoolClass models, sending their complete new data without the removed Students
removeSelectedFromAllClasses() {
const schoolClasses = A()
get(this, 'selectedRecords').forEach((student) => {
schoolClasses.pushObjects(get(student, 'schoolClasses').toArray())
get(student, 'schoolClasses').clear()
})
schoolClasses.uniq().forEach((schoolClass) => schoolClass.save())
get(this, 'selectedRecords').clear()
},
moveSelectedIntoGrade() {
const newGrade = get(this, 'updateGradeTo')
if (!newGrade) return
const savePromises = []
get(this, 'selectedRecords').forEach((student) => {
if (get(student, 'grade') !== newGrade) {
set(student, 'grade', newGrade)
savePromises.push(student.save({ adapterOptions: { bulkSave: true } }))
}
})
all(savePromises).then((students) => {
console.log('Saved selected students:', A(students).mapBy('fullName').join(', '))
})
},
},
})
<h4>{{schoolClass.name}} ({{studentCount}})</h4>
<p>
<button {{action "removeSelectedFromClass"}} disabled={{noRecordsSelected}} title="Remove selected Students from this Class">🔥</button>
<button {{action "removeSelectedFromAllClasses"}} disabled={{noRecordsSelected}} title="Remove selected Students from all Classes">🔥🔥</button>
<label title="Move selected Students to Grade">
<select onchange={{action (mut updateGradeTo) value="target.value"}}>
{{#each gradeOptions as |grade|}}
<option value={{grade}}>{{grade}}</option>
{{/each}}
</select>
</label>
<button {{action "moveSelectedIntoGrade"}}>🏃💨</button>
</p>
<table>
<thead>
<tr>
<th>
<input id={{concat "select_all_" schoolClass.id}} type="checkbox" checked={{allRecordsSelected}} onchange={{action "toggleAllSelected" value="target.checked"}}>
</th>
<th>
<label for={{concat "select_all_" schoolClass.id}}>Name</label>
</th>
<th>
<label for={{concat "select_all_" schoolClass.id}}>Grade</label>
</th>
<th>Actions</th>
</tr>
</thead>
{{#each students as |student|}}
<tr title={{concat "#" student.id " " student.fullName}}>
<td>
<input id={{concat "select_" schoolClass.id "_" student.id}} type="checkbox" checked={{includes selectedRecords student}} onchange={{action (action "selectRecord" student) value="target.checked"}}>
</td>
<td>
<label for={{concat "select_" schoolClass.id "_" student.id}}>{{student.fullName}}</label>
</td>
<td>
<label for={{concat "select_" schoolClass.id "_" student.id}}>{{student.grade}}</label>
</td>
<td>
<button {{action "removeFromClass" student}}>🔥</button>
</td>
</tr>
{{/each}}
</table>
import Ember from 'ember'
const {
Component,
get,
computed,
} = Ember
export default Component.extend({
tagName: 'tr',
student: null,
schoolClasses: null,
studentSchoolClasses: computed('student.schoolClasses.[]', {
get() {
return get(this, 'student.schoolClasses').mapBy('id')
},
}),
actions: {
addStudentTo(schoolClass) {
const student = get(this, 'student')
get(student, 'schoolClasses').pushObject(schoolClass)
student.save()
},
},
})
<td>{{student.fullName}}</td>
<td>{{student.grade}}</td>
<td>
{{#each schoolClasses as |schoolClass|}}
<button {{action "addStudentTo" schoolClass}} disabled={{includes studentSchoolClasses schoolClass.id}}>
{{schoolClass.name}}
</button>
{{/each}}
</td>
import Ember from 'ember'
import ApplicationAdapter from '../application/adapter'
const {
A,
RSVP: { Promise },
get,
run,
set,
} = Ember
export default ApplicationAdapter.extend({
_bulkUpdatesQueue: null,
_bulkUpdatesResolvers: null,
_bulkUpdates(type) {
// Bulk updates make a PUT request to the collection endpoint (e.g. /api/students)
const url = this.urlForFindAll(type)
const method = 'PUT'
const options = {
data: {
data: get(this, '_bulkUpdatesQueue'),
},
}
set(this, '_bulkUpdatesQueue', A())
this.ajax(url, method, options)
.then(() => get(this, '_bulkUpdatesResolvers').forEach((p) => p.resolve()))
.catch(() => get(this, '_bulkUpdatesResolvers').forEach((p) => p.reject()))
.finally(() => set(this, '_bulkUpdatesResolvers', A()))
},
init() {
set(this, '_bulkUpdatesQueue', A())
set(this, '_bulkUpdatesResolvers', A())
},
updateRecord(store, type, snapshot) {
// Bulk save functionality is triggered by an adapter option passed to model.save()
// Do the normal thing if we're not bulk saving...
if (!get(snapshot, 'adapterOptions.bulkSave')) return this._super(...arguments)
// Build an object with changed properties only
const record = {
attributes: {},
id: snapshot.id,
type: type.modelName,
}
const changedAttributes = snapshot.changedAttributes()
Object.keys(changedAttributes).forEach((key) => {
// changed attributes are returned from the snapshop as an array [oldValue, newValue]
record.attributes[key] = changedAttributes[key][1]
})
// Add the record to the queue
get(this, '_bulkUpdatesQueue').pushObject(record)
// Trigger the updates on the queue (debounced)
run.debounce(this, get(this, '_bulkUpdates'), type.modelName, 50)
return new Promise((resolve, reject) => {
get(this, '_bulkUpdatesResolvers').pushObject({ resolve, reject })
})
}
})
html { box-sizing: border-box; }
*, *:before, *:after { box-sizing: inherit; }
body {
color: #333;
font-family: sans-serif;
}
.row {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-column-gap: 1em;
}
table {
border-collapse: collapse;
}
th, td {
padding: 0.75em 0.5em;
text-align: left;
border-bottom: 1px solid #eee;
}
tr:hover td {
background: #f4fbff;
}
td label {
display: block;
padding: 0.75em 0.5em;
margin: -0.75em -0.5em;
}
[disabled] {
opacity: 0.6;
}
button {
font-size: inherit;
padding: 0.5em 0.8em;
background: transparent;
border: 1px solid #eee;
border-radius: 3px;
line-height: 1;
}
{
"version": "0.13.1",
"ENV": {
"ember-cli-mirage": {
"enabled": true
}
},
"EmberENV": {
"FEATURES": {}
},
"options": {
"use_pods": true,
"enable-testing": false
},
"dependencies": {
"jquery": "https://cdnjs.cloudflare.com/ajax/libs/jquery/1.11.3/jquery.js",
"ember": "2.12.0",
"ember-template-compiler": "2.12.0",
"ember-testing": "2.12.0"
},
"addons": {
"ember-data": "2.12.2",
"ember-cli-mirage": "0.3.2"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment