terça-feira, 26 de setembro de 2017

CRM-GT - Consumo de Web Api usando Javascript

Este artigo tem como objetivo documentar o uso de um importante recurso do meu sistema de gerenciamento com o cliente, o CRM-GT. Este recurso se chama Web Api, e serve como um [conector] entre o sistema CRMGT e qualquer sistema legado.

Esta é a URL principal de consumo REST: /api/CRMGT/QueryXML

Com este conector, você pode efetuar todo CRUD de operações. Portanto, segue abaixo o uso prático deste recurso, usando Javascript/JQuery.


Para obter o Token de acesso (OAuth)
function getToken() {
	var param = {
		"orgName": "CRMGT1",
		"clientId": "+n8c++NABWZrkldVUaf0XYH2T2lG5xvUhZEVxFz46/4=__5@_",
		"clientSecret": "7RclDz1Ilbp3jyOuXJNnyfaK3bW+MIt5fU0BL1f46to=__0@_"
	};

	$.ajax({
		type: 'GET',
		url: 'https://crm-gt.com:82/api/CRMGT/GetToken',
		data: param,
		dataType: 'json',
		async: false,
		beforeSend: function(xmlHttpRequest) {
			xmlHttpRequest.withCredentials = true;
		},
		success: function (data, textStatus, jqXHR) {
			token = data.token;
		},
		error: function (jqXHR, textStatus, errorThrown) {
			debugger;
		}
	});
}

Considerações importantes sobre o token.
- Ele expira a cada 3 horas.
- O [clientId] e o [clientSecret] são criados por Organização. O administrador da organização cria estas chaves e as disponibiliza para obtenção do Token.

Para Renovar o Token de acesso (OAuth)
function renewToken() {
    var param = {
        'orgName': 'CRMGT1',
        'clientId': '73ySJQsDnHDYugLRRYmtATjCxVmcNo2/Alz8RG9D+VQ=__5|@_11|4_13|._',
        'clientSecret': 'EPkMdqQcm0XgDxpn4lDcKKpEz0WPDQhgbXN6Nkl0JWw='
    };

    $.ajax({
        type: 'GET',
        url: 'https://crm-gt.com:82/api/CRMGT/RenewToken',
        data: param,
        dataType: 'json',
        async: false,
        beforeSend: function (xmlHttpRequest) {
            xmlHttpRequest.withCredentials = true;
        },
        success: function (data, textStatus, jqXHR) {
            if (data == null) {
                return 0;
            } else {
                token = data.token;
            }
        },
        error: function (jqXHR, textStatus, errorThrown) {
            console.log(errorThrown);
        }
    });
}

Como saber se o token de acesso expirou? Ao executar o endpoint /GetToken, o sistema retorna um atributo informando que o tempo expirou ("expires_in" : "0").

Para os métodos de CREATE e UPDATE, no momento da configuração da tag [fields/field] atributo [TYPE], informe os tipos correspondentes aos do [SQL SERVER].
Segue os principais:
bit
char
nchar
varchar
nvarchar
datetime
int
decimal
text
uniqueidentifier
Desta forma, se precisar adicionar um campo do tipo [string], informe [varchar].
Se precisar adicionar um campo do tipo [valor ou moeda], informe [decimal], e assim por diante.


Para efetuar um SELECT

function selectRecords() {
    var xmlSELECT = 
	"<select>" +
		"<organization name='CRMGT1' />" +
		"<table name='User' alias='tb1' />" +
		"<fields>" +
		"	<field name='tb1.UserId' label='ID' />" +
		"	<field name='tb1.Name' />" +
		"	<field name='tb1.Email' />" +
		"	<field name='tb2.Name' />" +
		"	<field name='tb3.Name' />" +
		"</fields>" +
		"<join isLeftJoin='false' name='Status' alias='tb2' fieldFrom='statuscode' fieldTo='statuscode'>" +
		"	<filter type='and'>" +
		"		<condition field='tb2.name' operator='=' value='Ativo' />" +
		"	</filter>" +
		"</join>" +
		"<join isLeftJoin='true' name='salesorder' alias='tb3' fieldFrom='userid' fieldTo='ownerid'>" +
		"	<filter type='and'>" +
		"		<condition field='tb3.name' operator='like' value='%ped%' />" +
		"	</filter>" +
		"</join>" +
		"<filter type='and'>" +
		"	<condition field='tb1.name' operator='like' value='%usr%' />" +
		"</filter>" +
		"<order>" +
		"	<field name='tb1.Name ASC'/>" +
		"	<field name='tb1.Email DESC'/>" +
		"</order>" +
	"</select>";

    var param = {
        "Type": "GET",
        "xmlContent": xmlSELECT
    };

    $.ajax({
        type: 'POST',
        url: "https://crm-gt.com:82/api/CRMGT/QueryXML",
        data: param,
        dataType: 'json',
        contentType: "application/x-www-form-urlencoded; charset=utf-8",
        beforeSend: function (xhr) {
            xhr.setRequestHeader("Authorization", "Bearer " + token);
        },
        success: function (data, textStatus, jqXHR) {
            if (textStatus == "success") {
                // JSON que representa os [registros] retornados
                var json = JSON.parse(data.Result);
            }
        },
        error: function (jqXHR, textStatus, errorThrown) {
            debugger;
        }
    });
}

Para efetuar um CREATE

function createRecord() {
    var xmlINSERT =
        "<insert>" +
        "<organization name='CRMGT1' />" +
        "<table name='account' isNN='false' setInternalFields='true' setWorkflowTrigger='false' />" +
        "<records>" +
        "    <record>" +
        "        <fields>" +
        "            <field name='accountid' type='uniqueidentifier' value='NEWID' />" +
        "            <field name='name' type='varchar' value='Create sample 1...' />" +
        "            <field name='AccountNumber' type='varchar' value='1' />" +
        "            <field name='ownerid' type='uniqueidentifier' lookuptable='user' lookupsearchfield='name' value='system' />" +
        "        </fields>" +
        "    </record>" +
        "    <record>" +
        "        <fields>" +
        "            <field name='accountid' type='uniqueidentifier' value='NEWID' />" +
        "            <field name='name' type='varchar' value='Create sample 2...' />" +
        "            <field name='AccountNumber' type='varchar' value='2' />" +
        "        </fields>" +
        "    </record>" +
        "</records>" +
        "</insert>";

    var param = {
        "Type": "SET",
        "xmlContent": xmlINSERT
    };

    $.ajax({
        type: 'POST',
        url: "https://crm-gt.com:82/api/CRMGT/QueryXML",        
        data: param,
        dataType: 'json',
        contentType: "application/x-www-form-urlencoded; charset=utf-8",
        beforeSend: function (xhr) {
            xhr.setRequestHeader("Authorization", "Bearer " + token);
        },
        success: function (data, textStatus, jqXHR) {
        },
        error: function (jqXHR, textStatus, errorThrown) {
        }
    });
}

Se precisar definir uma regra antes da criação dos registros, você pode adicionar a seguinte tag [rules] depois da tag [table]:
<rules>
	<rule action="insert_ignoreifexists" actionoperator="AND">
		<fields>
			<field name="protocol" type="VarChar" />
			<field name="incidentNumber" type="Int" />
		</fields>
	</rule>
</rules>

Desta forma, através da regra [insert_ignoreifexists] você não permite a inclusão do registro se os campos especificados na tag [fields] existirem na tabela.
Mas se você quiser [atualizar] o registro se ele for encontrado, use a regra [insert_updateifexists]. Para esta regra, o update será realizado usando na cláusula [where] a [chave primária] da tabela.

Formato de Campo Data
Sempre que passar um valor de data, passar no seguinte formato:


yyyyMMddTHHmmssZ


Exemplo: 20230616T095600Z


Se precisar [atualizar] o campo no CRM com valor Nulo:
<field name='dtFieldName' type='datetime' value='NULL' />


Se precisar [atualizar] o campo no CRM com a data [corrente]:
<field name='dtFieldName' type='datetime' value='GETDATE' />


Formato de Campo Decimal
Sempre que passar um valor decimal, usar o formato americano, somente com o separador de centavos:


Ex: 1234.56




Para efetuar um UPDATE

function updateRecord() {
    var xmlUPDATE =
       "<update>" +
       " <organization name='CRMGT1' />" +
       " <table name='account' setWorkflowTrigger='false' />" +
       " <set>" +
       "  <field name='name' type='varchar' value='UPDATED - Create sample 1...' setWorkflowFieldTrigger='false' />" +
       "  <field name='modifiedOn' type='datetime' value='GETDATE' />" +
       " </set>" +
       " <filter type='and'>" +
       "  <condition field='name' operator='=' value='Create sample 1...' />" +
       " </filter>" +
       "</update>";

    var param = {
       "Type": "SET",
       "xmlContent": xmlUPDATE
    };

    $.ajax({
        type: 'POST',
        url: "https://crm-gt.com:82/api/CRMGT/QueryXML",        
        data: param,
        dataType: 'json',
        contentType: "application/x-www-form-urlencoded; charset=utf-8",
        beforeSend: function (xhr) {
            xhr.setRequestHeader("Authorization", "Bearer " + token);
        },
        success: function (data, textStatus, jqXHR) {
        },
        error: function (jqXHR, textStatus, errorThrown) {
        }
    });
}

Para efetuar um DELETE

function deleteRecord() {
    var xmlDELETE =
        "<delete>" +
        " <organization name='CRMGT1' />" +
        " <table name='account' setWorkflowTrigger='false' />" +
        " <filter type='and'>" +
        "  <condition field='name' operator='like' value='%Create sample%' />" +
        " </filter>" +
        "</delete>";

    var param = {
        "Type": "SET",
        "xmlContent": xmlDELETE
    };

    $.ajax({
        type: 'POST',
        url: "https://crm-gt.com:82/api/CRMGT/QueryXML",        
        data: param,
        dataType: 'json',
        contentType: "application/x-www-form-urlencoded; charset=utf-8",
        beforeSend: function (xhr) {
            xhr.setRequestHeader("Authorization", "Bearer " + token);
        },
        success: function (data, textStatus, jqXHR) {
        },
        error: function (jqXHR, textStatus, errorThrown) {
        }
    });
}

Para executar uma [QUERY EXEC]

function executeQE() {
    var xmlQE =
	"<queryexec>" +
	" <organization name='CRMGT1' />" +
	" <queryexecid value='buscarocorrencia' />" +
	" <paramset>" +
	"	<param name='@_id_' type='uniqueidentifier' value='62a9a715-8574-4813-a69f-e732c76313e0' />" +
	" </paramset>" +
	"<filter type='and'>" +
	"	<condition field='inc.StatusCode' operator='=' value='1' />" +
	"</filter>" +
	"</queryexec>";

    var param = {
        "Type": "QueryExec",
        "xmlContent": xmlQE
    };

    $.ajax({
        type: 'POST',
        url: "https://crm-gt.com:82/api/CRMGT/QueryXML",        
        data: param,
        dataType: 'json',
        contentType: "application/x-www-form-urlencoded; charset=utf-8",
        beforeSend: function (xhr) {
            xhr.setRequestHeader("Authorization", "Bearer " + token);
        },
        success: function (data, textStatus, jqXHR) {
        },
        error: function (jqXHR, textStatus, errorThrown) {
        }
    });
}

Obs: As tags <paramset> e <filter> são [opcionais]. Elas são úteis quando precisar passar mais parâmetros de filtro para a Query Exec criada no sistema ou para substituir o valor das macros do sistema (@_id_ por exemplo).

CRM-GT - Consumo de Web Api usando C#

Este artigo tem como objetivo documentar o uso de um importante recurso do meu sistema de gerenciamento com o cliente, o CRM-GT. Este recurso se chama Web Api, e serve como um [conector] entre o sistema CRMGT e qualquer sistema legado.

Esta é a URL principal de consumo REST: /api/CRMGT/QueryXML

Com este conector, você pode efetuar todo CRUD de operações. Portanto, segue abaixo o uso prático deste recurso, usando C#.

Busca do Token de acesso (OAuth)
var url = "https://crm-gt.com/api/CRMGT/GetToken";
url += "?orgName=CRMGT1";
url += "&clientId=+n8c++NABWZrkldVUaf0XYH2T2lG5xvUhZEVxFz46/4=__5@_";
url += "&clientSecret=7RclDz1Ilbp3jyOuXJNnyfaK3bW+MIt5fU0BL1f46to=__0@_";
var jToken = GetString(url);
var token = JsonConvert.DeserializeObject<JObject>(jToken).SelectToken("token").ToString();

Considerações importantes sobre o token.
- Ele expira a cada 3 horas.
- O [clientId] e o [clientSecret] são criados por Organização. O administrador da organização cria estas chaves e as disponibiliza para obtenção do Token.

Para Renovar o Token de acesso (OAuth)
var url = "https://crm-gt.com/api/CRMGT/RenewToken";
url += "?orgName=CRMGT1";
url += "&clientId=+n8c++NABWZrkldVUaf0XYH2T2lG5xvUhZEVxFz46/4=__5@_";
url += "&clientSecret=7RclDz1Ilbp3jyOuXJNnyfaK3bW+MIt5fU0BL1f46to=__0@_";
var jToken = GetString(url);
var token = JsonConvert.DeserializeObject<JObject>(jToken).SelectToken("token").ToString();

Como saber se o token de acesso expirou? Ao executar o endpoint /GetToken, o sistema retorna um atributo informando que o tempo expirou ("expires_in" : "0").


Para os métodos de CREATE e UPDATE, no momento da configuração da tag [fields/field] atributo [TYPE], informe os tipos correspondentes aos do [SQL SERVER].
Segue os principais:
bit
char
nchar
varchar
nvarchar
datetime
int
decimal
text
uniqueidentifier
Desta forma, se precisar adicionar um campo do tipo [string], informe [varchar].
Se precisar adicionar um campo do tipo [valor ou moeda], informe [decimal], e assim por diante.


Para efetuar um SELECT

1 - Monte o "SELECT" através de uma instrução XML:
string xmlSELECT = @"
<select>
	<organization name='{0}' />
	<table name='User' alias='tb1' />
	<fields>
		<field name='tb1.UserId' label='ID' />
		<field name='tb1.Name' />
		<field name='tb1.Email' />
		<field name='tb2.Name' />
		<field name='tb3.Name' />
	</fields>
	<join isLeftJoin='false' name='Status' alias='tb2' fieldFrom='statuscode' fieldTo='statuscode'>
		<filter type='and'>
			<condition field='tb2.name' operator='=' value='Ativo' />
		</filter>                     
	</join>
	<join isLeftJoin='true' name='salesorder' alias='tb3' fieldFrom='userid' fieldTo='ownerid'>
		<filter type='and'>
			<condition field='tb3.name' operator='like' value='%ped%' />
		</filter>                     
	</join>
	<filter type='and'>
		<condition field='tb1.name' operator='like' value='%usr%' />
	</filter>
	<order>
		<field name='tb1.Name ASC'/>
		<field name='tb1.Email DESC'/>
	</order>	
</select>";

O XML acima é a representação do SELECT-SQL abaixo:
SELECT tb1.UserId, tb1.Name, tb1.Email, tb2.Name, tb3.Name
FROM [User] tb1 
JOIN [Status] tb2 ON tb1.statuscode = tb2.statuscode AND tb2.name = 'Ativo'
LEFT JOIN [salesorder] tb3 ON TB1.UserId = tb3.ownerid AND tb3.name like '%ped%'
WHERE tb1.Name like '%usr%'
ORDER BY tb1.Name ASC, tb1.Email DESC

2 - Passe como parâmetro a informação de [Organização], conforme a sequência abaixo.
var xmlF = string.Format(xmlSELECT, "CRMGT1");

3 - Execute a instrução, chamando a Web Api:
var header = new Dictionary<string, object>();
header.Add("Authorization", $"Bearer {token}");

var content = new Dictionary<string, object>();
content.Add("Type", "GET");
content.Add("Caller", "My Function Caller Name");
content.Add("xmlContent", xmlF);

var result = ExecutePostAsync("https://crm-gt.com/api/CRMGT/QueryXML", content, header);

Segue abaixo métodos [GetString] e [ExecutePostAsync] que efetuam chamadas HTTP:
public static String GetString(String url)
{
   var urlContents = "";
   try
   {
      using (HttpClient client = new HttpClient())
      {
         urlContents = client.GetStringAsync(new Uri(url)).Result;
      }
   }
   catch (Exception ex)
   {
   }

   return urlContents;
}

public static string ExecutePostAsync(string url, Dictionary<string, object> content, Dictionary<string, object> headerList = null) {
	string result = "";

	try {
		using (HttpClient client = new HttpClient()) {
			if (headerList != null) {
				foreach (var item in headerList) {
					client.DefaultRequestHeaders.Add(item.Key, item.Value.ToString());
				}
			}

			if (content != null) {
				var multiPartContent = new MultipartFormDataContent();
				foreach (var item in content) {
					multiPartContent.Add(new StringContent(item.Value.ToString()), string.Format("\"{0}\"", item.Key));
				}

				var request = new HttpRequestMessage(HttpMethod.Post, url);
				request.Content = multiPartContent;
				result = client.SendAsync(request).Result.Content.ReadAsStringAsync().Result;
			}
		}
	}
	catch (Exception ex) {
	}

	return result;
}


Para efetuar um CREATE

1 - Monte o "CREATE" através de uma instrução XML:
string xmlINSERT = @"
    <insert>
        <organization name='{0}' />
        <table name='{1}' isNN='false' setInternalFields='true' setWorkflowTrigger='false' />
        <records>
            <record>
                <fields>
                    <field name='statuscode' type='int' value='998' />
                    <field name='name' type='varchar' value='test1...' />
                    <field name='ownerid' type='uniqueidentifier' lookuptable='user' lookupsearchfield='name' value='system' />
                </fields>
            </record>
            <record>
                <fields>
                    <field name='statuscode' type='int' value='999' />
                    <field name='name' type='varchar' value='test2...' />
                    <field name='ownerid' type='uniqueidentifier' value='9EC0ABA8-2947-4857-9698-1C43BBBFAB5B' />
                </fields>
            </record>
        </records>
    </insert>";

2 - Passe como parâmetro as informações de [Organização] e [tabela], conforme a sequência abaixo.
var xmlF = string.Format(xmlINSERT, "CRMGT1", "lead");

3 - Execute a instrução, chamando a Web Api:
var header = new Dictionary<string, object>();
header.Add("Authorization", $"Bearer {token}");

var content = new Dictionary<string, object>();
content.Add("Type", "SET");
content.Add("Caller", "My Function Caller Name");
content.Add("xmlContent", xmlF);

var result = ExecutePostAsync("https://crm-gt.com/api/CRMGT/QueryXML", content, header);

Para inserir dados em uma tabela N-N, adicione o atributo [isNN='true'] na tag xml [table], conforme exemplificado abaixo:
string xmlINSERT = @"
    <insert>
        <organization name='{0}' />
        <table name='{1}' isNN='true' />
        <records>
            <record>
                <fields>
                    <field name='pricelist_productid' type='uniqueidentifier' value='NEWID' />
                    <field name='pricelistid' type='uniqueidentifier' value='3BEF33F8-A159-4F77-964D-B820B8414E59' />
                    <field name='productid' type='uniqueidentifier' value='B091A317-4FF7-42BE-AD86-D77C22059E49' />
                </fields>
            </record>
            <record>
                <fields>
                    <field name='pricelist_productid' type='uniqueidentifier' value='NEWID' />
                    <field name='pricelistid' type='uniqueidentifier' value='3BEF33F8-A159-4F77-964D-B820B8414E59' />
                    <field name='productid' type='uniqueidentifier' value='6922B778-B4FF-4564-A992-E40A020C599F' />
                </fields>
            </record>
        </records>
    </insert>";

Se precisar definir uma regra antes da criação dos registros, você pode adicionar a seguinte tag [rules] depois da tag [table]:
<rules>
	<rule action="insert_ignoreifexists" actionoperator="AND">
		<fields>
			<field name="protocol" type="VarChar" />
			<field name="incidentNumber" type="Int" />
		</fields>
	</rule>
</rules>

Desta forma, através da regra [insert_ignoreifexists] você não permite a inclusão do registro se os campos especificados na tag [fields] existirem na tabela.
Mas se você quiser [atualizar] o registro se ele for encontrado, use a regra [insert_updateifexists]. Para esta regra, o update será realizado usando na cláusula [where] a [chave primária] da tabela.

Formato de Campo Data
Sempre que passar um valor de data, passar no seguinte formato:


yyyyMMddTHHmmssZ


Exemplo: 20230616T095600Z


Se precisar [atualizar] o campo no CRM com valor Nulo:
<field name='dtFieldName' type='datetime' value='NULL' />


Se precisar [atualizar] o campo no CRM com a data [corrente]:
<field name='dtFieldName' type='datetime' value='GETDATE' />


Formato de Campo Decimal
Sempre que passar um valor decimal, usar o formato americano, somente com o separador de centavos:


Ex: 1234.56



Para efetuar um UPDATE

1 - Monte o "UPDATE" através de uma instrução XML:
string xmlUPDATE = @"
    <update>
        <organization name='{0}' />
        <table name='{1}' setWorkflowTrigger='false' />
        <set>
            <field name='name' type='varchar' value='TEST UPD...' setWorkflowFieldTrigger='false' />
            <field name='modifiedOn' type='datetime' value='GETDATE' />
        </set>
        <filter type='and'>
            <condition field='name' operator='=' value='TEST' />
        </filter>
    </update>";

2 - Passe como parâmetro as informações de [Organização] e [tabela], conforme a sequência abaixo.
var xmlF = string.Format(xmlUPDATE, "CRMGT1", "Status");

3 - Execute a instrução, chamando a Web Api:
var header = new Dictionary<string, object>();
header.Add("Authorization", $"Bearer {token}");

var content = new Dictionary<string, object>();
content.Add("Type", "SET");
content.Add("Caller", "My Function Caller Name");
content.Add("xmlContent", xmlF);

var result = ExecutePostAsync("https://crm-gt.com/api/CRMGT/QueryXML", content, header);


Para efetuar um DELETE

1 - Monte o "DELETE" através de uma instrução XML:
string xmlDELETE = @"
    <delete>
        <organization name='{0}' />
        <table name='{1}' setWorkflowTrigger='false' />
        <filter type='and'>
            <condition field='name' operator='=' value='TEST...' />
        </filter>
    </delete>";

2 - Passe como parâmetro as informações de [Organização] e [tabela], conforme a sequência abaixo.
var xmlF = string.Format(xmlDELETE, "CRMGT1", "Status");

3 - Execute a instrução, chamado a Web Api:
var header = new Dictionary<string, object>();
header.Add("Authorization", $"Bearer {token}");

var content = new Dictionary<string, object>();
content.Add("Type", "SET");
content.Add("Caller", "My Function Caller Name");
content.Add("xmlContent", xmlF);

var result = ExecutePostAsync("https://crm-gt.com/api/CRMGT/QueryXML", content, header);


Para executar uma [QUERY EXEC]

1 - Monte a "QUERY EXEC" através de uma instrução XML:
string xmlQE = @"
	<queryexec>
	<organization name='{0}' />
	<queryexecid value='{1}' />
	<paramset>
		<param name='@_id_' type='uniqueidentifier' value='62a9a715-8574-4813-a69f-e732c76313e0' />
	</paramset>
	<filter type='and'>
		<condition field='inc.StatusCode' operator='=' value='1' />
	</filter>
	</queryexec>";

2 - Passe como parâmetro as informações de [Organização] e [ID da query exec], conforme a sequência abaixo.
var xmlF = string.Format(xmlQE, "CRMGT1", "buscarocorrencias");

3 - Execute a instrução, chamado a Web Api:
var header = new Dictionary<string, object>();
header.Add("Authorization", $"Bearer {token}");

var content = new Dictionary<string, object>();
content.Add("Type", "QueryExec");
content.Add("Caller", "My Function Caller Name");
content.Add("xmlContent", xmlF);

var result = ExecutePostAsync("https://crm-gt.com/api/CRMGT/QueryXML", content, header);

Obs: As tags <paramset> e <filter> são [opcionais]. Elas são úteis quando precisar passar mais parâmetros de filtro para a Query Exec criada no sistema ou para substituir o valor das macros do sistema (@_id_ por exemplo).

quarta-feira, 20 de setembro de 2017

Dynamics 365 Web Api - CRM REST Builder

Uma excelente ferramenta construtora de chamadas de Web Api está disponível no Github, chamada CRM REST Builder!

Com ela podemos acelerar e muito a construção das nossas queries, quais atributos usar (se são case sensitive, por exemplo), quais as actions e functions disponíveis, etc...

Baixe agora mesmo a [Solution] e importe no seu CRM - CRMRESTBuilder Solution - RELEASE


domingo, 10 de setembro de 2017

Um Tutorial para sua Primeira Aplicação AngularJS

O que é AngularJS?

AngularJS é um framework JavaScript MVC desenvolvido pelo Google, que permite que você construa aplicações front-end bem estruturadas, fácil de verificar e manter.

Por que eu deveria usá-lo?

Se ainda não experimentou o AngularJS, está perdendo. A estrutura consiste em um conjunto de ferramentas bem integradas que o ajudará a criar aplicativos bem estruturados e ricos do lado do cliente de forma modular, com menos código e mais flexibilidade.

O AngularJS amplia o HTML fornecendo diretrizes que adicionam funcionalidades à sua marcação e permitem que você crie modelos dinâmicos poderosos. Você também pode criar suas próprias diretrizes, criando componentes reutilizáveis ​​que preencham suas necessidades e abstraindo toda a lógica de manipulação de DOM.

Ele também implementa vinculação de dados bidirecionais, conectando seu HTML (visualizações) aos seus objetos JavaScript (modelos) de forma transparente. Em termos simples, isso significa que qualquer atualização em seu modelo será imediatamente refletida em sua visão sem a necessidade de manipulação de DOM ou manipulação de eventos (por exemplo, com jQuery).

Finalmente, adoro Angular devido à sua flexibilidade em relação à comunicação do servidor. Como a maioria dos frameworks MVC do JavaScript, ele permite que você trabalhe com qualquer tecnologia do lado do servidor, desde que possa atender seu aplicativo através de uma API da Web RESTful. Mas Angular também fornece serviços no topo do XHR que simplificam dramaticamente o seu código e permitem que você abstraia as chamadas da API em serviços reutilizáveis. Como resultado, você pode mover sua lógica de modelo e de negócios para o front-end e criar aplicativos web agnósticos back-end. Nesta publicação, faremos exatamente isso, um passo de cada vez.

Então, onde eu começo?

Primeiro, vamos decidir a natureza do aplicativo que queremos construir. Neste guia, preferimos não gastar muito tempo no back-end, então vamos escrever algo com base em dados facilmente acessíveis na Internet - como um aplicativo de alimentação esportiva!

Como eu sou um grande fã de automobilismo e Fórmula 1, usarei um serviço de API de autosport para atuar como nosso back-end. Felizmente, os caras da Ergast são gentis o suficiente para fornecer uma API gratuita de automobilismo que será perfeita para nós.

Para um pico furtivo no que vamos construir, dê uma olhada na demo ao vivo. Para embelezar a demonstração e mostrar alguns modelos angulares, apliquei um tema Bootstrap de WrapBootstrap, mas vendo que este artigo não é sobre CSS, vou resumi-lo dos exemplos e deixá-lo fora.

Tutorial de início

Vamos começar o nosso aplicativo de exemplo. Eu recomendo o projeto de sementes angulares, pois não só fornece um ótimo esqueleto para bootstrapping, mas também define o solo para teste de unidade com Karma e Jasmine (não vamos fazer nenhum teste nesta demo, então vamos apenas Deixar as coisas de lado por enquanto, consulte a Parte 2 deste tutorial para obter mais informações sobre a configuração do seu projeto para testes de unidade e de ponta a ponta).

EDIT (maio de 2014): desde que escrevi este tutorial, o projeto de sementes angulares passou por algumas mudanças pesadas (incluindo a adição de Bower como gerenciador de pacotes). Se você tiver dúvidas sobre como implantar o projeto, veja rapidamente a primeira seção de seu guia de referência. Na parte 2 do tutorial, o Bower , entre outras ferramentas, é coberto com maior detalhe.

OK, agora que clonamos o repositório e instalamos as dependências, o esqueleto do nosso aplicativo ficará assim:



Agora podemos começar a codificar. Como estamos tentando construir uma alimentação esportiva para um campeonato de corrida, vamos começar com a visão mais relevante: a tabela do campeonato.



Dado que já temos uma lista de drivers definida no nosso escopo (fique comigo - vamos chegar lá) e ignorando qualquer CSS (para legibilidade), nosso HTML pode parecer:

<body ng-app="F1FeederApp" ng-controller="driversController">
  <table>
    <thead>
      <tr><th colspan="4">Drivers Championship Standings</th></tr>
    </thead>
    <tbody>
      <tr ng-repeat="driver in driversList">
        <td>{{$index + 1}}</td>
        <td>
          <img src="img/flags/{{driver.Driver.nationality}}.png" />
          {{driver.Driver.givenName}}&nbsp;{{driver.Driver.familyName}}
        </td>
        <td>{{driver.Constructors[0].name}}</td>
        <td>{{driver.points}}</td>
      </tr>
    </tbody>
  </table>
</body>


A primeira coisa que você notará neste modelo é o uso de expressões ("{{" e "}}") para retornar valores variáveis. Em AngularJS, as expressões permitem que você execute alguma computação para retornar o valor desejado. Algumas expressões válidas seriam:

{{ 1 + 1 }}
{{ 946757880 | date }}
{{ user.name }}


Efetivamente, as expressões são snippets com JavaScript. Mas apesar de ser muito poderoso, você não deve usar expressões para implementar qualquer lógica de nível superior. Para isso, usamos diretrizes.

Compreender as Diretivas Básicas

A segunda coisa que você notará é a presença de ng-attributes, que você não veria na marcação típica. São diretrizes.

Em um nível alto, as diretivas são marcadores (como atributos, tags e nomes de classes) que indicam ao AngularJS para anexar um determinado comportamento a um elemento DOM (ou transformá-lo, substituí-lo, etc.). Vamos dar uma olhada nos que já vimos:

A ng-app diretiva é responsável por iniciar seu aplicativo definindo seu escopo. No AngularJS, você pode ter vários aplicativos na mesma página, então esta diretiva define onde cada aplicativo distinto começa e termina.
A ng-controller diretiva define qual controlador será responsável por sua visão. Nesse caso, denotamos o driversController, que fornecerá nossa lista de drivers ( driversList).
A ng-repeat diretriz é uma das mais usadas e serve para definir seu escopo de modelo ao fazer um loop pelas coleções. No exemplo acima, ele replica uma linha na tabela para cada driver em driversList.

Adicionando controladores

Claro, não há nenhum uso para nossa visão sem um controlador. Vamos adicionar driversController aos nossos controladores.js:

angular.module('F1FeederApp.controllers', []).
controller('driversController', function($scope) {
    $scope.driversList = [
      {
          Driver: {
              givenName: 'Sebastian',
              familyName: 'Vettel'
          },
          points: 322,
          nationality: "German",
          Constructors: [
              {name: "Red Bull"}
          ]
      },
      {
          Driver: {
          givenName: 'Fernando',
              familyName: 'Alonso'
          },
          points: 207,
          nationality: "Spanish",
          Constructors: [
              {name: "Ferrari"}
          ]
      }
    ];
});


Você pode ter notado a $scope variável que estamos passando como um parâmetro para o controlador. A $scope variável deve ligar o controlador e as visualizações. Em particular, ele contém todos os dados que serão usados ​​em seu modelo. Qualquer coisa que você adicionar a ele (como driversList no exemplo acima) será diretamente acessível em suas visualizações. Por enquanto, vamos trabalhar com uma matriz de dados estática, que nós vamos substituir mais tarde com nosso serviço API.

Agora, adicione isso ao app.js:

angular.module('F1FeederApp', [
  'F1FeederApp.controllers'
]);


Com esta linha de código, nós realmente inicializamos nosso aplicativo e registramos os módulos nos quais ele depende. Voltaremos a esse arquivo (app.js) mais tarde.

Agora, vamos colocar tudo em index.html:

<!DOCTYPE html>
<html>
<head>
  <title>F-1 Feeder</title>
</head>

<body ng-app="F1FeederApp" ng-controller="driversController">
  <table>
    <thead>
      <tr><th colspan="4">Drivers Championship Standings</th></tr>
    </thead>
    <tbody>
      <tr ng-repeat="driver in driversList">
        <td>{{$index + 1}}</td>
        <td>
          <img src="img/flags/{{driver.Driver.nationality}}.png" />
          {{driver.Driver.givenName}}&nbsp;{{driver.Driver.familyName}}
        </td>
        <td>{{driver.Constructors[0].name}}</td>
        <td>{{driver.points}}</td>
      </tr>
    </tbody>
  </table>
  <script src="bower_components/angular/angular.js"></script>
  <script src="bower_components/angular-route/angular-route.js"></script>
  <script src="js/app.js"></script>
  <script src="js/services.js"></script>
  <script src="js/controllers.js"></script>
</body>
</html>


Pequenos módulos, menores erros; agora você pode inicializar seu aplicativo e verificar sua lista (estática) de drivers.

Carregando dados do servidor

Como já sabemos exibir os dados do nosso controlador em nossa visão, é hora de buscar dados em tempo real de um servidor RESTful.

Para facilitar a comunicação com servidores HTTP, o AngularJS fornece os serviços $http e $resource. O primeiro é apenas uma camada em cima de XMLHttpRequest ou JSONP, enquanto o último fornece um nível de abstração mais alto. Nós usaremos $http.

Para abstrair as chamadas da API do servidor do controlador, vamos criar nosso próprio serviço personalizado que irá buscar nossos dados e atuar como um wrapper ao redor $http, adicionando isso ao nosso services.js:

angular.module('F1FeederApp.services', []).
  factory('ergastAPIservice', function($http) {

    var ergastAPI = {};

    ergastAPI.getDrivers = function() {
      return $http({
        method: 'JSONP', 
        url: 'http://ergast.com/api/f1/2013/driverStandings.json?callback=JSON_CALLBACK'
      });
    }

    return ergastAPI;
  });


Com as duas primeiras linhas, criamos um novo módulo (F1FeederApp.services) e registramos um serviço dentro desse módulo (ergastAPIservice). Observe que passamos $http como um parâmetro para esse serviço. Isso diz ao mecanismo de injeção de dependência da Angular que nosso novo serviço requer (ou depende) do serviço $http.

De forma semelhante, precisamos dizer ao Angular que inclua nosso novo módulo em nosso aplicativo. Vamos registrar com app.js, substituindo nosso código existente por:

angular.module('F1FeederApp', [
  'F1FeederApp.controllers',
  'F1FeederApp.services'
]);


Agora, tudo o que precisamos fazer é ajustar nosso controller.js um pouco, incluir ergastAPIservice como uma dependência e pronto:

angular.module('F1FeederApp.controllers', []).
  controller('driversController', function($scope, ergastAPIservice) {
    $scope.nameFilter = null;
    $scope.driversList = [];

    ergastAPIservice.getDrivers().success(function (response) {
        //Dig into the responde to get the relevant data
        $scope.driversList = response.MRData.StandingsTable.StandingsLists[0].DriverStandings;
    });
  });


Agora recarregue o aplicativo e confira o resultado. Observe que não fizemos nenhuma alteração no nosso modelo, mas adicionamos uma nameFilter variável ao nosso escopo. Vamos colocar essa variável para usar.

Filtros

Ótimo! Temos um controlador funcional. Mas isso mostra apenas uma lista de drivers. Vamos adicionar algumas funcionalidades implementando uma simples entrada de pesquisa de texto que irá filtrar nossa lista. Vamos adicionar a seguinte linha ao nosso index.html, logo abaixo da tag:

<input type="text" ng-model="nameFilter" placeholder="Search..."/>


Agora estamos fazendo uso da diretiva ng-model. Esta diretiva vincula nosso campo de texto à $scope.nameFilter variável e garante que seu valor esteja sempre atualizado com o valor de entrada. Agora, vamos visitar index.html mais uma vez e fazer um pequeno ajuste na linha que contém a ng-repeat:

<tr ng-repeat="driver in driversList | filter: nameFilter">


Esta linha informa ao ng-repeat que, antes de enviar os dados, a driversList matriz deve ser filtrada pelo valor armazenado nameFilter.

Neste ponto, a ligação de dados bidirecionais é iniciada: sempre que um valor é inserido no campo de busca, Angular imediatamente garante que o $scope.nameFilter que associamos é atualizado com o novo valor. Uma vez que a ligação funciona de ambas as formas, no momento em que o nameFilter valor é atualizado, a segunda diretiva associada a ela (ou seja, a ng-repeat) também obtém o novo valor e a visualização é atualizada imediatamente.

Recarregue o aplicativo e confira a barra de pesquisa.



Observe que este filtro procurará a palavra-chave em todos os atributos do modelo, incluindo aqueles que não estamos usando. Vamos dizer que nós só desejamos filtrar Driver.givenName e Driver.familyName: Primeiro, adicione a driversController, logo abaixo da $scope.driversList = []; linha:

$scope.searchFilter = function (driver) {
    var keyword = new RegExp($scope.nameFilter, 'i');
    return !$scope.nameFilter || keyword.test(driver.Driver.givenName) || keyword.test(driver.Driver.familyName);
};


Agora, de volta index.html, atualizamos a linha que contém a diretiva ng-repeat:

<tr ng-repeat="driver in driversList | filter: searchFilter">


Recarregue o aplicativo mais uma vez e agora temos uma pesquisa por nome.

Rotas

Nosso próximo objetivo é criar uma página de detalhes do driver que nos permita clicar em cada driver e ver os detalhes de sua carreira.

Primeiro, vamos incluir o serviço $routeProvider (em app.js) que nos ajudará a lidar com essas variadas rotas de aplicação. Então, vamos adicionar duas dessas rotas: uma para a tabela do campeonato e outra para os detalhes do driver. Aqui está o nosso novo app.js:

angular.module('F1FeederApp', [
  'F1FeederApp.services',
  'F1FeederApp.controllers',
  'ngRoute'
]).
config(['$routeProvider', function($routeProvider) {
  $routeProvider.
 when("/drivers", {templateUrl: "partials/drivers.html", controller: "driversController"}).
 when("/drivers/:id", {templateUrl: "partials/driver.html", controller: "driverController"}).
 otherwise({redirectTo: '/drivers'});
}]);


Com essa mudança, navegue para http://domain/#/drivers, carregue o driversController e procure a visão parcial para renderizar partials/drivers.html. Mas espere! Ainda não temos visualizações parciais, certo? Nós também precisamos criar esses.

Visões parciais

O AngularJS permitirá que você vincule suas rotas a controladores e visualizações específicos.

Mas primeiro, precisamos dizer a Angular para onde renderizar essas visualizações parciais. Para isso, usaremos a diretiva ng-view, modificando nossa index.html para refletir o seguinte:

<!DOCTYPE html>
<html>
<head>
  <title>F-1 Feeder</title>
</head>

<body ng-app="F1FeederApp">
  <ng-view></ng-view>
  <script src="bower_components/angular/angular.js"></script>
  <script src="bower_components/angular-route/angular-route.js"></script>
  <script src="js/app.js"></script>
  <script src="js/services.js"></script>
  <script src="js/controllers.js"></script>
</body>
</html>


Agora, sempre que navegarmos pelas rotas do nosso aplicativo, Angular irá carregar a visão associada e renderizá-la no lugar da tag. Tudo o que precisamos fazer é criar um arquivo chamado partials/drivers.html e colocar nossa tabela de campeonato HTML lá. Nós também usaremos essa chance de vincular o nome do driver com a rota de detalhes do driver:

<input type="text" ng-model="nameFilter" placeholder="Search..."/>
<table>
<thead>
  <tr><th colspan="4">Drivers Championship Standings</th></tr>
</thead>
<tbody>
  <tr ng-repeat="driver in driversList | filter: searchFilter">
    <td>{{$index + 1}}</td>
    <td>
      <img src="img/flags/{{driver.Driver.nationality}}.png" />
      <a href="#/drivers/{{driver.Driver.driverId}}">
    {{driver.Driver.givenName}}&nbsp;{{driver.Driver.familyName}}
   </a>
 </td>
    <td>{{driver.Constructors[0].name}}</td>
    <td>{{driver.points}}</td>
  </tr>
</tbody>
</table>


Finalmente, vamos decidir o que queremos mostrar na página de detalhes. Que tal um resumo de todos os fatos relevantes sobre o piloto (por exemplo, nascimento, nacionalidade), juntamente com uma tabela contendo seus resultados recentes? Para fazer isso, adicionamos a services.js:

angular.module('F1FeederApp.services', [])
  .factory('ergastAPIservice', function($http) {

    var ergastAPI = {};

    ergastAPI.getDrivers = function() {
      return $http({
        method: 'JSONP', 
        url: 'http://ergast.com/api/f1/2013/driverStandings.json?callback=JSON_CALLBACK'
      });
    }

    ergastAPI.getDriverDetails = function(id) {
      return $http({
        method: 'JSONP', 
        url: 'http://ergast.com/api/f1/2013/drivers/'+ id +'/driverStandings.json?callback=JSON_CALLBACK'
      });
    }

    ergastAPI.getDriverRaces = function(id) {
      return $http({
        method: 'JSONP', 
        url: 'http://ergast.com/api/f1/2013/drivers/'+ id +'/results.json?callback=JSON_CALLBACK'
      });
    }

    return ergastAPI;
  });


Desta vez, fornecemos a identificação do driver para o serviço para que possamos recuperar as informações relevantes apenas para um driver específico. Agora, modificamos controllers.js:

angular.module('F1FeederApp.controllers', []).

  /* Drivers controller */
  controller('driversController', function($scope, ergastAPIservice) {
    $scope.nameFilter = null;
    $scope.driversList = [];
    $scope.searchFilter = function (driver) {
        var re = new RegExp($scope.nameFilter, 'i');
        return !$scope.nameFilter || re.test(driver.Driver.givenName) || re.test(driver.Driver.familyName);
    };

    ergastAPIservice.getDrivers().success(function (response) {
        //Digging into the response to get the relevant data
        $scope.driversList = response.MRData.StandingsTable.StandingsLists[0].DriverStandings;
    });
  }).

  /* Driver controller */
  controller('driverController', function($scope, $routeParams, ergastAPIservice) {
    $scope.id = $routeParams.id;
    $scope.races = [];
    $scope.driver = null;

    ergastAPIservice.getDriverDetails($scope.id).success(function (response) {
        $scope.driver = response.MRData.StandingsTable.StandingsLists[0].DriverStandings[0]; 
    });

    ergastAPIservice.getDriverRaces($scope.id).success(function (response) {
        $scope.races = response.MRData.RaceTable.Races; 
    }); 
  });


O importante para notar aqui é que acabamos de injetar o serviço $routeParams no controlador do driver. Este serviço nos permitirá acessar nossos parâmetros de URL (para o id, neste caso) usando $routeParams.id.

Agora que temos nossos dados no escopo, precisamos apenas da visão parcial restante. Vamos criar um arquivo chamado partials/driver.html e adicionar:

<section id="main">
  <a href="./#/drivers"><- Back to drivers list</a>
  <nav id="secondary" class="main-nav">
    <div class="driver-picture">
      <div class="avatar">
        <img ng-show="driver" src="img/drivers/{{driver.Driver.driverId}}.png" />
        <img ng-show="driver" src="img/flags/{{driver.Driver.nationality}}.png" /><br/>
        {{driver.Driver.givenName}} {{driver.Driver.familyName}}
      </div>
    </div>
    <div class="driver-status">
      Country: {{driver.Driver.nationality}}   <br/>
      Team: {{driver.Constructors[0].name}}<br/>
      Birth: {{driver.Driver.dateOfBirth}}<br/>
      <a href="{{driver.Driver.url}}" target="_blank">Biography</a>
    </div>
  </nav>

  <div class="main-content">
    <table class="result-table">
      <thead>
        <tr><th colspan="5">Formula 1 2013 Results</th></tr>
      </thead>
      <tbody>
        <tr>
          <td>Round</td> <td>Grand Prix</td> <td>Team</td> <td>Grid</td> <td>Race</td>
        </tr>
        <tr ng-repeat="race in races">
          <td>{{race.round}}</td>
          <td><img  src="img/flags/{{race.Circuit.Location.country}}.png" />{{race.raceName}}</td>
          <td>{{race.Results[0].Constructor.name}}</td>
          <td>{{race.Results[0].grid}}</td>
          <td>{{race.Results[0].position}}</td>
        </tr>
      </tbody>
    </table>
  </div>

</section>


Note que estamos agora colocando a directiva ng-show em bom uso. Esta diretiva mostrará apenas o elemento HTML se a expressão fornecida for true (ou seja, nem false, nem null). Nesse caso, o avatar só será exibido uma vez que o objeto do driver tenha sido carregado no escopo pelo controlador.

Toques finais

Adicione um monte de CSS e renderize sua página. Você deve acabar com algo como isto:



Agora você está pronto para ativar seu aplicativo e certifique-se de que ambas as rotas funcionem conforme desejado. Você também pode adicionar um menu estático index.html para melhorar as capacidades de navegação do usuário. As possibilidades são infinitas.

EDIT (maio de 2014): recebi muitos pedidos para uma versão para download do código que criamos neste tutorial. Por isso, decidi liberá-lo AQUI (despojado de qualquer CSS). No entanto, eu realmente não recomendo baixá-lo, pois este guia contém todas as etapas que você precisa para construir o mesmo aplicativo com suas próprias mãos, o que será um exercício de aprendizagem muito mais útil e eficaz.

Conclusão

Neste ponto do tutorial, cobrimos tudo o que você precisaria para escrever um aplicativo simples (como um alimentador de Fórmula 1). Cada uma das páginas restantes da demonstração ao vivo (por exemplo, tabela de campeão do construtor, detalhes da equipe, calendário) compartilham a mesma estrutura e conceitos básicos que revisamos aqui.

Finalmente, tenha em mente que a Angular é uma estrutura muito poderosa e que mal conseguimos arranhar a superfície em termos de tudo o que tem para oferecer. Na Parte 2 deste tutorial, daremos exemplos de por que Angular se destaca entre seus frameworks MVC front-end: testabilidade. Analisaremos o processo de construção e execução de testes unitários com o Karma, conseguindo uma integração contínua com Yeomen, Grunt e Bower e outros pontos fortes desta fantástica estrutura de front-end.

POR RAONI BOAVENTURA - ENGENHEIRO JAVASCRIPT @ TOPTAL