I've been working a project that allows a user to manage Option Types and Options. Basically user can add a new Option Type, let's say they name it Color and then they add the options - Black, Red, Purple, etc. When the collection first loads up the existing records, an empty option should be added at the end
When a user starts typing in the text field, I want to add a new empty option , thereby always giving the user a new field to work with.
I have this almost working, but can't figure how to properly add new empty option to a new Option Type or to existing option types. The push method keeps crashing Plunkr. Any input is appreciated, short sample review of the plunkr is below
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
$scope.optionTypeId = 0;
$scope.productOptionId = 0;
$scope.productEditorModel = {
"ProductOptions": [0],
"OptionTypes": [0]
};
$scope.productEditorModel.optionTypeName = '';
$scope.addEmptyOption = function (optionTypeId) {
var emptyOption = { ProductOptionId: 3, ProductId: '1066', OptionTypeId: 1, OptionValue: '', Price: '', IsStocked: true };
console.log(emptyOption);
//$scope.productEditorModel.ProductOptions.push(emptyOption);
};
$scope.loadData = function () {
$scope.productEditorModel.OptionTypes = [{ OptionTypeId: 1, OptionName: 'Color' },{ OptionTypeId: 2, OptionName: 'Size' },];
$scope.productEditorModel.ProductOptions = [{ ProductOptionId: 1, ProductId: '1066', OptionTypeId: 2, OptionValue: 'Medium', Price: '', IsStocked: true, },{ ProductOptionId: 2, ProductId: '1066', OptionTypeId: 1, OptionValue: 'Black', Price: '', IsStocked: true }];
angular.forEach($scope.productEditorModel.ProductOptions, function (item) {
//console.log(item.OptionTypeId);
$scope.addEmptyOption(item.OptionTypeId);
});
};
$scope.loadData();
$scope.removeOption = function (option) {
var index = $scope.productEditorModel.ProductOptions.indexOf(option);
$scope.productEditorModel.ProductOptions.splice(index, 1);
};
$scope.filterEmptyElements = function (optionTypeId) {
$scope.emptyElements = $.grep($scope.productEditorModel.ProductOptions, function (k) { return k.OptionValue === "" || angular.isUndefined(k.OptionValue) && k.OptionTypeId == optionTypeId });
};
$scope.update = function (option, index) {
var optionTypeId = option.OptionTypeId;
$scope.filterEmptyElements(optionTypeId);
if (!angular.isUndefined(option.OptionValue) && $scope.emptyElements.length == 1 && option.OptionValue.length > 0) {
$scope.addOption(optionTypeId);
} else if (angular.isUndefined(option.OptionValue)) {
$scope.removeOption(option);
}
};
$scope.addOptionType = function () {
var optionTypeId = --$scope.optionTypeId;
var optionName = $scope.productEditorModel.optionTypeName;
var newOptionType = { OptionTypeId: optionTypeId, OptionName: optionName };
$scope.productEditorModel.OptionTypes.push(newOptionType);
$scope.addEmptyOption(optionTypeId);
};
$scope.editOptionType = function (optionType) {
$scope.editing = true;
};
$scope.saveOptionType = function (optionType) {
$scope.editing = false;
};
$scope.trackOptionTypesCount = function () {
if ($scope.productEditorModel.OptionTypes.length == 3) {
$scope.isMaxOptionTypes = true;
} else {
$scope.isMaxOptionTypes = false;
}
};
$scope.removeOptionType = function (optionType) {
var index = $scope.productEditorModel.OptionTypes.indexOf(optionType);
$scope.productEditorModel.OptionTypes.splice(index, 1);
$scope.trackOptionTypesCount();
};
});
See the plunker below:
http://plnkr.co/edit/YHLtSwQWVb2swhNVTQzU?p=info
The error you get that $ is not defined is because you haven't included jQuery. You don't need jQuery for this though, array.map should be able to perform the same functionality.
$scope.emptyElements = $scope.productEditorModel.ProductOptions.map(function (k) {
return k.OptionValue === "" || angular.isUndefined(k.OptionValue) && k.OptionTypeId == optionTypeId
});
And it crashes because inside $scope.loadData you have
angular.forEach($scope.productEditorModel.ProductOptions, function (item) {
$scope.addEmptyOption(item.OptionTypeId);
});
and then inside $scope.addEmptyOption you try to
$scope.productEditorModel.ProductOptions.push(emptyOption);
So the foreach will loop for each item in $scope.productEditorModel.ProductOptions, which you keep adding options to so....? Infinite loop.
Non-crashing version: http://plnkr.co/edit/5Sc2sWfhKBs9kLCk83f1?p=preview
What you really should do though is look over your data structure. Make the ProductOptions a sub-object of OptionTypes and just rename it Options. Remove ALL code about creating id's here in your GUI, that should be handled by the backend. Instead in the GUI there should be a Sortorder property on the Options (which also of course gets stored by the backend). Then when you store, the ones without an id get inserted, the ones with an id get updated. Much easier to handle everything that way.
I'd also break out optionTypes and options to their own services/providers. Much easier to keep track of what needs to be done. And each just basically contains add, remove and maybe a find/getJSON or something.
Here's a restructured version. Much easier to keep track of what belongs where. And it has more features than the original with less code. http://plnkr.co/edit/BHcu6vAfcpEYQpZKHc5G?p=preview
Related
I'm quite new to JavaScript, and I'm stuck with an issue which I didn't manage to find an answer for.
I have an array, each item of which is tied to a card in markup.
When a button is pressed, a form pops up and allows to enter name and url for the new image, which is then unshifted to thebeginning of the array.
The last element of the array is popped, so there's always the same number of elements.
And the issue itself is:
When I add a single card, it works perfect: last card is popped and the new one is tucked in the beginning.
But when I use and submit the form again, the card that I have previously edited changes as well, although it should not.
I suspect that the mistake lies in the function that updates the cards, but I'm all out of ideas how to fix it.
Is there a way to fix that function?
const formAdd = document.querySelector('.popup__form-add')
const newElement = { name: '', link: '' };
const blank = { name: '', link: '' };
const placeName = document.querySelector('.popup__edit_type_place-name');
const placeImage = document.querySelector('.popup__edit_type_place-picture');
const elements = document.querySelector('.elements');
const cards = elements.querySelectorAll('.element');
const buttonAdd = document.querySelector('.profile__button-add');
const buttonAddClose = document.querySelector('.popup__add-button-close');
//content popup functions
function popupAddDisplay() {
popupAdd.classList.add('popup_opened');
}
function popupAddHide() {
popupAdd.classList.remove('popup_opened');
}
function contentUpdate() {
event.preventDefault();
newElement.name = placeName.value;
newElement.link = placeImage.value;
console.log(newElement);
initialCards.pop();
initialCards.unshift(blank);
blank.name = newElement.name;
blank.link = newElement.link;
console.log(initialCards);
cardsCheck();
popupAddHide();
};
//cards array, page content loads from here
const initialCards = [{
name: 'Архыз',
link: 'https://pictures.s3.yandex.net/frontend-developer/cards-compressed/arkhyz.jpg'
},
{
name: 'Челябинская область',
link: 'https://pictures.s3.yandex.net/frontend-developer/cards-compressed/chelyabinsk-oblast.jpg'
},
{
name: 'Иваново',
link: 'https://pictures.s3.yandex.net/frontend-developer/cards-compressed/ivanovo.jpg'
},
{
name: 'Камчатка',
link: 'https://pictures.s3.yandex.net/frontend-developer/cards-compressed/kamchatka.jpg'
},
{
name: 'Холмогорский район',
link: 'https://pictures.s3.yandex.net/frontend-developer/cards-compressed/kholmogorsky-rayon.jpg'
},
{
name: 'Байкал',
link: 'https://pictures.s3.yandex.net/frontend-developer/cards-compressed/baikal.jpg'
}
];
//compares info from the markup with the array, then pushes array info into markup
function cardsCheck() {
cards.forEach(function(item, index) {
const nameAlt = initialCards[index].name;
const imageSrcAlt = initialCards[index].link;
item.querySelector('.element__name').textContent = nameAlt;
item.querySelector('.element__image').src = imageSrcAlt;
if (nameAlt == '') {
item.style.display = 'none'
}
console.log(nameAlt);
});
}
document.onload = cardsCheck();
buttonAdd.addEventListener('click', () => {
popupAddDisplay()
});
buttonAddClose.addEventListener('click', popupAddHide);
formAdd.addEventListener('submit', contentUpdate);```
You always use the same blank object. So at the end you have the same object multiple times in the array. If you change an attribute of this object it is changed wherever it is in the list also.
To avoid this you need to create a new object before adding it to the array
function contentUpdate() {
event.preventDefault();
newElement.name = placeName.value;
newElement.link = placeImage.value;
console.log(newElement);
initialCards.pop();
let blank = { name: '', link: '' };
initialCards.unshift(blank);
blank.name = newElement.name;
blank.link = newElement.link;
console.log(initialCards);
cardsCheck();
popupAddHide();
};
Every time you create a new card, it seems like you override the global variables blank and newElement.
However, you don't have to. You could create a local variable in your contentUpdate function called newElement.
That way, each time updateContent is called, a new local variable is created.
You could therefore try the following code:
const formAdd = document.querySelector('.popup__form-add')
const placeName = document.querySelector('.popup__edit_type_place-name');
const placeImage = document.querySelector('.popup__edit_type_place-picture');
const elements = document.querySelector('.elements');
const cards = elements.querySelectorAll('.element');
const buttonAdd = document.querySelector('.profile__button-add');
const buttonAddClose = document.querySelector('.popup__add-button-close');
//content popup functions
function popupAddDisplay() {
popupAdd.classList.add('popup_opened');
}
function popupAddHide() {
popupAdd.classList.remove('popup_opened');
}
function contentUpdate() {
event.preventDefault();
const newElement = {}
newElement.name = placeName.value;
newElement.link = placeImage.value;
console.log(newElement);
initialCards.pop();
initialCards.unshift(newElement);
console.log(initialCards);
cardsCheck();
popupAddHide();
}
Hi, I'm trying to make my own Pokédex site, but sinds I'm not very good at databases, SQL, PHP and such, I'm doing it in JavaScript, JSON style. While doing this I bumped into a problem: I wanted to refer gen1.gps.Route_1.Exits.South to gen1.gps.Pallet_Town.Title but I didn't know how. Of course I searched the internet for a while first, to find a solution, but I didn't understand it in the context.
var gen1 = {
'gps': {
'Route_1': {
'Title': 'Route 1',
'Exits': {
'North': 0,
'South': // pallet town link here
},
},
'Pallet_Town': {
'Title': 'Pallet Town',
'Exits': {
'North': /* route 1 here */,
'South': 0
}
}
}
}
I expect to be able to call the gen1.gps.Route_1.Exits.South object to get the Title object of gen1.gps.Pallet_Town.
I haven't debugged anything yet so I have no idea what my errors are or will be.
I hope somebody can help me by posting a useful answer.
Need to implement some kind of id system. MySQL takes care of this for you, but like you said, you don't want to have to learn new languages just to see something come to life.
I added IDs to the routes, and then wrote a function to find routes by their IDs. The bottom bit of code (Object.size()) is to get the total number of routes in your gps object. (Same as array.length)
var gen1 = {
'gps': {
'Route_1': {
'Id': 1,
'Title': 'Route 1',
'Exits': {
'North': 0,
'South': 2
},
},
'Pallet_Town': {
'Id': 2,
'Title': 'Pallet Town',
'Exits': {
'North': 1,
'South': 0
}
}
}
};
function getRoute (id)
{
for (var i = 0; i < gen1.gps.size(); i++)
{
if (gen1.gps[i].id == id)
return gen1.gps[i];
}
return false;
}
Object.size = function (obj)
{
var size = 0, key;
for (key in obj)
if (obj.hasOwnProperty (key)) size++;
return size;
};
You can save a lot of work if you create your object dynamically, like this:
var gen1 = {
gps: {}
};
function checkPlace(a) {
if (!gen1.gps[a]) gen1.gps[a] = {};
if (!gen1.gps[a].Title) gen1.gps[a].Title = a.split("_").join(" ");
if (!gen1.gps[a].Exits) gen1.gps[a].Exits = {};
}
function connect(a, b, dir) {
checkPlace(a);
checkPlace(b);
if (dir == "ns") {
gen1.gps[a].Exits.South = gen1.gps[b];
gen1.gps[b].Exits.North = gen1.gps[a];
}
if (dir == "we") {
gen1.gps[a].Exits.East = gen1.gps[b];
gen1.gps[b].Exits.West = gen1.gps[a];
}
}
connect("Route_1", "Pallet_Town", "ns");
console.log(gen1.gps.Route_1.Exits.South.Title);
console.log(gen1.gps);
I'm using RubaXa's excellent Sortable JS library to allow drag-and-drop rearranging of divs on a Bootstrap-based dashboard. Since the divs are all in 2 columns (left and right), I have the columns defined with ids of "leftColumn" and "rightColumn".
In order to allow dragging between columns, I set up both sortables with the same group, like this:
Sortable.create(leftColumn, {
group: 'dash_sections',
});
Sortable.create(rightColumn, {
group: 'dash_sections',
});
Now I am trying to load and save the order from both lists (the entire group). I placed data-id fields in each of the div tags, and I'm trying to use the following code to save and restore the order of everything.
Sortable.create(rightColumn, {
group: 'dash_sections',
store: {
get: function (sortable) {
var order = localStorage.getItem(sortable.options.group);
return order ? order.split('|') : [];
},
set: function (sortable) {
var order = sortable.toArray();
localStorage.setItem(sortable.options.group, order.join('|'));
}
}
});
However, I'm only saving and restoring the order for that column, not the entire group. I eventually want to have the group's order stored in a single string in the database. How do I go about saving and restoring the entire group's order?
Update:
I put similar code in both sortable.create functions, using "leftcol" and "rightcol" instead of sortable.options.group. This properly saves the order of each sortable as long as you don't drag between columns. I'm still looking for a way to save the order even when dragging between columns.
Here's how I implemented a similar functionality
Introduced a category flag to sortable options:
var leftColumnOptions = {
group: "dash_sections",
category: "left_column",
store: setupStore()
};
var rightColumnOptions = {
group: "dash_sections",
category: "right_column",
store: setupStore()
}
setupStore function checks for localStorage availability and applies get and set
function setupStore() {
if (localStorageAvailable) { // basic localStorage check: (typeof (localStorage) !== "undefined")
return {
get: getValue,
set: setValue
};
}
return {};
}
getValue and setValue retreive and store item ids based on category name defined in options above
function getValue(sortable) {
var order = localStorage.getItem(sortable.options.category);
return order ? order.split('|') : [];
}
function setValue(sortable) {
var order = sortable.toArray();
localStorage.setItem(sortable.options.category, order.join('|'));
}
It is a good idea to check for stored order information in localStorage before initializing Sortable, I'm using lodash for convenience
function applyState($section, categoryName) {
if (localStorageAvailable) {
var order = localStorage.getItem(categoryName);
var itemIds = order ? order.split('|') : [];
var $items = _.map(itemIds, function(itemId, index) {
return $("[data-id='" + itemId + "'");
});
$section.append($items);
}
}
usage would be:
applyState($(".js-left-column"), "left_column");
applyState($(".js-right-column"), "right_column");
// initialize sortable
Entire code:
HTML:
<div class="js-two-column-sortable js-left-column" data-category="left_column">
<!-- elements -->
</div>
<div class="js-two-column-sortable js-right-column" data-category="right_column">
<!-- elements -->
</div>
JS:
var localStorageAvailable = (typeof localStorage !== "undefined");
function getValue(sortable) {
var order = localStorage.getItem(sortable.options.category);
return order ? order.split('|') : [];
}
function setValue(sortable) {
var order = sortable.toArray();
localStorage.setItem(sortable.options.category, order.join('|'));
}
function setupStore() {
if (localStorageAvailable) {
return {
get: getValue,
set: setValue
};
}
return {};
}
function onAdd(evt) {
setValue(this);
}
function applyState($section, categoryName) {
if (localStorageAvailable) {
var order = localStorage.getItem(categoryName);
var itemIds = order ? order.split('|') : [];
var $items = _.map(itemIds, function(itemId, index) {
return $("[data-id='" + itemId + "'");
});
$section.append($items);
}
}
var options = {
group: "two-column-sortable",
store: setupStore(),
onAdd: onAdd
};
function init() {
$(".js-two-column-sortable").each(function(index, section) {
var $section = $(section);
var category = $section.attr("data-category");
var sortableOptions = _.extend({}, options, { category: category });
applyState($section, category);
$section.data("twoColumnSortable", new Sortable(section, sortableOptions));
});
}
init();
Following issue: Let's say, we have an object like this:
$scope.Something = { 'a' : { object... }, 'b' : { another object... } }
This Something-object is also rendered in the view as follows:
<div ng-repeat="s in Something">{{ Something[s].someProperty }}</div>
The user wants to edit Something.a. For this, we show him a form. Before the form is shown, I save the current Something.a as a copy:
$scope.copyForUndo= angular.copy($scope.Something.a);
Now, if the user clicks "Cancel", it gets:
$scope.Something.a = angular.copy($scope.copyForUndo);
But since then, the association seems to disappear. No matter, what changes the user now makes to Something.a, the view doesn't get updated.
Why?
I know, what the equality of objects is (for example, that { object1: true } != {object1 : true} but still I cannot understand, why it doesn't work.
If you can make $scope.Something an array, then you can edit the copy and then update the array when the changes are saved. It still provides an undo, but in reverse of how you presented it.
fiddle here: http://jsfiddle.net/GYeSZ/1/
function MyCtrl($scope) {
$scope.Something = [
{ name: "Aye", desc: new Date() },
{ name: "Bee", desc: new Date() },
{ name: "See", desc: new Date() }
];
$scope.edit = function(idx) {
$scope.copy = angular.copy($scope.Something[idx]);
$scope.idx = idx;
}
$scope.save = function() {
$scope.Something[$scope.idx] = angular.copy($scope.copy);
$scope.cancel();
}
$scope.cancel = function() {
$scope.copy = null;
$scope.idx = -1;
}
}
Update
There is an alternate syntax for ng-repeat that can be used to enumerate dictionaries to get their key. Using this syntax, you can use the data structure you describe in the question
fiddle here: http://jsfiddle.net/GYeSZ/3/
function MyCtrl($scope) {
$scope.edit = function(key) {
$scope.copy = angular.copy($scope.Something[key]);
$scope.key = key;
}
$scope.Something = {
"a": { name: "Aye", desc: new Date() },
"b": { name: "Bee", desc: new Date() },
"c": { name: "See", desc: new Date() }
};
$scope.save = function() {
$scope.Something[$scope.key] = angular.copy($scope.copy);
$scope.cancel();
}
$scope.cancel = function() {
$scope.copy = null;
$scope.key = null;
}
}
Html
<div ng-repeat="(key, value) in Something" ....>
If the copy source is ng-repeat iterative item, you should use
angular.copy($scope.copyForUndo, $scopy.sourceItem)
insteads of
$scope.sourceItem = angular.copy($scope.copyForUndo)
Otherwise, your data-binding dom is not tracking because the $$hashkey of
the iterative item was erased by the misuse copy statement.
https://github.com/angular/angular.js/blob/g3_v1_2/src/Angular.js#L777
it seems a bit odd but if you can save the original array $scope.Something then on canceling you can rebind it.
// saving original array to get the original copy of edited object
var originalArrayCopy = angular.copy($scope.Something);
............
// When user clicks cancel then simply filter the originalArray to get the original copy, here i am assuming there is a field in object which can uniquely identify it.
// var originalObject = originalArrayCopy .filter(function(elm)
{
if(editedObject.Id == elm.Id)
return elm;
} );
// once i get the original object , i can rebind it to the currentObject which is being edited.
Non destructive form editing: http://egghead.io/lessons/angularjs-angular-copy-for-deep-copy
Synopsis:
create a pointer to the object that the user clicked edit
create a copy of object that user edits and can decide to save|cancel
I'm not sure if it has to do with the fact that you are copying a copy, or what, but here is a working plunker
Copy Example Plunker
I'm passing a config object with names for each slide to a function that instantiates a jQuery tools scrollable. I want the URL in the location bar to match the active tab id. I have it working so that passing the URL with the name in will navigate to the correct slide (that's at the bottom of the provided code), but I'm trying to get the URL updating when the slide changes. I know what I need to do to get that, but not how to do that, which is like in the question title.. pass a value to an object and get a property that has that value.
$(function () {
Scrollablenav.init({
"#tulips": 0,
"#daffodils": 1,
"#zebras": 2,
"#horseshoes": 3
});
});
Scrollablenav.init = function(config){
var scroller = $(".scrollable").scrollable({
circular: true,
onSeek: function (event) {
parent.location.hash = function(){
//something that looks at config, sends it the value of the current slide and returns corresponding property name
}
}
}).navigator({
navi: '#slideTabs',
naviItem: 'a',
activeClass: 'current',
history: true
}).data('scrollable');
if (!isNaN(config[location.hash])){
scroller.seekTo(config[location.hash], 0);
}
}
You can create your own function to find property name based on its value:
function findPropertyName(object, property_value, strict) {
if (typeof strict=='undefined'){
strict = false;
};
for (property in object){
if ((strict && object[property] === property_value) ||
(!strict && object[property] == property_value)){
return property;
}
}
return false;
}
Function description:
It will return name of the first property having given value, or false if no such property has been found.
Third param is responsible for determining whether strict comparison should be made (eg. string "123" is equal to integer 123 - like in '123'==123, but not strictly equal - like in '123'===123).
Function could be improved to return all the properties having given value, but I think you do not need it.
To check the function in action see this jsfiddle.
Something like this?
function getMyHash(config, value) {
for (item in config) {
if (config[item] === value) {
return item;
}
}
};
basically you have to iterate and match values; you can't lookup by value.
Can you change the format of your config? In other words, can we do:
$(function () {
Scrollablenav.init({
"#tulips": { Key: 'tulips', Index: 0 },
"#daffodils": { Key: 'daffodils', Index: 1 },
"#zebras": { Key: 'zebras', Index: 2 },
"#horseshoes": { Key: 'horseshoes', Index: 3 }
});
});
If that doesn't work, you can make a new object somewhere that maps the indexes back to the key:
var keys = {
1: 'tulips',
2: 'daffodils',
3: 'zebras',
4: 'horseshoes',
};
UPDATE:
You could also build this config dynamically:
$(function () {
var values = ['#tulips', '#daffodils', '#zebras', '#horseshoes'];
var config = {};
for(var i = 0; i < values.length; i++)
{
config[values[i]] = { Index: i, Key: values[i] };
}
Scrollablenav.init(config);
});