Skip to content

Commit

Permalink
Merge pull request #14 from guillotinaweb/fix-update-context
Browse files Browse the repository at this point in the history
Fix update context
  • Loading branch information
ebrehault authored May 28, 2020
2 parents 6a6f097 + 46dab9b commit 7bcdca3
Show file tree
Hide file tree
Showing 17 changed files with 205 additions and 65 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
# 1.3.1 (2020-05-28)

### Improvement
- Improve the demo
- More documentation
- `updateContext` returns `onComplete` observable

### Bug fix:
- Fix `updateContext` method

# 1.3.0 (2020-05-24)

### Feature
Expand Down
92 changes: 90 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ The good thing about a Grange app is it is totally neutral about the Guillotina

## Building a basic Grange app

### Init a Grange project

If you don't have one already, create an angular project:
```
ng new my-project
Expand Down Expand Up @@ -107,6 +109,92 @@ npm start

The Angular app is now offering all the Grange standard views (login, content creation, view, etc.).

### Create custom views

We have in our Guillotina `config.yaml` file a custom content type named `player`:
```yaml
player:
title: Player
inherited_interface: guillotina.interfaces.IItem
inherited_class: guillotina.content.Item
add_permission: guillotina.AddContent
properties:
team:
type: guillotina.schema.TextLine
title: Team
rank:
type: guillotina.schema.Int
title: Rank
```
We would like to use our own custom form to edit `player` contents.

We create regular Angular component named `PlayerComponent` and we declare it in our `AppComponent` to become the `player` edit view:

```typescript
this.grange.traverser.addView('edit', 'player', PlayerComponent);
```

The template is a simple form (based on Pastanaga UI elements, but any form elements would work):

```html
<pa-input [(value)]="title">Title</pa-input>
<pa-input [(value)]="team">Team</pa-input>
<pa-input [(value)]="rank" type="number">Rank</pa-input>
<pa-button (click)="save()">Save</pa-button>
```

In the component itself, we inject `grange` service:
```typescript
constructor(private grange: Grange) { }
```

Thank to this service, we can get the values we need from the context:
```typescript
ngOnInit() {
this.grange.getContext().subscribe(context => {
this.title = context.title;
this.team = context.team;
this.rank = context.rank;
});
}
```

The good thing about `getContext()` is it returns an Observable that will emit the context object everytime it changes in our state, so our form is always reflecting the current state values.

`grange` service also allows us to save the changes the user enters in the form:
```typescript
save() {
this.grange.updateContext({
title: this.title,
team: this.team,
rank: this.rank,
});
}
```
`updateContext()` updates the state (hence the form is immediately updated because `getContext()` will emit the new values), and it also updates Guillotina backend by doing a PATCH call.

If we want to take an action after saving – like redirecting to the home page – `updateContext()` has a `onComplete` property which is a boolean observable (returning `true` for success, and `false` if saving produced a backend error):

```typescript
save() {
this.grange.updateContext({
title: this.title,
team: this.team,
rank: this.rank,
}).onComplete.subscribe(success => {
if (success) {
this.grange.ui.toaster.open('Saved', 2000);
this.grange.traverser.traverse('/');
} else {
this.grange.ui.toaster.open('Error when saving.', 'common.dismiss');
}
});
}
```

See [the full code example](projects/demo/src/app).

## Reference

### Grange views
Expand Down Expand Up @@ -160,7 +248,7 @@ import { Grange } from 'grange';
constructor(private grange: Grange) {}
notify() {
this.grange.ui.toaster.open('Hello');
this.grange.ui.toaster.open('Hello', 'Close');
}
```

Expand Down Expand Up @@ -233,7 +321,7 @@ It allows to select state information or to dispatch actions. See below the "Sta

`this.grange.ui`

It gives access to the main Pastanaga services: calendar, popup, sidebar, and translate.
It gives access to the main Pastanaga services: calendar, dialog, popup, sidebar, toaster and translate.

See "Pastanaga UI library" section below.

Expand Down
12 changes: 12 additions & 0 deletions g-api/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,18 @@ behaviors:
title: Text
default: Hello
contents:
player:
title: Player
inherited_interface: guillotina.interfaces.IItem
inherited_class: guillotina.content.Item
add_permission: guillotina.AddContent
properties:
team:
type: guillotina.schema.TextLine
title: Team
rank:
type: guillotina.schema.Int
title: Rank
canvas:
title: Canvas
inherited_interface: guillotina.interfaces.IItem
Expand Down
26 changes: 3 additions & 23 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"@guillotinaweb/grange-core": "latest",
"@guillotinaweb/grange-form": "latest",
"@guillotinaweb/ngx-state-traverser": "latest",
"@guillotinaweb/pastanaga-angular": "^1.17.18",
"@guillotinaweb/pastanaga-angular": "^1.18.2",
"@ngrx/core": "^1.2.0",
"@ngrx/effects": "^8.4.0",
"@ngrx/store": "^8.4.0",
Expand Down
2 changes: 2 additions & 0 deletions projects/demo/src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Component } from '@angular/core';
import { GrangeViews, Grange } from '../../../grange/src';
import { CanvasComponent } from './canvas/canvas.component';
import { PlayerComponent } from './player/player.component';

@Component({
selector: 'app-root',
Expand All @@ -16,6 +17,7 @@ export class AppComponent {
this.views.initialize();
this.grange.core.auth.isAuthenticated.subscribe(auth => this.isAuthenticated = auth.state);
this.grange.traverser.addView('view', 'canvas', CanvasComponent);
this.grange.traverser.addView('edit', 'player', PlayerComponent);
}

logout() {
Expand Down
6 changes: 5 additions & 1 deletion projects/demo/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,19 @@ import { environment } from '../environments/environment';

import { AppComponent } from './app.component';
import { CanvasComponent } from './canvas/canvas.component';
import { PlayerComponent } from './player/player.component';
import { GrangeRootModule } from '../../../grange/src';
import { TraversalModule } from 'angular-traversal';
import { StoreModule } from '@ngrx/store';
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
import { ButtonModule } from '@guillotinaweb/pastanaga-angular';
import { ButtonModule, TextFieldModule } from '@guillotinaweb/pastanaga-angular';
import { AngularSvgIconModule } from 'angular-svg-icon';

@NgModule({
declarations: [
AppComponent,
CanvasComponent,
PlayerComponent,
],
imports: [
BrowserModule,
Expand All @@ -27,6 +29,8 @@ import { AngularSvgIconModule } from 'angular-svg-icon';
}),
AngularSvgIconModule.forRoot(),
ButtonModule,
TextFieldModule,
ButtonModule,
],
providers: [
{
Expand Down
4 changes: 1 addition & 3 deletions projects/demo/src/app/canvas/canvas.component.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { Component, OnInit } from '@angular/core';
import { Grange } from '../../../../grange/src';
import { select } from '@ngrx/store';
import { TraverserSelectors } from '@guillotinaweb/ngx-state-traverser';
import { map, tap, concatMap } from 'rxjs/operators';
import { map, tap } from 'rxjs/operators';
import { Observable } from 'rxjs';

@Component({
Expand Down
4 changes: 4 additions & 0 deletions projects/demo/src/app/player/player.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<pa-input [(value)]="title">Title</pa-input>
<pa-input [(value)]="team">Team</pa-input>
<pa-input [(value)]="rank" type="number">Rank</pa-input>
<pa-button (click)="save()">Save</pa-button>
37 changes: 37 additions & 0 deletions projects/demo/src/app/player/player.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Component, OnInit } from '@angular/core';
import { Grange } from '../../../../grange/src';

@Component({
selector: 'app-player',
templateUrl: 'player.component.html'
})
export class PlayerComponent implements OnInit {
title = '';
team = '';
rank = 0;

constructor(private grange: Grange) { }

ngOnInit() {
this.grange.getContext().subscribe(context => {
this.title = context.title;
this.team = context.team;
this.rank = context.rank;
});
}

save() {
this.grange.updateContext({
title: this.title,
team: this.team,
rank: this.rank,
}).onComplete.subscribe(success => {
if (success) {
this.grange.ui.toaster.open('Saved', 2000);
this.grange.traverser.traverse('/');
} else {
this.grange.ui.toaster.open('Error when saving.', 'common.dismiss');
}
});
}
}
2 changes: 1 addition & 1 deletion projects/grange/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@guillotinaweb/grange",
"version": "1.3.0",
"version": "1.3.1",
"license": "MIT",
"author": {
"name": "Eric Brehault",
Expand Down
8 changes: 4 additions & 4 deletions projects/grange/src/lib/components/breadcrumbs.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
<h2 class="pa-sr" id="breadcrumbs-youarehere" translate>breadcrumbs.here</h2>
<ol class="breadcrumb" aria-labeledby="breadcrumbs-youarehere">
<li>
<pa-button-link traverseTo="/" icon="home" [hasButtonDisplay]="true" paTooltip="workingbar.home" paTooltipType="action">
</pa-button-link>
<pa-button traverseTo="/" icon="home" paTooltip="workingbar.home" paTooltipType="action">
</pa-button>
</li>
<ng-container *ngFor="let ancestor of (ancestors | async)">
<li class="breadcrumb-item">
<pa-button-link [traverseTo]="ancestor['@id']">
<a href="#" [traverseTo]="ancestor['@id']" class="pa-button pa-button-link">
{{ ancestor.title }}
</pa-button-link>
</a>
</li>
</ng-container>
</ol>
Expand Down
6 changes: 5 additions & 1 deletion projects/grange/src/lib/components/breadcrumbs.scss
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,12 @@
right: - rythm(2);
top: 1px;
}
& ::ng-deep .pa-button-link {
& .pa-button {
white-space: nowrap;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
& .o-breadcrumb-item-more ::ng-deep svg {
fill: $separator-color;
Expand Down
33 changes: 25 additions & 8 deletions projects/grange/src/lib/grange.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { Traverser } from 'angular-traversal';
import { GrangeCore } from '@guillotinaweb/grange-core';
import { GrangeState } from './state/state';
import { PastanagaService } from '@guillotinaweb/pastanaga-angular';
import { Observable } from 'rxjs';
import { Observable, AsyncSubject } from 'rxjs';
import { TraverserSelectors, TraverserActions } from '@guillotinaweb/ngx-state-traverser';
import { take } from 'rxjs/operators';
import { take, concatMap, tap, skip } from 'rxjs/operators';

@Injectable({
providedIn: 'root'
Expand All @@ -26,12 +26,29 @@ export class Grange {
return this.store.pipe(select(TraverserSelectors.getContext));
}

updateContext(changes: any) {
updateContext(changes: any): {onComplete: Observable<boolean>} {
const onComplete = new AsyncSubject<boolean>();
let initialContext: any;
let path: string;
this.getContext().pipe(
take(1)
).subscribe(context => this.store.dispatch(new TraverserActions.UpdateTraverserResource({
path: this.core.api.getPath(context['@id']),
changes
})));
take(1),
tap(context => {
initialContext = context;
path = this.core.api.getPath(context['@id']);
this.store.dispatch(new TraverserActions.UpdateTraverserResource({path, changes}));
}),
concatMap(newContext => this.core.resource.update(path, changes)),
).subscribe(
() => {
onComplete.next(true);
onComplete.complete();
},
() => {
this.store.dispatch(new TraverserActions.UpdateTraverserResource({path, changes: initialContext}));
onComplete.next(false);
onComplete.complete();
},
);
return {onComplete};
}
}
Loading

0 comments on commit 7bcdca3

Please sign in to comment.