How to call actual service instead of local JSON in Getting Started Demos complete example (search,sort,page,and filter) - ng-bootstrap

Working with complete table example (with sorting, filtering and pagination) that simulates a server call using a JSON constant.
I am trying to make it call a real API that returns JSON instead of a local constant.
I was successful in changing the code from the demo from using countries to suppliers. For example countries.ts is suppliers.ts, country.ts is supplier.ts, and country.service is supplier.service.
That works with no problems, but I want to remove the countries.ts (suppliers.ts in my case), export the JSON and replace it with a http.get call to a local API service.
Here's a sample of a working code from the API service that I am trying to call:
getSuppliers(): Observable<SupplierVM[]> {
return this.http.get<SupplierVM[]>(apiUrl+'supplier')
.pipe(
tap(heroes => console.log('fetched Suppliers')),
catchError(this.handleError('getSuppliers', []))
);
}
Here is a sample of a working call from within an Angular component:
allSuppliers:Observable<SupplierVM[]>;
this.allSuppliers=this.api.getSuppliers();
This is the method that does the work in the demo (the only difference is that I am using suppliers instead of countries)
private _search(): Observable<SearchResult> {
const {sortColumn, sortDirection, pageSize, page, searchTerm} = this._state;
//1. sort
let suppliers = sort(SUPPLIERS, sortColumn, sortDirection);
//2. filter
suppliers = suppliers.filter(country => matches(country, searchTerm/*, this.pipe*/));
const total = suppliers.length;
//3. paginate
suppliers = suppliers.slice((page - 1) * pageSize, (page - 1) * pageSize + pageSize);
return of({suppliers, total});
}
This works when I call suppliers from the import statement, but I want to replace the suppliers from the sort method to something like this.allSuppliers (kind of like the sample method call above).
//1. sort
let suppliers = sort(this.allSuppliers, sortColumn, sortDirection);
Everything works when using local imported constant composed of JSON and should work just the same when calling actual service because the JSON response is the exact same.

I had a go at this and ended up implementing a switchable server-side/client-side sorting/pagination/filtering system (Angular 12).
Base Service: All services are derived from this, even ones without sorting/pagination/filtering.
import { HttpClient, HttpParams } from "#angular/common/http";
import { Observable } from "rxjs";
export abstract class BaseService<T> {
constructor(protected _http: HttpClient, protected actionUrl: string) {
}
getAll(params?: HttpParams): Observable<T[]> {
if (params)
return this._http.get(this.actionUrl, { params: params }) as Observable<T[]>;
return this._http.get(this.actionUrl) as Observable<T[]>;
}
getSingle(id: number, params?: HttpParams): Observable<T> {
if (params)
return this._http.get(`${this.actionUrl}/${id}`, { params: params }) as Observable<T>;
return this._http.get(`${this.actionUrl}/${id}`) as Observable<T>;
}
push(content: any, id: number) {
if (id === 0)
return this._http.post<any>(`${this.actionUrl}`, content);
return this._http.post<any>(`${this.actionUrl}/${id}`, content);
}
post(content: any) {
return this._http.post<any>(`${this.actionUrl}`, content);
}
}
Sortable Service: Services with a sortable/paginated/filtered result set are always derived from this base service. Derived services need to implement their own matching algorithm (_matches()).
import { HttpClient } from "#angular/common/http";
import { BehaviorSubject, Observable, Subject } from "rxjs";
import { BaseService } from "./base.service";
import { SearchResult, SearchState } from "#app/common/_models";
import { SortDirection } from '#app/common/_directives';
import { debounceTime, delay, switchMap, tap } from "rxjs/operators";
export abstract class SortableService<T> extends BaseService<T> {
private _loading$ = new BehaviorSubject<boolean>(true);
private _search$ = new Subject<void>();
private _items$ = new BehaviorSubject<T[]>([]);
private _total$ = new BehaviorSubject<number>(0);
protected _state: SearchState = {
page: 1,
pageSize: 10,
searchTerm: '',
sortColumn: '',
sortDirection: ''
};
constructor(protected _http: HttpClient, protected actionUrl:string) {
super(_http, actionUrl);
this._search$.pipe(
tap(() => this._loading$.next(true)),
debounceTime(200),
switchMap(() => this._search()),
tap(() => this._loading$.next(false))
).subscribe(response => {
this._items$.next(response.result);
this._total$.next(response.total);
});
this._search$.next();
}
get items$() { return this._items$.asObservable(); }
get total$() { return this._total$.asObservable(); }
get loading$() { return this._loading$.asObservable(); }
get page() { return this._state.page; }
get pageSize() { return this._state.pageSize; }
get searchTerm() { return this._state.searchTerm; }
set page(page: number) { this._set({ page }); }
set pageSize(pageSize: number) { this._set({ pageSize }); }
set searchTerm(searchTerm: string) { this._set({ searchTerm }); }
set sortColumn(sortColumn: string) { this._set({ sortColumn }); }
set sortDirection(sortDirection: SortDirection) { this._set({ sortDirection }); }
private _set(patch: Partial<SearchState>) {
Object.assign(this._state, patch);
this._search$.next();
}
protected _compare(v1: string | number | boolean | T[keyof T], v2: string | number | boolean | T[keyof T]) {
return v1 < v2 ? -1 : v1 > v2 ? 1 : 0;
}
private _getValue<T>(obj: T, column: string) {
var parts = column.split('.');
var result : any = obj;
for (let part of parts) {
for (let res in result) {
if (res === part)
result = result[res];
}
}
return result;
}
protected _sort(items: T[], column: string, direction: string): T[] {
if (direction === '' || column === '')
return items;
return [...items].sort((a, b) => {
var aa = this._getValue(a, column);
var bb = this._getValue(b, column);
const res = aa === undefined ? -1 : bb === undefined ? 1 : this._compare(aa, bb);
return direction === 'asc' ? res : -res;
});
}
protected abstract _matches(items: T, term: string) : boolean;
protected abstract _search(): Observable<SearchResult<T>>;
}
Client-Sortable Service: If the list manipulation happens in the client, derive the services from this one. I use a simple 1-minute cache to hold data.
import { HttpClient } from "#angular/common/http";
import { Observable, of } from "rxjs";
import { mergeMap } from "rxjs/operators";
import { SortableService } from "./sortable.service";
import { SearchResult } from "#app/common/_models";
export abstract class ClientSortableService<T> extends SortableService<T> {
cache?: T[];
cacheUpdatedIn?: Date;
constructor(protected _http: HttpClient, protected actionUrl: string) {
super(_http, actionUrl);
}
private getCache(): Observable<T[]> {
var expected = new Date();
if (this.cacheUpdatedIn !== undefined) {
expected = this.cacheUpdatedIn;
expected.setMinutes(expected.getMinutes() + 1);
}
//Search again.
if (this.cache == undefined || this.cache.length == 0 || expected == undefined || expected < new Date())
{
this.cacheUpdatedIn = new Date();
return this.getAll();
}
return of(this.cache || []);
}
protected _search(): Observable<SearchResult<T>> {
return this.getCache().pipe(mergeMap(items => {
this.cache = items;
//1: Sort.
let siteGroups = this._sort(items, this._state.sortColumn, this._state.sortDirection);
//2: Filter.
siteGroups = siteGroups.filter(group => this._matches(group, this._state.searchTerm));
const total = siteGroups.length;
//3: Paginate.
siteGroups = siteGroups.slice((this._state.page - 1) * this._state.pageSize, (this._state.page - 1) * this._state.pageSize + this._state.pageSize);
return of({ result: siteGroups, total: total });
}));
}
}
Server-Sortable Service: If the list manipulation happens in the server, derive the services from this one. The api need to handle the results before returning it.
import { HttpClient } from "#angular/common/http";
import { Observable, of } from "rxjs";
import { SortableService } from "./sortable.service";
import { SearchResult } from "#app/common/_models";
export abstract class ServerSortableService<T> extends SortableService<T> {
constructor(protected _http: HttpClient, protected actionUrl: string) {
super(_http, actionUrl);
}
protected _search(): Observable<SearchResult<T>> {
return super.post(this._state);
}
}
Custom Service: This custom service derives from the client-side base service. See that I need to declare a method that is used for the filtering, to select which fields to compare to.
#Injectable({ providedIn: 'root' })
export class ExampleService extends ClientSortableService<Example> {
constructor(protected _http: HttpClient) {
super(_http, `${environment.apiUrl}/sites`);
}
protected _matches(items: Example, term: string): boolean {
return items.displayName.toLowerCase().includes(term.toLowerCase());
}
}

So I beat my head against this for over a day.. I had it but I was doing something silly that broke pagination.
Anyway, what I did was all client-side since my data isn't going to be huge. In the constructor I wrapped the search.next() call in the subscribe of the API call. I also assigned the results to an array there. That way I had data when the search.next() was called. That array is what the actual function manipulates.
If you can follow my bad code here are the relevant snippets:
Declare a new variable to hold the results.
private myData: MyDataType[] = [];
In the constructor add your API call to get your data.
this.httpClient.get<MyDataType[]>('GetMyData').subscribe(data => {
this.myData = data;
this._search$.next();
});
This will load your data client-side to be used by the search function and trigger the initial call to said function.
And finally, in the search function, use your newly created and data-loaded variable for sorting and filtering.
//1. Sort
let theData = sort(this.myData, sortColumn, sortDirection);
I don't know what I was trying to do at first.. I went around the world and googled for several hours and found nothing that made it seem so simple. Just a few lines of code was all it took!

Related

React-Admin can't post to ASP.Net Core API

I am trying to connect ASP.NET Core 3 api with React-Admin. So far I can list the entries from the DB and show a single record - apparently the two GET methods are working fine. (Sticking to this example)
When I try to Create a record though with a POST method I receive a 400 Bad Request and can't track the issue.
My App.js looks like this:
import simpleRestProvider from 'ra-data-simple-rest';
import realApi from './dataProvider/myDataProvider';
const dataProvider = simpleRestProvider('api/');
const App = () =>
<Admin
dashboard={Dashboard}
dataProvider={realApi}
>
<Resource name={projects.basePath} {...projects.crud} />
...
</Admin>;
I have custom dataProvider which is copied from the official react-admin tutorial
export default (type, resource, params) => {
let url = '';
const options = {
headers: new Headers({
Accept: 'application/json',
"Content-Type": 'application/json; charset=utf-8',
}),
};
let query = "";
switch (type) {
case GET_LIST: {
const { page, perPage } = params.pagination;
const { field, order } = params.sort;
query = {
sort: JSON.stringify([field, order]),
range: JSON.stringify([
(page - 1) * perPage,
page * perPage - 1,
]),
filter: JSON.stringify(params.filter),
};
url = `${apiUrl}/${resource}?${stringify(query)}`;
break;
}
case GET_ONE:
url = `${apiUrl}/${resource}/${params.id}`;
break;
case CREATE:
url = `${apiUrl}/${resource}`;
options.method = 'POST';
options.body = JSON.stringify(params.data);
break;
case UPDATE:
url = `${apiUrl}/${resource}/${params.id}`;
options.method = 'PUT';
options.body = JSON.stringify(params.data);
break;
case UPDATE_MANY:
query = {
filter: JSON.stringify({ id: params.ids }),
};
url = `${apiUrl}/${resource}?${stringify(query)}`;
options.method = 'PATCH';
options.body = JSON.stringify(params.data);
break;
case DELETE:
url = `${apiUrl}/${resource}/${params.id}`;
options.method = 'DELETE';
break;
case DELETE_MANY:
query = {
filter: JSON.stringify({ id: params.ids }),
};
url = `${apiUrl}/${resource}?${stringify(query)}`;
options.method = 'DELETE';
break;
case GET_MANY: {
query = {
filter: JSON.stringify({ id: params.ids }),
};
url = `${apiUrl}/${resource}?${stringify(query)}`;
break;
}
case GET_MANY_REFERENCE: {
const { page, perPage } = params.pagination;
const { field, order } = params.sort;
query = {
sort: JSON.stringify([field, order]),
range: JSON.stringify([
(page - 1) * perPage,
page * perPage - 1,
]),
filter: JSON.stringify({
...params.filter,
[params.target]: params.id,
}),
};
url = `${apiUrl}/${resource}?${stringify(query)}`;
break;
}
default:
throw new Error(`Unsupported Data Provider request type ${type}`);
}
let headers;
return fetch(url, options)
.then(res => {
headers = res.headers;
debugger
return res.json();
})
.then(json => {
switch (type) {
case GET_LIST:
case GET_MANY_REFERENCE:
if (!headers.has('content-range')) {
throw new Error(
'The Content-Range header is missing in the HTTP Response. The simple REST data provider expects responses for lists of resources to contain this header with the total number of results to build the pagination. If you are using CORS, did you declare Content-Range in the Access-Control-Expose-Headers header?'
);
}
return {
data: json,
total: parseInt(
headers
.get('content-range')
.split('/')
.pop(),
10
),
};
case CREATE:
return { data: { ...params.data, id: json.id } };
default:
return { data: json };
}
});
};
And finally there is the ASP.NET Controller:
[Route("api/[controller]")]
[ApiController]
public abstract class RaController<T> : ControllerBase, IRaController<T> where T : class, new()
{
protected readonly IDveDbContext _context;
protected DbSet<T> _table;
public RaController(IDveDbContext context)
{
_context = context;
_table = _context.Set<T>();
}
[HttpGet("{id}")]
public async Task<ActionResult<T>> Get(int id)
{
var entity = await _table.FindAsync(id);
if (entity == null)
{
return NotFound();
}
return entity;
}
[HttpPost]
public async Task<ActionResult<T>> Post(T entity)
{
_table.Add(entity);
await _context.SaveChangesAsync();
var id = (int)typeof(T).GetProperty("Id").GetValue(entity);
return Ok(await _table.FindAsync(id));
}
}
And the exact ProjectsController
[Route("api/[controller]")]
public class ProjectsController : RaController<Project>
{
private IProjectService projectService;
public ProjectsController(IProjectService projectService, IDveDbContext dbContext)
: base (dbContext)
{
this.projectService = projectService;
}
}
I've been searching around for a solution but couldn't find any. If somebody give a hint on where the problem might be or provide an example for successflly integrated ASP.Net Core with React-Admin I would be extremely thankful!
I've gotten it to work well with a .net REST API, starting with the same pieces you started with. I see a few issues in yours:
1. Use a better data provider.
The built-in provider is more of an example. It only works with an identifier of "id." zachrybaker/ra-data-rest-client allows for you to have more realistic flexibility in what your identifier is called, for existing APIs.
2. Pattern your base controller.
Your base controller needs some adjustment. It should describe what it produces (application/json) and the POST method is "Create" not "Post" since you need to differentiate between create(insert) and update (hence the 404).
I'll also note that your base method could use to describe what type if identifier, rather than assuming int, if you need that. Here's mine. I'm hoping to open source the whole thing, stay tuned.
[ApiController]
[Produces("application/json")]
public abstract class EntityWithIdControllerBase<TEntity, TReadModel, TCreateModel, TUpdateModel, TIndentifier> : BaseEFCoreAPIController
where TEntity : class, IHaveIdentifier<TIndentifier>
{
////....
////constructor....
////....
[HttpPost("")]
public async virtual Task<ActionResult<TReadModel>> Create(CancellationToken cancellationToken, TCreateModel createModel)
{
return await CreateModel(createModel, cancellationToken);
}
////....
protected virtual async Task<TReadModel> CreateModel(TCreateModel createModel, CancellationToken cancellationToken = default(CancellationToken))
{
// create new entity from model
var entity = Mapper.Map<TEntity>(createModel);
// add to data set, id should be generated
await DataContext
.Set<TEntity>()
.AddAsync(entity, cancellationToken);
// save to database
await DataContext
.SaveChangesAsync(cancellationToken);
// convert to read model
var readModel = await ReadModel(entity.Id, cancellationToken);
return readModel;
}
////....
protected virtual async Task<TReadModel> ReadModel(TIndentifier id, CancellationToken cancellationToken = default(CancellationToken))
{
var model = await IncludeTheseForDetailResponse(DataContext
.Set<TEntity>()
.AsNoTracking())
.Where(GetIdentifierEqualityFn(id))
.ProjectTo<TReadModel>(Mapper.ConfigurationProvider)
.FirstOrDefaultAsync(cancellationToken);
return model;
}
////....
#region Expressions
protected virtual Expression<Func<TEntity, bool>> IdentityExpressionFn(ExpressionStarter<TEntity> expressionStarter, JToken token)
{
return expressionStarter.Or(GetIdentifierEqualityFn(token.Value<TIndentifier>()));
}
protected abstract Expression<Func<TEntity, bool>> GetIdentifierEqualityFn(TIndentifier o);
/// <summary>
/// When you need to include related entities on the detail response, you can add them via this IQueryable by overriding this function
/// </summary>
/// <param name="queryable"></param>
/// <returns></returns>
protected virtual IQueryable<TEntity> IncludeTheseForDetailResponse(IQueryable<TEntity> queryable) => queryable;
#endregion Expressions
}
Just to let you know, the real fun begins when you start to deal with the filtering ability of react admin. That is where my "Expressions" come into play. I have a base class that can leverage expression builders and uses LinqKit for dynamic when really necessary.
Many of my controllers are Guid, so I have an intermediate controller for that:
[ApiController]
[Produces("application/json")]
public abstract class EntityWithGuidIdController<TEntity, TReadModel, TCreateModel, TUpdateModel> :
EntityWithIdControllerBase<TEntity, TReadModel, TCreateModel, TUpdateModel, Guid>
where TEntity : class, IHaveIdentifier<Guid>, new()
{
////....
////constructor....
////....
#region Expressions
/// <summary>
/// If you need to use a primay key other than "Id", then override this.
/// </summary>
/// <param name="g"></param>
/// <returns></returns>
protected override Expression<Func<TEntity, bool>> GetIdentifierEqualityFn(Guid g) => (x => x.Id == g);
protected override Expression<Func<TEntity, bool>> IdentityExpressionFn(ExpressionStarter<TEntity> expressionStarter, JToken token)
{
if (Guid.TryParse((string)token, out Guid g))
return expressionStarter.Or(GetIdentifierEqualityFn(g));
Debug.WriteLine((string)token + " Is NOT a valid Guid. why was it sent?");
return null;
}
#endregion Expressions
}
Then I can consume it as simply as this -> in this case the detail response also includes a second object:
[Route("api/[controller]")]
public class UserController : EntityWithGuidIdController<User, UserReadModel, UserCreateModel, UserUpdateModel>
{
public UserController(ZachTestContext dataContext, IMapper mapper) : base(dataContext, mapper, CacheValidInterval.OneDay)
{ }
// Include the Adloc on the detail response.
protected override IQueryable<User> IncludeTheseForDetailResponse(IQueryable<User> queryable) => queryable.Include(x => x.Adloc);
}

Nativescript custom control - Update Dependency Property

I'm creating a custom component which is a list of Buttons.
When the user clicks on a button, i change its css class and then i would like to add it in a custom "selectedItems" property to retrieve it in my ViewModel.
When I do a push on the "selectedItems" array property, no event is raised and I don't get the information.
Also, I tried to re-set the entire array but not better.
I don't know how to achieve this.
Here is the code of my component :
import {WrapLayout} from "ui/layouts/wrap-layout";
import {EventData} from "data/observable";
import {ValueButton} from "./value-button";
import dependencyObservableModule = require("ui/core/dependency-observable");
export class ValuesSelector extends WrapLayout {
public static itemsProperty = new dependencyObservableModule.Property(
"items",
"ValuesSelector",
new dependencyObservableModule.PropertyMetadata(
[],
dependencyObservableModule.PropertyMetadataSettings.None,
function(data: dependencyObservableModule.PropertyChangeData) {
if (data.newValue) {
let instance = <ValuesSelector>data.object;
instance.items = data.newValue;
}
}));
public static deleteOnClickProperty = new dependencyObservableModule.Property(
"deleteOnClick",
"ValuesSelector",
new dependencyObservableModule.PropertyMetadata(
false,
dependencyObservableModule.PropertyMetadataSettings.None));
public static selectedItemsProperty = new dependencyObservableModule.Property(
"selectedItems",
"ValuesSelector",
new dependencyObservableModule.PropertyMetadata(
[],
dependencyObservableModule.PropertyMetadataSettings.None));
public static singleSelectionProperty = new dependencyObservableModule.Property(
"singleSelection",
"ValuesSelector",
new dependencyObservableModule.PropertyMetadata(
false,
dependencyObservableModule.PropertyMetadataSettings.None));
public get selectedItems() {
return this._getValue(ValuesSelector.selectedItemsProperty);
}
public set selectedItems(value: any[]) {
this._setValue(ValuesSelector.selectedItemsProperty, value);
}
public get deleteOnClick() {
return this._getValue(ValuesSelector.deleteOnClickProperty);
}
public set deleteOnClick(value: boolean) {
this._setValue(ValuesSelector.deleteOnClickProperty, value);
}
public get singleSelection() {
return this._getValue(ValuesSelector.singleSelectionProperty);
}
public set singleSelection(value: boolean) {
this._setValue(ValuesSelector.singleSelectionProperty, value);
}
public get items() {
return this._getValue(ValuesSelector.itemsProperty);
}
public set items(value: any) {
this._setValue(ValuesSelector.itemsProperty, value);
this.createUI();
}
private _buttons: ValueButton[];
constructor() {
super();
this.orientation = "horizontal";
this._buttons = [];
}
private createUI() {
this.removeChildren();
let itemsLength = this.items.length;
for (let i = 0; i < itemsLength; i++) {
let itemButton = new ValueButton();
itemButton.text = this.items[i].label;
itemButton.value = this.items[i];
itemButton.className = "values-selector-item";
if (this.deleteOnClick) {
itemButton.className = "values-selector-selected-item";
}
itemButton.on(ValueButton.tapEvent, (data: EventData) => {
let clickedButton = <ValueButton>data.object;
if (this.deleteOnClick) {
let itemIndex = this.items.indexOf(clickedButton.value);
if (itemIndex > -1) {
let newSelectedItems = this.items;
newSelectedItems.splice(itemIndex, 1);
this.items = newSelectedItems;
}
return;
}
let internalSelectedItems = this.selectedItems;
if (clickedButton.className === "values-selector-item") {
if (this.singleSelection && this.selectedItems.length > 0) {
internalSelectedItems = [];
for (let i = 0; i < this._buttons.length; i++) {
this._buttons[i].className = "values-selector-item";
}
}
internalSelectedItems.push(clickedButton.value);
clickedButton.className = "values-selector-selected-item";
} else {
let itemIndex = internalSelectedItems.indexOf(clickedButton.value);
if (itemIndex > -1) {
internalSelectedItems.splice(itemIndex, 1);
}
clickedButton.className = "values-selector-item";
}
this.selectedItems = internalSelectedItems;
}, this);
this._buttons.push(itemButton);
this.addChild(itemButton);
}
}
}
Can you help me ?
Thanks
Ok I made a mistake by databinding my property.
In fact, in the XML I use the component like this :
<vs:ValuesSelector items="{{ criterias }}" selectedItems="{{ myObject.selectedCriterias }}" />
But in the ViewModel, I never initialized the selectedCriterias property because I thought that the default value [] specified in the component would create it.
So in the ViewModel, here is the fix :
Before
this.myObject = {
id : 0
};
After
this.myObject = {
id : 0,
selectedCriterias: []
};

cakephp user authentication for adobe air app

I have a web application developed using flex and cakephp. My client need to make a desktop application of that web application using Adobe Air. The conversion of the flex to Air is done successfully. I the flex application the communication of flex and cakephp is handled using a remotes controller.
In air application I have a problem of authenticating the user with cakephp default user authentication. Can anyone help me to find a solution for this?
i suggest you to send your user credentials via POST to your cakephp backend.
A login function in your UsersController would look something like this:
public function login() {
if ($this->Auth->login()) {
$this->serviceResponse(Status::SUCCESS);
} else {
$this->serviceResponse(Status::AUTH_FAILED);
}
}
// this is just an example out of my appcontroller to send json responses
public function serviceResponse($code, $data = array()) {
$response = compact('code', 'data');
$this->response->body(json_encode($response));
$this->response->send();
$this->shutdownProcess();
exit;
}
// I also defined a class for return statuses
class Status {
const SUCCESS = 'success';
const ERROR = 'error';
...
}
Basically, you want to send the validation request as an ajax request. To do that you need to modify headers, capture session ids through cookies and post them to keep the session alive. It expects a JSON object returned from Cake.
I've created a couple Flex classes that achieve just this for a Flex Mobile App and a CakePHP backend. It should work the same for your needs.
It's in two files, the AutoValidationUrlRequest.as extends the HeaderURLRequest.as file. I'm not positive but there may be a couple variables to change, but overall it works very well and probably won't take more than a couple changes to get it working on your app.
To use it, simply create a new AutoValidationUrlRequest object and add an event listener on the headerUrlRequestComplete Event and then run AutoValidationUrlRequest.send(...) to POST. there is also a handy method called convertToPostVars for easy Cake Friendly Post variables.
AutoValidationUrlRequest.as:
package libs
{
import flash.events.Event;
import flash.events.EventDispatcher;
import flash.events.HTTPStatusEvent;
import models.LocalDeviceData;
import models.Model;
import mx.collections.ArrayCollection;
import mx.core.FlexGlobals;
import mx.utils.ObjectUtil;
[Event("LoginFailed")]
[Event("MobileUserDoesNotExist")]
public class AutoValidationURLRequest extends HeaderURLRequest
{
public static const LOGIN_FAILED:String = "LoginFailed";
public static const MOBILE_USER_DOES_NOT_EXIST:String = "MobileUserDoesNotExist";
/**
* will automatically be set by this class, if not set will login
*/
public var requestHeaders:Object = new Object();
private var _cookie:Object;
/**
* should be an object with name of the cookie variables parsed in as key/value pairs
*/
protected function set cookie(ck:Object):void{
_cookie = ck;
}
protected function get cookie():Object{
return _cookie;
}
public function AutoValidationURLRequest(){
};
public function send(url:String, postData:Object, generateUrlVars:Boolean = true):void{
if(generateUrlVars){
postData = convertToPostVars(postData);
}
sendRequest("http://yourwebsite.com"+url, postData, requestHeaders);
}
override protected function parseHeaders(e:HTTPStatusEvent):void{
super.parseHeaders(e);
if('Set-Cookie' in headers){
requestHeaders['Cookie'] = parseCookie(headers['Set-Cookie']);
//requestHeaders['User-Agent'] = headers['User-Agent'];
}
}
/**
* returns the cookie key/val string for send requests back to the server
*/
protected function parseCookie(cookieString:String):String{
var cookieObj:Object = new Object();
var cookieBits:Array = cookieString.split("; ");
return cookieBits[0];
/*
for each(var ck:String in cookieBits){
var cb:Array = ck.split("=");
cookieObj[cb[0]] = cb[1];
}
return cookieObj;
*/
}
}
}
HeaderURLRequest.as:
package libs
{
import flash.events.Event;
import flash.events.EventDispatcher;
import flash.events.HTTPStatusEvent;
import flash.net.URLLoader;
import flash.net.URLRequest;
import flash.net.URLRequestHeader;
import flash.net.URLRequestMethod;
import flash.net.URLVariables;
import mx.core.FlexGlobals;
import mx.rpc.events.ResultEvent;
import mx.utils.ObjectUtil;
[Event("headerUrlRequestComplete")]
public class HeaderURLRequest extends EventDispatcher
{
public static const HEADERURLREQUEST_COMPLETE:String = "headerUrlRequestComplete";
public var headers:Array = [];
public var data:Object = new Object();
public var variables:Object = new Object();
public var invalidFields:Object = new Object();
public var errorMsg:String = "";
/**
* the headers array must contain an object with a 'name' key and a 'value' key eg: cookie: <cookieStr>
*/
public function HeaderURLRequest():void{
}
public function sendRequest(url:String, postData:Object = null, requestHeaders:Object = null):void{
var urlLoader:URLLoader = new URLLoader()
var urlRequest : URLRequest = new URLRequest(url);
//make it an ajax request
urlRequest.requestHeaders.push(new URLRequestHeader('X-Requested-With', 'XMLHttpRequest'));
for(var header:* in requestHeaders){
var authHeader:URLRequestHeader = new URLRequestHeader(header as String, requestHeaders[header]);
urlRequest.requestHeaders.push(authHeader)
}
var urlVariables:URLVariables = new URLVariables();
for (var vars:* in postData){
urlVariables[vars] = postData[vars];
}
urlRequest.method = URLRequestMethod.POST
urlRequest.data = urlVariables;
urlLoader.addEventListener(Event.COMPLETE, getData);
urlLoader.addEventListener(HTTPStatusEvent.HTTP_RESPONSE_STATUS, parseHeaders);
urlLoader.load(urlRequest);
}
public function convertToPostVars(postData:Object, prependKeyName:String = ""):Object{
var params:Object = {};
if(prependKeyName == ""){
params['_method'] = 'POST';
}
for (var item:* in postData){
var objtype:Object = ObjectUtil.getClassInfo(postData[item]);
if(objtype.name == "Object"){
var modelKeyName:String = prependKeyName+"["+item+"]";
var subParams:Object = convertToPostVars(postData[item],modelKeyName);
params = merge(params, subParams);
}else{
params["data"+prependKeyName+"["+item+"]"] = postData[item];
}
}
return params;
}
public function flashErrorMsg():String{
var err:String = errorMsg;
errorMsg = "";
return err;
}
protected function parseHeaders(e:HTTPStatusEvent):void{
var i:Number=0;
headers = [];
for each(var header:URLRequestHeader in e.responseHeaders){
headers[header.name] = header.value;
i++;
}
}
protected function getData(e:Event):void{
//trace('data: ');
if(e.currentTarget.data == ''){
e.currentTarget.data = '{}';
}
data = JSON.parse(e.currentTarget.data);
//trace(ObjectUtil.toString(data));
if(data.hasOwnProperty('variables')){
variables = data.variables;
if (variables != null){
if(variables.hasOwnProperty('invalidFields')){
invalidFields = variables.invalidFields;
for (var error:String in invalidFields){
errorMsg += invalidFields[error] + "\n\n";
}
}
}else{
//no variable data found!!
}
}
dispatchEvent(new Event(HEADERURLREQUEST_COMPLETE));
}
public function isEmpty(obj:Object){
var isEmpty:Boolean = true;
for (var n in obj) { isEmpty = false; break; }
return isEmpty;
}
public function merge( obj0:Object, obj1:Object ):Object
{
var obj:Object = { };
for( var p:String in obj0 )
{
obj[ p ] = ( obj1[ p ] != null ) ? obj1[ p ] : obj0[ p ];
//trace( p, ' : obj0', obj0[ p ], 'obj1', obj1[ p ], '-> new value = ', obj[ p ] );
}
for (var p1:String in obj1){
if(!obj.hasOwnProperty(p1)){
obj[ p1 ] = obj1[ p1 ] ;
}
}
return obj;
}
}
}
Also, using this with my forked version of Jose Gonzalez's CakePHP ajax_controller plugin is really handy. It basically takes any Ajax Request and converts all view variables and outputs them into a JSON object, no rendered view. Otherwise, if you're not using an ajax request, Cake will render the views normally. Good Luck!

Getting an URL parameter value in Flex

I'm have the following URL :
www.example.com?arg1=foobar
I want to get the arg1 value.
I tried to use the following code:
var bm:IBrowserManager;
bm= bm.getInstance();
browserManager.init();
var o:Object = URLUtil.stringToObject(bm.fragment, "&");
Alert.show(o.arg1);
It works with the following URL:
www.example.com#arg1=foobar
So it requires the # instead of the ? .
How can get the parameter "arg1" from Flex (nicely) from URLs such as :
www.example.com?arg1=foobar
Thanks ;)
I answered a similar question a while ago.
I use the class Adobe provided in this article. An updated version of this class can also be found here, although I haven't tried this one.
package
{
import flash.external.*;
import flash.utils.*;
public class QueryString
{
private var _queryString:String;
private var _all:String;
private var _params:Object;
public function get queryString():String
{
return _queryString;
}
public function get url():String
{
return _all;
}
public function get parameters():Object
{
return _params;
}
public function QueryString()
{
readQueryString();
}
private function readQueryString():void
{
_params = {};
try
{
_all =
ExternalInterface.call("window.location.href.toString");
_queryString =
ExternalInterface.call("window.location.search.substring", 1);
if(_queryString)
{
var params:Array = _queryString.split('&');
var length:uint = params.length;
for (var i:uint=0,index:int=-1; i 0)
{
var key:String = kvPair.substring(0,index);
var value:String = kvPair.substring(index+1);
_params[key] = value;
}
}
}
}catch(e:Error) { trace("Some error occured.
ExternalInterface doesn't work in Standalone player."); }
}
}
}
Here's an example on how to use the Querystring class:
public function CheckForIDInQuerystring():void
{
// sample URL: http://www.mysite.com/index.aspx?id=12345
var qs:QueryString = new QueryString;
if (qs.parameters.id != null)
{
// URL contains the "id" parameter
trace(qs.parameters.id);
}
else
{
// URL doesn't contain the "id" parameter
trace("No id found.");
}
}

Flex 3: Getting variables from URL

If I have an application located at http://sitename.com/myapp/ and i want to pass in a variable via the url (i.e. - http://sitename.com/myapp/?name=Joe), how can I get that name variable into a string var?
I use the class Adobe provided in this article.
package
{
import flash.external.*;
import flash.utils.*;
public class QueryString
{
private var _queryString:String;
private var _all:String;
private var _params:Object;
public function get queryString():String
{
return _queryString;
}
public function get url():String
{
return _all;
}
public function get parameters():Object
{
return _params;
}
public function QueryString()
{
readQueryString();
}
private function readQueryString():void
{
_params = {};
try
{
_all =
ExternalInterface.call("window.location.href.toString");
_queryString =
ExternalInterface.call("window.location.search.substring", 1);
if(_queryString)
{
var params:Array = _queryString.split('&');
var length:uint = params.length;
for (var i:uint=0,index:int=-1; i 0)
{
var key:String = kvPair.substring(0,index);
var value:String = kvPair.substring(index+1);
_params[key] = value;
}
}
}
}catch(e:Error) { trace("Some error occured.
ExternalInterface doesn't work in Standalone player."); }
}
}
}
UPDATE: An updated version of this class can also be found here, although I haven't tried this one.
UPDATE 2:
Here's an example on how to use the Querystring class:
public function CheckForIDInQuerystring():void
{
// sample URL: http://www.mysite.com/index.aspx?id=12345
var qs:QueryString = new QueryString;
if (qs.parameters.id != null)
{
// URL contains the "id" parameter
trace(qs.parameters.id);
}
else
{
// URL doesn't contain the "id" parameter
trace("No id found.");
}
}
Divide 'em with String.split() and conquer:
var url:String = "http://www.xxx.zzz/myapp?arg1=vae&arg2=victus";
var params:String = url.substr(url.lastIndexOf("?") + 1);
if (params.length == url.length) return; //no params
for each (var pair:String in params.split("&"))
{
trace("Got parameter:");
var nameValue:Array = pair.split("=");
trace("name: " + nameValue[0] + ", value: " + nameValue[1]);
}
Output:
Got parameter:
name: arg1, value: vae
Got parameter:
name: arg2, value: victus

Resources